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 { 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 (
|
||||
<GraphUiContext.Provider value={ui}>
|
||||
<div className={styles.canvasWrap}>
|
||||
@@ -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(
|
||||
<>
|
||||
<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>
|
||||
</GraphUiContext.Provider>
|
||||
);
|
||||
|
||||
@@ -159,7 +159,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
|
||||
'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<EditorLocale, Record<string, string>> = {
|
||||
|
||||
'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':
|
||||
|
||||
Reference in New Issue
Block a user