Skip to content

Measure links between their pins rather than between node centres - #364

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/pin-accurate-links
Sep 8, 2026
Merged

Measure links between their pins rather than between node centres#364
matt-edmondson merged 1 commit into
mainfrom
claude/pin-accurate-links

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Problem

Every force that shapes a link worked on body geometry, not on where the link is actually drawn:

Force Measured between
Link spring centre → centre
Levelling (#363) centre Y → centre Y
Bezier clearance (#360) facing edges, at centre heights

EdgeRef carried only SourceIndex, TargetIndex and an unused Anisotropy — 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 newFunction in the editor has five input pins spread over ~130px, so a link into Parameters[0] enters near the node's top. Two consequences:

  • A pair whose centres are perfectly level can still show a visibly steep link — the levelling force was flattening something the user cannot see.
  • The clearance bound was computed from a |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/EdgeInit carry a pin offset per end, relative to that body's origin, plus a HasPinOffsets flag — 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:

  • The spring falls back to centres — what it used before.
  • 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 horizontal 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.

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

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 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.Tests 37/37, ImGui.NodeEditor.Tests 71/71, ForceDirectedLayout.Native and ImGuiAppDemo build 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-lfs is 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

…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
@matt-edmondson
matt-edmondson merged commit a5989ed into main Sep 8, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/pin-accurate-links branch September 8, 2026 10:02
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants