Untwist links that cross where they meet at a node - #366
Merged
Conversation
Two links arriving at one node from two different bodies cross whenever the bodies sit in the opposite vertical order to the pins they arrive at: the upper body's link has to dive under the lower body's to reach the lower pin. Nothing in the simulation could see this. Every force acts on one link or one pair of bodies at a time, and each of those two links is individually short, level and well spaced - they are only wrong about each other. Measured over 150 random starting arrangements of a graph the size of a small class, that is what a settled tangle is mostly made of: 5.76 twisted pairs against 5.79 crossings between links sharing a node, a one-to-one match, and nearly three times the 2.09 crossings between links sharing nothing. An untwisting force puts the far ends into their pins' order. Unlike a clearance force it does not decay as it succeeds: it holds full strength until the two ends draw level and switches off only once they have passed, which is what carries a swap through instead of stalling it half-done against the springs. The overlap pass had to give it room. Separating a pair along the very axis it is trying to move on holds it exactly one clearance apart on the wrong side of itself, which is the standoff a backward edge already hit. The two untangles need opposite freedoms, so each now gets the axis it travels on and is separated on the other: a reorder along X is pushed apart on Y, a swap along Y is pushed apart on X, and a pair in the same column - where an X push would move them a whole body width to achieve nothing the swap needs - passes through. A settled graph is still left with no overlaps at all. Over the same 150 starts: | measure | before | after | |--------------------------------------|--------|-------| | crossings, links sharing a node | 5.79 | 4.65 | | crossings, links sharing nothing | 2.09 | 1.04 | | links drawn over an unrelated body | 3.19 | 3.03 | | mean angle off horizontal | 27.7d | 24.8d | | worst body overlap | 0 | 0 | Total crossings fall 28%. The one cost is hidden link length, 228 to 241 pixels: the same number of links are hidden, slightly more of each. Two approaches were measured and rejected on the way. A force pushing a body off a link it is drawn across did nothing a stronger one did not undo, because it decays to zero exactly where the body needs to end up. The same geometry as a positional correction over-constrains a dense graph and thrashes. Exempting an untangling pair from repulsion as well halves the crossings but doubles the hidden length and leaves 58-pixel overlaps. Repulsion_IsWhatSpreadsAGraphOut asserted shape and edge angle, and both stopped meaning what they measured: without repulsion a graph used to collapse into a tall column of near-vertical links, and now collapses into a flat crushed ribbon whose links are flatter than the properly spread graph's. Its claim still holds - the graph is a third of the area and six times as many links are drawn over a body - so it now asserts the room itself, and what the want of it does to the links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
|
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.



What a tangle is actually made of
Two links arriving at one node from two different bodies cross whenever the bodies sit in the opposite vertical order to the pins they arrive at — the upper body's link has to dive under the lower body's to reach the lower pin. In the screenshot that prompted this,
assignandreturnboth feedfunction Add, arriving atBody[0]andBody[1]from bodies stacked the other way round.Nothing in the simulation could see it. Every force acts on one link or one pair of bodies at a time, and each of those two links is individually short, level and well spaced — they are only wrong about each other.
Measured over 150 random starting arrangements of a graph the size of the
Counterdocument:A one-to-one match between twists and shared-node crossings, and nearly three times the crossings between unrelated links. Sibling twist is the tangle.
The force
CalculateLinkUntwistForcesputs the far ends of two links meeting at a node into their pins' order. Unlike a clearance force it does not decay as it succeeds: it holds full strength until the two ends draw level and switches off only once they have passed, which is what carries a swap through instead of stalling it half-done against the springs. Edges are bucketed by the body at each end, so the pass costs the sum of squared degrees rather than the square of the edge count.Links with no pin offsets fall back to their bodies' mid-heights, giving both links at a shared node the same pin height and no order to preserve — so the pass correctly does nothing when there is no pin data to be wrong about.
Letting an untangle move
The overlap pass had to give it room. Separating a pair along the very axis it is trying to move on holds it exactly one clearance apart on the wrong side of itself — the standoff a backward edge already hit. The two untangles need opposite freedoms, so each now gets the axis it travels on and is separated on the other:
A settled graph is still left with no overlaps at all. Without the exemption a twisted pair comes to rest at exactly the clearance — 70px for the test's bodies, to the pixel; with it they settle 17px apart and take the separation on X instead.
Measured effect
Over the same 150 starts:
Total crossings fall 28%. The one cost is hidden link length, 228 → 241px: the same number of links are hidden, slightly more of each.
Rejected on the way
Three approaches were built and measured before this one, and none shipped:
One existing test changed meaning
Repulsion_IsWhatSpreadsAGraphOutasserted shape and edge angle, and both stopped measuring what they claimed. Without repulsion a graph used to collapse into a tall column of near-vertical links; it now collapses into a flat crushed ribbon whose links are flatter than the properly spread graph's. The test's claim still holds — the graph is a third of the area and six times as many links are drawn over a body — so it now asserts the room itself, and what the want of it does to the links.Verification
ForceDirectedLayout.Tests43/43,ImGui.NodeEditor.Tests71/71,ImGuiAppDemo.UITestsClean ImNodes 3/3.LayoutCoreis covered (69 new executable lines, 0 missed) — checked against a cobertura report, not assumed.Not verified visually. This environment is headless and cannot run the app. Every number above comes from the simulation and the test suites.
🤖 Generated with Claude Code
https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
Generated by Claude Code