What is wrong
ForceLayout.SetNodes (ForceDirectedLayout/ForceLayout.cs:138-170) rebuilds the body array and idToIndex. Previously submitted edges are kept, and the method tries to "Drop any edges whose endpoints no longer exist" (line 160), but the check at line 164 only looks at whether each edge's stored index is still in range:
if ((uint)edgeBuf[i].SourceIndex >= (uint)nodes.Length || (uint)edgeBuf[i].TargetIndex >= (uint)nodes.Length)
{
edgeBuf[i].SourceIndex = -1;
edgeBuf[i].TargetIndex = -1;
}
EdgeRef stores only indices, not the body ids given in EdgeInit. As soon as the new body set differs in order or membership, an index points at a different body:
- A valid edge is dropped. Bodies
[1,2,3] with edge 2->3 (indices 1,2); remove the unconnected body 1 with SetNodes([2,3]). Index 2 is now out of range, so the edge is invalidated though both endpoints still exist.
- A phantom edge appears. Bodies
[1,2,3] with edge 1->2 (indices 0,1); remove body 1 with SetNodes([2,3]). Indices 0 and 1 are still in range, so the edge now connects bodies 2 and 3, which were never linked, and its spring pulls them together.
Reordering (SetNodes([3,2,1])) also quietly rewires every edge. This affects the id-based ForceLayout surface and the C ABI (Layout_SetNodes / Layout_SetEdges), which the README presents as bulk submission of POD state keyed by id.
Failure scenario (verified)
Defaults with Enabled = 1.
Case 1: SetNodes([1,2,3]), SetEdges([2->3]), SetNodes([2,3]) → edge becomes src=-1 tgt=-1; the valid edge between 2 and 3 is gone.
Case 2: SetNodes([1,2,3]), SetEdges([1->2]), SetNodes([2,3]) → edge kept as src=0 tgt=1, i.e. ids 2 and 3.
After 600 steps at 1/60 s, distance between bodies 2 and 3 in case 2:
stale=False EdgeCount=0 distance(2,3)=227.7 (fresh layout, no edges)
stale=True EdgeCount=1 distance(2,3)=134.2 (phantom edge pulling 2 and 3 together)
How it was verified
Scratch console program against the HEAD ForceDirectedLayout project; it reads the core's edge buffer by reflection to print SourceIndex/TargetIndex after each SetNodes, then steps the simulation to show the effect on positions.
Suggested fix / acceptance criteria
- Keep the endpoint ids for each submitted edge (e.g. a parallel
(sourceId, targetId) array filled by SetEdges); SetNodes re-resolves them through the new idToIndex and invalidates an edge only when an id is missing.
- Alternatively, clear all edges in
SetNodes and document that SetEdges must follow.
- Tests: removing an unconnected body keeps an edge between the remaining bodies; removing an endpoint invalidates its edge instead of re-targeting it; reordering keeps each edge on the same pair of ids.
What is wrong
ForceLayout.SetNodes(ForceDirectedLayout/ForceLayout.cs:138-170) rebuilds the body array andidToIndex. Previously submitted edges are kept, and the method tries to "Drop any edges whose endpoints no longer exist" (line 160), but the check at line 164 only looks at whether each edge's stored index is still in range:EdgeRefstores only indices, not the body ids given inEdgeInit. As soon as the new body set differs in order or membership, an index points at a different body:[1,2,3]with edge2->3(indices 1,2); remove the unconnected body 1 withSetNodes([2,3]). Index 2 is now out of range, so the edge is invalidated though both endpoints still exist.[1,2,3]with edge1->2(indices 0,1); remove body 1 withSetNodes([2,3]). Indices 0 and 1 are still in range, so the edge now connects bodies 2 and 3, which were never linked, and its spring pulls them together.Reordering (
SetNodes([3,2,1])) also quietly rewires every edge. This affects the id-basedForceLayoutsurface and the C ABI (Layout_SetNodes/Layout_SetEdges), which the README presents as bulk submission of POD state keyed by id.Failure scenario (verified)
Defaults with
Enabled = 1.Case 1:
SetNodes([1,2,3]),SetEdges([2->3]),SetNodes([2,3])→ edge becomessrc=-1 tgt=-1; the valid edge between 2 and 3 is gone.Case 2:
SetNodes([1,2,3]),SetEdges([1->2]),SetNodes([2,3])→ edge kept assrc=0 tgt=1, i.e. ids 2 and 3.After 600 steps at 1/60 s, distance between bodies 2 and 3 in case 2:
How it was verified
Scratch console program against the HEAD
ForceDirectedLayoutproject; it reads the core's edge buffer by reflection to printSourceIndex/TargetIndexafter eachSetNodes, then steps the simulation to show the effect on positions.Suggested fix / acceptance criteria
(sourceId, targetId)array filled bySetEdges);SetNodesre-resolves them through the newidToIndexand invalidates an edge only when an id is missing.SetNodesand document thatSetEdgesmust follow.