Measure repulsion across the clear space between node boxes - #375
Merged
Conversation
Repulsion was measured between body centres, which counts each body's own extent as part of the distance between them. A node pressed against the side of a wide one still had distant centres, so nothing pushed back; two small nodes with a screen of space between them read as crowded. The same setting spaced a graph differently depending only on how big its nodes were drawn, which in a node editor is the difference between a literal and a class. The distance is now the one between the two closest points on the pair's bounding boxes: their gap along each axis they are disjoint on, zero once they overlap on both, floored at MinRepulsionDistance so touching bodies push hard rather than infinitely hard. The direction stays along the line between the centres. Taking it from the closest points as well makes every force between a pair sharing a row exactly horizontal and every force between a pair sharing a column exactly vertical, which leaves repulsion unable to move a body diagonally out of another's way - measured over ten starting arrangements of the Counter graph, that draws around half again as many links across bodies they are not an end of (5.6 against 3.1) and leaves the edges several degrees steeper. Since the distance no longer includes the bodies' own extents it is the smaller number, so RepulsionStrength is recalibrated from 1,200,000 to 600,000, and the demo's two physics presets are halved with it. Over the same ten starts that leaves the settled graph the size it was (area 1,144,300 against 1,143,328) with the same edge angle and the same number of links drawn over bodies, while the tightest pair in it goes from 22 units of clear space to 69, and eight of the ten starts settle where one did before. TwistedLinks_AreNotHeldApartOnTheAxisTheySwapAlong pinned the held pair's resting distance to exactly the overlap clearance. Repulsion is now the binding constraint on that axis, so the test asserts the difference the untwist makes - which is what its name claims - rather than a distance the overlap pass no longer sets alone. Fixes #371 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018b2bn5CKSzpjRV4nxv3c3y
|
This was referenced Sep 9, 2026
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #371.
The problem
CalculateRepulsionForcesmeasured between body centres, which counts each body's own extent as part of the distance between them. A node pressed against the side of a wide one still had distant centres, so nothing pushed back; two small nodes with a screen of empty space between them read as crowded. The sameRepulsionStrengththerefore spaced a graph differently depending only on how big its nodes were drawn — which in a node editor is the difference between a literal and the class it belongs to.The change
The distance is now the one between the two closest points on the pair's bounding boxes: their gap along each axis they are disjoint on, zero once they overlap on both, floored at
MinRepulsionDistanceso touching bodies push hard rather than infinitely hard. It is a newLayoutCore.ClearDistancehelper feeding the existing inverse-square law.The direction stays along the line between the centres. Taking it from the closest points as well makes every force between a pair sharing a row exactly horizontal, and every force between a pair sharing a column exactly vertical, which leaves repulsion unable to move a body diagonally out of another's way. Measured over ten starting arrangements of
ForceLayoutTests.CounterGraph, that draws around half again as many links across bodies they are not an end of (5.6 against 3.1) and leaves the edges several degrees steeper. What the closest points establish is how much room a pair has, not which way they should go.Recalibration
Because the distance no longer includes the bodies' own extents it is the smaller number, so the same spacing needs a smaller strength.
RepulsionStrengthgoes from 1,200,000 to 600,000 in bothLayoutSettings.DefaultsandPhysicsSettings, and the demo's two physics presets are halved with it. A caller carrying a value tuned against the old measurement should expect to do the same — that is called out in the XML docs.Measured over the same ten starts, 6000 steps each:
Spacing and readability are held where they were; the tightest pair in the graph gains three times the room, and the simulation now actually converges.
Tests
New
tests/ForceDirectedLayout.Tests/RepulsionTests.csreads the force directly, one substep in with every other force switched off:One existing test changed
TwistedLinks_AreNotHeldApartOnTheAxisTheySwapAlongpinned the held pair's resting distance to exactly the overlap clearance (70). Repulsion between two bodies stacked in a column is now their vertical gap alone, so it too pushes along that axis and is the binding constraint — the pair rests at 122. The test now asserts the difference the untwist makes (the free pair closes to 95 vertically and takes 347 of separation on X against the held pair's 278), which is what its name claims, rather than a distance the overlap pass no longer sets by itself. The untwisting itself is unaffected:TwistedLinks_SwapTheirFarEndsIntoPinOrderandUntwisting_ReducesCrossingsWithoutLeavingBodiesOverlappingpass unchanged.I considered giving repulsion the untangle-axis exemption
SeparateOverlapshas, and measured it — it made layouts distinctly worse (links over bodies 7.7–14.3, and 5/10 starts settling) without fixing anything that was actually broken, so it is not in this PR.Also updated
SeparateOverlapsandOverlapSeparationTestsremarks, which claimed every force treats a body as a point — no longer true of repulsion, though the pass is still needed for the floor, the springs and coincident bodiesMinRepulsionDistancearound a node's centre; it is now the node's box grown by that distance, which is the region the floor actually covers, and it goes throughEditorToScreenso it is correct under zoomForceDirectedLayout's README and DESCRIPTION, and the repo READMEVerification
ForceDirectedLayout.Tests49/49,ImGui.NodeEditor.Tests71/71,NodeGraph.Tests106/106, full solution builds clean in Release with no warnings.Not verified here:
ImGuiAppDemo.UITestscannot start in this container —examples/ImGuiAppDemo/icon.pngis an unfetched Git LFS pointer and the harness fails decoding it inOnStart. It fails identically 26/26 on an unmodified tree, so it is environmental and pre-existing, but it does mean the physics panel and debug overlay changes are not covered by a run here.🤖 Generated with Claude Code
https://claude.ai/code/session_018b2bn5CKSzpjRV4nxv3c3y
Generated by Claude Code