Centre a settled graph on the origin, separately from gravity - #383
Merged
Conversation
A settled document sat noticeably off to one side, and gravity turned out to be unable to do anything about it. It pulls every body the same amount whichever side of the target it sits and however far out, so summed over a graph it is a step function of position: it counts bodies rather than measuring them, and anywhere the counts balance it is exactly zero. That is a dead band, not an equilibrium. A twelve-node chain settled 79 units to one side and stayed there; pushed 600 the other way it came to rest 79 units to the other side, the same distance out, on whichever side it happened to arrive from. And where the counts do balance is the median of the body centres, which for a document with a dense cluster of literals on one side and a few large functions on the other is 200 units from the middle of what is actually drawn. Making gravity proportional to distance fixes both and costs something worse. A body further out is then pulled harder, so wide nodes are squeezed closer together than narrow ones and settled spacing depends on node size again - which is what measuring repulsion across clear space rather than between centres was for. Measured, a 400-wide pair settled 160 apart against a 60-wide pair's 224. So placement is separated from cohesion. RecentreOnOrigin slides the whole arrangement, positionally, after integration: every body moves by the same vector, so no distance between any two of them changes, and gravity is left holding the graph together. It is positional rather than a force because as a force MaxForce clamped it per body, and a body already at the ceiling lost its share while its neighbour kept theirs - which reshaped the graph, the one thing this must not do. It stands down when any body is pinned, since whoever pinned it is saying where the graph goes. Mean offset from the origin over the corpus goes from 62.7 units to 5.4, worst from 172 to 19.5, and the corpus score is unchanged at 1.166 against 1.194 - inside the measurement's own deviation. A re-sweep of all fifteen settings afterwards moved none of them, GravityStrength included, so no default changes here. The rate was picked from a sweep rather than assumed: 0.1 leaves a graph a mean 18 units out, 0.5 leaves it 5.4, 1.0 leaves it 2.8, and 2.0 begins to overshoot. It is a constant rather than a setting because it decides only how quickly a graph arrives, never where. FlatteningOnly now also pins OriginAnchorWeight to zero. Left on, the slide moves both bodies of a stacked pair equally and splays neither, which is true and not what those tests are asking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018b2bn5CKSzpjRV4nxv3c3y
|
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.



A settled document sits noticeably off to one side. Gravity turns out to be structurally unable to fix that, and the two obvious repairs both make something else worse — so placement is separated from cohesion instead.
Why gravity cannot do it
Gravity pulls every body the same amount whichever side of the target it sits and however far out. Summed over a graph that is a step function of position: it counts bodies rather than measuring them, so anywhere the counts balance it is exactly zero. That is a dead band, not an equilibrium.
The chain shows it unmistakably — settled, then shoved 600 units each way:
Same distance out, on whichever side it arrived from: both are edges of one dead band. And where the counts do balance is the median of body centres, which for a document with a dense cluster of literals one side and a few large functions the other is nowhere near the middle of what is drawn — on
Counter, median −42 against a drawn centre of +161.Why the obvious fix is worse
Making gravity proportional to distance kills the dead band and improves centring. It also pulls a distant body harder than a near one, so wide nodes get squeezed closer together than narrow ones and settled spacing depends on node size again — exactly what measuring repulsion across clear space rather than between centres was for. Measured: a 400-wide pair settled 160 apart against a 60-wide pair's 224, and
SettledPairs_KeepTheSameClearSpace_WhateverTheirSizefails.What this does instead
LayoutCore.RecentreOnOriginslides the whole arrangement, positionally, after integration. Every body moves by the same vector, so no distance between any two changes; gravity is left doing the one thing it is good at.Score is unchanged (1.166 vs 1.194, inside the measurement's own 0.032 deviation). No defaults change: a re-sweep of all fifteen settings afterwards moved none of them,
GravityStrengthincluded, every candidate gain landing inside the 0.1 floor.Three details that cost a round each to find:
MaxForceclamped it per body — a body already at the ceiling lost its share while its neighbour kept theirs, which reshaped the graph, the one thing this must not do. It failed its own rigidity test that way.OriginAnchorWeight, so a fixture isolating one force sets it to zero.The rate is a constant rather than a setting because it decides only how quickly a graph arrives, never where. Picked from a sweep: 0.1 leaves a mean 18 units out, 0.5 leaves 5.4, 1.0 leaves 2.8, 2.0 begins to overshoot.
Testing
76/76 in
ForceDirectedLayout.Tests, 98/98 inImGui.NodeEditor.Tests.New
RecentringTests, verified to fail without the fix with the diagnostics you would want —Counter settled 161 off the origin horizontally, andit settled 80 from one side and -80 from the other, which names the dead band exactly. The third test asserts the whole simulation is translation-equivariant, which is the property that makes a uniform slide safe; it passes either way by design, guarding the invariant rather than the fix.FlatteningOnlynow also pinsOriginAnchorWeightto zero. Left on, the slide moves both bodies of a stacked pair equally and splays neither — true, and not what those tests are asking.🤖 Generated with Claude Code
https://claude.ai/code/session_018b2bn5CKSzpjRV4nxv3c3y
Generated by Claude Code