From fcd43e9ad946bb2482c0f16d73e09bfa0e096f8a Mon Sep 17 00:00:00 2001 From: stroblme Date: Sun, 16 Aug 2026 18:47:17 +0200 Subject: [PATCH] Keep an endpoint's measurement so its drag stops relighting the edges Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A --- frontend/src/components/Flow/FlowEditor.tsx | 50 ++++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx index 23136d3..cfe0fef 100644 --- a/frontend/src/components/Flow/FlowEditor.tsx +++ b/frontend/src/components/Flow/FlowEditor.tsx @@ -3,6 +3,7 @@ import { BackgroundVariant, type Connection, type Node as FlowCanvasNode, + type NodeChange, ReactFlow, ReactFlowProvider, useNodesState, @@ -412,6 +413,25 @@ function FlowEditorInner({ () => placementsFor(flowName), ) + // React Flow measures a node once and keeps the size on it. Endpoints are + // rebuilt on every drag frame, so unless the measurement is carried over + // they arrive unmeasured and React Flow drops the edges attached to them + // until it has measured again — remounting those edges, which makes them + // pulse as if a value had just landed. Their own drag lit up the canvas. + const measured = useRef(new Map()) + const trackMeasured = useCallback( + (changes: NodeChange[]) => { + for (const change of changes) { + if (change.type !== "dimensions" || !change.dimensions) continue + if (isEndpointNode({ id: change.id })) { + measured.current.set(change.id, change.dimensions) + } + } + onNodesChange(changes) + }, + [onNodesChange], + ) + /** Where clicking an endpoint takes you: the thing it stands for. */ const openEndpoint = useCallback( (id: string) => { @@ -435,17 +455,23 @@ function FlowEditorInner({ // stored: they join at render, after everything that reads or writes // canvasNodes, so an autosave, an undo or a delete cannot reach them. // biome-ignore lint/correctness/useExhaustiveDependencies: positions change on every drag frame; the key covers the wiring. - const external = useMemo( - () => - deriveEndpoints( - detail.endpoints ?? [], - definitions, - flowName, - new Map(canvasNodes.map((node) => [node.id, node.position])), - moved, - ), - [detail.endpoints, key, flowName, moved], - ) + const external = useMemo(() => { + const built = deriveEndpoints( + detail.endpoints ?? [], + definitions, + flowName, + new Map(canvasNodes.map((node) => [node.id, node.position])), + moved, + ) + return { + ...built, + nodes: built.nodes.map((node) => { + const size = measured.current.get(node.id) + return size ? { ...node, measured: size, ...size } : node + }), + } + // biome-ignore lint/correctness/useExhaustiveDependencies: positions change on every drag frame; the key covers the wiring. + }, [detail.endpoints, key, flowName, moved]) // Edges follow from the name bindings, so they are derived, never stored. // Kept off `external` deliberately: an endpoint's edges depend on which @@ -856,7 +882,7 @@ function FlowEditorInner({ { // An endpoint's position is ours, not React Flow's, so it only // follows the pointer if we move it every frame.