From 0e0108acbf16a899686dd9bd026178fab8742f7f Mon Sep 17 00:00:00 2001 From: Cristian Edwards Date: Sun, 22 Mar 2026 05:55:29 -0700 Subject: [PATCH] feat: add Edit Path button to materialize auto-routed connector bends as editable waypoints When a connector is selected, the new Edit Path button converts its computed route points into real waypoints that can be dragged on the canvas. For straight-line connectors (no intermediate points), a midpoint is added. This allows editing the default path without manually adding additional bends. --- src/features/inspector/InspectorPanel.tsx | 2 ++ src/state/useEditorStore.ts | 27 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/features/inspector/InspectorPanel.tsx b/src/features/inspector/InspectorPanel.tsx index 16e1fd0..796ef63 100644 --- a/src/features/inspector/InspectorPanel.tsx +++ b/src/features/inspector/InspectorPanel.tsx @@ -105,6 +105,7 @@ export default function InspectorPanel() { const renameDocument = useEditorStore((state) => state.renameDocument); const addConnectorWaypoint = useEditorStore((state) => state.addConnectorWaypoint); const removeLastConnectorWaypoint = useEditorStore((state) => state.removeLastConnectorWaypoint); + const materializeConnectorRoute = useEditorStore((state) => state.materializeConnectorRoute); const setNodeArea = useEditorStore((state) => state.setNodeArea); const bringToFront = useEditorStore((state) => state.bringToFront); const sendToBack = useEditorStore((state) => state.sendToBack); @@ -394,6 +395,7 @@ export default function InspectorPanel() { updateConnector(selectedConnector.id, { tunnel: event.target.checked })} />
+
diff --git a/src/state/useEditorStore.ts b/src/state/useEditorStore.ts index 3808e66..acdbd57 100644 --- a/src/state/useEditorStore.ts +++ b/src/state/useEditorStore.ts @@ -5,6 +5,7 @@ import { snapToGrid } from '@/lib/geometry/grid'; import { projectIso } from '@/lib/geometry/iso'; import { resizeRectFromHandle, type ResizeHandle } from '@/lib/geometry/resize'; import { containsArea, containsNode, containsPipe, containsText, type SelectionBounds } from '@/lib/geometry/selection'; +import { buildConnectorPath } from '@/lib/geometry/routing'; import { companionPalette, hexToRgba, palette } from '@/lib/rendering/tokens'; import { cloneDocument, withCommittedHistory, type HistoryState } from '@/state/history'; import type { @@ -85,6 +86,7 @@ interface EditorStore { addConnectorWaypoint: () => void; removeLastConnectorWaypoint: () => void; moveConnectorWaypoint: (index: number, point: Point) => void; + materializeConnectorRoute: () => void; resizeSelection: (handle: ResizeHandle, point: Point) => void; selectWithinRect: (bounds: SelectionBounds, additive?: boolean) => void; selectAtPoint: (point: Point, additive?: boolean) => void; @@ -670,6 +672,31 @@ export const useEditorStore = create((set, get) => ({ return { document }; }); }, + materializeConnectorRoute: () => { + set((state) => { + if (state.selection.type !== 'connector' || state.selection.ids.length !== 1) { + return state; + } + const document = cloneDocument(state.document); + const connector = document.connectors.find((item) => item.id === state.selection.ids[0]); + const source = connector ? document.nodes.find((node) => node.id === connector.sourceId) : null; + const target = connector ? document.nodes.find((node) => node.id === connector.targetId) : null; + if (!connector || !source || !target) { + return state; + } + const path = buildConnectorPath(connector, source, target, document.nodes); + const intermediates = path.slice(1, -1); + if (intermediates.length === 0) { + intermediates.push({ x: (path[0].x + path[path.length - 1].x) / 2, y: (path[0].y + path[path.length - 1].y) / 2 }); + } + connector.waypoints = intermediates; + return { + document, + history: withCommittedHistory(state.history, state.document), + toasts: [...state.toasts, createToast('Route converted to editable waypoints', 'success')], + }; + }); + }, resizeSelection: (handle, point) => { set((state) => { if (state.selection.ids.length !== 1 || !state.selection.type || state.selection.type === 'connector' || state.selection.type === 'text') {