Add a link-flattening force to keep steep edges' curves visible - #360
Merged
Conversation
Near-vertical and backward edges had their rendered curves swallowed by their own endpoint bodies. Links are drawn in a channel beneath the node backgrounds, so a curve that doubles back over a node disappears. ImNodes renders a link as a cubic bezier whose inner control points are offset horizontally by 0.25 * length from each pin. Writing gap for the clear horizontal span between the source's right edge and the target's left edge, the curve's x-coordinate is monotonic - it never doubles back over either body - exactly when gap >= 0.25 * length. Substituting length = sqrt(gap^2 + dy^2) gives gap >= |dy| / sqrt(15), a cap of about 75.5 degrees off horizontal. CalculateLinkFlatteningForces splays an edge's endpoints apart until that bound is met, and switches off once it is, so it shapes angles rather than stretching the graph. It is separate from DirectionalBias, which enforces a flat 20px ordering floor and does nothing about slope; the two are complementary and both splay along X. Being a soft constraint balanced against the link spring, equilibrium settles just inside the bound rather than exactly on it. Adds LinkFlatteningStrength (0.5, 0 disables) and LinkFlatteningMargin (0.0) to LayoutSettings and its PhysicsSettings mirror, sliders and preset values to the ImNodes demo, and four tests covering the splay, the no-op on already-flat edges, the disable switch and the margin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
The gate failed at 78.4% coverage on new code against a required 80%, and 'Analyze & Release' failed only because of that gate. Examples are not in sonar.coverage.exclusions, so the demo's new slider lines counted as new code and none of them ran. Measured with dotnet-coverage rather than guessed. Eight demo lines were uncovered: the two sliders sit under a collapsing header that starts shut, inside a panel BeginDisabled until physics is on, so neither the reads nor the change branches ever executed. CleanImNodes_LinkFlatteningSlidersRespond enables physics, expands Link Springs, and drags each slider. The first attempt clicked at 0.75 of the item rectangle and did not move either value: a slider's rect spans the track and the label drawn to its right, so that point landed on the text. Dragging across the left 10-45% stays on the track, which is what makes the change branch run. LinkFlattening_DanglingEdge_IsSkipped covers the bounds guard in the new force. An edge naming an absent node resolves to index -1 at both ends, and neither resolvable endpoint may be pushed. Verified per-line against the cobertura report: 29/29 new library lines and 8/8 new demo lines now covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
|
matt-edmondson
deleted the
claude/force-directed-vertical-links-4zjaqb
branch
September 8, 2026 06:03
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.



Problem
Near-vertical and backward edges had their rendered curves swallowed by their own endpoint bodies. ImNodes draws links in a channel beneath the node backgrounds, so a curve that doubles back over a node simply disappears.
Nothing in the simulation was angle-aware.
DirectionalBiaslooked like it might help, but it only enforces a flat minimum horizontal gap ofhalfWidths + 20and switches off past that threshold — it says nothing about slope, so a steep edge clears it easily and still vanishes.The bound
ImNodes renders a link as a cubic bezier whose inner control points are offset horizontally by
0.25 * lengthfrom each pin. Writinggapfor the clear horizontal span between the source's right edge and the target's left edge, the curve's x-coordinate is monotonic — it never doubles back over either body — exactly when:Substituting
length = sqrt(gap^2 + dy^2)and solving gives:That ratio is
LayoutCore.BezierClearanceRatio, derived rather than tuned, with the derivation in its doc comment.Changes
LayoutCore.CalculateLinkFlatteningForces— new force pass, running between the plain link spring and the ordering bias. Splays an edge's endpoints apart along X until the bound is met, then switches off, so it shapes angles rather than stretching the graph. Pins are approximated by the facing edges of the two bodies at their centre heights.LinkFlatteningStrength(default0.5,0disables) andLinkFlatteningMargin(default0.0, extra clearance on top of the derived bound) added toLayoutSettingsand itsPhysicsSettingsmirror, plus both converters.LayoutSettingscrosses the C ABI as a whole struct, so no per-field export changes were needed.This is orthogonal to
DirectionalBias: that one holds a flat 20px ordering floor, this one caps slope. Both splay along X and they compose without fighting.Behavior worth knowing
It is a soft constraint balanced against the link spring, so equilibrium settles just inside the bound rather than exactly on it. For a vertically stacked pair at default settings the residual violation is ~1.8%, i.e. a hair of backward travel — negligible in practice. Raise
LinkFlatteningStrength, or add aLinkFlatteningMargin, to tighten it. The tests assert against 95% of the bound for this reason, and the doc comments say so.Testing
Four new tests in
ForceDirectedLayout.Testscovering the splay on a vertical stack, the no-op on an already-flat edge, the disable switch at zero strength, and the margin holding a flat edge apart.PhysicsSettings_RoundTrips_ThroughLayoutSettingsextended to cover both new fields.ForceDirectedLayout.Tests— 30/30 passImGui.NodeEditor.Tests— 63/63 pass (the force is on by default, so this confirms it doesn't disturb existing expectations)ImGui.NodeEditor,ForceDirectedLayout.NativeandImGuiAppDemoall build clean in ReleaseNote: this container's SDK is 10.0.111 (Roslyn 5.0) while
ktsu.Sdk.Analyzers2.28.0 requires Roslyn 5.9, so every build here fails withCS9057on a clean checkout ofmaintoo. I verified locally with the analyzer stripped via an out-of-tree targets file; nothing in the repo was changed to work around it, and CI's newer SDK is unaffected.🤖 Generated with Claude Code
https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
Generated by Claude Code