Keep bodies off one another in the layout core - #351
Merged
Conversation
Every force in the simulation treats a body as a point: repulsion is measured between centers, the link spring pulls to a fixed rest length, and neither knows how wide a body is. Two bodies can therefore sit at a distance the forces are entirely happy with and still have their rectangles squarely on top of each other, which is what a consumer drawing them sees. Tuning the forces cannot fix it, because the comfortable distance depends on the pair's sizes and the forces do not have them. Two bodies at exactly the same point are worse still: repulsion needs a direction between centers and coincident centers have none, so it skips them and they stay there. LayoutCore now resolves that positionally after integration, the same way ApplyDirectionalConstraints does. For each overlapping pair it pushes along the axis they overlap least on — the shorter push, and the one that leaves the arrangement the forces worked out most nearly as it was — sharing the correction equally so the centroid does not drift, and skipping a body that is pinned or frozen so the movable one takes the whole of it. The whole overlap is resolved rather than a fraction of it, because a fraction loses: between two linked bodies the spring pulls back harder each substep than a fraction of the overlap pushes, and the pair comes to rest still overlapping. The step is capped instead, so a deep overlap slides apart over a few frames rather than snapping. Two settings drive it, on both the POD LayoutSettings and the managed PhysicsSettings, and carried through the C ABI header: OverlapMargin, the clear space kept between rectangles, which at 0 disables the pass; and MaxOverlapCorrection, the per-substep cap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba
|
This was referenced Sep 8, 2026
This was referenced Sep 8, 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.



Every force in the simulation treats a body as a point. Repulsion is measured between centers, the link spring pulls to a fixed rest length, and neither knows how wide a body is. Two bodies can therefore sit at a distance the forces are entirely happy with and still have their rectangles squarely on top of each other — which is what a consumer drawing them sees.
Tuning the forces cannot fix it: the comfortable distance depends on the pair's sizes and the forces do not have them. Two bodies at exactly the same point are worse still — repulsion needs a direction between centers, coincident centers have none, so
CalculateRepulsionForcesskips the pair and they stay where they are for ever.So
LayoutCorenow resolves it positionally after integration, the same wayApplyDirectionalConstraintsalready does.What it does
For each overlapping pair,
SeparateOverlaps:ApplyDirectionalConstraintstreats them;The whole overlap rather than a fraction of it, because a fraction loses: between two linked bodies the spring pulls back harder each substep than a fraction of the overlap pushes, and the pair comes to rest still overlapping — just less. Capping the step instead keeps the correction decisive while still letting a deep overlap slide apart over a few frames rather than snapping.
Settings
Two new fields, on the POD
LayoutSettings, mirrored on the managedPhysicsSettings, and carried through the C ABI header:OverlapMargin20.00disables the pass, for a consumer that arranges its own bodies.MaxOverlapCorrection40.020.0matches the minimum horizontal gapCalculateDirectionalForcesalready assumes, so the library keeps one notion of clearance.This is a behaviour change for existing consumers, deliberately: it is on by default, because overlapping bodies are a defect rather than a preference. Anything that wants the old behaviour sets
OverlapMargin = 0.LayoutSettingsis a blittable POD crossing the C ABI, so the two doubles were appended to the end of the struct andktsu_force_directed_layout.hupdated to match. Native callers must rebuild against the new header — the struct has grown by 16 bytes.Testing
tests/ForceDirectedLayout.Tests— 26 passing (20 before). The newOverlapSeparationTestscover linked bodies wider than the spring's rest length ending up clear, coincident bodies separating at all (the case no force tuning reaches),OverlapMargin = 0leaving them overlapping, a pinned body taking none of the correction, two pinned bodies being left alone, and the per-substep cap holding.ImGuiNodeEditor.Testspasses, and the whole solution builds.Where this came from
ktsu.Coder's graph editor had the same problem and fixed it in its own view layer (ktsu-dev/Coder#17). It belongs here instead, so every consumer of the layout gets it. Once this ships in a package, Coder's localAstGraph.SeparateOverlapscan go.🤖 Generated with Claude Code
https://claude.ai/code/session_01QwzCFb8zhd263sZeRbF7ba
Generated by Claude Code