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 <cursoragent@cursor.com>
This commit is contained in:
@@ -364,15 +364,19 @@ function SceneGraphCanvas({
|
|||||||
const ui = graphUi ?? DEFAULT_SCENE_GRAPH_UI;
|
const ui = graphUi ?? DEFAULT_SCENE_GRAPH_UI;
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const [menu, setMenu] = useState<{ x: number; y: number; graphNodeId: GraphNodeId } | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!menu) return;
|
if (!menu && !edgeMenu) return;
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') setMenu(null);
|
if (e.key === 'Escape') {
|
||||||
|
setMenu(null);
|
||||||
|
setEdgeMenu(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => window.removeEventListener('keydown', onKey);
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
}, [menu]);
|
}, [edgeMenu, menu]);
|
||||||
|
|
||||||
const menuNodeIsStart = useMemo(() => {
|
const menuNodeIsStart = useMemo(() => {
|
||||||
if (!menu) return false;
|
if (!menu) return false;
|
||||||
@@ -439,6 +443,7 @@ function SceneGraphCanvas({
|
|||||||
target: e.targetGraphNodeId,
|
target: e.targetGraphNodeId,
|
||||||
type: 'smoothstep',
|
type: 'smoothstep',
|
||||||
animated: false,
|
animated: false,
|
||||||
|
selectable: false,
|
||||||
}));
|
}));
|
||||||
}, [currentSceneId, sceneGraphEdges, sceneGraphNodes]);
|
}, [currentSceneId, sceneGraphEdges, sceneGraphNodes]);
|
||||||
|
|
||||||
@@ -491,6 +496,16 @@ function SceneGraphCanvas({
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
}, [menu]);
|
}, [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 (
|
return (
|
||||||
<GraphUiContext.Provider value={ui}>
|
<GraphUiContext.Provider value={ui}>
|
||||||
<div className={styles.canvasWrap}>
|
<div className={styles.canvasWrap}>
|
||||||
@@ -505,32 +520,33 @@ function SceneGraphCanvas({
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
isValidConnection={isValidConnection}
|
isValidConnection={isValidConnection}
|
||||||
onConnect={onConnectInternal}
|
onConnect={onConnectInternal}
|
||||||
onEdgesDelete={(eds) => {
|
onEdgeContextMenu={(e, edge) => {
|
||||||
for (const ed of eds) {
|
e.preventDefault();
|
||||||
onDisconnect(ed.id);
|
setMenu(null);
|
||||||
}
|
setEdgeMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id });
|
||||||
}}
|
|
||||||
onEdgeClick={(_, edge) => {
|
|
||||||
onDisconnect(edge.id);
|
|
||||||
}}
|
}}
|
||||||
onNodesDelete={(nds) => {
|
onNodesDelete={(nds) => {
|
||||||
onRemoveGraphNodes(nds.map((n) => n.id as GraphNodeId));
|
onRemoveGraphNodes(nds.map((n) => n.id as GraphNodeId));
|
||||||
}}
|
}}
|
||||||
onNodeClick={(_, node) => {
|
onNodeClick={(_, node) => {
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
|
setEdgeMenu(null);
|
||||||
const d = node.data as SceneCardData;
|
const d = node.data as SceneCardData;
|
||||||
onCurrentSceneChange(d.sceneId);
|
onCurrentSceneChange(d.sceneId);
|
||||||
}}
|
}}
|
||||||
onNodeContextMenu={(e, node) => {
|
onNodeContextMenu={(e, node) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setEdgeMenu(null);
|
||||||
setMenu({ x: e.clientX, y: e.clientY, graphNodeId: node.id as GraphNodeId });
|
setMenu({ x: e.clientX, y: e.clientY, graphNodeId: node.id as GraphNodeId });
|
||||||
}}
|
}}
|
||||||
onPaneClick={() => {
|
onPaneClick={() => {
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
|
setEdgeMenu(null);
|
||||||
}}
|
}}
|
||||||
onPaneContextMenu={(e) => {
|
onPaneContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
|
setEdgeMenu(null);
|
||||||
}}
|
}}
|
||||||
onInit={(instance) => {
|
onInit={(instance) => {
|
||||||
instance.fitView({ padding: 0.25 });
|
instance.fitView({ padding: 0.25 });
|
||||||
@@ -595,6 +611,41 @@ function SceneGraphCanvas({
|
|||||||
document.body,
|
document.body,
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
{edgeMenu && edgeMenuPosition
|
||||||
|
? createPortal(
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={ui.closeMenu}
|
||||||
|
className={styles.menuBackdrop}
|
||||||
|
onClick={() => setEdgeMenu(null)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
tabIndex={-1}
|
||||||
|
className={styles.ctxMenu}
|
||||||
|
style={{ left: edgeMenuPosition.x, top: edgeMenuPosition.y }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') setEdgeMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className={styles.ctxItemDanger}
|
||||||
|
onClick={() => {
|
||||||
|
onDisconnect(edgeMenu.edgeId);
|
||||||
|
setEdgeMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ui.delete}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</GraphUiContext.Provider>
|
</GraphUiContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
|
|
||||||
'help.section.graph.title': 'Граф сцен',
|
'help.section.graph.title': 'Граф сцен',
|
||||||
'help.section.graph.body':
|
'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.title': 'Свойства сцены',
|
||||||
'help.section.sceneProps.body':
|
'help.section.sceneProps.body':
|
||||||
@@ -476,7 +476,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
|
|
||||||
'help.section.graph.title': 'Scene graph',
|
'help.section.graph.title': 'Scene graph',
|
||||||
'help.section.graph.body':
|
'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.title': 'Scene properties',
|
||||||
'help.section.sceneProps.body':
|
'help.section.sceneProps.body':
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
Reference in New Issue
Block a user