canvas/dist/hooks/index.mjs
2026-03-11 18:42:08 -07:00

6460 lines
No EOL
187 KiB
JavaScript

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/hooks/useNodeSelection.ts
import { c as _c } from "react/compiler-runtime";
import { useAtom, useSetAtom } from "jotai";
// src/core/selection-store.ts
import { atom } from "jotai";
// src/utils/debug.ts
import debugFactory from "debug";
var NAMESPACE = "canvas";
function createDebug(module) {
const base = debugFactory(`${NAMESPACE}:${module}`);
const warn = debugFactory(`${NAMESPACE}:${module}:warn`);
const error = debugFactory(`${NAMESPACE}:${module}:error`);
warn.enabled = true;
error.enabled = true;
warn.log = console.warn.bind(console);
error.log = console.error.bind(console);
const debugFn = Object.assign(base, {
warn,
error
});
return debugFn;
}
var debug = {
graph: {
node: createDebug("graph:node"),
edge: createDebug("graph:edge"),
sync: createDebug("graph:sync")
},
ui: {
selection: createDebug("ui:selection"),
drag: createDebug("ui:drag"),
resize: createDebug("ui:resize")
},
sync: {
status: createDebug("sync:status"),
mutations: createDebug("sync:mutations"),
queue: createDebug("sync:queue")
},
viewport: createDebug("viewport")
};
// src/core/selection-store.ts
var debug2 = createDebug("selection");
var selectedNodeIdsAtom = atom(/* @__PURE__ */ new Set());
var selectedEdgeIdAtom = atom(null);
var handleNodePointerDownSelectionAtom = atom(null, (get, set, {
nodeId,
isShiftPressed
}) => {
const currentSelection = get(selectedNodeIdsAtom);
debug2("handleNodePointerDownSelection: nodeId=%s, shift=%s, current=%o", nodeId, isShiftPressed, Array.from(currentSelection));
set(selectedEdgeIdAtom, null);
if (isShiftPressed) {
const newSelection = new Set(currentSelection);
if (newSelection.has(nodeId)) {
newSelection.delete(nodeId);
} else {
newSelection.add(nodeId);
}
debug2("Shift-click, setting selection to: %o", Array.from(newSelection));
set(selectedNodeIdsAtom, newSelection);
} else {
if (!currentSelection.has(nodeId)) {
debug2("Node not in selection, selecting: %s", nodeId);
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId]));
} else {
debug2("Node already selected, preserving multi-select");
}
}
});
var selectSingleNodeAtom = atom(null, (get, set, nodeId) => {
debug2("selectSingleNode: %s", nodeId);
set(selectedEdgeIdAtom, null);
if (nodeId === null || nodeId === void 0) {
debug2("Clearing selection");
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
} else {
const currentSelection = get(selectedNodeIdsAtom);
if (currentSelection.has(nodeId) && currentSelection.size === 1) {
return;
}
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId]));
}
});
var toggleNodeInSelectionAtom = atom(null, (get, set, nodeId) => {
const currentSelection = get(selectedNodeIdsAtom);
const newSelection = new Set(currentSelection);
if (newSelection.has(nodeId)) {
newSelection.delete(nodeId);
} else {
newSelection.add(nodeId);
}
set(selectedNodeIdsAtom, newSelection);
});
var clearSelectionAtom = atom(null, (_get, set) => {
debug2("clearSelection");
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
});
var addNodesToSelectionAtom = atom(null, (get, set, nodeIds) => {
const currentSelection = get(selectedNodeIdsAtom);
const newSelection = new Set(currentSelection);
for (const nodeId of nodeIds) {
newSelection.add(nodeId);
}
set(selectedNodeIdsAtom, newSelection);
});
var removeNodesFromSelectionAtom = atom(null, (get, set, nodeIds) => {
const currentSelection = get(selectedNodeIdsAtom);
const newSelection = new Set(currentSelection);
for (const nodeId of nodeIds) {
newSelection.delete(nodeId);
}
set(selectedNodeIdsAtom, newSelection);
});
var selectEdgeAtom = atom(null, (get, set, edgeId) => {
set(selectedEdgeIdAtom, edgeId);
if (edgeId !== null) {
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
}
});
var clearEdgeSelectionAtom = atom(null, (_get, set) => {
set(selectedEdgeIdAtom, null);
});
var focusedNodeIdAtom = atom(null);
var setFocusedNodeAtom = atom(null, (_get, set, nodeId) => {
set(focusedNodeIdAtom, nodeId);
});
var hasFocusedNodeAtom = atom((get) => get(focusedNodeIdAtom) !== null);
var selectedNodesCountAtom = atom((get) => get(selectedNodeIdsAtom).size);
var hasSelectionAtom = atom((get) => get(selectedNodeIdsAtom).size > 0);
// src/hooks/useNodeSelection.ts
function useNodeSelection(nodeId) {
const $ = _c(13);
const [selectedIds] = useAtom(selectedNodeIdsAtom);
const selectSingle = useSetAtom(selectSingleNodeAtom);
const toggleNode = useSetAtom(toggleNodeInSelectionAtom);
let t0;
if ($[0] !== nodeId || $[1] !== selectedIds) {
t0 = selectedIds.has(nodeId);
$[0] = nodeId;
$[1] = selectedIds;
$[2] = t0;
} else {
t0 = $[2];
}
let t1;
if ($[3] !== nodeId || $[4] !== selectSingle) {
t1 = () => selectSingle(nodeId);
$[3] = nodeId;
$[4] = selectSingle;
$[5] = t1;
} else {
t1 = $[5];
}
let t2;
if ($[6] !== nodeId || $[7] !== toggleNode) {
t2 = () => toggleNode(nodeId);
$[6] = nodeId;
$[7] = toggleNode;
$[8] = t2;
} else {
t2 = $[8];
}
let t3;
if ($[9] !== t0 || $[10] !== t1 || $[11] !== t2) {
t3 = {
isSelected: t0,
selectNode: t1,
toggleNode: t2
};
$[9] = t0;
$[10] = t1;
$[11] = t2;
$[12] = t3;
} else {
t3 = $[12];
}
return t3;
}
// src/hooks/useNodeDrag.ts
import { c as _c2 } from "react/compiler-runtime";
import { useAtom as useAtom2, useAtomValue, useSetAtom as useSetAtom2, atom as atom15 } from "jotai";
import { useGesture } from "@use-gesture/react";
import { useRef } from "react";
// src/core/graph-store.ts
import { atom as atom2 } from "jotai";
import Graph from "graphology";
var graphOptions = {
type: "directed",
multi: true,
allowSelfLoops: true
};
var currentGraphIdAtom = atom2(null);
var graphAtom = atom2(new Graph(graphOptions));
var graphUpdateVersionAtom = atom2(0);
var edgeCreationAtom = atom2({
isCreating: false,
sourceNodeId: null,
sourceNodePosition: null,
targetPosition: null,
hoveredTargetNodeId: null,
sourceHandle: null,
targetHandle: null,
sourcePort: null,
targetPort: null,
snappedTargetPosition: null
});
var draggingNodeIdAtom = atom2(null);
var preDragNodeAttributesAtom = atom2(null);
// src/core/graph-position.ts
import { atom as atom4 } from "jotai";
import { atomFamily } from "jotai-family";
import Graph2 from "graphology";
// src/utils/mutation-queue.ts
var pendingNodeMutations = /* @__PURE__ */ new Map();
function getPendingState(nodeId) {
let state = pendingNodeMutations.get(nodeId);
if (!state) {
state = {
inFlight: false,
queuedPosition: null,
queuedUiProperties: null,
graphId: null
};
pendingNodeMutations.set(nodeId, state);
}
return state;
}
function clearAllPendingMutations() {
pendingNodeMutations.clear();
}
// src/core/perf.ts
import { atom as atom3 } from "jotai";
var perfEnabledAtom = atom3(false);
var _enabled = false;
function setPerfEnabled(enabled) {
_enabled = enabled;
}
if (typeof window !== "undefined") {
window.__canvasPerf = setPerfEnabled;
}
function canvasMark(name) {
if (!_enabled) return _noop;
const markName = `canvas:${name}`;
try {
performance.mark(markName);
} catch {
return _noop;
}
return () => {
try {
performance.measure(`canvas:${name}`, markName);
} catch {
}
};
}
function _noop() {
}
// src/core/graph-position.ts
var debug3 = createDebug("graph:position");
var _positionCacheByGraph = /* @__PURE__ */ new WeakMap();
function getPositionCache(graph) {
let cache = _positionCacheByGraph.get(graph);
if (!cache) {
cache = /* @__PURE__ */ new Map();
_positionCacheByGraph.set(graph, cache);
}
return cache;
}
var nodePositionUpdateCounterAtom = atom4(0);
var nodePositionAtomFamily = atomFamily((nodeId) => atom4((get) => {
get(nodePositionUpdateCounterAtom);
const graph = get(graphAtom);
if (!graph.hasNode(nodeId)) {
return {
x: 0,
y: 0
};
}
const x = graph.getNodeAttribute(nodeId, "x");
const y = graph.getNodeAttribute(nodeId, "y");
const cache = getPositionCache(graph);
const prev = cache.get(nodeId);
if (prev && prev.x === x && prev.y === y) {
return prev;
}
const pos = {
x,
y
};
cache.set(nodeId, pos);
return pos;
}));
var updateNodePositionAtom = atom4(null, (get, set, {
nodeId,
position
}) => {
const end = canvasMark("drag-frame");
const graph = get(graphAtom);
if (graph.hasNode(nodeId)) {
debug3("Updating node %s position to %o", nodeId, position);
graph.setNodeAttribute(nodeId, "x", position.x);
graph.setNodeAttribute(nodeId, "y", position.y);
set(nodePositionUpdateCounterAtom, (c) => c + 1);
}
end();
});
var cleanupNodePositionAtom = atom4(null, (get, _set, nodeId) => {
nodePositionAtomFamily.remove(nodeId);
const graph = get(graphAtom);
getPositionCache(graph).delete(nodeId);
debug3("Removed position atom for node: %s", nodeId);
});
var cleanupAllNodePositionsAtom = atom4(null, (get, _set) => {
const graph = get(graphAtom);
const nodeIds = graph.nodes();
nodeIds.forEach((nodeId) => {
nodePositionAtomFamily.remove(nodeId);
});
_positionCacheByGraph.delete(graph);
debug3("Removed %d position atoms", nodeIds.length);
});
var clearGraphOnSwitchAtom = atom4(null, (get, set) => {
debug3("Clearing graph for switch");
set(cleanupAllNodePositionsAtom);
clearAllPendingMutations();
const emptyGraph = new Graph2(graphOptions);
set(graphAtom, emptyGraph);
set(graphUpdateVersionAtom, (v) => v + 1);
});
// src/core/graph-mutations.ts
import { atom as atom12 } from "jotai";
import Graph3 from "graphology";
// src/core/graph-derived.ts
import { atom as atom8 } from "jotai";
import { atomFamily as atomFamily2 } from "jotai-family";
// src/core/viewport-store.ts
import { atom as atom5 } from "jotai";
// src/utils/layout.ts
var FitToBoundsMode = /* @__PURE__ */ (function(FitToBoundsMode2) {
FitToBoundsMode2["Graph"] = "graph";
FitToBoundsMode2["Selection"] = "selection";
return FitToBoundsMode2;
})({});
var calculateBounds = (nodes) => {
if (nodes.length === 0) {
return {
x: 0,
y: 0,
width: 0,
height: 0
};
}
const minX = Math.min(...nodes.map((node) => node.x));
const minY = Math.min(...nodes.map((node) => node.y));
const maxX = Math.max(...nodes.map((node) => node.x + node.width));
const maxY = Math.max(...nodes.map((node) => node.y + node.height));
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
};
function getNodeCenter(node) {
return {
x: node.x + node.width / 2,
y: node.y + node.height / 2
};
}
function checkNodesOverlap(node1, node2) {
const center1 = getNodeCenter(node1);
const center2 = getNodeCenter(node2);
const dx = Math.abs(center1.x - center2.x);
const dy = Math.abs(center1.y - center2.y);
const minDistanceX = (node1.width + node2.width) / 2;
const minDistanceY = (node1.height + node2.height) / 2;
return dx < minDistanceX && dy < minDistanceY;
}
// src/core/viewport-store.ts
var zoomAtom = atom5(1);
var panAtom = atom5({
x: 0,
y: 0
});
var viewportRectAtom = atom5(null);
var screenToWorldAtom = atom5((get) => {
return (screenX, screenY) => {
const pan = get(panAtom);
const zoom = get(zoomAtom);
const rect = get(viewportRectAtom);
if (!rect) {
return {
x: screenX,
y: screenY
};
}
const relativeX = screenX - rect.left;
const relativeY = screenY - rect.top;
return {
x: (relativeX - pan.x) / zoom,
y: (relativeY - pan.y) / zoom
};
};
});
var worldToScreenAtom = atom5((get) => {
return (worldX, worldY) => {
const pan = get(panAtom);
const zoom = get(zoomAtom);
const rect = get(viewportRectAtom);
if (!rect) {
return {
x: worldX,
y: worldY
};
}
return {
x: worldX * zoom + pan.x + rect.left,
y: worldY * zoom + pan.y + rect.top
};
};
});
var setZoomAtom = atom5(null, (get, set, {
zoom,
centerX,
centerY
}) => {
const currentZoom = get(zoomAtom);
const pan = get(panAtom);
const rect = get(viewportRectAtom);
const newZoom = Math.max(0.1, Math.min(5, zoom));
if (centerX !== void 0 && centerY !== void 0 && rect) {
const relativeX = centerX - rect.left;
const relativeY = centerY - rect.top;
const worldX = (relativeX - pan.x) / currentZoom;
const worldY = (relativeY - pan.y) / currentZoom;
const newPanX = relativeX - worldX * newZoom;
const newPanY = relativeY - worldY * newZoom;
set(panAtom, {
x: newPanX,
y: newPanY
});
}
set(zoomAtom, newZoom);
});
var resetViewportAtom = atom5(null, (_get, set) => {
set(zoomAtom, 1);
set(panAtom, {
x: 0,
y: 0
});
});
var fitToBoundsAtom = atom5(null, (get, set, {
mode,
padding = 20
}) => {
const normalizedMode = typeof mode === "string" ? mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection : mode;
const viewportSize = get(viewportRectAtom);
if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return;
get(nodePositionUpdateCounterAtom);
let bounds;
if (normalizedMode === FitToBoundsMode.Graph) {
const graph = get(graphAtom);
const nodes = graph.nodes().map((node) => {
const attrs = graph.getNodeAttributes(node);
return {
x: attrs.x,
y: attrs.y,
width: attrs.width || 500,
height: attrs.height || 500
};
});
bounds = calculateBounds(nodes);
} else {
const selectedIds = get(selectedNodeIdsAtom);
const allNodes = get(uiNodesAtom);
const selectedNodes = allNodes.filter((n) => selectedIds.has(n.id)).map((n) => ({
x: n.position.x,
y: n.position.y,
width: n.width ?? 500,
height: n.height ?? 500
}));
bounds = calculateBounds(selectedNodes);
}
if (bounds.width <= 0 || bounds.height <= 0) return;
const maxHPad = Math.max(0, viewportSize.width / 2 - 1);
const maxVPad = Math.max(0, viewportSize.height / 2 - 1);
const safePadding = Math.max(0, Math.min(padding, maxHPad, maxVPad));
const effW = Math.max(1, viewportSize.width - 2 * safePadding);
const effH = Math.max(1, viewportSize.height - 2 * safePadding);
const scale = Math.min(effW / bounds.width, effH / bounds.height);
if (scale <= 0 || !isFinite(scale)) return;
set(zoomAtom, scale);
const scaledW = bounds.width * scale;
const scaledH = bounds.height * scale;
const startX = safePadding + (effW - scaledW) / 2;
const startY = safePadding + (effH - scaledH) / 2;
set(panAtom, {
x: startX - bounds.x * scale,
y: startY - bounds.y * scale
});
});
var centerOnNodeAtom = atom5(null, (get, set, nodeId) => {
const nodes = get(uiNodesAtom);
const node = nodes.find((n) => n.id === nodeId);
if (!node) return;
const {
x,
y,
width = 200,
height = 100
} = node;
const zoom = get(zoomAtom);
const centerX = x + width / 2;
const centerY = y + height / 2;
const rect = get(viewportRectAtom);
const halfWidth = rect ? rect.width / 2 : 400;
const halfHeight = rect ? rect.height / 2 : 300;
set(panAtom, {
x: halfWidth - centerX * zoom,
y: halfHeight - centerY * zoom
});
});
var ZOOM_TRANSITION_THRESHOLD = 3.5;
var ZOOM_EXIT_THRESHOLD = 2;
var zoomFocusNodeIdAtom = atom5(null);
var zoomTransitionProgressAtom = atom5(0);
var isZoomTransitioningAtom = atom5((get) => {
const progress = get(zoomTransitionProgressAtom);
return progress > 0 && progress < 1;
});
var zoomAnimationTargetAtom = atom5(null);
var animateZoomToNodeAtom = atom5(null, (get, set, {
nodeId,
targetZoom,
duration = 300
}) => {
const nodes = get(uiNodesAtom);
const node = nodes.find((n) => n.id === nodeId);
if (!node) return;
const {
x,
y,
width = 200,
height = 100
} = node;
const centerX = x + width / 2;
const centerY = y + height / 2;
const rect = get(viewportRectAtom);
const halfWidth = rect ? rect.width / 2 : 400;
const halfHeight = rect ? rect.height / 2 : 300;
const finalZoom = targetZoom ?? get(zoomAtom);
const targetPan = {
x: halfWidth - centerX * finalZoom,
y: halfHeight - centerY * finalZoom
};
set(zoomFocusNodeIdAtom, nodeId);
set(zoomAnimationTargetAtom, {
targetZoom: finalZoom,
targetPan,
startZoom: get(zoomAtom),
startPan: {
...get(panAtom)
},
duration,
startTime: performance.now()
});
});
var animateFitToBoundsAtom = atom5(null, (get, set, {
mode,
padding = 20,
duration = 300
}) => {
const viewportSize = get(viewportRectAtom);
if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return;
get(nodePositionUpdateCounterAtom);
let bounds;
if (mode === "graph") {
const graph = get(graphAtom);
const nodes = graph.nodes().map((node) => {
const attrs = graph.getNodeAttributes(node);
return {
x: attrs.x,
y: attrs.y,
width: attrs.width || 500,
height: attrs.height || 500
};
});
bounds = calculateBounds(nodes);
} else {
const selectedIds = get(selectedNodeIdsAtom);
const allNodes = get(uiNodesAtom);
const selectedNodes = allNodes.filter((n) => selectedIds.has(n.id)).map((n) => ({
x: n.position.x,
y: n.position.y,
width: n.width ?? 500,
height: n.height ?? 500
}));
bounds = calculateBounds(selectedNodes);
}
if (bounds.width <= 0 || bounds.height <= 0) return;
const safePadding = Math.max(0, Math.min(padding, viewportSize.width / 2 - 1, viewportSize.height / 2 - 1));
const effW = Math.max(1, viewportSize.width - 2 * safePadding);
const effH = Math.max(1, viewportSize.height - 2 * safePadding);
const scale = Math.min(effW / bounds.width, effH / bounds.height);
if (scale <= 0 || !isFinite(scale)) return;
const scaledW = bounds.width * scale;
const scaledH = bounds.height * scale;
const startX = safePadding + (effW - scaledW) / 2;
const startY = safePadding + (effH - scaledH) / 2;
const targetPan = {
x: startX - bounds.x * scale,
y: startY - bounds.y * scale
};
set(zoomAnimationTargetAtom, {
targetZoom: scale,
targetPan,
startZoom: get(zoomAtom),
startPan: {
...get(panAtom)
},
duration,
startTime: performance.now()
});
});
// src/core/group-store.ts
import { atom as atom7 } from "jotai";
// src/core/history-store.ts
import { atom as atom6 } from "jotai";
// src/core/history-actions.ts
function applyDelta(graph, delta) {
switch (delta.type) {
case "move-node": {
if (!graph.hasNode(delta.nodeId)) return false;
graph.setNodeAttribute(delta.nodeId, "x", delta.to.x);
graph.setNodeAttribute(delta.nodeId, "y", delta.to.y);
return false;
}
case "resize-node": {
if (!graph.hasNode(delta.nodeId)) return false;
graph.setNodeAttribute(delta.nodeId, "width", delta.to.width);
graph.setNodeAttribute(delta.nodeId, "height", delta.to.height);
return false;
}
case "add-node": {
if (graph.hasNode(delta.nodeId)) return false;
graph.addNode(delta.nodeId, delta.attributes);
return true;
}
case "remove-node": {
if (!graph.hasNode(delta.nodeId)) return false;
graph.dropNode(delta.nodeId);
return true;
}
case "add-edge": {
if (graph.hasEdge(delta.edgeId)) return false;
if (!graph.hasNode(delta.source) || !graph.hasNode(delta.target)) return false;
graph.addEdgeWithKey(delta.edgeId, delta.source, delta.target, delta.attributes);
return true;
}
case "remove-edge": {
if (!graph.hasEdge(delta.edgeId)) return false;
graph.dropEdge(delta.edgeId);
return true;
}
case "update-node-attr": {
if (!graph.hasNode(delta.nodeId)) return false;
graph.setNodeAttribute(delta.nodeId, delta.key, delta.to);
return false;
}
case "batch": {
let structuralChange = false;
for (const d of delta.deltas) {
if (applyDelta(graph, d)) structuralChange = true;
}
return structuralChange;
}
case "full-snapshot": {
graph.clear();
for (const node of delta.nodes) {
graph.addNode(node.id, node.attributes);
}
for (const edge of delta.edges) {
if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge.attributes);
}
}
return true;
}
}
}
function invertDelta(delta) {
switch (delta.type) {
case "move-node":
return {
...delta,
from: delta.to,
to: delta.from
};
case "resize-node":
return {
...delta,
from: delta.to,
to: delta.from
};
case "add-node":
return {
type: "remove-node",
nodeId: delta.nodeId,
attributes: delta.attributes,
connectedEdges: []
};
case "remove-node": {
const batch = [{
type: "add-node",
nodeId: delta.nodeId,
attributes: delta.attributes
}, ...delta.connectedEdges.map((e) => ({
type: "add-edge",
edgeId: e.id,
source: e.source,
target: e.target,
attributes: e.attributes
}))];
return batch.length === 1 ? batch[0] : {
type: "batch",
deltas: batch
};
}
case "add-edge":
return {
type: "remove-edge",
edgeId: delta.edgeId,
source: delta.source,
target: delta.target,
attributes: delta.attributes
};
case "remove-edge":
return {
type: "add-edge",
edgeId: delta.edgeId,
source: delta.source,
target: delta.target,
attributes: delta.attributes
};
case "update-node-attr":
return {
...delta,
from: delta.to,
to: delta.from
};
case "batch":
return {
type: "batch",
deltas: delta.deltas.map(invertDelta).reverse()
};
case "full-snapshot":
return delta;
}
}
function createSnapshot(graph, label) {
const nodes = [];
const edges = [];
graph.forEachNode((nodeId, attributes) => {
nodes.push({
id: nodeId,
attributes: {
...attributes
}
});
});
graph.forEachEdge((edgeId, attributes, source, target) => {
edges.push({
id: edgeId,
source,
target,
attributes: {
...attributes
}
});
});
return {
timestamp: Date.now(),
label,
nodes,
edges
};
}
// src/core/history-store.ts
var debug4 = createDebug("history");
var MAX_HISTORY_SIZE = 50;
var historyStateAtom = atom6({
past: [],
future: [],
isApplying: false
});
var canUndoAtom = atom6((get) => {
const history = get(historyStateAtom);
return history.past.length > 0 && !history.isApplying;
});
var canRedoAtom = atom6((get) => {
const history = get(historyStateAtom);
return history.future.length > 0 && !history.isApplying;
});
var undoCountAtom = atom6((get) => get(historyStateAtom).past.length);
var redoCountAtom = atom6((get) => get(historyStateAtom).future.length);
var pushDeltaAtom = atom6(null, (get, set, delta) => {
const history = get(historyStateAtom);
if (history.isApplying) return;
const {
label,
...cleanDelta
} = delta;
const entry = {
forward: cleanDelta,
reverse: invertDelta(cleanDelta),
timestamp: Date.now(),
label
};
const newPast = [...history.past, entry];
if (newPast.length > MAX_HISTORY_SIZE) newPast.shift();
set(historyStateAtom, {
past: newPast,
future: [],
// Clear redo stack
isApplying: false
});
debug4("Pushed delta: %s (past: %d)", label || delta.type, newPast.length);
});
var pushHistoryAtom = atom6(null, (get, set, label) => {
const history = get(historyStateAtom);
if (history.isApplying) return;
const graph = get(graphAtom);
const snapshot = createSnapshot(graph, label);
const forward = {
type: "full-snapshot",
nodes: snapshot.nodes,
edges: snapshot.edges
};
const entry = {
forward,
reverse: forward,
// For full snapshots, reverse IS the current state
timestamp: Date.now(),
label
};
const newPast = [...history.past, entry];
if (newPast.length > MAX_HISTORY_SIZE) newPast.shift();
set(historyStateAtom, {
past: newPast,
future: [],
isApplying: false
});
debug4("Pushed snapshot: %s (past: %d)", label || "unnamed", newPast.length);
});
var undoAtom = atom6(null, (get, set) => {
const history = get(historyStateAtom);
if (history.past.length === 0 || history.isApplying) return false;
set(historyStateAtom, {
...history,
isApplying: true
});
try {
const graph = get(graphAtom);
const newPast = [...history.past];
const entry = newPast.pop();
let forwardForRedo = entry.forward;
if (entry.reverse.type === "full-snapshot") {
const currentSnapshot = createSnapshot(graph, "current");
forwardForRedo = {
type: "full-snapshot",
nodes: currentSnapshot.nodes,
edges: currentSnapshot.edges
};
}
const structuralChange = applyDelta(graph, entry.reverse);
if (structuralChange) {
set(graphAtom, graph);
set(graphUpdateVersionAtom, (v) => v + 1);
}
set(nodePositionUpdateCounterAtom, (c) => c + 1);
const redoEntry = {
forward: forwardForRedo,
reverse: entry.reverse,
timestamp: entry.timestamp,
label: entry.label
};
set(historyStateAtom, {
past: newPast,
future: [redoEntry, ...history.future],
isApplying: false
});
debug4("Undo: %s (past: %d, future: %d)", entry.label, newPast.length, history.future.length + 1);
return true;
} catch (error) {
debug4.error("Undo failed: %O", error);
set(historyStateAtom, {
...history,
isApplying: false
});
return false;
}
});
var redoAtom = atom6(null, (get, set) => {
const history = get(historyStateAtom);
if (history.future.length === 0 || history.isApplying) return false;
set(historyStateAtom, {
...history,
isApplying: true
});
try {
const graph = get(graphAtom);
const newFuture = [...history.future];
const entry = newFuture.shift();
let reverseForUndo = entry.reverse;
if (entry.forward.type === "full-snapshot") {
const currentSnapshot = createSnapshot(graph, "current");
reverseForUndo = {
type: "full-snapshot",
nodes: currentSnapshot.nodes,
edges: currentSnapshot.edges
};
}
const structuralChange = applyDelta(graph, entry.forward);
if (structuralChange) {
set(graphAtom, graph);
set(graphUpdateVersionAtom, (v) => v + 1);
}
set(nodePositionUpdateCounterAtom, (c) => c + 1);
const undoEntry = {
forward: entry.forward,
reverse: reverseForUndo,
timestamp: entry.timestamp,
label: entry.label
};
set(historyStateAtom, {
past: [...history.past, undoEntry],
future: newFuture,
isApplying: false
});
debug4("Redo: %s (past: %d, future: %d)", entry.label, history.past.length + 1, newFuture.length);
return true;
} catch (error) {
debug4.error("Redo failed: %O", error);
set(historyStateAtom, {
...history,
isApplying: false
});
return false;
}
});
var clearHistoryAtom = atom6(null, (_get, set) => {
set(historyStateAtom, {
past: [],
future: [],
isApplying: false
});
debug4("History cleared");
});
var historyLabelsAtom = atom6((get) => {
const history = get(historyStateAtom);
return {
past: history.past.map((e) => e.label || "Unnamed"),
future: history.future.map((e) => e.label || "Unnamed")
};
});
// src/core/group-store.ts
var collapsedGroupsAtom = atom7(/* @__PURE__ */ new Set());
var toggleGroupCollapseAtom = atom7(null, (get, set, groupId) => {
const current = get(collapsedGroupsAtom);
const next = new Set(current);
if (next.has(groupId)) {
next.delete(groupId);
} else {
next.add(groupId);
}
set(collapsedGroupsAtom, next);
});
var collapseGroupAtom = atom7(null, (get, set, groupId) => {
const current = get(collapsedGroupsAtom);
if (!current.has(groupId)) {
const next = new Set(current);
next.add(groupId);
set(collapsedGroupsAtom, next);
}
});
var expandGroupAtom = atom7(null, (get, set, groupId) => {
const current = get(collapsedGroupsAtom);
if (current.has(groupId)) {
const next = new Set(current);
next.delete(groupId);
set(collapsedGroupsAtom, next);
}
});
var nodeChildrenAtom = atom7((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
return (parentId) => {
const children = [];
graph.forEachNode((nodeId, attrs) => {
if (attrs.parentId === parentId) {
children.push(nodeId);
}
});
return children;
};
});
var nodeParentAtom = atom7((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
return (nodeId) => {
if (!graph.hasNode(nodeId)) return void 0;
return graph.getNodeAttribute(nodeId, "parentId");
};
});
var isGroupNodeAtom = atom7((get) => {
const getChildren = get(nodeChildrenAtom);
return (nodeId) => getChildren(nodeId).length > 0;
});
var groupChildCountAtom = atom7((get) => {
const getChildren = get(nodeChildrenAtom);
return (groupId) => getChildren(groupId).length;
});
var setNodeParentAtom = atom7(null, (get, set, {
nodeId,
parentId
}) => {
const graph = get(graphAtom);
if (!graph.hasNode(nodeId)) return;
if (parentId) {
if (parentId === nodeId) return;
let current = parentId;
while (current) {
if (current === nodeId) return;
if (!graph.hasNode(current)) break;
current = graph.getNodeAttribute(current, "parentId");
}
}
graph.setNodeAttribute(nodeId, "parentId", parentId);
set(graphUpdateVersionAtom, (v) => v + 1);
});
var moveNodesToGroupAtom = atom7(null, (get, set, {
nodeIds,
groupId
}) => {
for (const nodeId of nodeIds) {
set(setNodeParentAtom, {
nodeId,
parentId: groupId
});
}
});
var removeFromGroupAtom = atom7(null, (get, set, nodeId) => {
set(setNodeParentAtom, {
nodeId,
parentId: void 0
});
});
var groupSelectedNodesAtom = atom7(null, (get, set, {
nodeIds,
groupNodeId
}) => {
set(pushHistoryAtom, `Group ${nodeIds.length} nodes`);
const graph = get(graphAtom);
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const nodeId of nodeIds) {
if (!graph.hasNode(nodeId)) continue;
const attrs = graph.getNodeAttributes(nodeId);
minX = Math.min(minX, attrs.x);
minY = Math.min(minY, attrs.y);
maxX = Math.max(maxX, attrs.x + (attrs.width || 200));
maxY = Math.max(maxY, attrs.y + (attrs.height || 100));
}
const padding = 20;
if (graph.hasNode(groupNodeId)) {
graph.setNodeAttribute(groupNodeId, "x", minX - padding);
graph.setNodeAttribute(groupNodeId, "y", minY - padding - 30);
graph.setNodeAttribute(groupNodeId, "width", maxX - minX + 2 * padding);
graph.setNodeAttribute(groupNodeId, "height", maxY - minY + 2 * padding + 30);
}
for (const nodeId of nodeIds) {
if (nodeId !== groupNodeId && graph.hasNode(nodeId)) {
graph.setNodeAttribute(nodeId, "parentId", groupNodeId);
}
}
set(graphUpdateVersionAtom, (v) => v + 1);
set(nodePositionUpdateCounterAtom, (c) => c + 1);
});
var ungroupNodesAtom = atom7(null, (get, set, groupId) => {
set(pushHistoryAtom, "Ungroup nodes");
const graph = get(graphAtom);
graph.forEachNode((nodeId, attrs) => {
if (attrs.parentId === groupId) {
graph.setNodeAttribute(nodeId, "parentId", void 0);
}
});
set(graphUpdateVersionAtom, (v) => v + 1);
});
var nestNodesOnDropAtom = atom7(null, (get, set, {
nodeIds,
targetId
}) => {
set(pushHistoryAtom, "Nest nodes");
for (const nodeId of nodeIds) {
if (nodeId === targetId) continue;
set(setNodeParentAtom, {
nodeId,
parentId: targetId
});
}
set(autoResizeGroupAtom, targetId);
});
function getNodeDescendants(graph, groupId) {
const descendants = [];
const stack = [groupId];
while (stack.length > 0) {
const current = stack.pop();
graph.forEachNode((nodeId, attrs) => {
if (attrs.parentId === current) {
descendants.push(nodeId);
stack.push(nodeId);
}
});
}
return descendants;
}
var collapsedEdgeRemapAtom = atom7((get) => {
const collapsed = get(collapsedGroupsAtom);
if (collapsed.size === 0) return /* @__PURE__ */ new Map();
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
const remap = /* @__PURE__ */ new Map();
for (const nodeId of graph.nodes()) {
let current = nodeId;
let outermost = null;
while (true) {
if (!graph.hasNode(current)) break;
const parent = graph.getNodeAttribute(current, "parentId");
if (!parent) break;
if (collapsed.has(parent)) outermost = parent;
current = parent;
}
if (outermost) remap.set(nodeId, outermost);
}
return remap;
});
var autoResizeGroupAtom = atom7(null, (get, set, groupId) => {
const graph = get(graphAtom);
if (!graph.hasNode(groupId)) return;
const children = [];
graph.forEachNode((nodeId, attrs) => {
if (attrs.parentId === groupId) {
children.push(nodeId);
}
});
if (children.length === 0) return;
const padding = 20;
const headerHeight = 30;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const childId of children) {
const attrs = graph.getNodeAttributes(childId);
minX = Math.min(minX, attrs.x);
minY = Math.min(minY, attrs.y);
maxX = Math.max(maxX, attrs.x + (attrs.width || 200));
maxY = Math.max(maxY, attrs.y + (attrs.height || 100));
}
graph.setNodeAttribute(groupId, "x", minX - padding);
graph.setNodeAttribute(groupId, "y", minY - padding - headerHeight);
graph.setNodeAttribute(groupId, "width", maxX - minX + 2 * padding);
graph.setNodeAttribute(groupId, "height", maxY - minY + 2 * padding + headerHeight);
set(nodePositionUpdateCounterAtom, (c) => c + 1);
});
// src/core/graph-derived.ts
var highestZIndexAtom = atom8((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
let maxZ = 0;
graph.forEachNode((_node, attributes) => {
if (attributes.zIndex > maxZ) {
maxZ = attributes.zIndex;
}
});
return maxZ;
});
var _prevUiNodesByGraph = /* @__PURE__ */ new WeakMap();
var uiNodesAtom = atom8((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
const currentDraggingId = get(draggingNodeIdAtom);
const collapsed = get(collapsedGroupsAtom);
const nodes = [];
graph.forEachNode((nodeId, attributes) => {
if (collapsed.size > 0) {
let current = nodeId;
let hidden = false;
while (true) {
if (!graph.hasNode(current)) break;
const pid = graph.getNodeAttributes(current).parentId;
if (!pid) break;
if (collapsed.has(pid)) {
hidden = true;
break;
}
current = pid;
}
if (hidden) return;
}
const position = get(nodePositionAtomFamily(nodeId));
nodes.push({
...attributes,
id: nodeId,
position,
isDragging: nodeId === currentDraggingId
});
});
const prev = _prevUiNodesByGraph.get(graph) ?? [];
if (nodes.length === prev.length && nodes.every((n, i) => n.id === prev[i].id && n.position === prev[i].position && n.isDragging === prev[i].isDragging)) {
return prev;
}
_prevUiNodesByGraph.set(graph, nodes);
return nodes;
});
var nodeKeysAtom = atom8((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
return graph.nodes();
});
var nodeFamilyAtom = atomFamily2((nodeId) => atom8((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
if (!graph.hasNode(nodeId)) {
return null;
}
const attributes = graph.getNodeAttributes(nodeId);
const position = get(nodePositionAtomFamily(nodeId));
const currentDraggingId = get(draggingNodeIdAtom);
return {
...attributes,
id: nodeId,
position,
isDragging: nodeId === currentDraggingId
};
}), (a, b) => a === b);
var edgeKeysAtom = atom8((get) => {
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
return graph.edges();
});
var edgeKeysWithTempEdgeAtom = atom8((get) => {
const keys = get(edgeKeysAtom);
const edgeCreation = get(edgeCreationAtom);
if (edgeCreation.isCreating) {
return [...keys, "temp-creating-edge"];
}
return keys;
});
var _edgeCacheByGraph = /* @__PURE__ */ new WeakMap();
function getEdgeCache(graph) {
let cache = _edgeCacheByGraph.get(graph);
if (!cache) {
cache = /* @__PURE__ */ new Map();
_edgeCacheByGraph.set(graph, cache);
}
return cache;
}
var edgeFamilyAtom = atomFamily2((key) => atom8((get) => {
get(graphUpdateVersionAtom);
if (key === "temp-creating-edge") {
const edgeCreationState = get(edgeCreationAtom);
const graph2 = get(graphAtom);
if (edgeCreationState.isCreating && edgeCreationState.sourceNodeId && edgeCreationState.targetPosition) {
const sourceNodeAttrs = graph2.getNodeAttributes(edgeCreationState.sourceNodeId);
const sourceNodePosition = get(nodePositionAtomFamily(edgeCreationState.sourceNodeId));
const pan = get(panAtom);
const zoom = get(zoomAtom);
const viewportRect = get(viewportRectAtom);
if (sourceNodeAttrs && viewportRect) {
const mouseX = edgeCreationState.targetPosition.x - viewportRect.left;
const mouseY = edgeCreationState.targetPosition.y - viewportRect.top;
const worldTargetX = (mouseX - pan.x) / zoom;
const worldTargetY = (mouseY - pan.y) / zoom;
const tempEdge = {
key: "temp-creating-edge",
sourceId: edgeCreationState.sourceNodeId,
targetId: "temp-cursor",
sourcePosition: sourceNodePosition,
targetPosition: {
x: worldTargetX,
y: worldTargetY
},
sourceNodeSize: sourceNodeAttrs.size,
sourceNodeWidth: sourceNodeAttrs.width,
sourceNodeHeight: sourceNodeAttrs.height,
targetNodeSize: 0,
targetNodeWidth: 0,
targetNodeHeight: 0,
type: "dashed",
color: "#FF9800",
weight: 2,
label: void 0,
dbData: {
id: "temp-creating-edge",
graph_id: get(currentGraphIdAtom) || "",
source_node_id: edgeCreationState.sourceNodeId,
target_node_id: "temp-cursor",
edge_type: "temp",
filter_condition: null,
ui_properties: null,
data: null,
created_at: (/* @__PURE__ */ new Date()).toISOString(),
updated_at: (/* @__PURE__ */ new Date()).toISOString()
}
};
return tempEdge;
}
}
return null;
}
const graph = get(graphAtom);
if (!graph.hasEdge(key)) {
getEdgeCache(graph).delete(key);
return null;
}
const sourceId = graph.source(key);
const targetId = graph.target(key);
const attributes = graph.getEdgeAttributes(key);
const remap = get(collapsedEdgeRemapAtom);
const effectiveSourceId = remap.get(sourceId) ?? sourceId;
const effectiveTargetId = remap.get(targetId) ?? targetId;
if (!graph.hasNode(effectiveSourceId) || !graph.hasNode(effectiveTargetId)) {
getEdgeCache(graph).delete(key);
return null;
}
const sourceAttributes = graph.getNodeAttributes(effectiveSourceId);
const targetAttributes = graph.getNodeAttributes(effectiveTargetId);
const sourcePosition = get(nodePositionAtomFamily(effectiveSourceId));
const targetPosition = get(nodePositionAtomFamily(effectiveTargetId));
if (sourceAttributes && targetAttributes) {
const next = {
...attributes,
key,
sourceId: effectiveSourceId,
targetId: effectiveTargetId,
sourcePosition,
targetPosition,
sourceNodeSize: sourceAttributes.size,
targetNodeSize: targetAttributes.size,
sourceNodeWidth: sourceAttributes.width ?? sourceAttributes.size,
sourceNodeHeight: sourceAttributes.height ?? sourceAttributes.size,
targetNodeWidth: targetAttributes.width ?? targetAttributes.size,
targetNodeHeight: targetAttributes.height ?? targetAttributes.size
};
const edgeCache = getEdgeCache(graph);
const prev = edgeCache.get(key);
if (prev && prev.sourcePosition === next.sourcePosition && prev.targetPosition === next.targetPosition && prev.sourceId === next.sourceId && prev.targetId === next.targetId && prev.type === next.type && prev.color === next.color && prev.weight === next.weight && prev.label === next.label && prev.sourceNodeSize === next.sourceNodeSize && prev.targetNodeSize === next.targetNodeSize && prev.sourceNodeWidth === next.sourceNodeWidth && prev.sourceNodeHeight === next.sourceNodeHeight && prev.targetNodeWidth === next.targetNodeWidth && prev.targetNodeHeight === next.targetNodeHeight) {
return prev;
}
edgeCache.set(key, next);
return next;
}
getEdgeCache(graph).delete(key);
return null;
}), (a, b) => a === b);
// src/core/graph-mutations-edges.ts
import { atom as atom10 } from "jotai";
// src/core/reduced-motion-store.ts
import { atom as atom9 } from "jotai";
var prefersReducedMotionAtom = atom9(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false);
var watchReducedMotionAtom = atom9(null, (_get, set) => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const handler = (e) => {
set(prefersReducedMotionAtom, e.matches);
};
set(prefersReducedMotionAtom, mql.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
});
// src/core/graph-mutations-edges.ts
var debug5 = createDebug("graph:mutations:edges");
var addEdgeToLocalGraphAtom = atom10(null, (get, set, newEdge) => {
const graph = get(graphAtom);
if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) {
const uiProps = newEdge.ui_properties || {};
const attributes = {
type: typeof uiProps.style === "string" ? uiProps.style : "solid",
color: typeof uiProps.color === "string" ? uiProps.color : "#999",
label: newEdge.edge_type ?? void 0,
weight: typeof uiProps.weight === "number" ? uiProps.weight : 1,
dbData: newEdge
};
if (!graph.hasEdge(newEdge.id)) {
try {
debug5("Adding edge %s to local graph", newEdge.id);
graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes);
set(graphAtom, graph.copy());
set(graphUpdateVersionAtom, (v) => v + 1);
} catch (e) {
debug5("Failed to add edge %s: %o", newEdge.id, e);
}
}
}
});
var removeEdgeFromLocalGraphAtom = atom10(null, (get, set, edgeId) => {
const graph = get(graphAtom);
if (graph.hasEdge(edgeId)) {
graph.dropEdge(edgeId);
set(graphAtom, graph.copy());
set(graphUpdateVersionAtom, (v) => v + 1);
}
});
var swapEdgeAtomicAtom = atom10(null, (get, set, {
tempEdgeId,
newEdge
}) => {
const graph = get(graphAtom);
if (graph.hasEdge(tempEdgeId)) {
graph.dropEdge(tempEdgeId);
}
if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) {
const uiProps = newEdge.ui_properties || {};
const attributes = {
type: typeof uiProps.style === "string" ? uiProps.style : "solid",
color: typeof uiProps.color === "string" ? uiProps.color : "#999",
label: newEdge.edge_type ?? void 0,
weight: typeof uiProps.weight === "number" ? uiProps.weight : 1,
dbData: newEdge
};
if (!graph.hasEdge(newEdge.id)) {
try {
debug5("Atomically swapping temp edge %s with real edge %s", tempEdgeId, newEdge.id);
graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes);
} catch (e) {
debug5("Failed to add edge %s: %o", newEdge.id, e);
}
}
}
set(graphAtom, graph.copy());
set(graphUpdateVersionAtom, (v) => v + 1);
});
var departingEdgesAtom = atom10(/* @__PURE__ */ new Map());
var EDGE_ANIMATION_DURATION = 300;
var removeEdgeWithAnimationAtom = atom10(null, (get, set, edgeKey) => {
const edgeState = get(edgeFamilyAtom(edgeKey));
if (edgeState) {
const departing = new Map(get(departingEdgesAtom));
departing.set(edgeKey, edgeState);
set(departingEdgesAtom, departing);
set(removeEdgeFromLocalGraphAtom, edgeKey);
const duration = get(prefersReducedMotionAtom) ? 0 : EDGE_ANIMATION_DURATION;
setTimeout(() => {
const current = new Map(get(departingEdgesAtom));
current.delete(edgeKey);
set(departingEdgesAtom, current);
}, duration);
}
});
var editingEdgeLabelAtom = atom10(null);
var updateEdgeLabelAtom = atom10(null, (get, set, {
edgeKey,
label
}) => {
const graph = get(graphAtom);
if (graph.hasEdge(edgeKey)) {
graph.setEdgeAttribute(edgeKey, "label", label || void 0);
set(graphUpdateVersionAtom, (v) => v + 1);
set(nodePositionUpdateCounterAtom, (c) => c + 1);
}
});
// src/core/graph-mutations-advanced.ts
import { atom as atom11 } from "jotai";
var debug6 = createDebug("graph:mutations:advanced");
var dropTargetNodeIdAtom = atom11(null);
var splitNodeAtom = atom11(null, (get, set, {
nodeId,
position1,
position2
}) => {
const graph = get(graphAtom);
if (!graph.hasNode(nodeId)) return;
const attrs = graph.getNodeAttributes(nodeId);
const graphId = get(currentGraphIdAtom) || attrs.dbData.graph_id;
set(pushHistoryAtom, "Split node");
graph.setNodeAttribute(nodeId, "x", position1.x);
graph.setNodeAttribute(nodeId, "y", position1.y);
const edges = [];
graph.forEachEdge(nodeId, (_key, eAttrs, source, target) => {
edges.push({
source,
target,
attrs: eAttrs
});
});
const cloneId = `split-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const cloneDbNode = {
...attrs.dbData,
id: cloneId,
graph_id: graphId,
ui_properties: {
...attrs.dbData.ui_properties || {},
x: position2.x,
y: position2.y
},
created_at: (/* @__PURE__ */ new Date()).toISOString(),
updated_at: (/* @__PURE__ */ new Date()).toISOString()
};
set(addNodeToLocalGraphAtom, cloneDbNode);
for (const edge of edges) {
const newSource = edge.source === nodeId ? cloneId : edge.source;
const newTarget = edge.target === nodeId ? cloneId : edge.target;
set(addEdgeToLocalGraphAtom, {
...edge.attrs.dbData,
id: `split-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
source_node_id: newSource,
target_node_id: newTarget
});
}
set(graphUpdateVersionAtom, (v) => v + 1);
set(nodePositionUpdateCounterAtom, (c) => c + 1);
debug6("Split node %s \u2192 clone %s", nodeId, cloneId);
});
var mergeNodesAtom = atom11(null, (get, set, {
nodeIds
}) => {
if (nodeIds.length < 2) return;
const graph = get(graphAtom);
const [survivorId, ...doomed] = nodeIds;
if (!graph.hasNode(survivorId)) return;
set(pushHistoryAtom, `Merge ${nodeIds.length} nodes`);
const doomedSet = new Set(doomed);
for (const doomedId of doomed) {
if (!graph.hasNode(doomedId)) continue;
const edges = [];
graph.forEachEdge(doomedId, (_key, eAttrs, source, target) => {
edges.push({
source,
target,
attrs: eAttrs
});
});
for (const edge of edges) {
const newSource = doomedSet.has(edge.source) ? survivorId : edge.source;
const newTarget = doomedSet.has(edge.target) ? survivorId : edge.target;
if (newSource === newTarget) continue;
set(addEdgeToLocalGraphAtom, {
...edge.attrs.dbData,
id: `merge-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
source_node_id: newSource,
target_node_id: newTarget
});
}
set(optimisticDeleteNodeAtom, {
nodeId: doomedId
});
}
set(graphUpdateVersionAtom, (v) => v + 1);
debug6("Merged nodes %o \u2192 survivor %s", nodeIds, survivorId);
});
// src/core/graph-mutations.ts
var debug7 = createDebug("graph:mutations");
var startNodeDragAtom = atom12(null, (get, set, {
nodeId
}) => {
const graph = get(graphAtom);
if (!graph.hasNode(nodeId)) return;
const currentAttributes = graph.getNodeAttributes(nodeId);
set(preDragNodeAttributesAtom, JSON.parse(JSON.stringify(currentAttributes)));
const currentHighestZIndex = get(highestZIndexAtom);
const newZIndex = currentHighestZIndex + 1;
graph.setNodeAttribute(nodeId, "zIndex", newZIndex);
set(draggingNodeIdAtom, nodeId);
});
var endNodeDragAtom = atom12(null, (get, set, _payload) => {
const currentDraggingId = get(draggingNodeIdAtom);
if (currentDraggingId) {
debug7("Node %s drag ended", currentDraggingId);
const graph = get(graphAtom);
if (graph.hasNode(currentDraggingId)) {
const parentId = graph.getNodeAttribute(currentDraggingId, "parentId");
if (parentId) {
set(autoResizeGroupAtom, parentId);
}
}
}
set(draggingNodeIdAtom, null);
set(preDragNodeAttributesAtom, null);
});
var optimisticDeleteNodeAtom = atom12(null, (get, set, {
nodeId
}) => {
const graph = get(graphAtom);
if (graph.hasNode(nodeId)) {
graph.dropNode(nodeId);
set(cleanupNodePositionAtom, nodeId);
set(graphAtom, graph.copy());
debug7("Optimistically deleted node %s", nodeId);
}
});
var optimisticDeleteEdgeAtom = atom12(null, (get, set, {
edgeKey
}) => {
const graph = get(graphAtom);
if (graph.hasEdge(edgeKey)) {
graph.dropEdge(edgeKey);
set(graphAtom, graph.copy());
debug7("Optimistically deleted edge %s", edgeKey);
}
});
var addNodeToLocalGraphAtom = atom12(null, (get, set, newNode) => {
const graph = get(graphAtom);
if (graph.hasNode(newNode.id)) {
debug7("Node %s already exists, skipping", newNode.id);
return;
}
const uiProps = newNode.ui_properties || {};
const attributes = {
x: typeof uiProps.x === "number" ? uiProps.x : Math.random() * 800,
y: typeof uiProps.y === "number" ? uiProps.y : Math.random() * 600,
size: typeof uiProps.size === "number" ? uiProps.size : 15,
width: typeof uiProps.width === "number" ? uiProps.width : 500,
height: typeof uiProps.height === "number" ? uiProps.height : 500,
color: typeof uiProps.color === "string" ? uiProps.color : "#ccc",
label: newNode.label || newNode.node_type || newNode.id,
zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : 0,
dbData: newNode
};
debug7("Adding node %s to local graph at (%d, %d)", newNode.id, attributes.x, attributes.y);
graph.addNode(newNode.id, attributes);
set(graphAtom, graph.copy());
set(graphUpdateVersionAtom, (v) => v + 1);
set(nodePositionUpdateCounterAtom, (c) => c + 1);
});
var loadGraphFromDbAtom = atom12(null, (get, set, fetchedNodes, fetchedEdges) => {
debug7("========== START SYNC ==========");
debug7("Fetched nodes: %d, edges: %d", fetchedNodes.length, fetchedEdges.length);
const currentGraphId = get(currentGraphIdAtom);
if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) {
debug7("Skipping sync - data belongs to different graph");
return;
}
const existingGraph = get(graphAtom);
const isDragging = get(draggingNodeIdAtom) !== null;
if (isDragging) {
debug7("Skipping sync - drag in progress");
return;
}
const existingNodeIds = new Set(existingGraph.nodes());
const fetchedNodeIds = new Set(fetchedNodes.map((n) => n.id));
const hasAnyCommonNodes = Array.from(existingNodeIds).some((id) => fetchedNodeIds.has(id));
let graph;
if (hasAnyCommonNodes && existingNodeIds.size > 0) {
debug7("Merging DB data into existing graph");
graph = existingGraph.copy();
} else {
debug7("Creating fresh graph (graph switch detected)");
graph = new Graph3(graphOptions);
}
const fetchedEdgeIds = new Set(fetchedEdges.map((e) => e.id));
if (hasAnyCommonNodes && existingNodeIds.size > 0) {
graph.forEachNode((nodeId) => {
if (!fetchedNodeIds.has(nodeId)) {
debug7("Removing deleted node: %s", nodeId);
graph.dropNode(nodeId);
nodePositionAtomFamily.remove(nodeId);
}
});
}
fetchedNodes.forEach((node) => {
const uiProps = node.ui_properties || {};
const newX = typeof uiProps.x === "number" ? uiProps.x : Math.random() * 800;
const newY = typeof uiProps.y === "number" ? uiProps.y : Math.random() * 600;
if (graph.hasNode(node.id)) {
const currentAttrs = graph.getNodeAttributes(node.id);
const attributes = {
x: newX,
y: newY,
size: typeof uiProps.size === "number" ? uiProps.size : currentAttrs.size,
width: typeof uiProps.width === "number" ? uiProps.width : currentAttrs.width ?? 500,
height: typeof uiProps.height === "number" ? uiProps.height : currentAttrs.height ?? 500,
color: typeof uiProps.color === "string" ? uiProps.color : currentAttrs.color,
label: node.label || node.node_type || node.id,
zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : currentAttrs.zIndex,
dbData: node
};
graph.replaceNodeAttributes(node.id, attributes);
} else {
const attributes = {
x: newX,
y: newY,
size: typeof uiProps.size === "number" ? uiProps.size : 15,
width: typeof uiProps.width === "number" ? uiProps.width : 500,
height: typeof uiProps.height === "number" ? uiProps.height : 500,
color: typeof uiProps.color === "string" ? uiProps.color : "#ccc",
label: node.label || node.node_type || node.id,
zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : 0,
dbData: node
};
graph.addNode(node.id, attributes);
}
});
graph.forEachEdge((edgeId) => {
if (!fetchedEdgeIds.has(edgeId)) {
debug7("Removing deleted edge: %s", edgeId);
graph.dropEdge(edgeId);
}
});
fetchedEdges.forEach((edge) => {
if (graph.hasNode(edge.source_node_id) && graph.hasNode(edge.target_node_id)) {
const uiProps = edge.ui_properties || {};
const attributes = {
type: typeof uiProps.style === "string" ? uiProps.style : "solid",
color: typeof uiProps.color === "string" ? uiProps.color : "#999",
label: edge.edge_type ?? void 0,
weight: typeof uiProps.weight === "number" ? uiProps.weight : 1,
dbData: edge
};
if (graph.hasEdge(edge.id)) {
graph.replaceEdgeAttributes(edge.id, attributes);
} else {
try {
graph.addEdgeWithKey(edge.id, edge.source_node_id, edge.target_node_id, attributes);
} catch (e) {
debug7("Failed to add edge %s: %o", edge.id, e);
}
}
}
});
set(graphAtom, graph);
set(graphUpdateVersionAtom, (v) => v + 1);
debug7("========== SYNC COMPLETE ==========");
debug7("Final graph: %d nodes, %d edges", graph.order, graph.size);
});
// src/core/sync-store.ts
import { atom as atom13 } from "jotai";
var debug8 = createDebug("sync");
var syncStatusAtom = atom13("synced");
var pendingMutationsCountAtom = atom13(0);
var isOnlineAtom = atom13(typeof navigator !== "undefined" ? navigator.onLine : true);
var lastSyncErrorAtom = atom13(null);
var lastSyncTimeAtom = atom13(Date.now());
var mutationQueueAtom = atom13([]);
var syncStateAtom = atom13((get) => ({
status: get(syncStatusAtom),
pendingMutations: get(pendingMutationsCountAtom),
lastError: get(lastSyncErrorAtom),
lastSyncTime: get(lastSyncTimeAtom),
isOnline: get(isOnlineAtom),
queuedMutations: get(mutationQueueAtom).length
}));
var startMutationAtom = atom13(null, (get, set) => {
const currentCount = get(pendingMutationsCountAtom);
const newCount = currentCount + 1;
set(pendingMutationsCountAtom, newCount);
debug8("Mutation started. Pending count: %d -> %d", currentCount, newCount);
if (newCount > 0 && get(syncStatusAtom) !== "syncing") {
set(syncStatusAtom, "syncing");
debug8("Status -> syncing");
}
});
var completeMutationAtom = atom13(null, (get, set, success = true) => {
const currentCount = get(pendingMutationsCountAtom);
const newCount = Math.max(0, currentCount - 1);
set(pendingMutationsCountAtom, newCount);
debug8("Mutation completed (success: %s). Pending count: %d -> %d", success, currentCount, newCount);
if (success) {
set(lastSyncTimeAtom, Date.now());
if (newCount === 0) {
set(lastSyncErrorAtom, null);
}
}
if (newCount === 0) {
const isOnline = get(isOnlineAtom);
const hasError = get(lastSyncErrorAtom) !== null;
if (hasError) {
set(syncStatusAtom, "error");
debug8("Status -> error");
} else if (!isOnline) {
set(syncStatusAtom, "offline");
debug8("Status -> offline");
} else {
set(syncStatusAtom, "synced");
debug8("Status -> synced");
}
}
});
var trackMutationErrorAtom = atom13(null, (_get, set, error) => {
set(lastSyncErrorAtom, error);
debug8("Mutation failed: %s", error);
});
var setOnlineStatusAtom = atom13(null, (get, set, isOnline) => {
set(isOnlineAtom, isOnline);
const pendingCount = get(pendingMutationsCountAtom);
const hasError = get(lastSyncErrorAtom) !== null;
const queueLength = get(mutationQueueAtom).length;
if (pendingCount === 0) {
if (hasError || queueLength > 0) {
set(syncStatusAtom, "error");
} else {
set(syncStatusAtom, isOnline ? "synced" : "offline");
}
}
});
var queueMutationAtom = atom13(null, (get, set, mutation) => {
const queue = get(mutationQueueAtom);
const newMutation = {
...mutation,
id: crypto.randomUUID(),
timestamp: Date.now(),
retryCount: 0,
maxRetries: mutation.maxRetries ?? 3
};
const newQueue = [...queue, newMutation];
set(mutationQueueAtom, newQueue);
debug8("Queued mutation: %s. Queue size: %d", mutation.type, newQueue.length);
if (get(pendingMutationsCountAtom) === 0) {
set(syncStatusAtom, "error");
}
return newMutation.id;
});
var dequeueMutationAtom = atom13(null, (get, set, mutationId) => {
const queue = get(mutationQueueAtom);
const newQueue = queue.filter((m) => m.id !== mutationId);
set(mutationQueueAtom, newQueue);
debug8("Dequeued mutation: %s. Queue size: %d", mutationId, newQueue.length);
if (newQueue.length === 0 && get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) {
set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline");
}
});
var incrementRetryCountAtom = atom13(null, (get, set, mutationId) => {
const queue = get(mutationQueueAtom);
const newQueue = queue.map((m) => m.id === mutationId ? {
...m,
retryCount: m.retryCount + 1
} : m);
set(mutationQueueAtom, newQueue);
});
var getNextQueuedMutationAtom = atom13((get) => {
const queue = get(mutationQueueAtom);
return queue.find((m) => m.retryCount < m.maxRetries) ?? null;
});
var clearMutationQueueAtom = atom13(null, (get, set) => {
set(mutationQueueAtom, []);
debug8("Cleared mutation queue");
if (get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) {
set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline");
}
});
// src/utils/gesture-configs.ts
var fingerGestureConfig = {
eventOptions: {
passive: false,
capture: false
},
drag: {
pointer: {
touch: true,
keys: false,
capture: false,
buttons: -1
},
filterTaps: true,
tapsThreshold: 10,
// Was 3 — too strict for fingers
threshold: 10
// Was 3 — needs larger dead zone
}
};
var pencilGestureConfig = {
eventOptions: {
passive: false,
capture: false
},
drag: {
pointer: {
touch: true,
keys: false,
capture: false,
buttons: -1
},
filterTaps: true,
tapsThreshold: 3,
threshold: 2
// Very precise — small dead zone
}
};
var mouseGestureConfig = {
eventOptions: {
passive: false,
capture: false
},
drag: {
pointer: {
touch: true,
keys: false,
capture: false,
buttons: -1
},
filterTaps: true,
tapsThreshold: 5,
// Was 3
threshold: 3
}
};
function getNodeGestureConfig(source) {
switch (source) {
case "finger":
return fingerGestureConfig;
case "pencil":
return pencilGestureConfig;
case "mouse":
return mouseGestureConfig;
}
}
// src/core/input-store.ts
import { atom as atom14 } from "jotai";
// src/core/input-classifier.ts
function detectInputCapabilities() {
if (typeof window === "undefined") {
return {
hasTouch: false,
hasStylus: false,
hasMouse: true,
hasCoarsePointer: false
};
}
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
const supportsMatchMedia = typeof window.matchMedia === "function";
const hasCoarsePointer = supportsMatchMedia ? window.matchMedia("(pointer: coarse)").matches : false;
const hasFinePointer = supportsMatchMedia ? window.matchMedia("(pointer: fine)").matches : true;
const hasMouse = hasFinePointer || !hasTouch;
return {
hasTouch,
hasStylus: false,
// Set to true on first pen event
hasMouse,
hasCoarsePointer
};
}
// src/core/input-store.ts
var activePointersAtom = atom14(/* @__PURE__ */ new Map());
var primaryInputSourceAtom = atom14("mouse");
var inputCapabilitiesAtom = atom14(detectInputCapabilities());
var isStylusActiveAtom = atom14((get) => {
const pointers = get(activePointersAtom);
for (const [, pointer] of pointers) {
if (pointer.source === "pencil") return true;
}
return false;
});
var isMultiTouchAtom = atom14((get) => {
const pointers = get(activePointersAtom);
let fingerCount = 0;
for (const [, pointer] of pointers) {
if (pointer.source === "finger") fingerCount++;
}
return fingerCount > 1;
});
var fingerCountAtom = atom14((get) => {
const pointers = get(activePointersAtom);
let count = 0;
for (const [, pointer] of pointers) {
if (pointer.source === "finger") count++;
}
return count;
});
var isTouchDeviceAtom = atom14((get) => {
const caps = get(inputCapabilitiesAtom);
return caps.hasTouch;
});
var pointerDownAtom = atom14(null, (get, set, pointer) => {
const pointers = new Map(get(activePointersAtom));
pointers.set(pointer.pointerId, pointer);
set(activePointersAtom, pointers);
set(primaryInputSourceAtom, pointer.source);
if (pointer.source === "pencil") {
const caps = get(inputCapabilitiesAtom);
if (!caps.hasStylus) {
set(inputCapabilitiesAtom, {
...caps,
hasStylus: true
});
}
}
});
var pointerUpAtom = atom14(null, (get, set, pointerId) => {
const pointers = new Map(get(activePointersAtom));
pointers.delete(pointerId);
set(activePointersAtom, pointers);
});
var clearPointersAtom = atom14(null, (_get, set) => {
set(activePointersAtom, /* @__PURE__ */ new Map());
});
// src/utils/hit-test.ts
var defaultProvider = {
elementFromPoint: (x, y) => document.elementFromPoint(x, y),
elementsFromPoint: (x, y) => document.elementsFromPoint(x, y)
};
var _provider = defaultProvider;
function hitTestNode(screenX, screenY) {
const element = _provider.elementFromPoint(screenX, screenY);
const nodeElement = element?.closest("[data-node-id]") ?? null;
const nodeId = nodeElement?.getAttribute("data-node-id") ?? null;
return {
nodeId,
element: nodeElement
};
}
// src/hooks/useDragStateMachine.ts
function buildDragPositions(graph, selectedNodeIds) {
const positions = /* @__PURE__ */ new Map();
for (const nodeId of selectedNodeIds) {
if (graph.hasNode(nodeId)) {
const attrs = graph.getNodeAttributes(nodeId);
positions.set(nodeId, {
x: attrs.x,
y: attrs.y
});
}
}
const currentKeys = Array.from(positions.keys());
for (const nodeId of currentKeys) {
const descendants = getNodeDescendants(graph, nodeId);
for (const descId of descendants) {
if (!positions.has(descId) && graph.hasNode(descId)) {
const attrs = graph.getNodeAttributes(descId);
positions.set(descId, {
x: attrs.x,
y: attrs.y
});
}
}
}
return positions;
}
function computeDragUpdates(initialPositions, movementX, movementY, zoom, graph) {
const deltaX = movementX / zoom;
const deltaY = movementY / zoom;
const updates = [];
initialPositions.forEach((initialPos, nodeId) => {
if (graph.hasNode(nodeId)) {
updates.push({
id: nodeId,
pos: {
x: initialPos.x + deltaX,
y: initialPos.y + deltaY
}
});
}
});
return updates;
}
function isDragPrevented(target) {
return !!target.closest('[data-no-drag="true"]') || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
}
// src/hooks/useNodeDrag.ts
var debug9 = createDebug("drag");
function useNodeDrag(id, t0) {
const $ = _c2(49);
let t1;
if ($[0] !== t0) {
t1 = t0 === void 0 ? {} : t0;
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
const options = t1;
const {
onPersist,
onPersistError,
heldKeys
} = options;
const graph = useAtomValue(graphAtom);
const [pan, setPan] = useAtom2(panAtom);
const startMutation = useSetAtom2(startMutationAtom);
const completeMutation = useSetAtom2(completeMutationAtom);
const setStartDrag = useSetAtom2(startNodeDragAtom);
const setEndDrag = useSetAtom2(endNodeDragAtom);
const getPreDragAttributes = useAtomValue(preDragNodeAttributesAtom);
const currentZoom = useAtomValue(zoomAtom);
const getSelectedNodeIds = useAtomValue(selectedNodeIdsAtom);
const currentGraphId = useAtomValue(currentGraphIdAtom);
const edgeCreation = useAtomValue(edgeCreationAtom);
const setGraph = useSetAtom2(graphAtom);
useSetAtom2(nodePositionUpdateCounterAtom);
const pushHistory = useSetAtom2(pushHistoryAtom);
const setDropTarget = useSetAtom2(dropTargetNodeIdAtom);
const nestNodesOnDrop = useSetAtom2(nestNodesOnDropAtom);
const inputSource = useAtomValue(primaryInputSourceAtom);
let t2;
if ($[2] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t2 = atom15(null, _temp2);
$[2] = t2;
} else {
t2 = $[2];
}
const batchUpdatePositions = useSetAtom2(t2);
let t3;
if ($[3] !== batchUpdatePositions) {
t3 = (updates_0) => {
batchUpdatePositions(updates_0);
};
$[3] = batchUpdatePositions;
$[4] = t3;
} else {
t3 = $[4];
}
const updateNodePositions = t3;
const gestureInstanceRef = useRef(0);
let t4;
if ($[5] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t4 = {
x: 0,
y: 0
};
$[5] = t4;
} else {
t4 = $[5];
}
const panStartRef = useRef(t4);
const isSpaceHeld = Boolean(heldKeys?.byCode.Space || heldKeys?.byKey[" "] || heldKeys?.byKey.Spacebar);
let t5;
if ($[6] !== isSpaceHeld || $[7] !== pan) {
t5 = (state) => {
if (isDragPrevented(state.event.target)) {
return;
}
gestureInstanceRef.current = gestureInstanceRef.current + 1;
if (isSpaceHeld) {
panStartRef.current = pan;
}
};
$[6] = isSpaceHeld;
$[7] = pan;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== currentZoom || $[10] !== edgeCreation || $[11] !== getSelectedNodeIds || $[12] !== graph || $[13] !== id || $[14] !== isSpaceHeld || $[15] !== pushHistory || $[16] !== setDropTarget || $[17] !== setPan || $[18] !== setStartDrag || $[19] !== startMutation || $[20] !== updateNodePositions) {
t6 = (state_0) => {
if (isDragPrevented(state_0.event.target)) {
return;
}
if (edgeCreation.isCreating) {
return;
}
state_0.event.stopPropagation();
if (isSpaceHeld) {
if (!state_0.tap && state_0.active) {
const [mx, my] = state_0.movement;
setPan({
x: panStartRef.current.x + mx,
y: panStartRef.current.y + my
});
}
return state_0.memo;
}
const {
movement: t72,
first,
active,
down,
pinching,
cancel,
tap
} = state_0;
const [mx_0, my_0] = t72;
let currentMemo = state_0.memo;
if (tap || !active) {
return currentMemo;
}
const currentGestureInstance = gestureInstanceRef.current;
if (first) {
const selectionSize = getSelectedNodeIds.size;
const label = selectionSize > 1 ? `Move ${selectionSize} nodes` : "Move node";
pushHistory(label);
setStartDrag({
nodeId: id
});
const initialPositions = buildDragPositions(graph, getSelectedNodeIds);
initialPositions.forEach(() => startMutation());
currentMemo = {
initialPositions,
gestureInstance: currentGestureInstance
};
}
if (!currentMemo || currentMemo.gestureInstance !== currentGestureInstance || !currentMemo.initialPositions) {
if (cancel && !pinching && !down && !tap && !active) {
cancel();
}
return currentMemo;
}
const updates_1 = computeDragUpdates(currentMemo.initialPositions, mx_0, my_0, currentZoom, graph);
if (updates_1.length > 0) {
updateNodePositions(updates_1);
}
if (state_0.event && "clientX" in state_0.event) {
const {
nodeId: hoveredId
} = hitTestNode(state_0.event.clientX, state_0.event.clientY);
const validTarget = hoveredId && !currentMemo.initialPositions.has(hoveredId) ? hoveredId : null;
setDropTarget(validTarget);
}
return currentMemo;
};
$[9] = currentZoom;
$[10] = edgeCreation;
$[11] = getSelectedNodeIds;
$[12] = graph;
$[13] = id;
$[14] = isSpaceHeld;
$[15] = pushHistory;
$[16] = setDropTarget;
$[17] = setPan;
$[18] = setStartDrag;
$[19] = startMutation;
$[20] = updateNodePositions;
$[21] = t6;
} else {
t6 = $[21];
}
let t7;
if ($[22] !== completeMutation || $[23] !== currentGraphId || $[24] !== currentZoom || $[25] !== edgeCreation || $[26] !== getPreDragAttributes || $[27] !== getSelectedNodeIds || $[28] !== graph || $[29] !== id || $[30] !== isSpaceHeld || $[31] !== nestNodesOnDrop || $[32] !== onPersist || $[33] !== onPersistError || $[34] !== setDropTarget || $[35] !== setEndDrag || $[36] !== setGraph || $[37] !== startMutation || $[38] !== updateNodePositions) {
t7 = (state_1) => {
if (isDragPrevented(state_1.event.target)) {
return;
}
if (edgeCreation.isCreating) {
setEndDrag({
nodeId: id
});
return;
}
if (isSpaceHeld) {
return;
}
state_1.event.stopPropagation();
const memo = state_1.memo;
setDropTarget(null);
if (state_1.event && "clientX" in state_1.event && memo?.initialPositions) {
const {
nodeId: hoveredId_0
} = hitTestNode(state_1.event.clientX, state_1.event.clientY);
if (hoveredId_0 && !memo.initialPositions.has(hoveredId_0)) {
const draggedNodeIds = Array.from(memo.initialPositions.keys()).filter((nid) => getSelectedNodeIds.has(nid));
if (draggedNodeIds.length > 0) {
nestNodesOnDrop({
nodeIds: draggedNodeIds,
targetId: hoveredId_0
});
}
}
}
if (!currentGraphId) {
debug9("Cannot update node position: currentGraphId is not set");
setEndDrag({
nodeId: id
});
return;
}
const nodesToUpdate = memo?.initialPositions ? Array.from(memo.initialPositions.keys()) : [id];
nodesToUpdate.forEach((draggedNodeId) => {
if (!graph.hasNode(draggedNodeId)) {
completeMutation(false);
return;
}
const finalAttrs = graph.getNodeAttributes(draggedNodeId);
const initialPos = memo?.initialPositions.get(draggedNodeId);
if (!initialPos) {
completeMutation(false);
return;
}
const [mx_1, my_1] = state_1.movement;
const deltaX = mx_1 / currentZoom;
const deltaY = my_1 / currentZoom;
const finalPosition = {
x: initialPos.x + deltaX,
y: initialPos.y + deltaY
};
updateNodePositions([{
id: draggedNodeId,
pos: finalPosition
}]);
if (!onPersist) {
completeMutation(true);
setEndDrag({
nodeId: draggedNodeId
});
return;
}
const existingDbUiProps = typeof finalAttrs.dbData.ui_properties === "object" && finalAttrs.dbData.ui_properties !== null && !Array.isArray(finalAttrs.dbData.ui_properties) ? finalAttrs.dbData.ui_properties : {};
const newUiProperties = {
...existingDbUiProps,
x: finalPosition.x,
y: finalPosition.y,
zIndex: finalAttrs.zIndex
};
const pendingState = getPendingState(draggedNodeId);
if (pendingState.inFlight) {
pendingState.queuedPosition = finalPosition;
pendingState.queuedUiProperties = newUiProperties;
pendingState.graphId = currentGraphId;
return;
}
pendingState.inFlight = true;
pendingState.graphId = currentGraphId;
const processQueuedUpdate = async (nodeId) => {
const state_2 = getPendingState(nodeId);
if (state_2 && state_2.queuedPosition && state_2.queuedUiProperties && state_2.graphId) {
const queuedProps = state_2.queuedUiProperties;
const queuedGraphId = state_2.graphId;
state_2.queuedPosition = null;
state_2.queuedUiProperties = null;
startMutation();
;
try {
await onPersist(nodeId, queuedGraphId, queuedProps);
completeMutation(true);
} catch (t82) {
const error = t82;
completeMutation(false);
onPersistError?.(nodeId, error);
}
state_2.inFlight = false;
processQueuedUpdate(nodeId);
} else {
if (state_2) {
state_2.inFlight = false;
}
}
};
onPersist(draggedNodeId, currentGraphId, newUiProperties).then(() => {
completeMutation(true);
processQueuedUpdate(draggedNodeId);
}).catch((error_0) => {
completeMutation(false);
const state_3 = getPendingState(draggedNodeId);
if (state_3) {
state_3.inFlight = false;
}
const preDragAttrsForNode = getPreDragAttributes;
if (preDragAttrsForNode && preDragAttrsForNode.dbData.id === draggedNodeId && graph.hasNode(draggedNodeId)) {
graph.replaceNodeAttributes(draggedNodeId, preDragAttrsForNode);
setGraph(graph);
}
onPersistError?.(draggedNodeId, error_0);
processQueuedUpdate(draggedNodeId);
}).finally(() => {
setEndDrag({
nodeId: draggedNodeId
});
});
});
};
$[22] = completeMutation;
$[23] = currentGraphId;
$[24] = currentZoom;
$[25] = edgeCreation;
$[26] = getPreDragAttributes;
$[27] = getSelectedNodeIds;
$[28] = graph;
$[29] = id;
$[30] = isSpaceHeld;
$[31] = nestNodesOnDrop;
$[32] = onPersist;
$[33] = onPersistError;
$[34] = setDropTarget;
$[35] = setEndDrag;
$[36] = setGraph;
$[37] = startMutation;
$[38] = updateNodePositions;
$[39] = t7;
} else {
t7 = $[39];
}
let t8;
if ($[40] !== t5 || $[41] !== t6 || $[42] !== t7) {
t8 = {
onPointerDown: t5,
onDrag: t6,
onDragEnd: t7
};
$[40] = t5;
$[41] = t6;
$[42] = t7;
$[43] = t8;
} else {
t8 = $[43];
}
let t9;
if ($[44] !== inputSource) {
t9 = getNodeGestureConfig(inputSource);
$[44] = inputSource;
$[45] = t9;
} else {
t9 = $[45];
}
const bind = useGesture(t8, t9);
let t10;
if ($[46] !== bind || $[47] !== updateNodePositions) {
t10 = {
bind,
updateNodePositions
};
$[46] = bind;
$[47] = updateNodePositions;
$[48] = t10;
} else {
t10 = $[48];
}
return t10;
}
function _temp2(get, set, updates) {
const graph_0 = get(graphAtom);
updates.forEach((u) => {
if (graph_0.hasNode(u.id)) {
graph_0.setNodeAttribute(u.id, "x", u.pos.x);
graph_0.setNodeAttribute(u.id, "y", u.pos.y);
}
});
set(nodePositionUpdateCounterAtom, _temp);
}
function _temp(c) {
return c + 1;
}
// src/hooks/useNodeResize.ts
import { c as _c3 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue2, useSetAtom as useSetAtom3 } from "jotai";
import { useRef as useRef2, useState, useEffect } from "react";
import { flushSync } from "react-dom";
var debug10 = createDebug("resize");
function useNodeResize(t0) {
const $ = _c3(38);
const {
id,
nodeData,
updateNodePositions,
options: t1
} = t0;
let t2;
if ($[0] !== t1) {
t2 = t1 === void 0 ? {} : t1;
$[0] = t1;
$[1] = t2;
} else {
t2 = $[1];
}
const options = t2;
const {
onPersist,
onPersistError,
minWidth: t3,
minHeight: t4
} = options;
const minWidth = t3 === void 0 ? 200 : t3;
const minHeight = t4 === void 0 ? 150 : t4;
const [localWidth, setLocalWidth] = useState(nodeData.width || 500);
const [localHeight, setLocalHeight] = useState(nodeData.height || 500);
const [isResizing, setIsResizing] = useState(false);
const resizeStartRef = useRef2(null);
const graph = useAtomValue2(graphAtom);
const currentZoom = useAtomValue2(zoomAtom);
const currentGraphId = useAtomValue2(currentGraphIdAtom);
const startMutation = useSetAtom3(startMutationAtom);
const completeMutation = useSetAtom3(completeMutationAtom);
const setGraphUpdateVersion = useSetAtom3(graphUpdateVersionAtom);
const setNodePositionUpdateCounter = useSetAtom3(nodePositionUpdateCounterAtom);
let t5;
let t6;
if ($[2] !== isResizing || $[3] !== nodeData.height || $[4] !== nodeData.width) {
t5 = () => {
if (!isResizing) {
setLocalWidth(nodeData.width || 500);
setLocalHeight(nodeData.height || 500);
}
};
t6 = [nodeData.width, nodeData.height, isResizing];
$[2] = isResizing;
$[3] = nodeData.height;
$[4] = nodeData.width;
$[5] = t5;
$[6] = t6;
} else {
t5 = $[5];
t6 = $[6];
}
useEffect(t5, t6);
let t7;
if ($[7] !== graph || $[8] !== id || $[9] !== localHeight || $[10] !== localWidth) {
t7 = (direction) => (e) => {
e.stopPropagation();
e.preventDefault();
setIsResizing(true);
const nodeAttrs = graph.hasNode(id) ? graph.getNodeAttributes(id) : {
x: 0,
y: 0
};
resizeStartRef.current = {
width: localWidth,
height: localHeight,
startX: e.clientX,
startY: e.clientY,
startNodeX: nodeAttrs.x,
startNodeY: nodeAttrs.y,
direction
};
e.target.setPointerCapture(e.pointerId);
};
$[7] = graph;
$[8] = id;
$[9] = localHeight;
$[10] = localWidth;
$[11] = t7;
} else {
t7 = $[11];
}
const createResizeStart = t7;
let t8;
if ($[12] !== currentZoom || $[13] !== graph || $[14] !== id || $[15] !== minHeight || $[16] !== minWidth || $[17] !== setGraphUpdateVersion || $[18] !== setNodePositionUpdateCounter || $[19] !== updateNodePositions) {
t8 = (e_0) => {
if (!resizeStartRef.current) {
return;
}
e_0.stopPropagation();
e_0.preventDefault();
const deltaX = (e_0.clientX - resizeStartRef.current.startX) / currentZoom;
const deltaY = (e_0.clientY - resizeStartRef.current.startY) / currentZoom;
const {
direction: direction_0,
width: startWidth,
height: startHeight,
startNodeX,
startNodeY
} = resizeStartRef.current;
let newWidth = startWidth;
let newHeight = startHeight;
let newX = startNodeX;
let newY = startNodeY;
if (direction_0.includes("e")) {
newWidth = Math.max(minWidth, startWidth + deltaX);
}
if (direction_0.includes("w")) {
newWidth = Math.max(minWidth, startWidth - deltaX);
newX = startNodeX + (startWidth - newWidth);
}
if (direction_0.includes("s")) {
newHeight = Math.max(minHeight, startHeight + deltaY);
}
if (direction_0.includes("n")) {
newHeight = Math.max(minHeight, startHeight - deltaY);
newY = startNodeY + (startHeight - newHeight);
}
if (graph.hasNode(id)) {
graph.setNodeAttribute(id, "width", newWidth);
graph.setNodeAttribute(id, "height", newHeight);
graph.setNodeAttribute(id, "x", newX);
graph.setNodeAttribute(id, "y", newY);
}
flushSync(() => {
setLocalWidth(newWidth);
setLocalHeight(newHeight);
setGraphUpdateVersion(_temp3);
});
if (direction_0.includes("n") || direction_0.includes("w")) {
updateNodePositions([{
id,
pos: {
x: newX,
y: newY
}
}]);
} else {
setNodePositionUpdateCounter(_temp22);
}
};
$[12] = currentZoom;
$[13] = graph;
$[14] = id;
$[15] = minHeight;
$[16] = minWidth;
$[17] = setGraphUpdateVersion;
$[18] = setNodePositionUpdateCounter;
$[19] = updateNodePositions;
$[20] = t8;
} else {
t8 = $[20];
}
const handleResizeMove = t8;
let t9;
if ($[21] !== completeMutation || $[22] !== currentGraphId || $[23] !== graph || $[24] !== id || $[25] !== localHeight || $[26] !== localWidth || $[27] !== onPersist || $[28] !== onPersistError || $[29] !== startMutation) {
t9 = (e_1) => {
if (!resizeStartRef.current) {
return;
}
e_1.stopPropagation();
e_1.target.releasePointerCapture(e_1.pointerId);
setIsResizing(false);
if (!currentGraphId || !resizeStartRef.current) {
resizeStartRef.current = null;
return;
}
const finalAttrs = graph.hasNode(id) ? graph.getNodeAttributes(id) : null;
if (!finalAttrs) {
resizeStartRef.current = null;
return;
}
const finalWidth = finalAttrs.width || localWidth;
const finalHeight = finalAttrs.height || localHeight;
const finalX = finalAttrs.x;
const finalY = finalAttrs.y;
setLocalWidth(finalWidth);
setLocalHeight(finalHeight);
if (!onPersist) {
resizeStartRef.current = null;
return;
}
const existingDbUiProps = typeof finalAttrs.dbData.ui_properties === "object" && finalAttrs.dbData.ui_properties !== null && !Array.isArray(finalAttrs.dbData.ui_properties) ? finalAttrs.dbData.ui_properties : {};
const newUiProperties = {
...existingDbUiProps,
width: finalWidth,
height: finalHeight,
x: finalX,
y: finalY
};
startMutation();
onPersist(id, currentGraphId, newUiProperties).then(() => {
completeMutation(true);
}).catch((error) => {
completeMutation(false);
if (resizeStartRef.current) {
setLocalWidth(resizeStartRef.current.width);
setLocalHeight(resizeStartRef.current.height);
}
onPersistError?.(id, error);
}).finally(() => {
resizeStartRef.current = null;
});
};
$[21] = completeMutation;
$[22] = currentGraphId;
$[23] = graph;
$[24] = id;
$[25] = localHeight;
$[26] = localWidth;
$[27] = onPersist;
$[28] = onPersistError;
$[29] = startMutation;
$[30] = t9;
} else {
t9 = $[30];
}
const handleResizeEnd = t9;
let t10;
if ($[31] !== createResizeStart || $[32] !== handleResizeEnd || $[33] !== handleResizeMove || $[34] !== isResizing || $[35] !== localHeight || $[36] !== localWidth) {
t10 = {
localWidth,
localHeight,
isResizing,
createResizeStart,
handleResizeMove,
handleResizeEnd
};
$[31] = createResizeStart;
$[32] = handleResizeEnd;
$[33] = handleResizeMove;
$[34] = isResizing;
$[35] = localHeight;
$[36] = localWidth;
$[37] = t10;
} else {
t10 = $[37];
}
return t10;
}
function _temp22(c) {
return c + 1;
}
function _temp3(v) {
return v + 1;
}
// src/hooks/useCanvasHistory.ts
import { c as _c4 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue3, useSetAtom as useSetAtom4, useStore } from "jotai";
// src/core/toast-store.ts
import { atom as atom16 } from "jotai";
var canvasToastAtom = atom16(null);
var showToastAtom = atom16(null, (_get, set, message) => {
const id = `toast-${Date.now()}`;
set(canvasToastAtom, {
id,
message,
timestamp: Date.now()
});
setTimeout(() => {
set(canvasToastAtom, (current) => current?.id === id ? null : current);
}, 2e3);
});
// src/hooks/useCanvasHistory.ts
function useCanvasHistory(t0) {
const $ = _c4(22);
const options = t0 === void 0 ? {} : t0;
const {
enableKeyboardShortcuts: t1
} = options;
t1 === void 0 ? false : t1;
const canUndo = useAtomValue3(canUndoAtom);
const canRedo = useAtomValue3(canRedoAtom);
const undoCount = useAtomValue3(undoCountAtom);
const redoCount = useAtomValue3(redoCountAtom);
const historyLabels = useAtomValue3(historyLabelsAtom);
const undoAction = useSetAtom4(undoAtom);
const redoAction = useSetAtom4(redoAtom);
const pushHistory = useSetAtom4(pushHistoryAtom);
const clearHistory = useSetAtom4(clearHistoryAtom);
const showToast = useSetAtom4(showToastAtom);
const store = useStore();
let t2;
if ($[0] !== showToast || $[1] !== store || $[2] !== undoAction) {
t2 = () => {
const state = store.get(historyStateAtom);
const label = state.past[state.past.length - 1]?.label;
const result = undoAction();
if (result && label) {
showToast(`Undo: ${label}`);
}
return result;
};
$[0] = showToast;
$[1] = store;
$[2] = undoAction;
$[3] = t2;
} else {
t2 = $[3];
}
const undo = t2;
let t3;
if ($[4] !== redoAction || $[5] !== showToast || $[6] !== store) {
t3 = () => {
const state_0 = store.get(historyStateAtom);
const label_0 = state_0.future[0]?.label;
const result_0 = redoAction();
if (result_0 && label_0) {
showToast(`Redo: ${label_0}`);
}
return result_0;
};
$[4] = redoAction;
$[5] = showToast;
$[6] = store;
$[7] = t3;
} else {
t3 = $[7];
}
const redo = t3;
let t4;
if ($[8] !== pushHistory) {
t4 = (label_1) => {
pushHistory(label_1);
};
$[8] = pushHistory;
$[9] = t4;
} else {
t4 = $[9];
}
const recordSnapshot = t4;
let t5;
if ($[10] !== clearHistory) {
t5 = () => {
clearHistory();
};
$[10] = clearHistory;
$[11] = t5;
} else {
t5 = $[11];
}
const clear = t5;
let t6;
if ($[12] !== canRedo || $[13] !== canUndo || $[14] !== clear || $[15] !== historyLabels || $[16] !== recordSnapshot || $[17] !== redo || $[18] !== redoCount || $[19] !== undo || $[20] !== undoCount) {
t6 = {
undo,
redo,
canUndo,
canRedo,
undoCount,
redoCount,
historyLabels,
recordSnapshot,
clear
};
$[12] = canRedo;
$[13] = canUndo;
$[14] = clear;
$[15] = historyLabels;
$[16] = recordSnapshot;
$[17] = redo;
$[18] = redoCount;
$[19] = undo;
$[20] = undoCount;
$[21] = t6;
} else {
t6 = $[21];
}
return t6;
}
// src/hooks/useCanvasSelection.ts
import { c as _c5 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue4 } from "jotai";
function useCanvasSelection() {
const $ = _c5(6);
const selectedNodeIds = useAtomValue4(selectedNodeIdsAtom);
const selectedEdgeId = useAtomValue4(selectedEdgeIdAtom);
const count = useAtomValue4(selectedNodesCountAtom);
const hasSelection = useAtomValue4(hasSelectionAtom);
const t0 = selectedEdgeId !== null;
let t1;
if ($[0] !== count || $[1] !== hasSelection || $[2] !== selectedEdgeId || $[3] !== selectedNodeIds || $[4] !== t0) {
t1 = {
selectedNodeIds,
selectedEdgeId,
count,
hasSelection,
hasEdgeSelection: t0
};
$[0] = count;
$[1] = hasSelection;
$[2] = selectedEdgeId;
$[3] = selectedNodeIds;
$[4] = t0;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
}
// src/hooks/useCanvasViewport.ts
import { c as _c6 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue5 } from "jotai";
function useCanvasViewport() {
const $ = _c6(9);
const zoom = useAtomValue5(zoomAtom);
const pan = useAtomValue5(panAtom);
const viewportRect = useAtomValue5(viewportRectAtom);
const screenToWorld = useAtomValue5(screenToWorldAtom);
const worldToScreen = useAtomValue5(worldToScreenAtom);
const zoomFocusNodeId = useAtomValue5(zoomFocusNodeIdAtom);
const zoomTransitionProgress = useAtomValue5(zoomTransitionProgressAtom);
const isZoomTransitioning = useAtomValue5(isZoomTransitioningAtom);
let t0;
if ($[0] !== isZoomTransitioning || $[1] !== pan || $[2] !== screenToWorld || $[3] !== viewportRect || $[4] !== worldToScreen || $[5] !== zoom || $[6] !== zoomFocusNodeId || $[7] !== zoomTransitionProgress) {
t0 = {
zoom,
pan,
viewportRect,
screenToWorld,
worldToScreen,
zoomFocusNodeId,
zoomTransitionProgress,
isZoomTransitioning,
zoomTransitionThreshold: ZOOM_TRANSITION_THRESHOLD,
zoomExitThreshold: ZOOM_EXIT_THRESHOLD
};
$[0] = isZoomTransitioning;
$[1] = pan;
$[2] = screenToWorld;
$[3] = viewportRect;
$[4] = worldToScreen;
$[5] = zoom;
$[6] = zoomFocusNodeId;
$[7] = zoomTransitionProgress;
$[8] = t0;
} else {
t0 = $[8];
}
return t0;
}
// src/hooks/useCanvasDrag.ts
import { c as _c7 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue6 } from "jotai";
function useCanvasDrag() {
const $ = _c7(3);
const draggingNodeId = useAtomValue6(draggingNodeIdAtom);
const t0 = draggingNodeId !== null;
let t1;
if ($[0] !== draggingNodeId || $[1] !== t0) {
t1 = {
draggingNodeId,
isDragging: t0
};
$[0] = draggingNodeId;
$[1] = t0;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
// src/hooks/useLayout.ts
import { c as _c8 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue7, useSetAtom as useSetAtom5 } from "jotai";
var useGetGraphBounds = () => {
const $ = _c8(6);
const graph = useAtomValue7(graphAtom);
useAtomValue7(nodePositionUpdateCounterAtom);
let nodes;
let t0;
if ($[0] !== graph) {
nodes = graph.nodes().map((node) => {
const nodeAttributes = graph.getNodeAttributes(node);
return {
x: nodeAttributes.x,
y: nodeAttributes.y,
width: nodeAttributes.width || 500,
height: nodeAttributes.height || 500
};
});
t0 = calculateBounds(nodes);
$[0] = graph;
$[1] = nodes;
$[2] = t0;
} else {
nodes = $[1];
t0 = $[2];
}
const bounds = t0;
let t1;
if ($[3] !== bounds || $[4] !== nodes) {
t1 = {
bounds,
nodes
};
$[3] = bounds;
$[4] = nodes;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
};
var useSelectionBounds = () => {
const $ = _c8(5);
const selectedNodeIds = useAtomValue7(selectedNodeIdsAtom);
const nodes = useAtomValue7(uiNodesAtom);
let t0;
if ($[0] !== nodes || $[1] !== selectedNodeIds) {
let t1;
if ($[3] !== selectedNodeIds) {
t1 = (node) => selectedNodeIds.has(node.id);
$[3] = selectedNodeIds;
$[4] = t1;
} else {
t1 = $[4];
}
const selectedNodes = nodes.filter(t1).map(_temp4);
t0 = calculateBounds(selectedNodes);
$[0] = nodes;
$[1] = selectedNodeIds;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
};
var useFitToBounds = () => {
const $ = _c8(2);
const setFitToBounds = useSetAtom5(fitToBoundsAtom);
let t0;
if ($[0] !== setFitToBounds) {
const fitToBounds = (mode, t1) => {
const padding = t1 === void 0 ? 20 : t1;
setFitToBounds({
mode,
padding
});
};
t0 = {
fitToBounds
};
$[0] = setFitToBounds;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
};
var useLayout = () => {
const $ = _c8(5);
const {
fitToBounds
} = useFitToBounds();
const {
bounds: graphBounds,
nodes: graphNodes
} = useGetGraphBounds();
const selectionBounds = useSelectionBounds();
let t0;
if ($[0] !== fitToBounds || $[1] !== graphBounds || $[2] !== graphNodes || $[3] !== selectionBounds) {
t0 = {
fitToBounds,
graphBounds,
graphNodes,
selectionBounds
};
$[0] = fitToBounds;
$[1] = graphBounds;
$[2] = graphNodes;
$[3] = selectionBounds;
$[4] = t0;
} else {
t0 = $[4];
}
return t0;
};
function _temp4(node_0) {
return {
x: node_0.position.x,
y: node_0.position.y,
width: node_0.width ?? 500,
height: node_0.height ?? 500
};
}
// src/hooks/useForceLayout.ts
import * as d3 from "d3-force";
import { useAtomValue as useAtomValue8, useSetAtom as useSetAtom6 } from "jotai";
import { useRef as useRef3 } from "react";
var debug11 = createDebug("force-layout");
var useForceLayout = (options = {}) => {
const {
onPositionsChanged,
maxIterations = 1e3,
chargeStrength = -6e3,
linkStrength = 0.03
} = options;
const nodes = useAtomValue8(uiNodesAtom);
const graph = useAtomValue8(graphAtom);
const updateNodePosition = useSetAtom6(updateNodePositionAtom);
const isRunningRef = useRef3(false);
const createVirtualLinks = () => {
const links = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = 1; j <= 3; j++) {
const targetIndex = (i + j) % nodes.length;
links.push({
source: nodes[i].id,
target: nodes[targetIndex].id,
strength: 0.05
// Very weak connection
});
}
}
return links;
};
const applyForceLayout = async () => {
if (isRunningRef.current) {
debug11.warn("Layout already running, ignoring request.");
return;
}
if (nodes.length === 0) {
debug11.warn("No nodes to layout.");
return;
}
isRunningRef.current = true;
const simulationNodes = nodes.map((node) => {
const width = node.width || 80;
const height = node.height || 80;
return {
id: node.id,
x: node.position?.x || 0,
y: node.position?.y || 0,
width,
height,
radius: Math.max(width, height) + 80
};
});
const simulation = d3.forceSimulation(simulationNodes).force("charge", d3.forceManyBody().strength(chargeStrength).distanceMax(900)).force("collide", d3.forceCollide().radius((d) => d.radius).strength(2).iterations(8)).force("link", d3.forceLink(createVirtualLinks()).id((d_0) => d_0.id).strength(linkStrength)).force("center", d3.forceCenter(0, 0)).stop();
debug11("Starting simulation...");
return new Promise((resolve) => {
let iterations = 0;
function runSimulationStep() {
if (iterations >= maxIterations) {
debug11("Reached max iterations (%d), finalizing.", maxIterations);
finalizeLayout();
return;
}
simulation.tick();
iterations++;
let hasOverlaps = false;
for (let i_0 = 0; i_0 < simulationNodes.length; i_0++) {
for (let j_0 = i_0 + 1; j_0 < simulationNodes.length; j_0++) {
if (checkNodesOverlap(simulationNodes[i_0], simulationNodes[j_0])) {
hasOverlaps = true;
break;
}
}
if (hasOverlaps) break;
}
if (!hasOverlaps) {
debug11("No overlaps after %d iterations.", iterations);
finalizeLayout();
return;
}
requestAnimationFrame(runSimulationStep);
}
function finalizeLayout() {
const positionUpdates = [];
simulationNodes.forEach((simNode) => {
if (graph.hasNode(simNode.id)) {
const newPosition = {
x: Math.round(simNode.x),
y: Math.round(simNode.y)
};
updateNodePosition({
nodeId: simNode.id,
position: newPosition
});
positionUpdates.push({
nodeId: simNode.id,
position: newPosition
});
}
});
if (onPositionsChanged && positionUpdates.length > 0) {
debug11("Saving %d positions via callback...", positionUpdates.length);
Promise.resolve(onPositionsChanged(positionUpdates)).then(() => debug11("Positions saved successfully.")).catch((err) => debug11.error("Error saving positions: %O", err));
}
debug11("Layout complete.");
isRunningRef.current = false;
resolve();
}
requestAnimationFrame(runSimulationStep);
});
};
return {
applyForceLayout,
isRunning: isRunningRef.current
};
};
// src/hooks/useCanvasSettings.ts
import { c as _c9 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue9, useSetAtom as useSetAtom7 } from "jotai";
// src/core/settings-store.ts
import { atom as atom17 } from "jotai";
import { atomWithStorage } from "jotai/utils";
// src/core/event-types.ts
var CanvasEventType = /* @__PURE__ */ (function(CanvasEventType2) {
CanvasEventType2["NodeClick"] = "node:click";
CanvasEventType2["NodeDoubleClick"] = "node:double-click";
CanvasEventType2["NodeTripleClick"] = "node:triple-click";
CanvasEventType2["NodeRightClick"] = "node:right-click";
CanvasEventType2["NodeLongPress"] = "node:long-press";
CanvasEventType2["EdgeClick"] = "edge:click";
CanvasEventType2["EdgeDoubleClick"] = "edge:double-click";
CanvasEventType2["EdgeRightClick"] = "edge:right-click";
CanvasEventType2["BackgroundClick"] = "background:click";
CanvasEventType2["BackgroundDoubleClick"] = "background:double-click";
CanvasEventType2["BackgroundRightClick"] = "background:right-click";
CanvasEventType2["BackgroundLongPress"] = "background:long-press";
return CanvasEventType2;
})({});
var EVENT_TYPE_INFO = {
[CanvasEventType.NodeClick]: {
type: CanvasEventType.NodeClick,
label: "Click Node",
description: "Triggered when clicking on a node",
category: "node"
},
[CanvasEventType.NodeDoubleClick]: {
type: CanvasEventType.NodeDoubleClick,
label: "Double-click Node",
description: "Triggered when double-clicking on a node",
category: "node"
},
[CanvasEventType.NodeTripleClick]: {
type: CanvasEventType.NodeTripleClick,
label: "Triple-click Node",
description: "Triggered when triple-clicking on a node",
category: "node"
},
[CanvasEventType.NodeRightClick]: {
type: CanvasEventType.NodeRightClick,
label: "Right-click Node",
description: "Triggered when right-clicking on a node",
category: "node"
},
[CanvasEventType.NodeLongPress]: {
type: CanvasEventType.NodeLongPress,
label: "Long-press Node",
description: "Triggered when long-pressing on a node (mobile/touch)",
category: "node"
},
[CanvasEventType.EdgeClick]: {
type: CanvasEventType.EdgeClick,
label: "Click Edge",
description: "Triggered when clicking on an edge",
category: "edge"
},
[CanvasEventType.EdgeDoubleClick]: {
type: CanvasEventType.EdgeDoubleClick,
label: "Double-click Edge",
description: "Triggered when double-clicking on an edge",
category: "edge"
},
[CanvasEventType.EdgeRightClick]: {
type: CanvasEventType.EdgeRightClick,
label: "Right-click Edge",
description: "Triggered when right-clicking on an edge",
category: "edge"
},
[CanvasEventType.BackgroundClick]: {
type: CanvasEventType.BackgroundClick,
label: "Click Background",
description: "Triggered when clicking on the canvas background",
category: "background"
},
[CanvasEventType.BackgroundDoubleClick]: {
type: CanvasEventType.BackgroundDoubleClick,
label: "Double-click Background",
description: "Triggered when double-clicking on the canvas background",
category: "background"
},
[CanvasEventType.BackgroundRightClick]: {
type: CanvasEventType.BackgroundRightClick,
label: "Right-click Background",
description: "Triggered when right-clicking on the canvas background",
category: "background"
},
[CanvasEventType.BackgroundLongPress]: {
type: CanvasEventType.BackgroundLongPress,
label: "Long-press Background",
description: "Triggered when long-pressing on the canvas background (mobile/touch)",
category: "background"
}
};
// src/core/action-types.ts
var ActionCategory = /* @__PURE__ */ (function(ActionCategory2) {
ActionCategory2["None"] = "none";
ActionCategory2["Selection"] = "selection";
ActionCategory2["Viewport"] = "viewport";
ActionCategory2["Node"] = "node";
ActionCategory2["Layout"] = "layout";
ActionCategory2["History"] = "history";
ActionCategory2["Custom"] = "custom";
return ActionCategory2;
})({});
var BuiltInActionId = {
// None
None: "none",
// Selection
SelectNode: "select-node",
SelectEdge: "select-edge",
AddToSelection: "add-to-selection",
ClearSelection: "clear-selection",
DeleteSelected: "delete-selected",
// Viewport
FitToView: "fit-to-view",
FitAllToView: "fit-all-to-view",
CenterOnNode: "center-on-node",
ResetViewport: "reset-viewport",
// Node
LockNode: "lock-node",
UnlockNode: "unlock-node",
ToggleLock: "toggle-lock",
OpenContextMenu: "open-context-menu",
SplitNode: "split-node",
GroupNodes: "group-nodes",
MergeNodes: "merge-nodes",
// Layout
ApplyForceLayout: "apply-force-layout",
// History
Undo: "undo",
Redo: "redo",
// Creation
CreateNode: "create-node"
};
// src/core/settings-state-types.ts
var DEFAULT_MAPPINGS = {
[CanvasEventType.NodeClick]: BuiltInActionId.None,
[CanvasEventType.NodeDoubleClick]: BuiltInActionId.FitToView,
[CanvasEventType.NodeTripleClick]: BuiltInActionId.ToggleLock,
[CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge,
[CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None,
[CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection,
[CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.FitAllToView,
[CanvasEventType.BackgroundRightClick]: BuiltInActionId.None,
[CanvasEventType.BackgroundLongPress]: BuiltInActionId.CreateNode
};
// src/core/settings-presets.ts
var BUILT_IN_PRESETS = [{
id: "default",
name: "Default",
description: "Standard canvas interactions",
isBuiltIn: true,
mappings: DEFAULT_MAPPINGS
}, {
id: "minimal",
name: "Minimal",
description: "Only essential selection and context menu actions",
isBuiltIn: true,
mappings: {
[CanvasEventType.NodeClick]: BuiltInActionId.None,
[CanvasEventType.NodeDoubleClick]: BuiltInActionId.None,
[CanvasEventType.NodeTripleClick]: BuiltInActionId.None,
[CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge,
[CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None,
[CanvasEventType.EdgeRightClick]: BuiltInActionId.None,
[CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection,
[CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.None,
[CanvasEventType.BackgroundRightClick]: BuiltInActionId.None,
[CanvasEventType.BackgroundLongPress]: BuiltInActionId.None
}
}, {
id: "power-user",
name: "Power User",
description: "Quick actions for experienced users",
isBuiltIn: true,
mappings: {
[CanvasEventType.NodeClick]: BuiltInActionId.None,
[CanvasEventType.NodeDoubleClick]: BuiltInActionId.ToggleLock,
[CanvasEventType.NodeTripleClick]: BuiltInActionId.DeleteSelected,
[CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.NodeLongPress]: BuiltInActionId.AddToSelection,
[CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge,
[CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None,
[CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection,
[CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.CreateNode,
[CanvasEventType.BackgroundRightClick]: BuiltInActionId.OpenContextMenu,
[CanvasEventType.BackgroundLongPress]: BuiltInActionId.ApplyForceLayout
}
}];
function getActionForEvent(mappings, event) {
return mappings[event] || BuiltInActionId.None;
}
// src/core/settings-store.ts
var debug12 = createDebug("settings");
var DEFAULT_STATE = {
mappings: DEFAULT_MAPPINGS,
activePresetId: "default",
customPresets: [],
isPanelOpen: false,
virtualizationEnabled: true
};
var canvasSettingsAtom = atomWithStorage("@blinksgg/canvas/settings", DEFAULT_STATE);
var eventMappingsAtom = atom17((get) => get(canvasSettingsAtom).mappings);
var activePresetIdAtom = atom17((get) => get(canvasSettingsAtom).activePresetId);
var allPresetsAtom = atom17((get) => {
const state = get(canvasSettingsAtom);
return [...BUILT_IN_PRESETS, ...state.customPresets];
});
var activePresetAtom = atom17((get) => {
const presetId = get(activePresetIdAtom);
if (!presetId) return null;
const allPresets = get(allPresetsAtom);
return allPresets.find((p) => p.id === presetId) || null;
});
var isPanelOpenAtom = atom17((get) => get(canvasSettingsAtom).isPanelOpen);
var virtualizationEnabledAtom = atom17((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true);
var hasUnsavedChangesAtom = atom17((get) => {
const state = get(canvasSettingsAtom);
const activePreset = get(activePresetAtom);
if (!activePreset) return true;
const events = Object.values(CanvasEventType);
return events.some((event) => state.mappings[event] !== activePreset.mappings[event]);
});
var setEventMappingAtom = atom17(null, (get, set, {
event,
actionId
}) => {
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
mappings: {
...current.mappings,
[event]: actionId
},
// Clear active preset since mappings have changed
activePresetId: null
});
});
var applyPresetAtom = atom17(null, (get, set, presetId) => {
const allPresets = get(allPresetsAtom);
const preset = allPresets.find((p) => p.id === presetId);
if (!preset) {
debug12.warn("Preset not found: %s", presetId);
return;
}
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
mappings: {
...preset.mappings
},
activePresetId: presetId
});
});
var saveAsPresetAtom = atom17(null, (get, set, {
name,
description
}) => {
const current = get(canvasSettingsAtom);
const id = `custom-${Date.now()}`;
const newPreset = {
id,
name,
description,
mappings: {
...current.mappings
},
isBuiltIn: false
};
set(canvasSettingsAtom, {
...current,
customPresets: [...current.customPresets, newPreset],
activePresetId: id
});
return id;
});
var updatePresetAtom = atom17(null, (get, set, presetId) => {
const current = get(canvasSettingsAtom);
const presetIndex = current.customPresets.findIndex((p) => p.id === presetId);
if (presetIndex === -1) {
debug12.warn("Cannot update preset: %s (not found or built-in)", presetId);
return;
}
const updatedPresets = [...current.customPresets];
updatedPresets[presetIndex] = {
...updatedPresets[presetIndex],
mappings: {
...current.mappings
}
};
set(canvasSettingsAtom, {
...current,
customPresets: updatedPresets,
activePresetId: presetId
});
});
var deletePresetAtom = atom17(null, (get, set, presetId) => {
const current = get(canvasSettingsAtom);
const newCustomPresets = current.customPresets.filter((p) => p.id !== presetId);
if (newCustomPresets.length === current.customPresets.length) {
debug12.warn("Cannot delete preset: %s (not found or built-in)", presetId);
return;
}
const newActiveId = current.activePresetId === presetId ? "default" : current.activePresetId;
const newMappings = newActiveId === "default" ? DEFAULT_MAPPINGS : current.mappings;
set(canvasSettingsAtom, {
...current,
customPresets: newCustomPresets,
activePresetId: newActiveId,
mappings: newMappings
});
});
var resetSettingsAtom = atom17(null, (get, set) => {
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
mappings: DEFAULT_MAPPINGS,
activePresetId: "default"
});
});
var togglePanelAtom = atom17(null, (get, set) => {
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
isPanelOpen: !current.isPanelOpen
});
});
var setPanelOpenAtom = atom17(null, (get, set, isOpen) => {
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
isPanelOpen: isOpen
});
});
var setVirtualizationEnabledAtom = atom17(null, (get, set, enabled) => {
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
virtualizationEnabled: enabled
});
});
var toggleVirtualizationAtom = atom17(null, (get, set) => {
const current = get(canvasSettingsAtom);
set(canvasSettingsAtom, {
...current,
virtualizationEnabled: !(current.virtualizationEnabled ?? true)
});
});
// src/hooks/useCanvasSettings.ts
function useCanvasSettings() {
const $ = _c9(34);
const mappings = useAtomValue9(eventMappingsAtom);
const activePresetId = useAtomValue9(activePresetIdAtom);
const activePreset = useAtomValue9(activePresetAtom);
const allPresets = useAtomValue9(allPresetsAtom);
const hasUnsavedChanges = useAtomValue9(hasUnsavedChangesAtom);
const isPanelOpen = useAtomValue9(isPanelOpenAtom);
const setEventMappingAction = useSetAtom7(setEventMappingAtom);
const applyPresetAction = useSetAtom7(applyPresetAtom);
const saveAsPresetAction = useSetAtom7(saveAsPresetAtom);
const updatePresetAction = useSetAtom7(updatePresetAtom);
const deletePresetAction = useSetAtom7(deletePresetAtom);
const resetSettingsAction = useSetAtom7(resetSettingsAtom);
const togglePanelAction = useSetAtom7(togglePanelAtom);
const setPanelOpenAction = useSetAtom7(setPanelOpenAtom);
let t0;
if ($[0] !== setEventMappingAction) {
t0 = (event, actionId) => {
setEventMappingAction({
event,
actionId
});
};
$[0] = setEventMappingAction;
$[1] = t0;
} else {
t0 = $[1];
}
const setEventMapping = t0;
let t1;
if ($[2] !== applyPresetAction) {
t1 = (presetId) => {
applyPresetAction(presetId);
};
$[2] = applyPresetAction;
$[3] = t1;
} else {
t1 = $[3];
}
const applyPreset = t1;
let t2;
if ($[4] !== saveAsPresetAction) {
t2 = (name, description) => saveAsPresetAction({
name,
description
});
$[4] = saveAsPresetAction;
$[5] = t2;
} else {
t2 = $[5];
}
const saveAsPreset = t2;
let t3;
if ($[6] !== updatePresetAction) {
t3 = (presetId_0) => {
updatePresetAction(presetId_0);
};
$[6] = updatePresetAction;
$[7] = t3;
} else {
t3 = $[7];
}
const updatePreset = t3;
let t4;
if ($[8] !== deletePresetAction) {
t4 = (presetId_1) => {
deletePresetAction(presetId_1);
};
$[8] = deletePresetAction;
$[9] = t4;
} else {
t4 = $[9];
}
const deletePreset = t4;
let t5;
if ($[10] !== resetSettingsAction) {
t5 = () => {
resetSettingsAction();
};
$[10] = resetSettingsAction;
$[11] = t5;
} else {
t5 = $[11];
}
const resetSettings = t5;
let t6;
if ($[12] !== togglePanelAction) {
t6 = () => {
togglePanelAction();
};
$[12] = togglePanelAction;
$[13] = t6;
} else {
t6 = $[13];
}
const togglePanel = t6;
let t7;
if ($[14] !== setPanelOpenAction) {
t7 = (isOpen) => {
setPanelOpenAction(isOpen);
};
$[14] = setPanelOpenAction;
$[15] = t7;
} else {
t7 = $[15];
}
const setPanelOpen = t7;
let t8;
if ($[16] !== mappings) {
t8 = (event_0) => getActionForEvent(mappings, event_0);
$[16] = mappings;
$[17] = t8;
} else {
t8 = $[17];
}
const getActionForEventFn = t8;
let t9;
if ($[18] !== activePreset || $[19] !== activePresetId || $[20] !== allPresets || $[21] !== applyPreset || $[22] !== deletePreset || $[23] !== getActionForEventFn || $[24] !== hasUnsavedChanges || $[25] !== isPanelOpen || $[26] !== mappings || $[27] !== resetSettings || $[28] !== saveAsPreset || $[29] !== setEventMapping || $[30] !== setPanelOpen || $[31] !== togglePanel || $[32] !== updatePreset) {
t9 = {
mappings,
activePresetId,
activePreset,
allPresets,
hasUnsavedChanges,
isPanelOpen,
setEventMapping,
applyPreset,
saveAsPreset,
updatePreset,
deletePreset,
resetSettings,
togglePanel,
setPanelOpen,
getActionForEvent: getActionForEventFn
};
$[18] = activePreset;
$[19] = activePresetId;
$[20] = allPresets;
$[21] = applyPreset;
$[22] = deletePreset;
$[23] = getActionForEventFn;
$[24] = hasUnsavedChanges;
$[25] = isPanelOpen;
$[26] = mappings;
$[27] = resetSettings;
$[28] = saveAsPreset;
$[29] = setEventMapping;
$[30] = setPanelOpen;
$[31] = togglePanel;
$[32] = updatePreset;
$[33] = t9;
} else {
t9 = $[33];
}
return t9;
}
// src/hooks/useActionExecutor.ts
import { c as _c10 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue10, useStore as useStore2 } from "jotai";
// src/core/actions-node.ts
function registerSelectionActions() {
registerAction({
id: BuiltInActionId.SelectNode,
label: "Select Node",
description: "Select this node (replacing current selection)",
category: ActionCategory.Selection,
icon: "pointer",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.selectNode(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.SelectEdge,
label: "Select Edge",
description: "Select this edge",
category: ActionCategory.Selection,
icon: "git-commit",
isBuiltIn: true,
handler: (context, helpers) => {
if (context.edgeId) {
helpers.selectEdge(context.edgeId);
}
}
});
registerAction({
id: BuiltInActionId.AddToSelection,
label: "Add to Selection",
description: "Add this node to the current selection",
category: ActionCategory.Selection,
icon: "plus-square",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.addToSelection(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.ClearSelection,
label: "Clear Selection",
description: "Deselect all nodes",
category: ActionCategory.Selection,
icon: "x-square",
isBuiltIn: true,
handler: (_context, helpers) => {
helpers.clearSelection();
}
});
registerAction({
id: BuiltInActionId.DeleteSelected,
label: "Delete Selected",
description: "Delete all selected nodes",
category: ActionCategory.Selection,
icon: "trash-2",
isBuiltIn: true,
handler: async (_context, helpers) => {
const selectedIds = helpers.getSelectedNodeIds();
for (const nodeId of selectedIds) {
await helpers.deleteNode(nodeId);
}
}
});
}
function registerNodeActions() {
registerAction({
id: BuiltInActionId.LockNode,
label: "Lock Node",
description: "Prevent this node from being moved",
category: ActionCategory.Node,
icon: "lock",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.lockNode(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.UnlockNode,
label: "Unlock Node",
description: "Allow this node to be moved",
category: ActionCategory.Node,
icon: "unlock",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.unlockNode(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.ToggleLock,
label: "Toggle Lock",
description: "Toggle whether this node can be moved",
category: ActionCategory.Node,
icon: "lock",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.toggleLock(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.OpenContextMenu,
label: "Open Context Menu",
description: "Show the context menu for this node",
category: ActionCategory.Node,
icon: "more-vertical",
isBuiltIn: true,
handler: (context, helpers) => {
if (helpers.openContextMenu) {
helpers.openContextMenu(context.screenPosition, context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.CreateNode,
label: "Create Node",
description: "Create a new node at this position",
category: ActionCategory.Node,
icon: "plus",
isBuiltIn: true,
handler: async (context, helpers) => {
if (helpers.createNode) {
await helpers.createNode(context.worldPosition);
}
}
});
registerAction({
id: BuiltInActionId.SplitNode,
label: "Split Node",
description: "Split a node into two separate nodes",
category: ActionCategory.Node,
icon: "split",
isBuiltIn: true,
handler: async (context, helpers) => {
if (helpers.splitNode && context.nodeId) {
await helpers.splitNode(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.GroupNodes,
label: "Group Nodes",
description: "Group selected nodes into a parent container",
category: ActionCategory.Node,
icon: "group",
isBuiltIn: true,
handler: async (context, helpers) => {
if (helpers.groupNodes) {
await helpers.groupNodes(context.selectedNodeIds ?? helpers.getSelectedNodeIds());
}
}
});
registerAction({
id: BuiltInActionId.MergeNodes,
label: "Merge Nodes",
description: "Merge selected nodes into one",
category: ActionCategory.Node,
icon: "merge",
isBuiltIn: true,
handler: async (context, helpers) => {
if (helpers.mergeNodes) {
await helpers.mergeNodes(context.selectedNodeIds ?? helpers.getSelectedNodeIds());
}
}
});
}
// src/core/actions-viewport.ts
function registerViewportActions() {
registerAction({
id: BuiltInActionId.FitToView,
label: "Fit to View",
description: "Zoom and pan to fit this node in view",
category: ActionCategory.Viewport,
icon: "maximize-2",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.centerOnNode(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.FitAllToView,
label: "Fit All to View",
description: "Zoom and pan to fit all nodes in view",
category: ActionCategory.Viewport,
icon: "maximize",
isBuiltIn: true,
handler: (_context, helpers) => {
helpers.fitToBounds("graph");
}
});
registerAction({
id: BuiltInActionId.CenterOnNode,
label: "Center on Node",
description: "Center the viewport on this node",
category: ActionCategory.Viewport,
icon: "crosshair",
requiresNode: true,
isBuiltIn: true,
handler: (context, helpers) => {
if (context.nodeId) {
helpers.centerOnNode(context.nodeId);
}
}
});
registerAction({
id: BuiltInActionId.ResetViewport,
label: "Reset Viewport",
description: "Reset zoom to 100% and center on origin",
category: ActionCategory.Viewport,
icon: "home",
isBuiltIn: true,
handler: (_context, helpers) => {
helpers.resetViewport();
}
});
}
function registerHistoryActions() {
registerAction({
id: BuiltInActionId.Undo,
label: "Undo",
description: "Undo the last action",
category: ActionCategory.History,
icon: "undo-2",
isBuiltIn: true,
handler: (_context, helpers) => {
if (helpers.canUndo()) {
helpers.undo();
}
}
});
registerAction({
id: BuiltInActionId.Redo,
label: "Redo",
description: "Redo the last undone action",
category: ActionCategory.History,
icon: "redo-2",
isBuiltIn: true,
handler: (_context, helpers) => {
if (helpers.canRedo()) {
helpers.redo();
}
}
});
registerAction({
id: BuiltInActionId.ApplyForceLayout,
label: "Apply Force Layout",
description: "Automatically arrange nodes using force-directed layout",
category: ActionCategory.Layout,
icon: "layout-grid",
isBuiltIn: true,
handler: async (_context, helpers) => {
await helpers.applyForceLayout();
}
});
}
// src/core/built-in-actions.ts
function registerBuiltInActions() {
registerAction({
id: BuiltInActionId.None,
label: "None",
description: "Do nothing",
category: ActionCategory.None,
icon: "ban",
isBuiltIn: true,
handler: () => {
}
});
registerSelectionActions();
registerNodeActions();
registerViewportActions();
registerHistoryActions();
}
// src/core/action-registry.ts
var actionRegistry = /* @__PURE__ */ new Map();
function registerAction(action) {
actionRegistry.set(action.id, action);
}
function getAction(id) {
return actionRegistry.get(id);
}
function unregisterAction(id) {
return actionRegistry.delete(id);
}
registerBuiltInActions();
// src/core/locked-node-store.ts
import { atom as atom18 } from "jotai";
var lockedNodeIdAtom = atom18(null);
var lockedNodeDataAtom = atom18((get) => {
const id = get(lockedNodeIdAtom);
if (!id) return null;
const nodes = get(uiNodesAtom);
return nodes.find((n) => n.id === id) || null;
});
var lockedNodePageIndexAtom = atom18(0);
var lockedNodePageCountAtom = atom18(1);
var lockNodeAtom = atom18(null, (_get, set, payload) => {
set(lockedNodeIdAtom, payload.nodeId);
set(lockedNodePageIndexAtom, 0);
});
var unlockNodeAtom = atom18(null, (_get, set) => {
set(lockedNodeIdAtom, null);
});
var nextLockedPageAtom = atom18(null, (get, set) => {
const current = get(lockedNodePageIndexAtom);
const pageCount = get(lockedNodePageCountAtom);
set(lockedNodePageIndexAtom, (current + 1) % pageCount);
});
var prevLockedPageAtom = atom18(null, (get, set) => {
const current = get(lockedNodePageIndexAtom);
const pageCount = get(lockedNodePageCountAtom);
set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount);
});
var goToLockedPageAtom = atom18(null, (get, set, index) => {
const pageCount = get(lockedNodePageCountAtom);
if (index >= 0 && index < pageCount) {
set(lockedNodePageIndexAtom, index);
}
});
var hasLockedNodeAtom = atom18((get) => get(lockedNodeIdAtom) !== null);
// src/core/action-executor.ts
var debug13 = createDebug("actions");
async function executeAction(actionId, context, helpers) {
if (actionId === BuiltInActionId.None) {
return {
success: true,
actionId
};
}
const action = getAction(actionId);
if (!action) {
debug13.warn("Action not found: %s", actionId);
return {
success: false,
actionId,
error: new Error(`Action not found: ${actionId}`)
};
}
if (action.requiresNode && !context.nodeId) {
debug13.warn("Action %s requires a node context", actionId);
return {
success: false,
actionId,
error: new Error(`Action ${actionId} requires a node context`)
};
}
try {
const result = action.handler(context, helpers);
if (result instanceof Promise) {
await result;
}
return {
success: true,
actionId
};
} catch (error) {
debug13.error("Error executing action %s: %O", actionId, error);
return {
success: false,
actionId,
error: error instanceof Error ? error : new Error(String(error))
};
}
}
function buildActionHelpers(store, options = {}) {
return {
selectNode: (nodeId) => store.set(selectSingleNodeAtom, nodeId),
addToSelection: (nodeId) => store.set(addNodesToSelectionAtom, [nodeId]),
clearSelection: () => store.set(clearSelectionAtom),
getSelectedNodeIds: () => Array.from(store.get(selectedNodeIdsAtom)),
fitToBounds: (mode, padding) => {
const fitMode = mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection;
store.set(fitToBoundsAtom, {
mode: fitMode,
padding
});
},
centerOnNode: (nodeId) => store.set(centerOnNodeAtom, nodeId),
resetViewport: () => store.set(resetViewportAtom),
lockNode: (nodeId) => store.set(lockNodeAtom, {
nodeId
}),
unlockNode: (_nodeId) => store.set(unlockNodeAtom),
toggleLock: (nodeId) => {
const currentLockedId = store.get(lockedNodeIdAtom);
if (currentLockedId === nodeId) {
store.set(unlockNodeAtom);
} else {
store.set(lockNodeAtom, {
nodeId
});
}
},
deleteNode: async (nodeId) => {
if (options.onDeleteNode) {
await options.onDeleteNode(nodeId);
} else {
debug13.warn("deleteNode called but onDeleteNode callback not provided");
}
},
isNodeLocked: (nodeId) => store.get(lockedNodeIdAtom) === nodeId,
applyForceLayout: async () => {
if (options.onApplyForceLayout) {
await options.onApplyForceLayout();
} else {
debug13.warn("applyForceLayout called but onApplyForceLayout callback not provided");
}
},
undo: () => store.set(undoAtom),
redo: () => store.set(redoAtom),
canUndo: () => store.get(canUndoAtom),
canRedo: () => store.get(canRedoAtom),
selectEdge: (edgeId) => store.set(selectEdgeAtom, edgeId),
clearEdgeSelection: () => store.set(clearEdgeSelectionAtom),
openContextMenu: options.onOpenContextMenu,
createNode: options.onCreateNode
};
}
// src/hooks/useActionExecutor.ts
function useActionExecutor(t0) {
const $ = _c10(13);
const options = t0 === void 0 ? {} : t0;
const store = useStore2();
const mappings = useAtomValue10(eventMappingsAtom);
const helpers = buildActionHelpers(store, {
onDeleteNode: options.onDeleteNode,
onOpenContextMenu: options.onOpenContextMenu,
onCreateNode: options.onCreateNode,
onApplyForceLayout: options.onApplyForceLayout
});
let t1;
if ($[0] !== helpers) {
t1 = async (actionId, context) => executeAction(actionId, context, helpers);
$[0] = helpers;
$[1] = t1;
} else {
t1 = $[1];
}
const executeActionById = t1;
let t2;
if ($[2] !== helpers || $[3] !== mappings) {
t2 = async (event, context_0) => {
const actionId_0 = getActionForEvent(mappings, event);
return executeAction(actionId_0, context_0, helpers);
};
$[2] = helpers;
$[3] = mappings;
$[4] = t2;
} else {
t2 = $[4];
}
const executeEventAction = t2;
let t3;
if ($[5] !== mappings) {
t3 = (event_0) => getActionForEvent(mappings, event_0);
$[5] = mappings;
$[6] = t3;
} else {
t3 = $[6];
}
const getActionForEventFn = t3;
let t4;
if ($[7] !== executeActionById || $[8] !== executeEventAction || $[9] !== getActionForEventFn || $[10] !== helpers || $[11] !== mappings) {
t4 = {
executeActionById,
executeEventAction,
getActionForEvent: getActionForEventFn,
mappings,
helpers
};
$[7] = executeActionById;
$[8] = executeEventAction;
$[9] = getActionForEventFn;
$[10] = helpers;
$[11] = mappings;
$[12] = t4;
} else {
t4 = $[12];
}
return t4;
}
// src/hooks/useGestureResolver.ts
import { c as _c11 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue11 } from "jotai";
// src/core/gesture-rules-defaults.ts
function mergeRules(defaults, overrides) {
const overrideMap = new Map(overrides.map((r) => [r.id, r]));
const result = [];
for (const rule of defaults) {
const override = overrideMap.get(rule.id);
if (override) {
result.push(override);
overrideMap.delete(rule.id);
} else {
result.push(rule);
}
}
for (const rule of overrideMap.values()) {
result.push(rule);
}
return result;
}
var DEFAULT_GESTURE_RULES = [
// ── Tap gestures ──────────────────────────────────────────────
{
id: "tap-node",
pattern: {
gesture: "tap",
target: "node"
},
actionId: "select-node"
},
{
id: "tap-edge",
pattern: {
gesture: "tap",
target: "edge"
},
actionId: "select-edge"
},
{
id: "tap-port",
pattern: {
gesture: "tap",
target: "port"
},
actionId: "select-node"
},
{
id: "tap-bg",
pattern: {
gesture: "tap",
target: "background"
},
actionId: "clear-selection"
},
// ── Double-tap ────────────────────────────────────────────────
{
id: "dtap-node",
pattern: {
gesture: "double-tap",
target: "node"
},
actionId: "fit-to-view"
},
{
id: "dtap-bg",
pattern: {
gesture: "double-tap",
target: "background"
},
actionId: "fit-all-to-view"
},
// ── Triple-tap ────────────────────────────────────────────────
{
id: "ttap-node",
pattern: {
gesture: "triple-tap",
target: "node"
},
actionId: "toggle-lock"
},
// ── Left-button drag ──────────────────────────────────────────
{
id: "drag-node",
pattern: {
gesture: "drag",
target: "node"
},
actionId: "move-node"
},
{
id: "drag-port",
pattern: {
gesture: "drag",
target: "port"
},
actionId: "create-edge"
},
{
id: "drag-bg-finger",
pattern: {
gesture: "drag",
target: "background",
source: "finger"
},
actionId: "pan"
},
{
id: "drag-bg-mouse",
pattern: {
gesture: "drag",
target: "background",
source: "mouse"
},
actionId: "pan"
},
{
id: "drag-bg-pencil",
pattern: {
gesture: "drag",
target: "background",
source: "pencil"
},
actionId: "lasso-select"
},
// ── Shift+drag overrides ──────────────────────────────────────
{
id: "shift-drag-bg",
pattern: {
gesture: "drag",
target: "background",
modifiers: {
shift: true
}
},
actionId: "rect-select"
},
// ── Right-click tap (context menu) ────────────────────────────
{
id: "rc-node",
pattern: {
gesture: "tap",
target: "node",
button: 2
},
actionId: "open-context-menu"
},
{
id: "rc-edge",
pattern: {
gesture: "tap",
target: "edge",
button: 2
},
actionId: "open-context-menu"
},
{
id: "rc-bg",
pattern: {
gesture: "tap",
target: "background",
button: 2
},
actionId: "open-context-menu"
},
// ── Long-press ────────────────────────────────────────────────
{
id: "lp-node",
pattern: {
gesture: "long-press",
target: "node"
},
actionId: "open-context-menu"
},
{
id: "lp-bg-finger",
pattern: {
gesture: "long-press",
target: "background",
source: "finger"
},
actionId: "create-node"
},
// ── Right-button drag (defaults to none — consumers override) ─
{
id: "rdrag-node",
pattern: {
gesture: "drag",
target: "node",
button: 2
},
actionId: "none"
},
{
id: "rdrag-bg",
pattern: {
gesture: "drag",
target: "background",
button: 2
},
actionId: "none"
},
// ── Middle-button drag (defaults to none) ─────────────────────
{
id: "mdrag-node",
pattern: {
gesture: "drag",
target: "node",
button: 1
},
actionId: "none"
},
{
id: "mdrag-bg",
pattern: {
gesture: "drag",
target: "background",
button: 1
},
actionId: "none"
},
// ── Zoom ──────────────────────────────────────────────────────
{
id: "pinch-bg",
pattern: {
gesture: "pinch",
target: "background"
},
actionId: "zoom"
},
{
id: "scroll-any",
pattern: {
gesture: "scroll"
},
actionId: "zoom"
},
// ── Split ─────────────────────────────────────────────────────
{
id: "pinch-node",
pattern: {
gesture: "pinch",
target: "node"
},
actionId: "split-node"
}
];
// src/core/gesture-rules.ts
var MODIFIER_KEYS = ["shift", "ctrl", "alt", "meta"];
function matchSpecificity(pattern, desc) {
let score = 0;
if (pattern.gesture !== void 0) {
if (pattern.gesture !== desc.gesture) return -1;
score += 32;
}
if (pattern.target !== void 0) {
if (pattern.target !== desc.target) return -1;
score += 16;
}
if (pattern.source !== void 0) {
if (pattern.source !== desc.source) return -1;
score += 4;
}
if (pattern.button !== void 0) {
if (pattern.button !== (desc.button ?? 0)) return -1;
score += 2;
}
if (pattern.modifiers !== void 0) {
const dm = desc.modifiers ?? {};
for (const key of MODIFIER_KEYS) {
const required = pattern.modifiers[key];
if (required === void 0) continue;
const actual = dm[key] ?? false;
if (required !== actual) return -1;
score += 8;
}
}
return score;
}
var PALM_REJECTION_RULE = {
id: "__palm-rejection__",
pattern: {},
actionId: "none",
label: "Palm rejection"
};
function resolveGesture(desc, rules, options) {
const palmRejection = options?.palmRejection !== false;
if (palmRejection && desc.isStylusActive && desc.source === "finger") {
if (desc.gesture === "tap" || desc.gesture === "long-press" || desc.gesture === "double-tap" || desc.gesture === "triple-tap") {
return {
actionId: "none",
rule: PALM_REJECTION_RULE,
score: Infinity
};
}
if (desc.gesture === "drag" && desc.target !== "background") {
return resolveGesture({
...desc,
target: "background",
isStylusActive: false
}, rules, {
palmRejection: false
});
}
}
let best = null;
for (const rule of rules) {
const specificity = matchSpecificity(rule.pattern, desc);
if (specificity < 0) continue;
const effectiveScore = specificity * 1e3 + (rule.priority ?? 0);
if (!best || effectiveScore > best.score) {
best = {
actionId: rule.actionId,
rule,
score: effectiveScore
};
}
}
return best;
}
function buildRuleIndex(rules) {
const buckets = /* @__PURE__ */ new Map();
const wildcardRules = [];
for (const rule of rules) {
const key = rule.pattern.gesture;
if (key === void 0) {
wildcardRules.push(rule);
} else {
const bucket = buckets.get(key);
if (bucket) {
bucket.push(rule);
} else {
buckets.set(key, [rule]);
}
}
}
const index = /* @__PURE__ */ new Map();
if (wildcardRules.length > 0) {
for (const [key, bucket] of buckets) {
index.set(key, bucket.concat(wildcardRules));
}
index.set("__wildcard__", wildcardRules);
} else {
for (const [key, bucket] of buckets) {
index.set(key, bucket);
}
}
return index;
}
function resolveGestureIndexed(desc, index, options) {
const rules = index.get(desc.gesture) ?? index.get("__wildcard__") ?? [];
return resolveGesture(desc, rules, options);
}
// src/core/gesture-rule-store.ts
import { atom as atom19 } from "jotai";
import { atomWithStorage as atomWithStorage2 } from "jotai/utils";
var DEFAULT_RULE_STATE = {
customRules: [],
palmRejection: true
};
var gestureRuleSettingsAtom = atomWithStorage2("canvas-gesture-rules", DEFAULT_RULE_STATE);
var consumerGestureRulesAtom = atom19([]);
var gestureRulesAtom = atom19((get) => {
const settings = get(gestureRuleSettingsAtom);
const consumerRules = get(consumerGestureRulesAtom);
let rules = mergeRules(DEFAULT_GESTURE_RULES, settings.customRules);
if (consumerRules.length > 0) {
rules = mergeRules(rules, consumerRules);
}
return rules;
});
var gestureRuleIndexAtom = atom19((get) => {
return buildRuleIndex(get(gestureRulesAtom));
});
var palmRejectionEnabledAtom = atom19((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => {
const current = get(gestureRuleSettingsAtom);
set(gestureRuleSettingsAtom, {
...current,
palmRejection: enabled
});
});
var addGestureRuleAtom = atom19(null, (get, set, rule) => {
const current = get(gestureRuleSettingsAtom);
const existing = current.customRules.findIndex((r) => r.id === rule.id);
const newRules = [...current.customRules];
if (existing >= 0) {
newRules[existing] = rule;
} else {
newRules.push(rule);
}
set(gestureRuleSettingsAtom, {
...current,
customRules: newRules
});
});
var removeGestureRuleAtom = atom19(null, (get, set, ruleId) => {
const current = get(gestureRuleSettingsAtom);
set(gestureRuleSettingsAtom, {
...current,
customRules: current.customRules.filter((r) => r.id !== ruleId)
});
});
var updateGestureRuleAtom = atom19(null, (get, set, {
id,
updates
}) => {
const current = get(gestureRuleSettingsAtom);
const index = current.customRules.findIndex((r) => r.id === id);
if (index < 0) return;
const newRules = [...current.customRules];
newRules[index] = {
...newRules[index],
...updates
};
set(gestureRuleSettingsAtom, {
...current,
customRules: newRules
});
});
var resetGestureRulesAtom = atom19(null, (get, set) => {
const current = get(gestureRuleSettingsAtom);
set(gestureRuleSettingsAtom, {
...current,
customRules: []
});
});
// src/hooks/useGestureResolver.ts
function useGestureResolver() {
const $ = _c11(4);
const index = useAtomValue11(gestureRuleIndexAtom);
const palmRejection = useAtomValue11(palmRejectionEnabledAtom);
const isStylusActive = useAtomValue11(isStylusActiveAtom);
let t0;
if ($[0] !== index || $[1] !== isStylusActive || $[2] !== palmRejection) {
t0 = (desc) => resolveGestureIndexed({
...desc,
isStylusActive: desc.isStylusActive ?? isStylusActive
}, index, {
palmRejection
});
$[0] = index;
$[1] = isStylusActive;
$[2] = palmRejection;
$[3] = t0;
} else {
t0 = $[3];
}
return t0;
}
// src/hooks/useCommandLine.ts
import { c as _c12 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue12, useSetAtom as useSetAtom8 } from "jotai";
// src/commands/store.ts
import { atom as atom21 } from "jotai";
// src/commands/registry.ts
var CommandRegistry = class {
constructor() {
__publicField(this, "commands", /* @__PURE__ */ new Map());
__publicField(this, "aliases", /* @__PURE__ */ new Map());
}
// alias -> command name
/**
* Register a command with the registry.
* @param command The command definition to register
* @throws Error if command name or alias already exists
*/
register(command) {
if (this.commands.has(command.name)) {
throw new Error(`Command "${command.name}" is already registered`);
}
this.commands.set(command.name, command);
if (command.aliases) {
for (const alias of command.aliases) {
if (this.aliases.has(alias)) {
throw new Error(`Alias "${alias}" is already registered for command "${this.aliases.get(alias)}"`);
}
if (this.commands.has(alias)) {
throw new Error(`Alias "${alias}" conflicts with existing command name`);
}
this.aliases.set(alias, command.name);
}
}
}
/**
* Unregister a command by name.
* @param name The command name to remove
*/
unregister(name) {
const command = this.commands.get(name);
if (command) {
if (command.aliases) {
for (const alias of command.aliases) {
this.aliases.delete(alias);
}
}
this.commands.delete(name);
}
}
/**
* Get a command by name or alias.
* @param nameOrAlias Command name or alias
* @returns The command definition or undefined if not found
*/
get(nameOrAlias) {
const direct = this.commands.get(nameOrAlias);
if (direct) return direct;
const commandName = this.aliases.get(nameOrAlias);
if (commandName) {
return this.commands.get(commandName);
}
return void 0;
}
/**
* Check if a command exists by name or alias.
* @param nameOrAlias Command name or alias
*/
has(nameOrAlias) {
return this.commands.has(nameOrAlias) || this.aliases.has(nameOrAlias);
}
/**
* Search for commands matching a query.
* Searches command names, aliases, and descriptions.
* @param query Search query (case-insensitive)
* @returns Array of matching commands, sorted by relevance
*/
search(query) {
if (!query.trim()) {
return this.all();
}
const lowerQuery = query.toLowerCase().trim();
const results = [];
const commands = Array.from(this.commands.values());
for (const command of commands) {
let score = 0;
if (command.name.toLowerCase() === lowerQuery) {
score = 100;
} else if (command.name.toLowerCase().startsWith(lowerQuery)) {
score = 80;
} else if (command.name.toLowerCase().includes(lowerQuery)) {
score = 60;
} else if (command.aliases?.some((a) => a.toLowerCase() === lowerQuery)) {
score = 90;
} else if (command.aliases?.some((a) => a.toLowerCase().startsWith(lowerQuery))) {
score = 70;
} else if (command.aliases?.some((a) => a.toLowerCase().includes(lowerQuery))) {
score = 50;
} else if (command.description.toLowerCase().includes(lowerQuery)) {
score = 30;
}
if (score > 0) {
results.push({
command,
score
});
}
}
return results.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name)).map((r) => r.command);
}
/**
* Get all registered commands.
* @returns Array of all commands, sorted alphabetically by name
*/
all() {
return Array.from(this.commands.values()).sort((a, b) => a.name.localeCompare(b.name));
}
/**
* Get commands by category.
* @param category The category to filter by
* @returns Array of commands in the category
*/
byCategory(category) {
return this.all().filter((cmd) => cmd.category === category);
}
/**
* Get all available categories.
* @returns Array of unique categories
*/
categories() {
const categories = /* @__PURE__ */ new Set();
const commands = Array.from(this.commands.values());
for (const command of commands) {
categories.add(command.category);
}
return Array.from(categories).sort();
}
/**
* Get the count of registered commands.
*/
get size() {
return this.commands.size;
}
/**
* Clear all registered commands.
* Useful for testing.
*/
clear() {
this.commands.clear();
this.aliases.clear();
}
/**
* Get a serializable list of commands for API responses.
*/
toJSON() {
return this.all().map((cmd) => ({
name: cmd.name,
aliases: cmd.aliases || [],
description: cmd.description,
category: cmd.category,
inputs: cmd.inputs.map((input) => ({
name: input.name,
type: input.type,
prompt: input.prompt,
required: input.required !== false
}))
}));
}
};
var commandRegistry = new CommandRegistry();
// src/commands/store-atoms.ts
import { atom as atom20 } from "jotai";
import { atomWithStorage as atomWithStorage3 } from "jotai/utils";
var inputModeAtom = atom20({
type: "normal"
});
var commandLineVisibleAtom = atom20(false);
var commandLineStateAtom = atom20({
phase: "idle"
});
var commandFeedbackAtom = atom20(null);
var commandHistoryAtom = atomWithStorage3("canvas-command-history", []);
var selectedSuggestionIndexAtom = atom20(0);
var pendingInputResolverAtom = atom20(null);
var isCommandActiveAtom = atom20((get) => {
const state = get(commandLineStateAtom);
return state.phase === "collecting" || state.phase === "executing";
});
var currentInputAtom = atom20((get) => {
const state = get(commandLineStateAtom);
if (state.phase !== "collecting") return null;
return state.command.inputs[state.inputIndex];
});
var commandProgressAtom = atom20((get) => {
const state = get(commandLineStateAtom);
if (state.phase !== "collecting") return null;
return {
current: state.inputIndex + 1,
total: state.command.inputs.length
};
});
// src/commands/store.ts
var openCommandLineAtom = atom21(null, (get, set) => {
set(commandLineVisibleAtom, true);
set(commandLineStateAtom, {
phase: "searching",
query: "",
suggestions: commandRegistry.all()
});
set(selectedSuggestionIndexAtom, 0);
});
var closeCommandLineAtom = atom21(null, (get, set) => {
set(commandLineVisibleAtom, false);
set(commandLineStateAtom, {
phase: "idle"
});
set(inputModeAtom, {
type: "normal"
});
set(commandFeedbackAtom, null);
set(pendingInputResolverAtom, null);
});
var updateSearchQueryAtom = atom21(null, (get, set, query) => {
const suggestions = commandRegistry.search(query);
set(commandLineStateAtom, {
phase: "searching",
query,
suggestions
});
set(selectedSuggestionIndexAtom, 0);
});
var selectCommandAtom = atom21(null, (get, set, command) => {
const history = get(commandHistoryAtom);
const newHistory = [command.name, ...history.filter((h) => h !== command.name)].slice(0, 50);
set(commandHistoryAtom, newHistory);
if (command.inputs.length === 0) {
set(commandLineStateAtom, {
phase: "executing",
command
});
return;
}
set(commandLineStateAtom, {
phase: "collecting",
command,
inputIndex: 0,
collected: {}
});
const firstInput = command.inputs[0];
set(inputModeAtom, inputDefToMode(firstInput));
});
var provideInputAtom = atom21(null, (get, set, value) => {
const state = get(commandLineStateAtom);
if (state.phase !== "collecting") return;
const {
command,
inputIndex,
collected
} = state;
const currentInput = command.inputs[inputIndex];
if (currentInput.validate) {
const result = currentInput.validate(value, collected);
if (result !== true) {
set(commandLineStateAtom, {
phase: "error",
message: typeof result === "string" ? result : `Invalid value for ${currentInput.name}`
});
return;
}
}
const newCollected = {
...collected,
[currentInput.name]: value
};
if (inputIndex < command.inputs.length - 1) {
const nextInputIndex = inputIndex + 1;
const nextInput = command.inputs[nextInputIndex];
set(commandLineStateAtom, {
phase: "collecting",
command,
inputIndex: nextInputIndex,
collected: newCollected
});
set(inputModeAtom, inputDefToMode(nextInput, newCollected));
if (command.feedback) {
const feedback = command.feedback(newCollected, nextInput);
if (feedback) {
const feedbackState = {
hoveredNodeId: feedback.highlightNodeId,
ghostNode: feedback.ghostNode,
crosshair: feedback.crosshair,
// Handle previewEdge conversion - toCursor variant needs cursorWorldPos
previewEdge: feedback.previewEdge && "to" in feedback.previewEdge ? {
from: feedback.previewEdge.from,
to: feedback.previewEdge.to
} : void 0
};
set(commandFeedbackAtom, feedbackState);
} else {
set(commandFeedbackAtom, null);
}
}
} else {
set(commandLineStateAtom, {
phase: "collecting",
command,
inputIndex,
collected: newCollected
});
set(inputModeAtom, {
type: "normal"
});
}
});
var skipInputAtom = atom21(null, (get, set) => {
const state = get(commandLineStateAtom);
if (state.phase !== "collecting") return;
const {
command,
inputIndex
} = state;
const currentInput = command.inputs[inputIndex];
if (currentInput.required !== false) {
return;
}
const value = currentInput.default;
set(provideInputAtom, value);
});
var goBackInputAtom = atom21(null, (get, set) => {
const state = get(commandLineStateAtom);
if (state.phase !== "collecting") return;
const {
command,
inputIndex,
collected
} = state;
if (inputIndex === 0) {
set(commandLineStateAtom, {
phase: "searching",
query: command.name,
suggestions: [command]
});
set(inputModeAtom, {
type: "normal"
});
return;
}
const prevInputIndex = inputIndex - 1;
const prevInput = command.inputs[prevInputIndex];
const newCollected = {
...collected
};
delete newCollected[prevInput.name];
set(commandLineStateAtom, {
phase: "collecting",
command,
inputIndex: prevInputIndex,
collected: newCollected
});
set(inputModeAtom, inputDefToMode(prevInput, newCollected));
});
var setCommandErrorAtom = atom21(null, (get, set, message) => {
set(commandLineStateAtom, {
phase: "error",
message
});
set(inputModeAtom, {
type: "normal"
});
});
var clearCommandErrorAtom = atom21(null, (get, set) => {
set(commandLineStateAtom, {
phase: "idle"
});
});
function inputDefToMode(input, collected) {
switch (input.type) {
case "point":
return {
type: "pickPoint",
prompt: input.prompt,
snapToGrid: input.snapToGrid
};
case "node":
return {
type: "pickNode",
prompt: input.prompt,
filter: input.filter ? (node) => input.filter(node, collected || {}) : void 0
};
case "nodes":
return {
type: "pickNodes",
prompt: input.prompt,
filter: input.filter ? (node) => input.filter(node, collected || {}) : void 0
};
case "select":
return {
type: "select",
prompt: input.prompt,
options: input.options || []
};
case "text":
case "number":
case "color":
case "boolean":
default:
return {
type: "text",
prompt: input.prompt
};
}
}
// src/hooks/useCommandLine.ts
function useCommandLine() {
const $ = _c12(16);
const visible = useAtomValue12(commandLineVisibleAtom);
const state = useAtomValue12(commandLineStateAtom);
const history = useAtomValue12(commandHistoryAtom);
const currentInput = useAtomValue12(currentInputAtom);
const progress = useAtomValue12(commandProgressAtom);
const open = useSetAtom8(openCommandLineAtom);
const close = useSetAtom8(closeCommandLineAtom);
const updateQuery = useSetAtom8(updateSearchQueryAtom);
const selectCommand = useSetAtom8(selectCommandAtom);
const t0 = state.phase === "searching";
const t1 = state.phase === "collecting";
const t2 = state.phase === "executing";
const t3 = state.phase === "error";
const t4 = state.phase === "error" ? state.message : null;
let t5;
if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t5 = commandRegistry.all();
$[0] = t5;
} else {
t5 = $[0];
}
let t6;
if ($[1] !== close || $[2] !== currentInput || $[3] !== history || $[4] !== open || $[5] !== progress || $[6] !== selectCommand || $[7] !== state || $[8] !== t0 || $[9] !== t1 || $[10] !== t2 || $[11] !== t3 || $[12] !== t4 || $[13] !== updateQuery || $[14] !== visible) {
t6 = {
visible,
state,
history,
currentInput,
progress,
open,
close,
updateQuery,
selectCommand,
isSearching: t0,
isCollecting: t1,
isExecuting: t2,
hasError: t3,
errorMessage: t4,
allCommands: t5,
searchCommands: _temp5
};
$[1] = close;
$[2] = currentInput;
$[3] = history;
$[4] = open;
$[5] = progress;
$[6] = selectCommand;
$[7] = state;
$[8] = t0;
$[9] = t1;
$[10] = t2;
$[11] = t3;
$[12] = t4;
$[13] = updateQuery;
$[14] = visible;
$[15] = t6;
} else {
t6 = $[15];
}
return t6;
}
function _temp5(query) {
return commandRegistry.search(query);
}
// src/hooks/useVirtualization.ts
import { c as _c13 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue13, useSetAtom as useSetAtom9 } from "jotai";
// src/core/virtualization-store.ts
import { atom as atom22 } from "jotai";
// src/core/spatial-index.ts
var SpatialGrid = class {
constructor(cellSize = 500) {
/** cell key → set of node IDs in that cell */
__publicField(this, "cells", /* @__PURE__ */ new Map());
/** node ID → entry data (for update/remove) */
__publicField(this, "entries", /* @__PURE__ */ new Map());
this.cellSize = cellSize;
}
/** Number of tracked entries */
get size() {
return this.entries.size;
}
cellKey(cx, cy) {
return `${cx},${cy}`;
}
getCellRange(x, y, w, h) {
const cs = this.cellSize;
return {
minCX: Math.floor(x / cs),
minCY: Math.floor(y / cs),
maxCX: Math.floor((x + w) / cs),
maxCY: Math.floor((y + h) / cs)
};
}
/**
* Insert a node into the index.
* If the node already exists, it is updated.
*/
insert(id, x, y, width, height) {
if (this.entries.has(id)) {
this.update(id, x, y, width, height);
return;
}
const entry = {
id,
x,
y,
width,
height
};
this.entries.set(id, entry);
const {
minCX,
minCY,
maxCX,
maxCY
} = this.getCellRange(x, y, width, height);
for (let cx = minCX; cx <= maxCX; cx++) {
for (let cy = minCY; cy <= maxCY; cy++) {
const key = this.cellKey(cx, cy);
let cell = this.cells.get(key);
if (!cell) {
cell = /* @__PURE__ */ new Set();
this.cells.set(key, cell);
}
cell.add(id);
}
}
}
/**
* Update a node's position/dimensions.
*/
update(id, x, y, width, height) {
const prev = this.entries.get(id);
if (!prev) {
this.insert(id, x, y, width, height);
return;
}
const prevRange = this.getCellRange(prev.x, prev.y, prev.width, prev.height);
const newRange = this.getCellRange(x, y, width, height);
prev.x = x;
prev.y = y;
prev.width = width;
prev.height = height;
if (prevRange.minCX === newRange.minCX && prevRange.minCY === newRange.minCY && prevRange.maxCX === newRange.maxCX && prevRange.maxCY === newRange.maxCY) {
return;
}
for (let cx = prevRange.minCX; cx <= prevRange.maxCX; cx++) {
for (let cy = prevRange.minCY; cy <= prevRange.maxCY; cy++) {
const key = this.cellKey(cx, cy);
const cell = this.cells.get(key);
if (cell) {
cell.delete(id);
if (cell.size === 0) this.cells.delete(key);
}
}
}
for (let cx = newRange.minCX; cx <= newRange.maxCX; cx++) {
for (let cy = newRange.minCY; cy <= newRange.maxCY; cy++) {
const key = this.cellKey(cx, cy);
let cell = this.cells.get(key);
if (!cell) {
cell = /* @__PURE__ */ new Set();
this.cells.set(key, cell);
}
cell.add(id);
}
}
}
/**
* Remove a node from the index.
*/
remove(id) {
const entry = this.entries.get(id);
if (!entry) return;
const {
minCX,
minCY,
maxCX,
maxCY
} = this.getCellRange(entry.x, entry.y, entry.width, entry.height);
for (let cx = minCX; cx <= maxCX; cx++) {
for (let cy = minCY; cy <= maxCY; cy++) {
const key = this.cellKey(cx, cy);
const cell = this.cells.get(key);
if (cell) {
cell.delete(id);
if (cell.size === 0) this.cells.delete(key);
}
}
}
this.entries.delete(id);
}
/**
* Query all node IDs whose bounding box overlaps the given bounds.
* Returns a Set for O(1) membership checks.
*/
query(bounds) {
const result = /* @__PURE__ */ new Set();
const {
minCX,
minCY,
maxCX,
maxCY
} = this.getCellRange(bounds.minX, bounds.minY, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY);
for (let cx = minCX; cx <= maxCX; cx++) {
for (let cy = minCY; cy <= maxCY; cy++) {
const cell = this.cells.get(this.cellKey(cx, cy));
if (!cell) continue;
for (const id of cell) {
if (result.has(id)) continue;
const entry = this.entries.get(id);
const entryRight = entry.x + entry.width;
const entryBottom = entry.y + entry.height;
if (entry.x <= bounds.maxX && entryRight >= bounds.minX && entry.y <= bounds.maxY && entryBottom >= bounds.minY) {
result.add(id);
}
}
}
}
return result;
}
/**
* Clear all entries.
*/
clear() {
this.cells.clear();
this.entries.clear();
}
/**
* Check if a node is tracked.
*/
has(id) {
return this.entries.has(id);
}
};
// src/core/virtualization-store.ts
var VIRTUALIZATION_BUFFER = 200;
var spatialIndexAtom = atom22((get) => {
get(graphUpdateVersionAtom);
get(nodePositionUpdateCounterAtom);
const graph = get(graphAtom);
const grid = new SpatialGrid(500);
graph.forEachNode((nodeId, attrs) => {
const a = attrs;
grid.insert(nodeId, a.x, a.y, a.width || 200, a.height || 100);
});
return grid;
});
var visibleBoundsAtom = atom22((get) => {
const viewport = get(viewportRectAtom);
const pan = get(panAtom);
const zoom = get(zoomAtom);
if (!viewport || zoom === 0) {
return null;
}
const buffer = VIRTUALIZATION_BUFFER;
return {
minX: (-buffer - pan.x) / zoom,
minY: (-buffer - pan.y) / zoom,
maxX: (viewport.width + buffer - pan.x) / zoom,
maxY: (viewport.height + buffer - pan.y) / zoom
};
});
var visibleNodeKeysAtom = atom22((get) => {
const end = canvasMark("virtualization-cull");
const enabled = get(virtualizationEnabledAtom);
const allKeys = get(nodeKeysAtom);
if (!enabled) {
end();
return allKeys;
}
const bounds = get(visibleBoundsAtom);
if (!bounds) {
end();
return allKeys;
}
const grid = get(spatialIndexAtom);
const visibleSet = grid.query(bounds);
const result = allKeys.filter((k) => visibleSet.has(k));
end();
return result;
});
var visibleEdgeKeysAtom = atom22((get) => {
const enabled = get(virtualizationEnabledAtom);
const allEdgeKeys = get(edgeKeysAtom);
const edgeCreation = get(edgeCreationAtom);
const remap = get(collapsedEdgeRemapAtom);
const tempEdgeKey = edgeCreation.isCreating ? "temp-creating-edge" : null;
get(graphUpdateVersionAtom);
const graph = get(graphAtom);
const filteredEdges = allEdgeKeys.filter((edgeKey) => {
const source = graph.source(edgeKey);
const target = graph.target(edgeKey);
const effectiveSource = remap.get(source) ?? source;
const effectiveTarget = remap.get(target) ?? target;
if (effectiveSource === effectiveTarget) return false;
return true;
});
if (!enabled) {
return tempEdgeKey ? [...filteredEdges, tempEdgeKey] : filteredEdges;
}
const visibleNodeKeys = get(visibleNodeKeysAtom);
const visibleNodeSet = new Set(visibleNodeKeys);
const visibleEdges = filteredEdges.filter((edgeKey) => {
const source = graph.source(edgeKey);
const target = graph.target(edgeKey);
const effectiveSource = remap.get(source) ?? source;
const effectiveTarget = remap.get(target) ?? target;
return visibleNodeSet.has(effectiveSource) && visibleNodeSet.has(effectiveTarget);
});
return tempEdgeKey ? [...visibleEdges, tempEdgeKey] : visibleEdges;
});
var virtualizationMetricsAtom = atom22((get) => {
const enabled = get(virtualizationEnabledAtom);
const totalNodes = get(nodeKeysAtom).length;
const totalEdges = get(edgeKeysAtom).length;
const visibleNodes = get(visibleNodeKeysAtom).length;
const visibleEdges = get(visibleEdgeKeysAtom).length;
const bounds = get(visibleBoundsAtom);
return {
enabled,
totalNodes,
totalEdges,
visibleNodes,
visibleEdges,
culledNodes: totalNodes - visibleNodes,
culledEdges: totalEdges - visibleEdges,
bounds
};
});
// src/hooks/useVirtualization.ts
function useVirtualization() {
const $ = _c13(8);
const metrics = useAtomValue13(virtualizationMetricsAtom);
const setEnabled = useSetAtom9(setVirtualizationEnabledAtom);
const toggle = useSetAtom9(toggleVirtualizationAtom);
let t0;
let t1;
if ($[0] !== setEnabled) {
t0 = () => setEnabled(true);
t1 = () => setEnabled(false);
$[0] = setEnabled;
$[1] = t0;
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
let t2;
if ($[3] !== metrics || $[4] !== t0 || $[5] !== t1 || $[6] !== toggle) {
t2 = {
...metrics,
enable: t0,
disable: t1,
toggle
};
$[3] = metrics;
$[4] = t0;
$[5] = t1;
$[6] = toggle;
$[7] = t2;
} else {
t2 = $[7];
}
return t2;
}
// src/hooks/useTapGesture.ts
import { c as _c14 } from "react/compiler-runtime";
import { useRef as useRef4 } from "react";
function useTapGesture(t0) {
const $ = _c14(12);
let t1;
if ($[0] !== t0) {
t1 = t0 === void 0 ? {} : t0;
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
const options = t1;
const {
onSingleTap,
onDoubleTap,
onTripleTap,
tapDelay: t2,
tapDistance: t3
} = options;
const tapDelay = t2 === void 0 ? 300 : t2;
const tapDistance = t3 === void 0 ? 25 : t3;
let t4;
if ($[2] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t4 = {
count: 0,
timer: null,
lastX: 0,
lastY: 0,
lastEvent: null
};
$[2] = t4;
} else {
t4 = $[2];
}
const stateRef = useRef4(t4);
let t5;
if ($[3] !== onDoubleTap || $[4] !== onSingleTap || $[5] !== onTripleTap || $[6] !== tapDelay || $[7] !== tapDistance) {
t5 = (event) => {
const state = stateRef.current;
const dx = Math.abs(event.clientX - state.lastX);
const dy = Math.abs(event.clientY - state.lastY);
const isSameSpot = state.count === 0 || dx < tapDistance && dy < tapDistance;
if (!isSameSpot) {
if (state.timer) {
clearTimeout(state.timer);
}
state.count = 0;
}
state.count = state.count + 1;
state.lastX = event.clientX;
state.lastY = event.clientY;
state.lastEvent = event;
if (state.timer) {
clearTimeout(state.timer);
}
if (state.count >= 3) {
onTripleTap?.(event);
state.count = 0;
state.timer = null;
return;
}
if (state.count === 2) {
onDoubleTap?.(event);
state.timer = setTimeout(() => {
state.count = 0;
state.timer = null;
}, tapDelay);
return;
}
state.timer = setTimeout(() => {
if (state.count === 1 && state.lastEvent) {
onSingleTap?.(state.lastEvent);
}
state.count = 0;
state.timer = null;
}, tapDelay);
};
$[3] = onDoubleTap;
$[4] = onSingleTap;
$[5] = onTripleTap;
$[6] = tapDelay;
$[7] = tapDistance;
$[8] = t5;
} else {
t5 = $[8];
}
const handleTap = t5;
let t6;
if ($[9] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t6 = () => {
const state_0 = stateRef.current;
if (state_0.timer) {
clearTimeout(state_0.timer);
state_0.timer = null;
}
state_0.count = 0;
};
$[9] = t6;
} else {
t6 = $[9];
}
const cleanup = t6;
let t7;
if ($[10] !== handleTap) {
t7 = {
handleTap,
cleanup
};
$[10] = handleTap;
$[11] = t7;
} else {
t7 = $[11];
}
return t7;
}
// src/hooks/useArrowKeyNavigation.ts
import { c as _c15 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue14 } from "jotai";
function useArrowKeyNavigation() {
const $ = _c15(2);
const focusedNodeId = useAtomValue14(focusedNodeIdAtom);
let t0;
if ($[0] !== focusedNodeId) {
t0 = {
focusedNodeId
};
$[0] = focusedNodeId;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// src/hooks/useCanvasGraph.ts
import { c as _c16 } from "react/compiler-runtime";
import { useAtomValue as useAtomValue15 } from "jotai";
function useCanvasGraph() {
const $ = _c16(9);
const graph = useAtomValue15(graphAtom);
const nodeKeys = useAtomValue15(nodeKeysAtom);
const edgeKeys = useAtomValue15(edgeKeysAtom);
let t0;
if ($[0] !== graph) {
t0 = (id) => graph.hasNode(id) ? graph.getNodeAttributes(id) : void 0;
$[0] = graph;
$[1] = t0;
} else {
t0 = $[1];
}
const getNode = t0;
let t1;
if ($[2] !== graph) {
t1 = (id_0) => graph.hasEdge(id_0) ? graph.getEdgeAttributes(id_0) : void 0;
$[2] = graph;
$[3] = t1;
} else {
t1 = $[3];
}
const getEdge = t1;
let t2;
if ($[4] !== edgeKeys || $[5] !== getEdge || $[6] !== getNode || $[7] !== nodeKeys) {
t2 = {
nodeCount: nodeKeys.length,
edgeCount: edgeKeys.length,
nodeKeys,
edgeKeys,
getNode,
getEdge
};
$[4] = edgeKeys;
$[5] = getEdge;
$[6] = getNode;
$[7] = nodeKeys;
$[8] = t2;
} else {
t2 = $[8];
}
return t2;
}
// src/hooks/useZoomTransition.ts
import { c as _c17 } from "react/compiler-runtime";
import { useEffect as useEffect2, useRef as useRef5 } from "react";
import { useAtomValue as useAtomValue16, useSetAtom as useSetAtom10 } from "jotai";
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function useZoomTransition() {
const $ = _c17(15);
const target = useAtomValue16(zoomAnimationTargetAtom);
const setZoom = useSetAtom10(zoomAtom);
const setPan = useSetAtom10(panAtom);
const setTarget = useSetAtom10(zoomAnimationTargetAtom);
const setProgress = useSetAtom10(zoomTransitionProgressAtom);
const setFocusNode = useSetAtom10(zoomFocusNodeIdAtom);
const progress = useAtomValue16(zoomTransitionProgressAtom);
const rafRef = useRef5(null);
let t0;
if ($[0] !== setFocusNode || $[1] !== setProgress || $[2] !== setTarget) {
t0 = () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
setTarget(null);
setProgress(0);
setFocusNode(null);
};
$[0] = setFocusNode;
$[1] = setProgress;
$[2] = setTarget;
$[3] = t0;
} else {
t0 = $[3];
}
const cancel = t0;
let t1;
let t2;
if ($[4] !== setPan || $[5] !== setProgress || $[6] !== setTarget || $[7] !== setZoom || $[8] !== target) {
t1 = () => {
if (!target) {
return;
}
const animate = () => {
const elapsed = performance.now() - target.startTime;
const rawT = Math.min(1, elapsed / target.duration);
const t = easeInOutCubic(rawT);
const currentZoom = target.startZoom + (target.targetZoom - target.startZoom) * t;
const currentPanX = target.startPan.x + (target.targetPan.x - target.startPan.x) * t;
const currentPanY = target.startPan.y + (target.targetPan.y - target.startPan.y) * t;
setZoom(currentZoom);
setPan({
x: currentPanX,
y: currentPanY
});
setProgress(t);
if (rawT < 1) {
rafRef.current = requestAnimationFrame(animate);
} else {
rafRef.current = null;
setTarget(null);
}
};
rafRef.current = requestAnimationFrame(animate);
return () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
};
t2 = [target, setZoom, setPan, setTarget, setProgress];
$[4] = setPan;
$[5] = setProgress;
$[6] = setTarget;
$[7] = setZoom;
$[8] = target;
$[9] = t1;
$[10] = t2;
} else {
t1 = $[9];
t2 = $[10];
}
useEffect2(t1, t2);
const t3 = target !== null;
let t4;
if ($[11] !== cancel || $[12] !== progress || $[13] !== t3) {
t4 = {
isAnimating: t3,
progress,
cancel
};
$[11] = cancel;
$[12] = progress;
$[13] = t3;
$[14] = t4;
} else {
t4 = $[14];
}
return t4;
}
// src/hooks/useSplitGesture.ts
import { c as _c18 } from "react/compiler-runtime";
import { useRef as useRef6 } from "react";
import { useAtomValue as useAtomValue17, useSetAtom as useSetAtom11 } from "jotai";
var SPLIT_THRESHOLD = 80;
function useSplitGesture(nodeId) {
const $ = _c18(9);
const splitNode = useSetAtom11(splitNodeAtom);
const screenToWorld = useAtomValue17(screenToWorldAtom);
let t0;
if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t0 = /* @__PURE__ */ new Map();
$[0] = t0;
} else {
t0 = $[0];
}
const pointersRef = useRef6(t0);
const initialDistanceRef = useRef6(null);
const splitFiredRef = useRef6(false);
const getDistance = _temp6;
let t1;
if ($[1] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t1 = (e) => {
if (e.pointerType !== "touch") {
return;
}
pointersRef.current.set(e.pointerId, {
pointerId: e.pointerId,
x: e.clientX,
y: e.clientY
});
if (pointersRef.current.size === 2) {
const [p1, p2] = Array.from(pointersRef.current.values());
initialDistanceRef.current = getDistance(p1, p2);
splitFiredRef.current = false;
}
};
$[1] = t1;
} else {
t1 = $[1];
}
const onPointerDown = t1;
let t2;
if ($[2] !== nodeId || $[3] !== screenToWorld || $[4] !== splitNode) {
t2 = (e_0) => {
if (e_0.pointerType !== "touch") {
return;
}
if (!pointersRef.current.has(e_0.pointerId)) {
return;
}
pointersRef.current.set(e_0.pointerId, {
pointerId: e_0.pointerId,
x: e_0.clientX,
y: e_0.clientY
});
if (pointersRef.current.size === 2 && initialDistanceRef.current !== null && !splitFiredRef.current) {
const [p1_0, p2_0] = Array.from(pointersRef.current.values());
const currentDistance = getDistance(p1_0, p2_0);
const delta = currentDistance - initialDistanceRef.current;
if (delta > SPLIT_THRESHOLD) {
splitFiredRef.current = true;
e_0.stopPropagation();
const world1 = screenToWorld(p1_0.x, p1_0.y);
const world2 = screenToWorld(p2_0.x, p2_0.y);
splitNode({
nodeId,
position1: world1,
position2: world2
});
}
}
};
$[2] = nodeId;
$[3] = screenToWorld;
$[4] = splitNode;
$[5] = t2;
} else {
t2 = $[5];
}
const onPointerMove = t2;
let t3;
if ($[6] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t3 = (e_1) => {
pointersRef.current.delete(e_1.pointerId);
if (pointersRef.current.size < 2) {
initialDistanceRef.current = null;
splitFiredRef.current = false;
}
};
$[6] = t3;
} else {
t3 = $[6];
}
const onPointerUp = t3;
let t4;
if ($[7] !== onPointerMove) {
t4 = {
onPointerDown,
onPointerMove,
onPointerUp
};
$[7] = onPointerMove;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
}
function _temp6(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
// src/hooks/useAnimatedLayout.ts
import { useAtomValue as useAtomValue18, useSetAtom as useSetAtom12 } from "jotai";
import { useRef as useRef7 } from "react";
var debug14 = createDebug("animated-layout");
function easeInOutCubic2(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function useAnimatedLayout(options = {}) {
const {
onPositionsChanged,
duration = 400
} = options;
const graph = useAtomValue18(graphAtom);
const updateNodePosition = useSetAtom12(updateNodePositionAtom);
const pushHistory = useSetAtom12(pushHistoryAtom);
const setPositionCounter = useSetAtom12(nodePositionUpdateCounterAtom);
const reducedMotion = useAtomValue18(prefersReducedMotionAtom);
const isAnimatingRef = useRef7(false);
const animate = async (targets, label) => {
if (isAnimatingRef.current) return;
if (targets.size === 0) return;
if (label) pushHistory(label);
isAnimatingRef.current = true;
if (reducedMotion) {
for (const [nodeId, target] of targets) {
updateNodePosition({
nodeId,
position: target
});
}
isAnimatingRef.current = false;
setPositionCounter((c) => c + 1);
if (onPositionsChanged) {
const updates = [];
for (const [nodeId_0, target_0] of targets) {
updates.push({
nodeId: nodeId_0,
position: target_0
});
}
Promise.resolve(onPositionsChanged(updates)).catch((err) => debug14.error("Position change callback failed: %O", err));
}
return;
}
const starts = /* @__PURE__ */ new Map();
for (const [nodeId_1] of targets) {
if (graph.hasNode(nodeId_1)) {
const attrs = graph.getNodeAttributes(nodeId_1);
starts.set(nodeId_1, {
x: attrs.x,
y: attrs.y
});
}
}
return new Promise((resolve) => {
const startTime = performance.now();
function tick() {
const elapsed = performance.now() - startTime;
const t = Math.min(elapsed / duration, 1);
const eased = easeInOutCubic2(t);
for (const [nodeId_2, target_1] of targets) {
const start = starts.get(nodeId_2);
if (!start) continue;
const x = Math.round(start.x + (target_1.x - start.x) * eased);
const y = Math.round(start.y + (target_1.y - start.y) * eased);
updateNodePosition({
nodeId: nodeId_2,
position: {
x,
y
}
});
}
if (t < 1) {
requestAnimationFrame(tick);
} else {
isAnimatingRef.current = false;
setPositionCounter((c_0) => c_0 + 1);
if (onPositionsChanged) {
const updates_0 = [];
for (const [nodeId_3, target_2] of targets) {
updates_0.push({
nodeId: nodeId_3,
position: target_2
});
}
Promise.resolve(onPositionsChanged(updates_0)).catch((err_0) => debug14.error("Position change callback failed: %O", err_0));
}
resolve();
}
}
requestAnimationFrame(tick);
});
};
return {
animate,
isAnimating: isAnimatingRef.current
};
}
// src/hooks/useTreeLayout.ts
import { useAtomValue as useAtomValue19 } from "jotai";
import { useRef as useRef8 } from "react";
function useTreeLayout(options = {}) {
const {
direction = "top-down",
levelGap = 200,
nodeGap = 100,
...animateOptions
} = options;
const graph = useAtomValue19(graphAtom);
const nodes = useAtomValue19(uiNodesAtom);
const {
animate,
isAnimating
} = useAnimatedLayout(animateOptions);
const isRunningRef = useRef8(false);
const applyLayout = async () => {
if (isRunningRef.current || isAnimating) return;
if (nodes.length === 0) return;
isRunningRef.current = true;
const nodeIds = new Set(nodes.map((n) => n.id));
const children = /* @__PURE__ */ new Map();
const hasIncoming = /* @__PURE__ */ new Set();
for (const nodeId of nodeIds) {
children.set(nodeId, []);
}
graph.forEachEdge((_key, _attrs, source, target) => {
if (nodeIds.has(source) && nodeIds.has(target) && source !== target) {
children.get(source)?.push(target);
hasIncoming.add(target);
}
});
const roots = [...nodeIds].filter((id) => !hasIncoming.has(id));
if (roots.length === 0) {
roots.push(nodes[0].id);
}
const levels = /* @__PURE__ */ new Map();
const queue = [...roots];
for (const r of roots) levels.set(r, 0);
while (queue.length > 0) {
const current = queue.shift();
const level = levels.get(current);
for (const child of children.get(current) || []) {
if (!levels.has(child)) {
levels.set(child, level + 1);
queue.push(child);
}
}
}
for (const nodeId_0 of nodeIds) {
if (!levels.has(nodeId_0)) levels.set(nodeId_0, 0);
}
const byLevel = /* @__PURE__ */ new Map();
for (const [nodeId_1, level_0] of levels) {
if (!byLevel.has(level_0)) byLevel.set(level_0, []);
byLevel.get(level_0).push(nodeId_1);
}
const targets = /* @__PURE__ */ new Map();
const maxLevel = Math.max(...byLevel.keys());
for (const [level_1, nodeIdsAtLevel] of byLevel) {
const count = nodeIdsAtLevel.length;
let maxNodeSize = 200;
for (const nid of nodeIdsAtLevel) {
if (graph.hasNode(nid)) {
const attrs = graph.getNodeAttributes(nid);
maxNodeSize = Math.max(maxNodeSize, attrs.width || 200);
}
}
const totalWidth = (count - 1) * (maxNodeSize + nodeGap);
const startX = -totalWidth / 2;
for (let i = 0; i < count; i++) {
const primary = level_1 * levelGap;
const secondary = startX + i * (maxNodeSize + nodeGap);
if (direction === "top-down") {
targets.set(nodeIdsAtLevel[i], {
x: secondary,
y: primary
});
} else {
targets.set(nodeIdsAtLevel[i], {
x: primary,
y: secondary
});
}
}
}
await animate(targets, direction === "top-down" ? "Tree layout" : "Horizontal layout");
isRunningRef.current = false;
};
return {
applyLayout,
isRunning: isRunningRef.current || isAnimating
};
}
// src/hooks/useGridLayout.ts
import { useAtomValue as useAtomValue20 } from "jotai";
import { useRef as useRef9 } from "react";
function useGridLayout(options = {}) {
const {
columns,
gap = 80,
...animateOptions
} = options;
const graph = useAtomValue20(graphAtom);
const nodes = useAtomValue20(uiNodesAtom);
const {
animate,
isAnimating
} = useAnimatedLayout(animateOptions);
const isRunningRef = useRef9(false);
const applyLayout = async () => {
if (isRunningRef.current || isAnimating) return;
if (nodes.length === 0) return;
isRunningRef.current = true;
const sorted = [...nodes].sort((a, b) => {
const ay = a.position?.y ?? 0;
const by = b.position?.y ?? 0;
if (Math.abs(ay - by) > 50) return ay - by;
return (a.position?.x ?? 0) - (b.position?.x ?? 0);
});
const cols = columns ?? Math.ceil(Math.sqrt(sorted.length));
let maxW = 200;
let maxH = 100;
for (const node of sorted) {
if (graph.hasNode(node.id)) {
const attrs = graph.getNodeAttributes(node.id);
maxW = Math.max(maxW, attrs.width || 200);
maxH = Math.max(maxH, attrs.height || 100);
}
}
const cellW = maxW + gap;
const cellH = maxH + gap;
const rows = Math.ceil(sorted.length / cols);
const totalW = (cols - 1) * cellW;
const totalH = (rows - 1) * cellH;
const offsetX = -totalW / 2;
const offsetY = -totalH / 2;
const targets = /* @__PURE__ */ new Map();
for (let i = 0; i < sorted.length; i++) {
const col = i % cols;
const row = Math.floor(i / cols);
targets.set(sorted[i].id, {
x: Math.round(offsetX + col * cellW),
y: Math.round(offsetY + row * cellH)
});
}
await animate(targets, "Grid layout");
isRunningRef.current = false;
};
return {
applyLayout,
isRunning: isRunningRef.current || isAnimating
};
}
// src/hooks/usePlugin.ts
import { c as _c20 } from "react/compiler-runtime";
import { useEffect as useEffect3, useRef as useRef10 } from "react";
// src/core/plugin-types.ts
var PluginError = class extends Error {
constructor(message, pluginId, code) {
super(`[Plugin "${pluginId}"] ${message}`);
this.pluginId = pluginId;
this.code = code;
this.name = "PluginError";
}
};
// src/core/node-type-registry.tsx
import { c as _c19 } from "react/compiler-runtime";
import React from "react";
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
var nodeTypeRegistry = /* @__PURE__ */ new Map();
function registerNodeTypes(types) {
for (const [nodeType, component] of Object.entries(types)) {
nodeTypeRegistry.set(nodeType, component);
}
}
function unregisterNodeType(nodeType) {
return nodeTypeRegistry.delete(nodeType);
}
// src/gestures/types.ts
var NO_MODIFIERS = Object.freeze({
shift: false,
ctrl: false,
alt: false,
meta: false
});
var NO_HELD_KEYS = Object.freeze({
byKey: Object.freeze({}),
byCode: Object.freeze({})
});
// src/gestures/dispatcher.ts
var handlers = /* @__PURE__ */ new Map();
function registerAction2(actionId, handler) {
handlers.set(actionId, handler);
}
function unregisterAction2(actionId) {
handlers.delete(actionId);
}
// src/utils/edge-path-registry.ts
var customCalculators = /* @__PURE__ */ new Map();
function registerEdgePathCalculator(name, calculator) {
customCalculators.set(name, calculator);
}
function unregisterEdgePathCalculator(name) {
return customCalculators.delete(name);
}
// src/core/plugin-registry.ts
var debug15 = createDebug("plugins");
var plugins = /* @__PURE__ */ new Map();
function registerPlugin(plugin) {
debug15("Registering plugin: %s", plugin.id);
if (plugins.has(plugin.id)) {
throw new PluginError("Plugin is already registered", plugin.id, "ALREADY_REGISTERED");
}
if (plugin.dependencies) {
for (const depId of plugin.dependencies) {
if (!plugins.has(depId)) {
throw new PluginError(`Missing dependency: "${depId}"`, plugin.id, "MISSING_DEPENDENCY");
}
}
}
detectConflicts(plugin);
const cleanups = [];
try {
if (plugin.nodeTypes) {
const nodeTypeNames = Object.keys(plugin.nodeTypes);
registerNodeTypes(plugin.nodeTypes);
cleanups.push(() => {
for (const name of nodeTypeNames) {
unregisterNodeType(name);
}
});
}
if (plugin.edgePathCalculators) {
for (const [name, calc] of Object.entries(plugin.edgePathCalculators)) {
registerEdgePathCalculator(name, calc);
cleanups.push(() => unregisterEdgePathCalculator(name));
}
}
if (plugin.actionHandlers) {
for (const [actionId, handler] of Object.entries(plugin.actionHandlers)) {
registerAction2(actionId, handler);
cleanups.push(() => unregisterAction2(actionId));
}
}
if (plugin.commands) {
for (const cmd of plugin.commands) {
commandRegistry.register(cmd);
cleanups.push(() => commandRegistry.unregister(cmd.name));
}
}
if (plugin.actions) {
for (const action of plugin.actions) {
registerAction(action);
cleanups.push(() => unregisterAction(action.id));
}
}
let lifecycleCleanup = null;
if (plugin.onRegister) {
const ctx = makePluginContext(plugin.id);
try {
const result = plugin.onRegister(ctx);
if (typeof result === "function") {
lifecycleCleanup = result;
}
} catch (err) {
for (const cleanup of cleanups.reverse()) {
try {
cleanup();
} catch {
}
}
throw new PluginError(`onRegister failed: ${err instanceof Error ? err.message : String(err)}`, plugin.id, "LIFECYCLE_ERROR");
}
}
plugins.set(plugin.id, {
plugin,
cleanup: () => {
for (const cleanup of cleanups.reverse()) {
try {
cleanup();
} catch {
}
}
if (lifecycleCleanup) {
try {
lifecycleCleanup();
} catch {
}
}
},
registeredAt: Date.now()
});
debug15("Plugin registered: %s (%d node types, %d commands, %d actions)", plugin.id, Object.keys(plugin.nodeTypes ?? {}).length, plugin.commands?.length ?? 0, plugin.actions?.length ?? 0);
} catch (err) {
if (err instanceof PluginError) throw err;
for (const cleanup of cleanups.reverse()) {
try {
cleanup();
} catch {
}
}
throw err;
}
}
function unregisterPlugin(pluginId) {
const registration = plugins.get(pluginId);
if (!registration) {
throw new PluginError("Plugin is not registered", pluginId, "NOT_FOUND");
}
for (const [otherId, other] of plugins) {
if (other.plugin.dependencies?.includes(pluginId)) {
throw new PluginError(`Cannot unregister: plugin "${otherId}" depends on it`, pluginId, "CONFLICT");
}
}
if (registration.cleanup) {
registration.cleanup();
}
plugins.delete(pluginId);
debug15("Plugin unregistered: %s", pluginId);
}
function getPlugin(id) {
return plugins.get(id)?.plugin;
}
function hasPlugin(id) {
return plugins.has(id);
}
function detectConflicts(plugin) {
if (plugin.commands) {
for (const cmd of plugin.commands) {
if (commandRegistry.has(cmd.name)) {
throw new PluginError(`Command "${cmd.name}" is already registered`, plugin.id, "CONFLICT");
}
}
}
if (plugin.edgePathCalculators) {
for (const name of Object.keys(plugin.edgePathCalculators)) {
for (const [otherId, other] of plugins) {
if (other.plugin.edgePathCalculators?.[name]) {
throw new PluginError(`Edge path calculator "${name}" already registered by plugin "${otherId}"`, plugin.id, "CONFLICT");
}
}
}
}
if (plugin.nodeTypes) {
for (const nodeType of Object.keys(plugin.nodeTypes)) {
for (const [otherId, other] of plugins) {
if (other.plugin.nodeTypes?.[nodeType]) {
throw new PluginError(`Node type "${nodeType}" already registered by plugin "${otherId}"`, plugin.id, "CONFLICT");
}
}
}
}
if (plugin.actionHandlers) {
for (const actionId of Object.keys(plugin.actionHandlers)) {
for (const [otherId, other] of plugins) {
if (other.plugin.actionHandlers?.[actionId]) {
throw new PluginError(`Action handler "${actionId}" already registered by plugin "${otherId}"`, plugin.id, "CONFLICT");
}
}
}
}
}
function makePluginContext(pluginId) {
return {
pluginId,
getPlugin,
hasPlugin
};
}
// src/hooks/usePlugin.ts
function usePlugin(plugin) {
const $ = _c20(4);
const registeredRef = useRef10(false);
let t0;
if ($[0] !== plugin) {
t0 = () => {
if (!hasPlugin(plugin.id)) {
registerPlugin(plugin);
registeredRef.current = true;
}
return () => {
if (registeredRef.current && hasPlugin(plugin.id)) {
try {
unregisterPlugin(plugin.id);
} catch {
}
registeredRef.current = false;
}
};
};
$[0] = plugin;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== plugin.id) {
t1 = [plugin.id];
$[2] = plugin.id;
$[3] = t1;
} else {
t1 = $[3];
}
useEffect3(t0, t1);
}
function usePlugins(plugins2) {
const $ = _c20(6);
let t0;
if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
$[0] = t0;
} else {
t0 = $[0];
}
const registeredRef = useRef10(t0);
let t1;
let t2;
if ($[1] !== plugins2) {
t1 = () => {
const registered = [];
for (const plugin of plugins2) {
if (!hasPlugin(plugin.id)) {
registerPlugin(plugin);
registered.push(plugin.id);
}
}
registeredRef.current = registered;
return () => {
for (const id of registeredRef.current.reverse()) {
if (hasPlugin(id)) {
try {
unregisterPlugin(id);
} catch {
}
}
}
registeredRef.current = [];
};
};
t2 = plugins2.map(_temp7).join(",");
$[1] = plugins2;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
let t3;
if ($[4] !== t2) {
t3 = [t2];
$[4] = t2;
$[5] = t3;
} else {
t3 = $[5];
}
useEffect3(t1, t3);
}
function _temp7(p) {
return p.id;
}
export {
FitToBoundsMode,
useActionExecutor,
useAnimatedLayout,
useArrowKeyNavigation,
useCanvasDrag,
useCanvasGraph,
useCanvasHistory,
useCanvasSelection,
useCanvasSettings,
useCanvasViewport,
useCommandLine,
useFitToBounds,
useForceLayout,
useGestureResolver,
useGetGraphBounds,
useGridLayout,
useLayout,
useNodeDrag,
useNodeResize,
useNodeSelection,
usePlugin,
usePlugins,
useSelectionBounds,
useSplitGesture,
useTapGesture,
useTreeLayout,
useVirtualization,
useZoomTransition
};
//# sourceMappingURL=index.mjs.map