Measure links between their pins rather than between node centres - #364
Merged
Conversation
…tres Every force that shapes a link worked on body geometry: the spring pulled centre to centre, the levelling force closed the gap between centre heights, and the bezier clearance measured a drop between centres across the facing edges. A renderer draws a link between pins, and on a node with several rows of pins those differ by most of its height - so a pair whose centres are perfectly level can still show a steep link, and the clearance was computed from a drop the curve is not drawn with. EdgeRef and EdgeInit now carry a pin offset for each end, relative to that body's origin, with a HasPinOffsets flag: zero offsets are a legitimate attachment point, so absence needs a flag rather than a sentinel. The spring and both flattening mechanisms measure between those points. Fallbacks are per-force and chosen so nothing changes for a caller that supplies no pins. The spring falls back to centres, which is what it used. The flattening pass falls back to the pair a node editor implies - source's right edge, target's left edge, each at mid-height - which is what it already assumed; centres there would put both points inside their bodies and overstate the room a curve has. EdgeAccessor takes two optional offset getters, both or neither: one alone would measure from a pin at one end and a centre at the other, which is worse than centres at both. NodeEditorRenderer measures where ImNodes actually put each pin - the middle of its attribute row, on the node's left or right edge - and hands it to the engine, scaled back out of the zoomed view space the same way node dimensions are. It does this during its own render pass, so every consumer gets it without changing a call. A pin nobody has drawn yet falls back to its node's centre. Renderer_MeasuresWhereEachPinSitsOnItsNode drives real ImNodes through the headless harness and checks the measurement rather than trusting it: inputs on the left edge, outputs on the right, pins in the order drawn, three rows spanning more than a few pixels, all inside the node's height. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
|
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
Every force that shapes a link worked on body geometry, not on where the link is actually drawn:
EdgeRefcarried onlySourceIndex,TargetIndexand an unusedAnisotropy— there was no pin information anywhere in the layout.A renderer draws a link between pins, and on a node with several rows of pins those differ by most of its height.
function newFunctionin the editor has five input pins spread over ~130px, so a link intoParameters[0]enters near the node's top. Two consequences:|dy|that isn't the one the curve is drawn with, so it could be too small (curve still hides) or needlessly large.Fix
EdgeRef/EdgeInitcarry a pin offset per end, relative to that body's origin, plus aHasPinOffsetsflag — zero offsets are a legitimate attachment point (a body's top-left), so absence needs a flag rather than a sentinel value. The spring and both flattening mechanisms now measure between those points.Fallbacks are per-force, chosen so nothing changes for a caller supplying no pins:
EdgeAccessortakes two optional offset getters, both or neither: one alone would measure from a pin at one end and a centre at the other, which is worse than centres at both.Ordering stays on body centres — which way round two nodes sit is a property of the bodies, not of where a link happens to attach.
Supplying the offsets
NodeEditorRenderermeasures where ImNodes actually put each pin — the middle of its attribute row, on the node's left or right edge — and hands it to the engine, scaled back out of the zoomed view space the same way node dimensions already are. It does this during its own render pass, so every consumer (including ktsu.Coder) picks it up without changing a call. A pin nobody has drawn yet falls back to its node's centre.Testing
The measurement is the part that could silently be wrong, so it is checked against real ImNodes rather than trusted:
Renderer_MeasuresWhereEachPinSitsOnItsNode— drives ImNodes through the headless harness and asserts inputs land on the left edge, outputs on the right, pins in the order drawn, three rows spanning more than a few pixels, all inside the node's height.PinOffsets_LevelThePinsRatherThanTheBodyCentres— a tall body with a top pin feeding a short one; the pins settle close to level while the centres stay much further apart, which is the reverse of what centre-based levelling produced.PinOffsets_MeasureTheSpringBetweenPins— the spring settles the pins at its rest length.ForceDirectedLayout.Tests37/37,ImGui.NodeEditor.Tests71/71,ForceDirectedLayout.NativeandImGuiAppDemobuild clean in Release. All pre-existing tests pass unchanged, which is the evidence the no-pin fallbacks preserve old behaviour.Not verified visually — this container is headless and the demo UI suite cannot start here (the repo's PNGs are Git LFS pointers,
git-lfsis not installed). The measurement is verified against real ImNodes geometry, but how the layout looks once it is shaping the real curves is worth a glance.🤖 Generated with Claude Code
https://claude.ai/code/session_01D2KNUKr1xJPTEdDmeF2tQN
Generated by Claude Code