import { animate, lerp } from '$lib/helpers'; import type { createKeyMap } from '$lib/helpers/createKeyMap'; import { panelState } from '$lib/sidebar/PanelState.svelte'; import FileSaver from 'file-saver'; import type { GraphManager } from './graph-manager.svelte'; import type { GraphState } from './graph-state.svelte'; type Keymap = ReturnType; export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) { keymap.addShortcut({ key: 'l', description: 'Select linked nodes', callback: () => { const activeNode = graph.getNode(graphState.activeNodeId); if (activeNode) { const nodes = graph.getLinkedNodes(activeNode); graphState.selectedNodes.clear(); for (const node of nodes) { graphState.selectedNodes.add(node.id); } } } }); keymap.addShortcut({ key: '?', description: 'Toggle Help', callback: () => { panelState.setActivePanel('shortcuts'); } }); keymap.addShortcut({ key: 'c', ctrl: true, description: 'Copy active nodes', callback: () => graphState.copyNodes() }); keymap.addShortcut({ key: 'v', ctrl: true, description: 'Paste nodes', callback: () => graphState.pasteNodes() }); keymap.addShortcut({ key: 'Escape', description: 'Deselect nodes', callback: () => { graphState.activeNodeId = -1; graphState.clearSelection(); graphState.edgeEndPosition = null; (document.activeElement as HTMLElement)?.blur(); } }); keymap.addShortcut({ key: 'A', shift: true, description: 'Add new Node', callback: () => { graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]]; } }); keymap.addShortcut({ key: '.', description: 'Center camera', callback: () => { if (!graphState.isBodyFocused()) return; const average = [0, 0]; for (const node of graph.nodes.values()) { average[0] += node.position[0]; average[1] += node.position[1]; } average[0] = average[0] ? average[0] / graph.nodes.size : 0; average[1] = average[1] ? average[1] / graph.nodes.size : 0; const camX = graphState.cameraPosition[0]; const camY = graphState.cameraPosition[1]; const camZ = graphState.cameraPosition[2]; const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); animate(500, (a: number) => { graphState.cameraPosition[0] = lerp(camX, average[0], ease(a)); graphState.cameraPosition[1] = lerp(camY, average[1], ease(a)); graphState.cameraPosition[2] = lerp(camZ, 2, ease(a)); if (graphState.mouseDown) return false; }); } }); keymap.addShortcut({ key: 'a', ctrl: true, preventDefault: true, description: 'Select all nodes', callback: () => { if (!graphState.isBodyFocused()) return; for (const node of graph.nodes.keys()) { graphState.selectedNodes.add(node); } } }); keymap.addShortcut({ key: 'z', ctrl: true, description: 'Undo', callback: () => { if (!graphState.isBodyFocused()) return; graph.undo(); for (const node of graph.nodes.values()) { graphState.updateNodePosition(node); } } }); keymap.addShortcut({ key: 'y', ctrl: true, description: 'Redo', callback: () => { graph.redo(); for (const node of graph.nodes.values()) { graphState.updateNodePosition(node); } } }); keymap.addShortcut({ key: 's', ctrl: true, description: 'Save', preventDefault: true, callback: () => { const state = graph.serialize(); const blob = new Blob([JSON.stringify(state)], { type: 'application/json;charset=utf-8' }); FileSaver.saveAs(blob, 'nodarium-graph.json'); } }); keymap.addShortcut({ key: ['Delete', 'Backspace', 'x'], description: 'Delete selected nodes', callback: (event) => { if (!graphState.isBodyFocused()) return; graph.startUndoGroup(); if (graphState.activeNodeId !== -1) { const node = graph.getNode(graphState.activeNodeId); if (node) { graph.removeNode(node, { restoreEdges: event.ctrlKey }); graphState.activeNodeId = -1; } } if (graphState.selectedNodes) { for (const nodeId of graphState.selectedNodes) { const node = graph.getNode(nodeId); if (node) { graph.removeNode(node, { restoreEdges: event.ctrlKey }); } } graphState.clearSelection(); } graph.saveUndoGroup(); } }); keymap.addShortcut({ key: 'f', description: 'Smart Connect Nodes', callback: () => { const nodes = [...graphState.selectedNodes.values()] .map((g) => graph.getNode(g)) .filter((n) => !!n); const edge = graph.smartConnect(nodes[0], nodes[1]); if (!edge) graph.smartConnect(nodes[1], nodes[0]); } }); }