Skip to content

Write connection paths during a drag in the same event that moves the node - #328

Merged
dovvnloading merged 1 commit into
mainfrom
fix/synchronous-edge-repaint
Aug 14, 2026
Merged

Write connection paths during a drag in the same event that moves the node#328
dovvnloading merged 1 commit into
mainfrom
fix/synchronous-edge-repaint

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

Described precisely by the reporter: the node itself tracks the pointer correctly, but the connection's pixels play catch-up. A gap opens between the node's connection dot and the end of its line, grows with drag speed, points opposite the direction of travel, and closes as soon as movement stops.

That is a late repaint, not wrong geometry. The shape the app eventually renders is correct - it just reaches the screen a frame after the node does. The two are not equally cheap to display: a node card's position is a transform on a small element, while a connection is a path inside a canvas-sized drawing surface whose shape must be recomputed and re-rasterized. Nothing forces them into the same frame, so while the pointer is moving, the card arrives at its new position while the line is still drawn for the previous one.

This also explains why the previous attempts missed. Every one of them - reducing re-renders, preserving measurements, suspending culling, correcting positions inside the library's own pipeline, handing node state to the library - changed what is computed or when state updates. None of them changed the fact that the line's repaint is a separate, heavier piece of work that can miss the frame the node made.

Change

Working node editors avoid this by not waiting for a render pass during a drag. Drawflow's pointer handler writes the node's position and then immediately writes the path data of every affected connection, so both are in the DOM before the browser paints that frame.

This change does the same here. When a gesture starts, the connections it will move are resolved once - including those attached to members carried by a group drag. On each frame, once the corrected position is known, their path data is written directly, inside the same pointer event that produced it.

It does not replace React Flow's rendering. The library renders the same edges from its own state immediately afterwards and computes the identical shape, using the same handle-anchoring rules this module reproduces deliberately. The synchronous write only ensures the correct shape is already in the DOM for the frame being painted. Anything unresolvable - a missing path element, handles not yet measured, an edge type this module does not draw - is skipped and left entirely to React Flow, which is the pre-existing behaviour. The plan is rebuilt every gesture, so it can never describe a stale graph.

Test plan

  • Full frontend check passes: schema, types, lint (0 errors), 1931/1931 tests, production build, bundle size
  • On the affected machine: drag a connected node slowly and at speed, and confirm the line stays attached to the connection dot

… node

Reported symptom, described precisely by the reporter: the node itself
tracks the pointer correctly, but the connection's pixels play catch-up.
A gap opens between the node's connection dot and the end of its line,
the gap grows with drag speed and points opposite the direction of
travel, and it closes as soon as movement stops. That is a repaint
arriving late, not wrong geometry - the shape the app eventually renders
is correct, it just reaches the screen a frame after the node does.

The cause is that the two are not equally cheap to put on screen. A node
card's position is a transform on a small element. A connection is a path
inside a canvas-sized drawing surface whose shape must be recomputed and
re-rasterized. Nothing forces those to land in the same frame, so under
movement the card arrives at its new position while the line is still
drawn for the previous one.

Working node editors avoid this by not waiting for a render pass at all
during a drag: Drawflow's pointer handler writes the node's position and
then immediately writes the path data of every affected connection, so
both are in the DOM before the browser paints. This change does the same
here. Once a gesture starts, the connections it will move are resolved
once, and on each frame - after the corrected position is known - their
path data is written directly, inside the same pointer event.

This does not replace React Flow's rendering. It renders the same edges
from its own state immediately afterwards and computes the identical
shape, using the same handle-anchoring rules this module reproduces. The
synchronous write only ensures the correct shape is already in the DOM
for the frame currently being painted. Anything unresolvable - a missing
path element, unmeasured handles, an unknown edge type - is skipped and
left entirely to React Flow, and the plan is rebuilt every gesture so it
can never describe a stale graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 2275a79 into main Aug 14, 2026
@dovvnloading
dovvnloading deleted the fix/synchronous-edge-repaint branch August 15, 2026 14:41
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.

1 participant