@@ -386,41 +386,23 @@ export const useStraightPathEdge = ({
386386 } ) ,
387387 [ adjustedTargetCoordinates . targetX , adjustedTargetCoordinates . targetY ]
388388 )
389- // The authored route is source → interior waypoints → target. Automatic
390- // straight-edge routing is deliberately a separate concern: this branch
391- // renders only user intent and the endpoint preview already present on main.
389+ // The synchronous truth: source → interior waypoints → target. Used as the
390+ // fallback before the solver's route lands and whenever the solver route is
391+ // stale (its endpoints no longer match, e.g. mid node-move) so the edge never
392+ // detaches from its nodes.
392393 const basePoints = useMemo < IPoint [ ] > (
393394 ( ) => [ sourceEndpoint , ...interiorPoints , targetEndpoint ] ,
394395 [ sourceEndpoint , interiorPoints , targetEndpoint ]
395396 )
396- const centralPreviewMatchesCommit =
397- dragPreviewPoints !== null &&
398- endpointPreviewCommit !== null &&
399- centralRoute !== undefined &&
400- centralRoute . length >= 2 &&
401- ( endpointPreviewCommit . endpoint === "source"
402- ? centralRoute [ 0 ] . x === endpointPreviewCommit . sourceEndpoint . x &&
403- centralRoute [ 0 ] . y === endpointPreviewCommit . sourceEndpoint . y
404- : centralRoute [ centralRoute . length - 1 ] . x ===
405- endpointPreviewCommit . targetEndpoint . x &&
406- centralRoute [ centralRoute . length - 1 ] . y ===
407- endpointPreviewCommit . targetEndpoint . y )
397+ // The solver's committed route is the source of truth: its endpoints are the
398+ // facing-side attachment sites the port assignment chose (not the drawn handle),
399+ // and it carries any automatic obstacle-avoidance bends. Fall back to the analytic
400+ // base polyline only before the first solve lands (never painted after that).
408401 const renderPoints = useMemo < IPoint [ ] > (
409402 ( ) =>
410- centralPreviewMatchesCommit
411- ? [
412- centralRoute [ 0 ] ,
413- ...interiorPoints ,
414- centralRoute [ centralRoute . length - 1 ] ,
415- ]
416- : ( dragPreviewPoints ?? basePoints ) ,
417- [
418- basePoints ,
419- centralPreviewMatchesCommit ,
420- centralRoute ,
421- dragPreviewPoints ,
422- interiorPoints ,
423- ]
403+ dragPreviewPoints ??
404+ ( centralRoute && centralRoute . length >= 2 ? centralRoute : basePoints ) ,
405+ [ basePoints , centralRoute , dragPreviewPoints ]
424406 )
425407 const renderSourcePosition =
426408 dragPreviewPositions ?. sourcePosition ?? resolvedSourcePosition
@@ -505,8 +487,10 @@ export const useStraightPathEdge = ({
505487
506488 const sourcePoint = renderPoints [ 0 ]
507489 const targetPoint = renderPoints [ renderPoints . length - 1 ]
508- // Every authored bend is editable. Automatic detours are introduced by the
509- // separate auto-layout/routing change and can later reuse the same controls.
490+ // Every bend on the RENDERED route is editable, not only the authored ones. An
491+ // automatic detour is a perfectly good starting point for a hand-placed route:
492+ // dragging one of its bends is how the user takes ownership of it, exactly as
493+ // dragging a computed step edge freezes its path into manual points.
510494 const editableWaypoints = useMemo < IPoint [ ] > (
511495 ( ) => ( dragHandleRoute ?? renderPoints ) . slice ( 1 , - 1 ) ,
512496 [ dragHandleRoute , renderPoints ]
@@ -777,9 +761,11 @@ export const useStraightPathEdge = ({
777761 ]
778762 )
779763
780- // Persist the interior waypoints and pin their visible endpoints when the first
781- // bend is authored. This mirrors the step-edge bend-commit behaviour and keeps
782- // later geometry updates from moving a hand-shaped route at its ends.
764+ // Persist the interior waypoints. A bend customises the whole visible route,
765+ // including the facing-side attachment sites the port assignment chose, so on the
766+ // first bend the endpoints are pinned to `pinSource`/`pinTarget` (when still
767+ // automatic) — otherwise a newly-bent edge would snap its endpoints back to the
768+ // drawn handle. Mirrors the step-edge bend-commit behaviour.
783769 const commitWaypoints = useCallback (
784770 ( nextInterior : IPoint [ ] , pinSource : IPoint , pinTarget : IPoint ) => {
785771 setEdges ( ( edges ) =>
@@ -825,7 +811,8 @@ export const useStraightPathEdge = ({
825811
826812 // Shared drag routine for both an existing waypoint and a freshly materialised
827813 // one. `startInterior` is the interior array the drag operates on; `index` is the
828- // waypoint being moved. Only pointer-up commits `data.points`.
814+ // waypoint being moved. The live route is published so neighbouring step edges
815+ // reflow around the dragged diagonal, and only pointer-up commits `data.points`.
829816 const beginWaypointDrag = useCallback (
830817 (
831818 pointerId : number ,
@@ -839,8 +826,10 @@ export const useStraightPathEdge = ({
839826 dragInteriorRef . current = startInterior
840827 dragMovedRef . current = false
841828 dragCollapseRef . current = false
842- // Capture the endpoints at gesture start so the preview and eventual commit
843- // pivot around stable attachment sites.
829+ // The endpoints the drag pivots around are the CURRENTLY RENDERED attachment
830+ // sites (the solver's facing-side ports), captured at gesture start — not the
831+ // drawn handle — so the route and the pinned commit keep the edge attached
832+ // exactly where it is on screen.
844833 const routeSource = sourcePoint
845834 const routeTarget = targetPoint
846835 const collapseTolerance =
@@ -851,7 +840,8 @@ export const useStraightPathEdge = ({
851840 const angleReference = routeAtStart [ index + 2 ] ?? routeAtStart [ index ]
852841
853842 // Drive the preview through state; the existing layout effect republishes it
854- // to shared edge geometry and clears it when the drag ends.
843+ // as an authoritative live override so neighbouring step edges reflow around
844+ // the dragged diagonal, and clears it when the drag ends.
855845 const publish = (
856846 pathInterior : IPoint [ ] ,
857847 handleInterior : IPoint [ ] = pathInterior
0 commit comments