Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion Source/AGXUnreal/Private/Wire/AGX_WireComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "Utilities/AGX_ObjectUtilities.h"
#include "Utilities/AGX_StringUtilities.h"
#include "Wire/AGX_WireInstanceData.h"
#include "Wire/AGX_WireLinkComponent.h"
#include "Wire/AGX_WireNode.h"
#include "Wire/AGX_WireUtilities.h"
#include "Wire/AGX_WireWinchComponent.h"
Expand Down Expand Up @@ -2288,7 +2289,7 @@ void UAGX_WireComponent::CreateNative()
NodeBarrier.AllocateNativeFreeNode(WorldLocation);
break;
}
NodeBarrier.AllocateNativeEyeNode(*Body, Location);
NodeBarrier.AllocateNativeEyeNode(*Body, Location, RouteNode.Radius.Value);
break;
}
case EWireNodeType::BodyFixed:
Expand All @@ -2310,6 +2311,104 @@ void UAGX_WireComponent::CreateNative()
NodeBarrier.AllocateNativeBodyFixedNode(*Body, Location);
break;
}
case EWireNodeType::Connecting:
{
FRigidBodyBarrier* Body;
FVector LocalLocation;
std::tie(Body, LocalLocation) = GetBodyAndLocalLocation(RouteNode, LocalToWorld);
if (Body == nullptr)
{
ErrorMessages.Add(FString::Printf(
TEXT("Wire node at index %d is a Connecting node but has no valid "
"body. Creating Free Node instead."),
I));
const FVector WorldLocation = RouteNode.Frame.GetWorldLocation(*this);
NodeBarrier.AllocateNativeFreeNode(WorldLocation);
break;
}

// Find the UAGX_WireLinkComponent attached as a direct child of the body.
UAGX_WireLinkComponent* LinkComp = nullptr;
if (UAGX_RigidBodyComponent* BodyComp = RouteNode.RigidBody.GetRigidBody())
{
TArray<USceneComponent*> Children;
BodyComp->GetChildrenComponents(
/*bIncludeAllDescendants=*/false, Children);
for (USceneComponent* Child : Children)
{
if (UAGX_WireLinkComponent* Candidate =
Cast<UAGX_WireLinkComponent>(Child))
{
LinkComp = Candidate;
break;
}
}
}
if (LinkComp == nullptr)
{
ErrorMessages.Add(FString::Printf(
TEXT("Wire node at index %d is a Connecting node but no "
"UAGX_WireLinkComponent is attached to the referenced body. "
"Attach a WireLinkComponent as a child of the body component. "
"Creating Free Node instead."),
I));
const FVector WorldLocation = RouteNode.Frame.GetWorldLocation(*this);
NodeBarrier.AllocateNativeFreeNode(WorldLocation);
break;
}

FWireLinkBarrier* LinkBarrier = LinkComp->GetOrCreateNative();
if (LinkBarrier == nullptr)
{
ErrorMessages.Add(FString::Printf(
TEXT("Wire node at index %d: failed to get/create native for "
"WireLinkComponent '%s'. Creating Free Node instead."),
I, *LinkComp->GetName()));
const FVector WorldLocation = RouteNode.Frame.GetWorldLocation(*this);
NodeBarrier.AllocateNativeFreeNode(WorldLocation);
break;
}

// Derive the wire side from the node's position in the route:
// - First node (index 0) → WIRE_BEGIN
// - Last node (LastIndex) → WIRE_END
// A Connecting node that is neither first nor last is invalid.
const int32 LastIndex = RouteNodes.Num() - 1;
const bool bIsWireBegin = (I == 0);
const bool bIsWireEnd = (I == LastIndex);
if (!bIsWireBegin && !bIsWireEnd)
{
ErrorMessages.Add(FString::Printf(
TEXT("Wire node at index %d is a Connecting node but is neither the first "
"nor the last node in the route (route has %d nodes). A Connecting "
"node must be at index 0 (WIRE_BEGIN) or at the last index (WIRE_END). "
"Creating Free Node instead."),
I, RouteNodes.Num()));
const FVector WorldLocation = RouteNode.Frame.GetWorldLocation(*this);
NodeBarrier.AllocateNativeFreeNode(WorldLocation);
break;
}

// Register which wire attaches at which body-local offset and on which side.
LinkBarrier->Connect(NativeBarrier, LocalLocation, bIsWireBegin);

// Insert the link into the wire route. AGX creates the ConnectingNode
// internally; the node must not also be added via AddRouteNode.
LinkBarrier->AddToWireRoute(NativeBarrier);

// Apply the link-level radius to the newly created ConnectingNode.
// This must be a post-creation call: the AGX Link API (link->connect +
// wire->add(link)) provides no radius parameter, so AGX always constructs
// the ConnectingNode with radius 0 internally. setRadius is the only hook
// available. Skip when 0 since that is already the AGX default.
if (LinkComp->Radius.Value > 0.0)
{
LinkBarrier->SetConnectingNodeRadius(NativeBarrier, LinkComp->Radius.Value);
}

LinkComp->RegisterConnectedWire(this);
continue; // Skip AddRouteNode — node insertion was handled by AddToWireRoute.
}
case EWireNodeType::Other:
UE_LOG(
LogAGX, Warning,
Expand Down
194 changes: 194 additions & 0 deletions Source/AGXUnreal/Private/Wire/AGX_WireLinkComponent.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright 2026, Algoryx Simulation AB.

#include "Wire/AGX_WireLinkComponent.h"

// AGX Dynamics for Unreal includes.
#include "AGX_LogCategory.h"
#include "AGX_NativeOwnerSceneComponentInstanceData.h"
#include "AGX_RigidBodyComponent.h"
#include "Utilities/AGX_ObjectUtilities.h"
#include "Utilities/AGX_StringUtilities.h"

// Unreal Engine includes.
#include "CoreGlobals.h"

UAGX_WireLinkComponent::UAGX_WireLinkComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}

UAGX_RigidBodyComponent* UAGX_WireLinkComponent::GetRigidBody() const
{
return FAGX_ObjectUtilities::FindFirstAncestorOfType<UAGX_RigidBodyComponent>(*this);
}

void UAGX_WireLinkComponent::RegisterConnectedWire(UAGX_WireComponent* Wire)
{
if (Wire == nullptr)
{
return;
}

// Deduplicate.
for (UAGX_WireComponent* const Existing : ConnectedWires)
{
if (Existing == Wire)
{
return;
}
}

ConnectedWires.Add(Wire);
}

bool UAGX_WireLinkComponent::HasNative() const
{
return NativeBarrier.HasNative();
}

uint64 UAGX_WireLinkComponent::GetNativeAddress() const
{
return static_cast<uint64>(NativeBarrier.GetNativeAddress());
}

void UAGX_WireLinkComponent::SetNativeAddress(uint64 NativeAddress)
{
check(!HasNative());
NativeBarrier.SetNativeAddress(static_cast<uintptr_t>(NativeAddress));
}

void UAGX_WireLinkComponent::BeginPlay()
{
Super::BeginPlay();

if (HasNative())
{
// Native was inherited from a Blueprint reconstruction — nothing more to do.
// The link activates implicitly when its connected wires are added to the simulation.
return;
}

if (GIsReconstructingBlueprintInstances)
{
return;
}

GetOrCreateNative();

if (!HasNative())
{
UE_LOG(
LogAGX, Error,
TEXT("UAGX_WireLinkComponent '%s' in '%s': Failed to create native agxWire::Link. "
"Check that this component is attached to a UAGX_RigidBodyComponent, "
"and that the AgX-WireLink license module is active."),
*GetName(), *GetLabelSafe(GetOwner()));
return;
}
}

void UAGX_WireLinkComponent::EndPlay(const EEndPlayReason::Type Reason)
{
if (!GIsReconstructingBlueprintInstances)
{
if (HasNative())
{
NativeBarrier.ReleaseNative();
}

ConnectedWires.Empty();
}
// If GIsReconstructingBlueprintInstances, another WireLinkComponent will inherit
// the native via Component Instance Data — do not release it here.

Super::EndPlay(Reason);
}

TStructOnScope<FActorComponentInstanceData>
UAGX_WireLinkComponent::GetComponentInstanceData() const
{
return MakeStructOnScope<FActorComponentInstanceData,
FAGX_NativeOwnerSceneComponentInstanceData>(
this, this,
[](UActorComponent* Component) -> IAGX_NativeOwner*
{ return Cast<UAGX_WireLinkComponent>(Component); });
}

FWireLinkBarrier* UAGX_WireLinkComponent::GetNative()
{
return HasNative() ? &NativeBarrier : nullptr;
}

const FWireLinkBarrier* UAGX_WireLinkComponent::GetNative() const
{
return HasNative() ? &NativeBarrier : nullptr;
}

FWireLinkBarrier* UAGX_WireLinkComponent::GetOrCreateNative()
{
if (HasNative())
{
return &NativeBarrier;
}

checkf(
!GIsReconstructingBlueprintInstances,
TEXT("UAGX_WireLinkComponent::GetOrCreateNative called while Blueprint reconstruction is "
"in progress. The native should be inherited via Component Instance Data."));

CreateNative();

if (!HasNative())
{
UE_LOG(
LogAGX, Error,
TEXT("UAGX_WireLinkComponent '%s' in '%s': GetOrCreateNative could not create a "
"native. Ensure this component is attached to a UAGX_RigidBodyComponent, "
"and that the AgX-WireLink module is licensed."),
*GetName(), *GetLabelSafe(GetOwner()));
return nullptr;
}

return &NativeBarrier;
}

void UAGX_WireLinkComponent::CreateNative()
{
check(!HasNative());
check(!GIsReconstructingBlueprintInstances);

// Resolve the body via the attachment hierarchy.
UAGX_RigidBodyComponent* BodyComponent = GetRigidBody();
if (BodyComponent == nullptr)
{
UE_LOG(
LogAGX, Error,
TEXT("UAGX_WireLinkComponent '%s' in '%s': Cannot create native — this component "
"must be attached as a child of the UAGX_RigidBodyComponent it wraps."),
*GetName(), *GetLabelSafe(GetOwner()));
return;
}

FRigidBodyBarrier* BodyBarrier = BodyComponent->GetOrCreateNative();
if (BodyBarrier == nullptr || !BodyBarrier->HasNative())
{
UE_LOG(
LogAGX, Error,
TEXT("UAGX_WireLinkComponent '%s' in '%s': Cannot create native — the attached "
"body '%s' does not have a native AGX rigid body."),
*GetName(), *GetLabelSafe(GetOwner()), *BodyComponent->GetName());
return;
}

NativeBarrier.AllocateNative(*BodyBarrier);

if (!HasNative())
{
UE_LOG(
LogAGX, Error,
TEXT("UAGX_WireLinkComponent '%s' in '%s': FWireLinkBarrier::AllocateNative "
"succeeded but HasNative() is still false. The AgX-WireLink license module "
"may be missing from the active license."),
*GetName(), *GetLabelSafe(GetOwner()));
}
}
Loading