From 374e041321fd7eee15cb3f7eb78d53e57ba8b226 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Wed, 1 Jul 2026 14:26:14 +0800 Subject: [PATCH] fix(editor): delete graph edges via right-click context menu Remove accidental edge deletion on click and document the new graph link workflow in help and docs. Co-authored-by: Cursor --- app/renderer/editor/graph/SceneGraph.tsx | 71 +++++++++++++++++++--- app/renderer/editor/i18n/editorMessages.ts | 4 +- docs/graph-editing.md | 9 +++ 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 docs/graph-editing.md diff --git a/app/renderer/editor/graph/SceneGraph.tsx b/app/renderer/editor/graph/SceneGraph.tsx index b75c621..b164642 100644 --- a/app/renderer/editor/graph/SceneGraph.tsx +++ b/app/renderer/editor/graph/SceneGraph.tsx @@ -364,15 +364,19 @@ function SceneGraphCanvas({ const ui = graphUi ?? DEFAULT_SCENE_GRAPH_UI; const { screenToFlowPosition } = useReactFlow(); const [menu, setMenu] = useState<{ x: number; y: number; graphNodeId: GraphNodeId } | null>(null); + const [edgeMenu, setEdgeMenu] = useState<{ x: number; y: number; edgeId: string } | null>(null); useEffect(() => { - if (!menu) return; + if (!menu && !edgeMenu) return; const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') setMenu(null); + if (e.key === 'Escape') { + setMenu(null); + setEdgeMenu(null); + } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); - }, [menu]); + }, [edgeMenu, menu]); const menuNodeIsStart = useMemo(() => { if (!menu) return false; @@ -439,6 +443,7 @@ function SceneGraphCanvas({ target: e.targetGraphNodeId, type: 'smoothstep', animated: false, + selectable: false, })); }, [currentSceneId, sceneGraphEdges, sceneGraphNodes]); @@ -491,6 +496,16 @@ function SceneGraphCanvas({ return { x, y }; }, [menu]); + const edgeMenuPosition = useMemo(() => { + if (!edgeMenu) return null; + const pad = 8; + const mw = 200; + const mh = 48; + const x = Math.max(pad, Math.min(edgeMenu.x, window.innerWidth - mw - pad)); + const y = Math.max(pad, Math.min(edgeMenu.y, window.innerHeight - mh - pad)); + return { x, y }; + }, [edgeMenu]); + return (
@@ -505,32 +520,33 @@ function SceneGraphCanvas({ onEdgesChange={onEdgesChange} isValidConnection={isValidConnection} onConnect={onConnectInternal} - onEdgesDelete={(eds) => { - for (const ed of eds) { - onDisconnect(ed.id); - } - }} - onEdgeClick={(_, edge) => { - onDisconnect(edge.id); + onEdgeContextMenu={(e, edge) => { + e.preventDefault(); + setMenu(null); + setEdgeMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id }); }} onNodesDelete={(nds) => { onRemoveGraphNodes(nds.map((n) => n.id as GraphNodeId)); }} onNodeClick={(_, node) => { setMenu(null); + setEdgeMenu(null); const d = node.data as SceneCardData; onCurrentSceneChange(d.sceneId); }} onNodeContextMenu={(e, node) => { e.preventDefault(); + setEdgeMenu(null); setMenu({ x: e.clientX, y: e.clientY, graphNodeId: node.id as GraphNodeId }); }} onPaneClick={() => { setMenu(null); + setEdgeMenu(null); }} onPaneContextMenu={(e) => { e.preventDefault(); setMenu(null); + setEdgeMenu(null); }} onInit={(instance) => { instance.fitView({ padding: 0.25 }); @@ -595,6 +611,41 @@ function SceneGraphCanvas({ document.body, ) : null} + {edgeMenu && edgeMenuPosition + ? createPortal( + <> + +
+ , + document.body, + ) + : null}
); diff --git a/app/renderer/editor/i18n/editorMessages.ts b/app/renderer/editor/i18n/editorMessages.ts index 0694b50..f46754e 100644 --- a/app/renderer/editor/i18n/editorMessages.ts +++ b/app/renderer/editor/i18n/editorMessages.ts @@ -159,7 +159,7 @@ export const EDITOR_MESSAGES: Record> = { 'help.section.graph.title': 'Граф сцен', 'help.section.graph.body': - 'Граф — визуальная карта кампании. Каждый узел на графе — экземпляр сцены; одна и та же сцена может встречаться на графе несколько раз (например, возврат в локацию).\n\nПеретащите сцену из левого списка на свободное место графа — появится карточка-узел. Перетаскивайте узлы, чтобы расставить схему. Соедините два узла: потяните от нижней точки (handle) одного узла к верхней точке другого — появится стрелка перехода. Одна карточка может иметь несколько исходящих связей (ветвление).\n\nНельзя провести вторую связь к тому же целевому узлу с той же карточки. Чтобы удалить связь, выделите стрелку и нажмите Delete или используйте контекстное меню React Flow.\n\nПравый клик по узлу: «Начальная сцена» — с какого узла стартует партия при «Запустить»; «Удалить» — убрать узел с графа (сама сцена в списке останется). На начальной сцене отображается метка «НАЧАЛО».\n\nПанель масштаба внизу графа: увеличение, уменьшение, «Показать всё». Колёсико мыши над графом тоже меняет масштаб.', + 'Граф — визуальная карта кампании. Каждый узел на графе — экземпляр сцены; одна и та же сцена может встречаться на графе несколько раз (например, возврат в локацию).\n\nПеретащите сцену из левого списка на свободное место графа — появится карточка-узел. Перетаскивайте узлы, чтобы расставить схему. Соедините два узла: потяните от нижней точки (handle) одного узла к верхней точке другого — появится стрелка перехода. Одна карточка может иметь несколько исходящих связей (ветвление).\n\nНельзя провести вторую связь к тому же целевому узлу с той же карточки. Обычный клик и двойной клик по линии ничего не делают. Чтобы удалить связь, нажмите по линии правой кнопкой мыши и выберите «Удалить» в контекстном меню.\n\nПравый клик по узлу: «Начальная сцена» — с какого узла стартует партия при «Запустить»; «Удалить» — убрать узел с графа (сама сцена в списке останется). На начальной сцене отображается метка «НАЧАЛО».\n\nПанель масштаба внизу графа: увеличение, уменьшение, «Показать всё». Колёсико мыши над графом тоже меняет масштаб.', 'help.section.sceneProps.title': 'Свойства сцены', 'help.section.sceneProps.body': @@ -476,7 +476,7 @@ export const EDITOR_MESSAGES: Record> = { 'help.section.graph.title': 'Scene graph', 'help.section.graph.body': - 'The graph is a visual map of your campaign. Each node is a scene instance; the same scene can appear on the graph more than once (e.g. returning to a location).\n\nDrag a scene from the left list onto empty graph space to create a node. Drag nodes to arrange the layout. Connect two nodes: pull from the bottom handle of one node to the top handle of another — a transition arrow appears. One card can have several outgoing links (branching).\n\nYou cannot add a second link to the same target from the same source. To remove a link, select the arrow and press Delete or use the flow context menu.\n\nRight-click a node: Start scene — where the party begins when you Run; Delete — remove the node from the graph (the scene stays in the list). The start node shows a START badge.\n\nThe zoom bar at the bottom: zoom in, zoom out, fit view. The mouse wheel over the graph also zooms.', + 'The graph is a visual map of your campaign. Each node is a scene instance; the same scene can appear on the graph more than once (e.g. returning to a location).\n\nDrag a scene from the left list onto empty graph space to create a node. Drag nodes to arrange the layout. Connect two nodes: pull from the bottom handle of one node to the top handle of another — a transition arrow appears. One card can have several outgoing links (branching).\n\nYou cannot add a second link to the same target from the same source. Regular clicks and double-clicks on a line do nothing. To remove a link, right-click the line and choose Delete in the context menu.\n\nRight-click a node: Start scene — where the party begins when you Run; Delete — remove the node from the graph (the scene stays in the list). The start node shows a START badge.\n\nThe zoom bar at the bottom: zoom in, zoom out, fit view. The mouse wheel over the graph also zooms.', 'help.section.sceneProps.title': 'Scene properties', 'help.section.sceneProps.body': diff --git a/docs/graph-editing.md b/docs/graph-editing.md new file mode 100644 index 0000000..f1437d9 --- /dev/null +++ b/docs/graph-editing.md @@ -0,0 +1,9 @@ +# Graph Editing + +## Scene Links + +- Create a link by dragging from the source node handle to the target node handle. +- A regular click or double-click on a link must not change the graph. +- Delete a link only through its context menu: right-click the line and choose **Delete**. +- Delete a node through the node context menu: right-click the node and choose **Delete**. Removing a node does not delete the scene from the scene list. +