Write connection paths during a drag in the same event that moves the node - #328
Merged
Conversation
… 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>
2 tasks
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
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