6491 lines
No EOL
195 KiB
JavaScript
6491 lines
No EOL
195 KiB
JavaScript
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
|
|
// src/hooks/index.ts
|
|
var index_exports = {};
|
|
__export(index_exports, {
|
|
FitToBoundsMode: () => FitToBoundsMode,
|
|
useActionExecutor: () => useActionExecutor,
|
|
useAnimatedLayout: () => useAnimatedLayout,
|
|
useArrowKeyNavigation: () => useArrowKeyNavigation,
|
|
useCanvasDrag: () => useCanvasDrag,
|
|
useCanvasGraph: () => useCanvasGraph,
|
|
useCanvasHistory: () => useCanvasHistory,
|
|
useCanvasSelection: () => useCanvasSelection,
|
|
useCanvasSettings: () => useCanvasSettings,
|
|
useCanvasViewport: () => useCanvasViewport,
|
|
useCommandLine: () => useCommandLine,
|
|
useFitToBounds: () => useFitToBounds,
|
|
useForceLayout: () => useForceLayout,
|
|
useGestureResolver: () => useGestureResolver,
|
|
useGetGraphBounds: () => useGetGraphBounds,
|
|
useGridLayout: () => useGridLayout,
|
|
useLayout: () => useLayout,
|
|
useNodeDrag: () => useNodeDrag,
|
|
useNodeResize: () => useNodeResize,
|
|
useNodeSelection: () => useNodeSelection,
|
|
usePlugin: () => usePlugin,
|
|
usePlugins: () => usePlugins,
|
|
useSelectionBounds: () => useSelectionBounds,
|
|
useSplitGesture: () => useSplitGesture,
|
|
useTapGesture: () => useTapGesture,
|
|
useTreeLayout: () => useTreeLayout,
|
|
useVirtualization: () => useVirtualization,
|
|
useZoomTransition: () => useZoomTransition
|
|
});
|
|
module.exports = __toCommonJS(index_exports);
|
|
|
|
// src/hooks/useNodeSelection.ts
|
|
var import_compiler_runtime = require("react/compiler-runtime");
|
|
var import_jotai2 = require("jotai");
|
|
|
|
// src/core/selection-store.ts
|
|
var import_jotai = require("jotai");
|
|
|
|
// src/utils/debug.ts
|
|
var import_debug = __toESM(require("debug"));
|
|
var NAMESPACE = "canvas";
|
|
function createDebug(module2) {
|
|
const base = (0, import_debug.default)(`${NAMESPACE}:${module2}`);
|
|
const warn = (0, import_debug.default)(`${NAMESPACE}:${module2}:warn`);
|
|
const error = (0, import_debug.default)(`${NAMESPACE}:${module2}: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 = (0, import_jotai.atom)(/* @__PURE__ */ new Set());
|
|
var selectedEdgeIdAtom = (0, import_jotai.atom)(null);
|
|
var handleNodePointerDownSelectionAtom = (0, import_jotai.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 = (0, import_jotai.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 = (0, import_jotai.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 = (0, import_jotai.atom)(null, (_get, set) => {
|
|
debug2("clearSelection");
|
|
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
|
|
});
|
|
var addNodesToSelectionAtom = (0, import_jotai.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 = (0, import_jotai.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 = (0, import_jotai.atom)(null, (get, set, edgeId) => {
|
|
set(selectedEdgeIdAtom, edgeId);
|
|
if (edgeId !== null) {
|
|
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
|
|
}
|
|
});
|
|
var clearEdgeSelectionAtom = (0, import_jotai.atom)(null, (_get, set) => {
|
|
set(selectedEdgeIdAtom, null);
|
|
});
|
|
var focusedNodeIdAtom = (0, import_jotai.atom)(null);
|
|
var setFocusedNodeAtom = (0, import_jotai.atom)(null, (_get, set, nodeId) => {
|
|
set(focusedNodeIdAtom, nodeId);
|
|
});
|
|
var hasFocusedNodeAtom = (0, import_jotai.atom)((get) => get(focusedNodeIdAtom) !== null);
|
|
var selectedNodesCountAtom = (0, import_jotai.atom)((get) => get(selectedNodeIdsAtom).size);
|
|
var hasSelectionAtom = (0, import_jotai.atom)((get) => get(selectedNodeIdsAtom).size > 0);
|
|
|
|
// src/hooks/useNodeSelection.ts
|
|
function useNodeSelection(nodeId) {
|
|
const $ = (0, import_compiler_runtime.c)(13);
|
|
const [selectedIds] = (0, import_jotai2.useAtom)(selectedNodeIdsAtom);
|
|
const selectSingle = (0, import_jotai2.useSetAtom)(selectSingleNodeAtom);
|
|
const toggleNode = (0, import_jotai2.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
|
|
var import_compiler_runtime2 = require("react/compiler-runtime");
|
|
var import_jotai16 = require("jotai");
|
|
var import_react = require("@use-gesture/react");
|
|
var import_react2 = require("react");
|
|
|
|
// src/core/graph-store.ts
|
|
var import_jotai3 = require("jotai");
|
|
var import_graphology = __toESM(require("graphology"));
|
|
var graphOptions = {
|
|
type: "directed",
|
|
multi: true,
|
|
allowSelfLoops: true
|
|
};
|
|
var currentGraphIdAtom = (0, import_jotai3.atom)(null);
|
|
var graphAtom = (0, import_jotai3.atom)(new import_graphology.default(graphOptions));
|
|
var graphUpdateVersionAtom = (0, import_jotai3.atom)(0);
|
|
var edgeCreationAtom = (0, import_jotai3.atom)({
|
|
isCreating: false,
|
|
sourceNodeId: null,
|
|
sourceNodePosition: null,
|
|
targetPosition: null,
|
|
hoveredTargetNodeId: null,
|
|
sourceHandle: null,
|
|
targetHandle: null,
|
|
sourcePort: null,
|
|
targetPort: null,
|
|
snappedTargetPosition: null
|
|
});
|
|
var draggingNodeIdAtom = (0, import_jotai3.atom)(null);
|
|
var preDragNodeAttributesAtom = (0, import_jotai3.atom)(null);
|
|
|
|
// src/core/graph-position.ts
|
|
var import_jotai5 = require("jotai");
|
|
var import_jotai_family = require("jotai-family");
|
|
var import_graphology2 = __toESM(require("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
|
|
var import_jotai4 = require("jotai");
|
|
var perfEnabledAtom = (0, import_jotai4.atom)(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 = (0, import_jotai5.atom)(0);
|
|
var nodePositionAtomFamily = (0, import_jotai_family.atomFamily)((nodeId) => (0, import_jotai5.atom)((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 = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(null, (get, set) => {
|
|
debug3("Clearing graph for switch");
|
|
set(cleanupAllNodePositionsAtom);
|
|
clearAllPendingMutations();
|
|
const emptyGraph = new import_graphology2.default(graphOptions);
|
|
set(graphAtom, emptyGraph);
|
|
set(graphUpdateVersionAtom, (v) => v + 1);
|
|
});
|
|
|
|
// src/core/graph-mutations.ts
|
|
var import_jotai13 = require("jotai");
|
|
var import_graphology3 = __toESM(require("graphology"));
|
|
|
|
// src/core/graph-derived.ts
|
|
var import_jotai9 = require("jotai");
|
|
var import_jotai_family2 = require("jotai-family");
|
|
|
|
// src/core/viewport-store.ts
|
|
var import_jotai6 = require("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 = (0, import_jotai6.atom)(1);
|
|
var panAtom = (0, import_jotai6.atom)({
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
var viewportRectAtom = (0, import_jotai6.atom)(null);
|
|
var screenToWorldAtom = (0, import_jotai6.atom)((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 = (0, import_jotai6.atom)((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 = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(null, (_get, set) => {
|
|
set(zoomAtom, 1);
|
|
set(panAtom, {
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
});
|
|
var fitToBoundsAtom = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(null);
|
|
var zoomTransitionProgressAtom = (0, import_jotai6.atom)(0);
|
|
var isZoomTransitioningAtom = (0, import_jotai6.atom)((get) => {
|
|
const progress = get(zoomTransitionProgressAtom);
|
|
return progress > 0 && progress < 1;
|
|
});
|
|
var zoomAnimationTargetAtom = (0, import_jotai6.atom)(null);
|
|
var animateZoomToNodeAtom = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(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
|
|
var import_jotai8 = require("jotai");
|
|
|
|
// src/core/history-store.ts
|
|
var import_jotai7 = require("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 = (0, import_jotai7.atom)({
|
|
past: [],
|
|
future: [],
|
|
isApplying: false
|
|
});
|
|
var canUndoAtom = (0, import_jotai7.atom)((get) => {
|
|
const history = get(historyStateAtom);
|
|
return history.past.length > 0 && !history.isApplying;
|
|
});
|
|
var canRedoAtom = (0, import_jotai7.atom)((get) => {
|
|
const history = get(historyStateAtom);
|
|
return history.future.length > 0 && !history.isApplying;
|
|
});
|
|
var undoCountAtom = (0, import_jotai7.atom)((get) => get(historyStateAtom).past.length);
|
|
var redoCountAtom = (0, import_jotai7.atom)((get) => get(historyStateAtom).future.length);
|
|
var pushDeltaAtom = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(null, (_get, set) => {
|
|
set(historyStateAtom, {
|
|
past: [],
|
|
future: [],
|
|
isApplying: false
|
|
});
|
|
debug4("History cleared");
|
|
});
|
|
var historyLabelsAtom = (0, import_jotai7.atom)((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 = (0, import_jotai8.atom)(/* @__PURE__ */ new Set());
|
|
var toggleGroupCollapseAtom = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)((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 = (0, import_jotai8.atom)((get) => {
|
|
get(graphUpdateVersionAtom);
|
|
const graph = get(graphAtom);
|
|
return (nodeId) => {
|
|
if (!graph.hasNode(nodeId)) return void 0;
|
|
return graph.getNodeAttribute(nodeId, "parentId");
|
|
};
|
|
});
|
|
var isGroupNodeAtom = (0, import_jotai8.atom)((get) => {
|
|
const getChildren = get(nodeChildrenAtom);
|
|
return (nodeId) => getChildren(nodeId).length > 0;
|
|
});
|
|
var groupChildCountAtom = (0, import_jotai8.atom)((get) => {
|
|
const getChildren = get(nodeChildrenAtom);
|
|
return (groupId) => getChildren(groupId).length;
|
|
});
|
|
var setNodeParentAtom = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)(null, (get, set, {
|
|
nodeIds,
|
|
groupId
|
|
}) => {
|
|
for (const nodeId of nodeIds) {
|
|
set(setNodeParentAtom, {
|
|
nodeId,
|
|
parentId: groupId
|
|
});
|
|
}
|
|
});
|
|
var removeFromGroupAtom = (0, import_jotai8.atom)(null, (get, set, nodeId) => {
|
|
set(setNodeParentAtom, {
|
|
nodeId,
|
|
parentId: void 0
|
|
});
|
|
});
|
|
var groupSelectedNodesAtom = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)(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 = (0, import_jotai8.atom)((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 = (0, import_jotai8.atom)(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 = (0, import_jotai9.atom)((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 = (0, import_jotai9.atom)((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 = (0, import_jotai9.atom)((get) => {
|
|
get(graphUpdateVersionAtom);
|
|
const graph = get(graphAtom);
|
|
return graph.nodes();
|
|
});
|
|
var nodeFamilyAtom = (0, import_jotai_family2.atomFamily)((nodeId) => (0, import_jotai9.atom)((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 = (0, import_jotai9.atom)((get) => {
|
|
get(graphUpdateVersionAtom);
|
|
const graph = get(graphAtom);
|
|
return graph.edges();
|
|
});
|
|
var edgeKeysWithTempEdgeAtom = (0, import_jotai9.atom)((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 = (0, import_jotai_family2.atomFamily)((key) => (0, import_jotai9.atom)((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
|
|
var import_jotai11 = require("jotai");
|
|
|
|
// src/core/reduced-motion-store.ts
|
|
var import_jotai10 = require("jotai");
|
|
var prefersReducedMotionAtom = (0, import_jotai10.atom)(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false);
|
|
var watchReducedMotionAtom = (0, import_jotai10.atom)(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 = (0, import_jotai11.atom)(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 = (0, import_jotai11.atom)(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 = (0, import_jotai11.atom)(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 = (0, import_jotai11.atom)(/* @__PURE__ */ new Map());
|
|
var EDGE_ANIMATION_DURATION = 300;
|
|
var removeEdgeWithAnimationAtom = (0, import_jotai11.atom)(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 = (0, import_jotai11.atom)(null);
|
|
var updateEdgeLabelAtom = (0, import_jotai11.atom)(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
|
|
var import_jotai12 = require("jotai");
|
|
var debug6 = createDebug("graph:mutations:advanced");
|
|
var dropTargetNodeIdAtom = (0, import_jotai12.atom)(null);
|
|
var splitNodeAtom = (0, import_jotai12.atom)(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 = (0, import_jotai12.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 import_graphology3.default(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
|
|
var import_jotai14 = require("jotai");
|
|
var debug8 = createDebug("sync");
|
|
var syncStatusAtom = (0, import_jotai14.atom)("synced");
|
|
var pendingMutationsCountAtom = (0, import_jotai14.atom)(0);
|
|
var isOnlineAtom = (0, import_jotai14.atom)(typeof navigator !== "undefined" ? navigator.onLine : true);
|
|
var lastSyncErrorAtom = (0, import_jotai14.atom)(null);
|
|
var lastSyncTimeAtom = (0, import_jotai14.atom)(Date.now());
|
|
var mutationQueueAtom = (0, import_jotai14.atom)([]);
|
|
var syncStateAtom = (0, import_jotai14.atom)((get) => ({
|
|
status: get(syncStatusAtom),
|
|
pendingMutations: get(pendingMutationsCountAtom),
|
|
lastError: get(lastSyncErrorAtom),
|
|
lastSyncTime: get(lastSyncTimeAtom),
|
|
isOnline: get(isOnlineAtom),
|
|
queuedMutations: get(mutationQueueAtom).length
|
|
}));
|
|
var startMutationAtom = (0, import_jotai14.atom)(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 = (0, import_jotai14.atom)(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 = (0, import_jotai14.atom)(null, (_get, set, error) => {
|
|
set(lastSyncErrorAtom, error);
|
|
debug8("Mutation failed: %s", error);
|
|
});
|
|
var setOnlineStatusAtom = (0, import_jotai14.atom)(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 = (0, import_jotai14.atom)(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 = (0, import_jotai14.atom)(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 = (0, import_jotai14.atom)(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 = (0, import_jotai14.atom)((get) => {
|
|
const queue = get(mutationQueueAtom);
|
|
return queue.find((m) => m.retryCount < m.maxRetries) ?? null;
|
|
});
|
|
var clearMutationQueueAtom = (0, import_jotai14.atom)(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
|
|
var import_jotai15 = require("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 = (0, import_jotai15.atom)(/* @__PURE__ */ new Map());
|
|
var primaryInputSourceAtom = (0, import_jotai15.atom)("mouse");
|
|
var inputCapabilitiesAtom = (0, import_jotai15.atom)(detectInputCapabilities());
|
|
var isStylusActiveAtom = (0, import_jotai15.atom)((get) => {
|
|
const pointers = get(activePointersAtom);
|
|
for (const [, pointer] of pointers) {
|
|
if (pointer.source === "pencil") return true;
|
|
}
|
|
return false;
|
|
});
|
|
var isMultiTouchAtom = (0, import_jotai15.atom)((get) => {
|
|
const pointers = get(activePointersAtom);
|
|
let fingerCount = 0;
|
|
for (const [, pointer] of pointers) {
|
|
if (pointer.source === "finger") fingerCount++;
|
|
}
|
|
return fingerCount > 1;
|
|
});
|
|
var fingerCountAtom = (0, import_jotai15.atom)((get) => {
|
|
const pointers = get(activePointersAtom);
|
|
let count = 0;
|
|
for (const [, pointer] of pointers) {
|
|
if (pointer.source === "finger") count++;
|
|
}
|
|
return count;
|
|
});
|
|
var isTouchDeviceAtom = (0, import_jotai15.atom)((get) => {
|
|
const caps = get(inputCapabilitiesAtom);
|
|
return caps.hasTouch;
|
|
});
|
|
var pointerDownAtom = (0, import_jotai15.atom)(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 = (0, import_jotai15.atom)(null, (get, set, pointerId) => {
|
|
const pointers = new Map(get(activePointersAtom));
|
|
pointers.delete(pointerId);
|
|
set(activePointersAtom, pointers);
|
|
});
|
|
var clearPointersAtom = (0, import_jotai15.atom)(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 $ = (0, import_compiler_runtime2.c)(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 = (0, import_jotai16.useAtomValue)(graphAtom);
|
|
const [pan, setPan] = (0, import_jotai16.useAtom)(panAtom);
|
|
const startMutation = (0, import_jotai16.useSetAtom)(startMutationAtom);
|
|
const completeMutation = (0, import_jotai16.useSetAtom)(completeMutationAtom);
|
|
const setStartDrag = (0, import_jotai16.useSetAtom)(startNodeDragAtom);
|
|
const setEndDrag = (0, import_jotai16.useSetAtom)(endNodeDragAtom);
|
|
const getPreDragAttributes = (0, import_jotai16.useAtomValue)(preDragNodeAttributesAtom);
|
|
const currentZoom = (0, import_jotai16.useAtomValue)(zoomAtom);
|
|
const getSelectedNodeIds = (0, import_jotai16.useAtomValue)(selectedNodeIdsAtom);
|
|
const currentGraphId = (0, import_jotai16.useAtomValue)(currentGraphIdAtom);
|
|
const edgeCreation = (0, import_jotai16.useAtomValue)(edgeCreationAtom);
|
|
const setGraph = (0, import_jotai16.useSetAtom)(graphAtom);
|
|
(0, import_jotai16.useSetAtom)(nodePositionUpdateCounterAtom);
|
|
const pushHistory = (0, import_jotai16.useSetAtom)(pushHistoryAtom);
|
|
const setDropTarget = (0, import_jotai16.useSetAtom)(dropTargetNodeIdAtom);
|
|
const nestNodesOnDrop = (0, import_jotai16.useSetAtom)(nestNodesOnDropAtom);
|
|
const inputSource = (0, import_jotai16.useAtomValue)(primaryInputSourceAtom);
|
|
let t2;
|
|
if ($[2] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
|
|
t2 = (0, import_jotai16.atom)(null, _temp2);
|
|
$[2] = t2;
|
|
} else {
|
|
t2 = $[2];
|
|
}
|
|
const batchUpdatePositions = (0, import_jotai16.useSetAtom)(t2);
|
|
let t3;
|
|
if ($[3] !== batchUpdatePositions) {
|
|
t3 = (updates_0) => {
|
|
batchUpdatePositions(updates_0);
|
|
};
|
|
$[3] = batchUpdatePositions;
|
|
$[4] = t3;
|
|
} else {
|
|
t3 = $[4];
|
|
}
|
|
const updateNodePositions = t3;
|
|
const gestureInstanceRef = (0, import_react2.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 = (0, import_react2.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 = (0, import_react.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
|
|
var import_compiler_runtime3 = require("react/compiler-runtime");
|
|
var import_jotai17 = require("jotai");
|
|
var import_react3 = require("react");
|
|
var import_react_dom = require("react-dom");
|
|
var debug10 = createDebug("resize");
|
|
function useNodeResize(t0) {
|
|
const $ = (0, import_compiler_runtime3.c)(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] = (0, import_react3.useState)(nodeData.width || 500);
|
|
const [localHeight, setLocalHeight] = (0, import_react3.useState)(nodeData.height || 500);
|
|
const [isResizing, setIsResizing] = (0, import_react3.useState)(false);
|
|
const resizeStartRef = (0, import_react3.useRef)(null);
|
|
const graph = (0, import_jotai17.useAtomValue)(graphAtom);
|
|
const currentZoom = (0, import_jotai17.useAtomValue)(zoomAtom);
|
|
const currentGraphId = (0, import_jotai17.useAtomValue)(currentGraphIdAtom);
|
|
const startMutation = (0, import_jotai17.useSetAtom)(startMutationAtom);
|
|
const completeMutation = (0, import_jotai17.useSetAtom)(completeMutationAtom);
|
|
const setGraphUpdateVersion = (0, import_jotai17.useSetAtom)(graphUpdateVersionAtom);
|
|
const setNodePositionUpdateCounter = (0, import_jotai17.useSetAtom)(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];
|
|
}
|
|
(0, import_react3.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);
|
|
}
|
|
(0, import_react_dom.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
|
|
var import_compiler_runtime4 = require("react/compiler-runtime");
|
|
var import_jotai19 = require("jotai");
|
|
|
|
// src/core/toast-store.ts
|
|
var import_jotai18 = require("jotai");
|
|
var canvasToastAtom = (0, import_jotai18.atom)(null);
|
|
var showToastAtom = (0, import_jotai18.atom)(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 $ = (0, import_compiler_runtime4.c)(22);
|
|
const options = t0 === void 0 ? {} : t0;
|
|
const {
|
|
enableKeyboardShortcuts: t1
|
|
} = options;
|
|
t1 === void 0 ? false : t1;
|
|
const canUndo = (0, import_jotai19.useAtomValue)(canUndoAtom);
|
|
const canRedo = (0, import_jotai19.useAtomValue)(canRedoAtom);
|
|
const undoCount = (0, import_jotai19.useAtomValue)(undoCountAtom);
|
|
const redoCount = (0, import_jotai19.useAtomValue)(redoCountAtom);
|
|
const historyLabels = (0, import_jotai19.useAtomValue)(historyLabelsAtom);
|
|
const undoAction = (0, import_jotai19.useSetAtom)(undoAtom);
|
|
const redoAction = (0, import_jotai19.useSetAtom)(redoAtom);
|
|
const pushHistory = (0, import_jotai19.useSetAtom)(pushHistoryAtom);
|
|
const clearHistory = (0, import_jotai19.useSetAtom)(clearHistoryAtom);
|
|
const showToast = (0, import_jotai19.useSetAtom)(showToastAtom);
|
|
const store = (0, import_jotai19.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
|
|
var import_compiler_runtime5 = require("react/compiler-runtime");
|
|
var import_jotai20 = require("jotai");
|
|
function useCanvasSelection() {
|
|
const $ = (0, import_compiler_runtime5.c)(6);
|
|
const selectedNodeIds = (0, import_jotai20.useAtomValue)(selectedNodeIdsAtom);
|
|
const selectedEdgeId = (0, import_jotai20.useAtomValue)(selectedEdgeIdAtom);
|
|
const count = (0, import_jotai20.useAtomValue)(selectedNodesCountAtom);
|
|
const hasSelection = (0, import_jotai20.useAtomValue)(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
|
|
var import_compiler_runtime6 = require("react/compiler-runtime");
|
|
var import_jotai21 = require("jotai");
|
|
function useCanvasViewport() {
|
|
const $ = (0, import_compiler_runtime6.c)(9);
|
|
const zoom = (0, import_jotai21.useAtomValue)(zoomAtom);
|
|
const pan = (0, import_jotai21.useAtomValue)(panAtom);
|
|
const viewportRect = (0, import_jotai21.useAtomValue)(viewportRectAtom);
|
|
const screenToWorld = (0, import_jotai21.useAtomValue)(screenToWorldAtom);
|
|
const worldToScreen = (0, import_jotai21.useAtomValue)(worldToScreenAtom);
|
|
const zoomFocusNodeId = (0, import_jotai21.useAtomValue)(zoomFocusNodeIdAtom);
|
|
const zoomTransitionProgress = (0, import_jotai21.useAtomValue)(zoomTransitionProgressAtom);
|
|
const isZoomTransitioning = (0, import_jotai21.useAtomValue)(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
|
|
var import_compiler_runtime7 = require("react/compiler-runtime");
|
|
var import_jotai22 = require("jotai");
|
|
function useCanvasDrag() {
|
|
const $ = (0, import_compiler_runtime7.c)(3);
|
|
const draggingNodeId = (0, import_jotai22.useAtomValue)(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
|
|
var import_compiler_runtime8 = require("react/compiler-runtime");
|
|
var import_jotai23 = require("jotai");
|
|
var useGetGraphBounds = () => {
|
|
const $ = (0, import_compiler_runtime8.c)(6);
|
|
const graph = (0, import_jotai23.useAtomValue)(graphAtom);
|
|
(0, import_jotai23.useAtomValue)(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 $ = (0, import_compiler_runtime8.c)(5);
|
|
const selectedNodeIds = (0, import_jotai23.useAtomValue)(selectedNodeIdsAtom);
|
|
const nodes = (0, import_jotai23.useAtomValue)(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 $ = (0, import_compiler_runtime8.c)(2);
|
|
const setFitToBounds = (0, import_jotai23.useSetAtom)(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 $ = (0, import_compiler_runtime8.c)(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
|
|
var d3 = __toESM(require("d3-force"));
|
|
var import_jotai24 = require("jotai");
|
|
var import_react4 = require("react");
|
|
var debug11 = createDebug("force-layout");
|
|
var useForceLayout = (options = {}) => {
|
|
const {
|
|
onPositionsChanged,
|
|
maxIterations = 1e3,
|
|
chargeStrength = -6e3,
|
|
linkStrength = 0.03
|
|
} = options;
|
|
const nodes = (0, import_jotai24.useAtomValue)(uiNodesAtom);
|
|
const graph = (0, import_jotai24.useAtomValue)(graphAtom);
|
|
const updateNodePosition = (0, import_jotai24.useSetAtom)(updateNodePositionAtom);
|
|
const isRunningRef = (0, import_react4.useRef)(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
|
|
var import_compiler_runtime9 = require("react/compiler-runtime");
|
|
var import_jotai26 = require("jotai");
|
|
|
|
// src/core/settings-store.ts
|
|
var import_jotai25 = require("jotai");
|
|
var import_utils = require("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 = (0, import_utils.atomWithStorage)("@blinksgg/canvas/settings", DEFAULT_STATE);
|
|
var eventMappingsAtom = (0, import_jotai25.atom)((get) => get(canvasSettingsAtom).mappings);
|
|
var activePresetIdAtom = (0, import_jotai25.atom)((get) => get(canvasSettingsAtom).activePresetId);
|
|
var allPresetsAtom = (0, import_jotai25.atom)((get) => {
|
|
const state = get(canvasSettingsAtom);
|
|
return [...BUILT_IN_PRESETS, ...state.customPresets];
|
|
});
|
|
var activePresetAtom = (0, import_jotai25.atom)((get) => {
|
|
const presetId = get(activePresetIdAtom);
|
|
if (!presetId) return null;
|
|
const allPresets = get(allPresetsAtom);
|
|
return allPresets.find((p) => p.id === presetId) || null;
|
|
});
|
|
var isPanelOpenAtom = (0, import_jotai25.atom)((get) => get(canvasSettingsAtom).isPanelOpen);
|
|
var virtualizationEnabledAtom = (0, import_jotai25.atom)((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true);
|
|
var hasUnsavedChangesAtom = (0, import_jotai25.atom)((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 = (0, import_jotai25.atom)(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 = (0, import_jotai25.atom)(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 = (0, import_jotai25.atom)(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 = (0, import_jotai25.atom)(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 = (0, import_jotai25.atom)(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 = (0, import_jotai25.atom)(null, (get, set) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
mappings: DEFAULT_MAPPINGS,
|
|
activePresetId: "default"
|
|
});
|
|
});
|
|
var togglePanelAtom = (0, import_jotai25.atom)(null, (get, set) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
isPanelOpen: !current.isPanelOpen
|
|
});
|
|
});
|
|
var setPanelOpenAtom = (0, import_jotai25.atom)(null, (get, set, isOpen) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
isPanelOpen: isOpen
|
|
});
|
|
});
|
|
var setVirtualizationEnabledAtom = (0, import_jotai25.atom)(null, (get, set, enabled) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
virtualizationEnabled: enabled
|
|
});
|
|
});
|
|
var toggleVirtualizationAtom = (0, import_jotai25.atom)(null, (get, set) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
virtualizationEnabled: !(current.virtualizationEnabled ?? true)
|
|
});
|
|
});
|
|
|
|
// src/hooks/useCanvasSettings.ts
|
|
function useCanvasSettings() {
|
|
const $ = (0, import_compiler_runtime9.c)(34);
|
|
const mappings = (0, import_jotai26.useAtomValue)(eventMappingsAtom);
|
|
const activePresetId = (0, import_jotai26.useAtomValue)(activePresetIdAtom);
|
|
const activePreset = (0, import_jotai26.useAtomValue)(activePresetAtom);
|
|
const allPresets = (0, import_jotai26.useAtomValue)(allPresetsAtom);
|
|
const hasUnsavedChanges = (0, import_jotai26.useAtomValue)(hasUnsavedChangesAtom);
|
|
const isPanelOpen = (0, import_jotai26.useAtomValue)(isPanelOpenAtom);
|
|
const setEventMappingAction = (0, import_jotai26.useSetAtom)(setEventMappingAtom);
|
|
const applyPresetAction = (0, import_jotai26.useSetAtom)(applyPresetAtom);
|
|
const saveAsPresetAction = (0, import_jotai26.useSetAtom)(saveAsPresetAtom);
|
|
const updatePresetAction = (0, import_jotai26.useSetAtom)(updatePresetAtom);
|
|
const deletePresetAction = (0, import_jotai26.useSetAtom)(deletePresetAtom);
|
|
const resetSettingsAction = (0, import_jotai26.useSetAtom)(resetSettingsAtom);
|
|
const togglePanelAction = (0, import_jotai26.useSetAtom)(togglePanelAtom);
|
|
const setPanelOpenAction = (0, import_jotai26.useSetAtom)(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
|
|
var import_compiler_runtime10 = require("react/compiler-runtime");
|
|
var import_jotai28 = require("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
|
|
var import_jotai27 = require("jotai");
|
|
var lockedNodeIdAtom = (0, import_jotai27.atom)(null);
|
|
var lockedNodeDataAtom = (0, import_jotai27.atom)((get) => {
|
|
const id = get(lockedNodeIdAtom);
|
|
if (!id) return null;
|
|
const nodes = get(uiNodesAtom);
|
|
return nodes.find((n) => n.id === id) || null;
|
|
});
|
|
var lockedNodePageIndexAtom = (0, import_jotai27.atom)(0);
|
|
var lockedNodePageCountAtom = (0, import_jotai27.atom)(1);
|
|
var lockNodeAtom = (0, import_jotai27.atom)(null, (_get, set, payload) => {
|
|
set(lockedNodeIdAtom, payload.nodeId);
|
|
set(lockedNodePageIndexAtom, 0);
|
|
});
|
|
var unlockNodeAtom = (0, import_jotai27.atom)(null, (_get, set) => {
|
|
set(lockedNodeIdAtom, null);
|
|
});
|
|
var nextLockedPageAtom = (0, import_jotai27.atom)(null, (get, set) => {
|
|
const current = get(lockedNodePageIndexAtom);
|
|
const pageCount = get(lockedNodePageCountAtom);
|
|
set(lockedNodePageIndexAtom, (current + 1) % pageCount);
|
|
});
|
|
var prevLockedPageAtom = (0, import_jotai27.atom)(null, (get, set) => {
|
|
const current = get(lockedNodePageIndexAtom);
|
|
const pageCount = get(lockedNodePageCountAtom);
|
|
set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount);
|
|
});
|
|
var goToLockedPageAtom = (0, import_jotai27.atom)(null, (get, set, index) => {
|
|
const pageCount = get(lockedNodePageCountAtom);
|
|
if (index >= 0 && index < pageCount) {
|
|
set(lockedNodePageIndexAtom, index);
|
|
}
|
|
});
|
|
var hasLockedNodeAtom = (0, import_jotai27.atom)((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 $ = (0, import_compiler_runtime10.c)(13);
|
|
const options = t0 === void 0 ? {} : t0;
|
|
const store = (0, import_jotai28.useStore)();
|
|
const mappings = (0, import_jotai28.useAtomValue)(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
|
|
var import_compiler_runtime11 = require("react/compiler-runtime");
|
|
var import_jotai30 = require("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
|
|
var import_jotai29 = require("jotai");
|
|
var import_utils2 = require("jotai/utils");
|
|
var DEFAULT_RULE_STATE = {
|
|
customRules: [],
|
|
palmRejection: true
|
|
};
|
|
var gestureRuleSettingsAtom = (0, import_utils2.atomWithStorage)("canvas-gesture-rules", DEFAULT_RULE_STATE);
|
|
var consumerGestureRulesAtom = (0, import_jotai29.atom)([]);
|
|
var gestureRulesAtom = (0, import_jotai29.atom)((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 = (0, import_jotai29.atom)((get) => {
|
|
return buildRuleIndex(get(gestureRulesAtom));
|
|
});
|
|
var palmRejectionEnabledAtom = (0, import_jotai29.atom)((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => {
|
|
const current = get(gestureRuleSettingsAtom);
|
|
set(gestureRuleSettingsAtom, {
|
|
...current,
|
|
palmRejection: enabled
|
|
});
|
|
});
|
|
var addGestureRuleAtom = (0, import_jotai29.atom)(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 = (0, import_jotai29.atom)(null, (get, set, ruleId) => {
|
|
const current = get(gestureRuleSettingsAtom);
|
|
set(gestureRuleSettingsAtom, {
|
|
...current,
|
|
customRules: current.customRules.filter((r) => r.id !== ruleId)
|
|
});
|
|
});
|
|
var updateGestureRuleAtom = (0, import_jotai29.atom)(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 = (0, import_jotai29.atom)(null, (get, set) => {
|
|
const current = get(gestureRuleSettingsAtom);
|
|
set(gestureRuleSettingsAtom, {
|
|
...current,
|
|
customRules: []
|
|
});
|
|
});
|
|
|
|
// src/hooks/useGestureResolver.ts
|
|
function useGestureResolver() {
|
|
const $ = (0, import_compiler_runtime11.c)(4);
|
|
const index = (0, import_jotai30.useAtomValue)(gestureRuleIndexAtom);
|
|
const palmRejection = (0, import_jotai30.useAtomValue)(palmRejectionEnabledAtom);
|
|
const isStylusActive = (0, import_jotai30.useAtomValue)(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
|
|
var import_compiler_runtime12 = require("react/compiler-runtime");
|
|
var import_jotai33 = require("jotai");
|
|
|
|
// src/commands/store.ts
|
|
var import_jotai32 = require("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
|
|
var import_jotai31 = require("jotai");
|
|
var import_utils3 = require("jotai/utils");
|
|
var inputModeAtom = (0, import_jotai31.atom)({
|
|
type: "normal"
|
|
});
|
|
var commandLineVisibleAtom = (0, import_jotai31.atom)(false);
|
|
var commandLineStateAtom = (0, import_jotai31.atom)({
|
|
phase: "idle"
|
|
});
|
|
var commandFeedbackAtom = (0, import_jotai31.atom)(null);
|
|
var commandHistoryAtom = (0, import_utils3.atomWithStorage)("canvas-command-history", []);
|
|
var selectedSuggestionIndexAtom = (0, import_jotai31.atom)(0);
|
|
var pendingInputResolverAtom = (0, import_jotai31.atom)(null);
|
|
var isCommandActiveAtom = (0, import_jotai31.atom)((get) => {
|
|
const state = get(commandLineStateAtom);
|
|
return state.phase === "collecting" || state.phase === "executing";
|
|
});
|
|
var currentInputAtom = (0, import_jotai31.atom)((get) => {
|
|
const state = get(commandLineStateAtom);
|
|
if (state.phase !== "collecting") return null;
|
|
return state.command.inputs[state.inputIndex];
|
|
});
|
|
var commandProgressAtom = (0, import_jotai31.atom)((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 = (0, import_jotai32.atom)(null, (get, set) => {
|
|
set(commandLineVisibleAtom, true);
|
|
set(commandLineStateAtom, {
|
|
phase: "searching",
|
|
query: "",
|
|
suggestions: commandRegistry.all()
|
|
});
|
|
set(selectedSuggestionIndexAtom, 0);
|
|
});
|
|
var closeCommandLineAtom = (0, import_jotai32.atom)(null, (get, set) => {
|
|
set(commandLineVisibleAtom, false);
|
|
set(commandLineStateAtom, {
|
|
phase: "idle"
|
|
});
|
|
set(inputModeAtom, {
|
|
type: "normal"
|
|
});
|
|
set(commandFeedbackAtom, null);
|
|
set(pendingInputResolverAtom, null);
|
|
});
|
|
var updateSearchQueryAtom = (0, import_jotai32.atom)(null, (get, set, query) => {
|
|
const suggestions = commandRegistry.search(query);
|
|
set(commandLineStateAtom, {
|
|
phase: "searching",
|
|
query,
|
|
suggestions
|
|
});
|
|
set(selectedSuggestionIndexAtom, 0);
|
|
});
|
|
var selectCommandAtom = (0, import_jotai32.atom)(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 = (0, import_jotai32.atom)(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 = (0, import_jotai32.atom)(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 = (0, import_jotai32.atom)(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 = (0, import_jotai32.atom)(null, (get, set, message) => {
|
|
set(commandLineStateAtom, {
|
|
phase: "error",
|
|
message
|
|
});
|
|
set(inputModeAtom, {
|
|
type: "normal"
|
|
});
|
|
});
|
|
var clearCommandErrorAtom = (0, import_jotai32.atom)(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 $ = (0, import_compiler_runtime12.c)(16);
|
|
const visible = (0, import_jotai33.useAtomValue)(commandLineVisibleAtom);
|
|
const state = (0, import_jotai33.useAtomValue)(commandLineStateAtom);
|
|
const history = (0, import_jotai33.useAtomValue)(commandHistoryAtom);
|
|
const currentInput = (0, import_jotai33.useAtomValue)(currentInputAtom);
|
|
const progress = (0, import_jotai33.useAtomValue)(commandProgressAtom);
|
|
const open = (0, import_jotai33.useSetAtom)(openCommandLineAtom);
|
|
const close = (0, import_jotai33.useSetAtom)(closeCommandLineAtom);
|
|
const updateQuery = (0, import_jotai33.useSetAtom)(updateSearchQueryAtom);
|
|
const selectCommand = (0, import_jotai33.useSetAtom)(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
|
|
var import_compiler_runtime13 = require("react/compiler-runtime");
|
|
var import_jotai35 = require("jotai");
|
|
|
|
// src/core/virtualization-store.ts
|
|
var import_jotai34 = require("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 = (0, import_jotai34.atom)((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 = (0, import_jotai34.atom)((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 = (0, import_jotai34.atom)((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 = (0, import_jotai34.atom)((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 = (0, import_jotai34.atom)((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 $ = (0, import_compiler_runtime13.c)(8);
|
|
const metrics = (0, import_jotai35.useAtomValue)(virtualizationMetricsAtom);
|
|
const setEnabled = (0, import_jotai35.useSetAtom)(setVirtualizationEnabledAtom);
|
|
const toggle = (0, import_jotai35.useSetAtom)(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
|
|
var import_compiler_runtime14 = require("react/compiler-runtime");
|
|
var import_react5 = require("react");
|
|
function useTapGesture(t0) {
|
|
const $ = (0, import_compiler_runtime14.c)(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 = (0, import_react5.useRef)(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
|
|
var import_compiler_runtime15 = require("react/compiler-runtime");
|
|
var import_jotai36 = require("jotai");
|
|
function useArrowKeyNavigation() {
|
|
const $ = (0, import_compiler_runtime15.c)(2);
|
|
const focusedNodeId = (0, import_jotai36.useAtomValue)(focusedNodeIdAtom);
|
|
let t0;
|
|
if ($[0] !== focusedNodeId) {
|
|
t0 = {
|
|
focusedNodeId
|
|
};
|
|
$[0] = focusedNodeId;
|
|
$[1] = t0;
|
|
} else {
|
|
t0 = $[1];
|
|
}
|
|
return t0;
|
|
}
|
|
|
|
// src/hooks/useCanvasGraph.ts
|
|
var import_compiler_runtime16 = require("react/compiler-runtime");
|
|
var import_jotai37 = require("jotai");
|
|
function useCanvasGraph() {
|
|
const $ = (0, import_compiler_runtime16.c)(9);
|
|
const graph = (0, import_jotai37.useAtomValue)(graphAtom);
|
|
const nodeKeys = (0, import_jotai37.useAtomValue)(nodeKeysAtom);
|
|
const edgeKeys = (0, import_jotai37.useAtomValue)(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
|
|
var import_compiler_runtime17 = require("react/compiler-runtime");
|
|
var import_react6 = require("react");
|
|
var import_jotai38 = require("jotai");
|
|
function easeInOutCubic(t) {
|
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
}
|
|
function useZoomTransition() {
|
|
const $ = (0, import_compiler_runtime17.c)(15);
|
|
const target = (0, import_jotai38.useAtomValue)(zoomAnimationTargetAtom);
|
|
const setZoom = (0, import_jotai38.useSetAtom)(zoomAtom);
|
|
const setPan = (0, import_jotai38.useSetAtom)(panAtom);
|
|
const setTarget = (0, import_jotai38.useSetAtom)(zoomAnimationTargetAtom);
|
|
const setProgress = (0, import_jotai38.useSetAtom)(zoomTransitionProgressAtom);
|
|
const setFocusNode = (0, import_jotai38.useSetAtom)(zoomFocusNodeIdAtom);
|
|
const progress = (0, import_jotai38.useAtomValue)(zoomTransitionProgressAtom);
|
|
const rafRef = (0, import_react6.useRef)(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];
|
|
}
|
|
(0, import_react6.useEffect)(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
|
|
var import_compiler_runtime18 = require("react/compiler-runtime");
|
|
var import_react7 = require("react");
|
|
var import_jotai39 = require("jotai");
|
|
var SPLIT_THRESHOLD = 80;
|
|
function useSplitGesture(nodeId) {
|
|
const $ = (0, import_compiler_runtime18.c)(9);
|
|
const splitNode = (0, import_jotai39.useSetAtom)(splitNodeAtom);
|
|
const screenToWorld = (0, import_jotai39.useAtomValue)(screenToWorldAtom);
|
|
let t0;
|
|
if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
|
|
t0 = /* @__PURE__ */ new Map();
|
|
$[0] = t0;
|
|
} else {
|
|
t0 = $[0];
|
|
}
|
|
const pointersRef = (0, import_react7.useRef)(t0);
|
|
const initialDistanceRef = (0, import_react7.useRef)(null);
|
|
const splitFiredRef = (0, import_react7.useRef)(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
|
|
var import_jotai40 = require("jotai");
|
|
var import_react8 = require("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 = (0, import_jotai40.useAtomValue)(graphAtom);
|
|
const updateNodePosition = (0, import_jotai40.useSetAtom)(updateNodePositionAtom);
|
|
const pushHistory = (0, import_jotai40.useSetAtom)(pushHistoryAtom);
|
|
const setPositionCounter = (0, import_jotai40.useSetAtom)(nodePositionUpdateCounterAtom);
|
|
const reducedMotion = (0, import_jotai40.useAtomValue)(prefersReducedMotionAtom);
|
|
const isAnimatingRef = (0, import_react8.useRef)(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
|
|
var import_jotai41 = require("jotai");
|
|
var import_react9 = require("react");
|
|
function useTreeLayout(options = {}) {
|
|
const {
|
|
direction = "top-down",
|
|
levelGap = 200,
|
|
nodeGap = 100,
|
|
...animateOptions
|
|
} = options;
|
|
const graph = (0, import_jotai41.useAtomValue)(graphAtom);
|
|
const nodes = (0, import_jotai41.useAtomValue)(uiNodesAtom);
|
|
const {
|
|
animate,
|
|
isAnimating
|
|
} = useAnimatedLayout(animateOptions);
|
|
const isRunningRef = (0, import_react9.useRef)(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
|
|
var import_jotai42 = require("jotai");
|
|
var import_react10 = require("react");
|
|
function useGridLayout(options = {}) {
|
|
const {
|
|
columns,
|
|
gap = 80,
|
|
...animateOptions
|
|
} = options;
|
|
const graph = (0, import_jotai42.useAtomValue)(graphAtom);
|
|
const nodes = (0, import_jotai42.useAtomValue)(uiNodesAtom);
|
|
const {
|
|
animate,
|
|
isAnimating
|
|
} = useAnimatedLayout(animateOptions);
|
|
const isRunningRef = (0, import_react10.useRef)(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
|
|
var import_compiler_runtime20 = require("react/compiler-runtime");
|
|
var import_react12 = require("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
|
|
var import_compiler_runtime19 = require("react/compiler-runtime");
|
|
var import_react11 = __toESM(require("react"));
|
|
var import_jsx_runtime = require("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 $ = (0, import_compiler_runtime20.c)(4);
|
|
const registeredRef = (0, import_react12.useRef)(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];
|
|
}
|
|
(0, import_react12.useEffect)(t0, t1);
|
|
}
|
|
function usePlugins(plugins2) {
|
|
const $ = (0, import_compiler_runtime20.c)(6);
|
|
let t0;
|
|
if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
|
|
t0 = [];
|
|
$[0] = t0;
|
|
} else {
|
|
t0 = $[0];
|
|
}
|
|
const registeredRef = (0, import_react12.useRef)(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];
|
|
}
|
|
(0, import_react12.useEffect)(t1, t3);
|
|
}
|
|
function _temp7(p) {
|
|
return p.id;
|
|
}
|
|
//# sourceMappingURL=index.js.map
|