7703 lines
No EOL
254 KiB
JavaScript
7703 lines
No EOL
254 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 __esm = (fn, res) => function __init() {
|
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
};
|
|
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/commands/registry.ts
|
|
function registerCommand(command) {
|
|
commandRegistry.register(command);
|
|
}
|
|
var CommandRegistry, commandRegistry;
|
|
var init_registry = __esm({
|
|
"src/commands/registry.ts"() {
|
|
"use strict";
|
|
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
|
|
}))
|
|
}));
|
|
}
|
|
};
|
|
commandRegistry = new CommandRegistry();
|
|
}
|
|
});
|
|
|
|
// src/core/graph-store.ts
|
|
var graph_store_exports = {};
|
|
__export(graph_store_exports, {
|
|
currentGraphIdAtom: () => currentGraphIdAtom,
|
|
draggingNodeIdAtom: () => draggingNodeIdAtom,
|
|
edgeCreationAtom: () => edgeCreationAtom,
|
|
graphAtom: () => graphAtom,
|
|
graphOptions: () => graphOptions,
|
|
graphUpdateVersionAtom: () => graphUpdateVersionAtom,
|
|
preDragNodeAttributesAtom: () => preDragNodeAttributesAtom
|
|
});
|
|
var import_jotai3, import_graphology, graphOptions, currentGraphIdAtom, graphAtom, graphUpdateVersionAtom, edgeCreationAtom, draggingNodeIdAtom, preDragNodeAttributesAtom;
|
|
var init_graph_store = __esm({
|
|
"src/core/graph-store.ts"() {
|
|
"use strict";
|
|
import_jotai3 = require("jotai");
|
|
import_graphology = __toESM(require("graphology"));
|
|
graphOptions = {
|
|
type: "directed",
|
|
multi: true,
|
|
allowSelfLoops: true
|
|
};
|
|
currentGraphIdAtom = (0, import_jotai3.atom)(null);
|
|
graphAtom = (0, import_jotai3.atom)(new import_graphology.default(graphOptions));
|
|
graphUpdateVersionAtom = (0, import_jotai3.atom)(0);
|
|
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
|
|
});
|
|
draggingNodeIdAtom = (0, import_jotai3.atom)(null);
|
|
preDragNodeAttributesAtom = (0, import_jotai3.atom)(null);
|
|
}
|
|
});
|
|
|
|
// src/utils/debug.ts
|
|
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 import_debug, NAMESPACE, debug;
|
|
var init_debug = __esm({
|
|
"src/utils/debug.ts"() {
|
|
"use strict";
|
|
import_debug = __toESM(require("debug"));
|
|
NAMESPACE = "canvas";
|
|
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 selection_store_exports = {};
|
|
__export(selection_store_exports, {
|
|
addNodesToSelectionAtom: () => addNodesToSelectionAtom,
|
|
clearEdgeSelectionAtom: () => clearEdgeSelectionAtom,
|
|
clearSelectionAtom: () => clearSelectionAtom,
|
|
focusedNodeIdAtom: () => focusedNodeIdAtom,
|
|
handleNodePointerDownSelectionAtom: () => handleNodePointerDownSelectionAtom,
|
|
hasFocusedNodeAtom: () => hasFocusedNodeAtom,
|
|
hasSelectionAtom: () => hasSelectionAtom,
|
|
removeNodesFromSelectionAtom: () => removeNodesFromSelectionAtom,
|
|
selectEdgeAtom: () => selectEdgeAtom,
|
|
selectSingleNodeAtom: () => selectSingleNodeAtom,
|
|
selectedEdgeIdAtom: () => selectedEdgeIdAtom,
|
|
selectedNodeIdsAtom: () => selectedNodeIdsAtom,
|
|
selectedNodesCountAtom: () => selectedNodesCountAtom,
|
|
setFocusedNodeAtom: () => setFocusedNodeAtom,
|
|
toggleNodeInSelectionAtom: () => toggleNodeInSelectionAtom
|
|
});
|
|
var import_jotai4, debug2, selectedNodeIdsAtom, selectedEdgeIdAtom, handleNodePointerDownSelectionAtom, selectSingleNodeAtom, toggleNodeInSelectionAtom, clearSelectionAtom, addNodesToSelectionAtom, removeNodesFromSelectionAtom, selectEdgeAtom, clearEdgeSelectionAtom, focusedNodeIdAtom, setFocusedNodeAtom, hasFocusedNodeAtom, selectedNodesCountAtom, hasSelectionAtom;
|
|
var init_selection_store = __esm({
|
|
"src/core/selection-store.ts"() {
|
|
"use strict";
|
|
import_jotai4 = require("jotai");
|
|
init_debug();
|
|
debug2 = createDebug("selection");
|
|
selectedNodeIdsAtom = (0, import_jotai4.atom)(/* @__PURE__ */ new Set());
|
|
selectedEdgeIdAtom = (0, import_jotai4.atom)(null);
|
|
handleNodePointerDownSelectionAtom = (0, import_jotai4.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");
|
|
}
|
|
}
|
|
});
|
|
selectSingleNodeAtom = (0, import_jotai4.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]));
|
|
}
|
|
});
|
|
toggleNodeInSelectionAtom = (0, import_jotai4.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);
|
|
});
|
|
clearSelectionAtom = (0, import_jotai4.atom)(null, (_get, set) => {
|
|
debug2("clearSelection");
|
|
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
|
|
});
|
|
addNodesToSelectionAtom = (0, import_jotai4.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);
|
|
});
|
|
removeNodesFromSelectionAtom = (0, import_jotai4.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);
|
|
});
|
|
selectEdgeAtom = (0, import_jotai4.atom)(null, (get, set, edgeId) => {
|
|
set(selectedEdgeIdAtom, edgeId);
|
|
if (edgeId !== null) {
|
|
set(selectedNodeIdsAtom, /* @__PURE__ */ new Set());
|
|
}
|
|
});
|
|
clearEdgeSelectionAtom = (0, import_jotai4.atom)(null, (_get, set) => {
|
|
set(selectedEdgeIdAtom, null);
|
|
});
|
|
focusedNodeIdAtom = (0, import_jotai4.atom)(null);
|
|
setFocusedNodeAtom = (0, import_jotai4.atom)(null, (_get, set, nodeId) => {
|
|
set(focusedNodeIdAtom, nodeId);
|
|
});
|
|
hasFocusedNodeAtom = (0, import_jotai4.atom)((get) => get(focusedNodeIdAtom) !== null);
|
|
selectedNodesCountAtom = (0, import_jotai4.atom)((get) => get(selectedNodeIdsAtom).size);
|
|
hasSelectionAtom = (0, import_jotai4.atom)((get) => get(selectedNodeIdsAtom).size > 0);
|
|
}
|
|
});
|
|
|
|
// src/utils/mutation-queue.ts
|
|
function clearAllPendingMutations() {
|
|
pendingNodeMutations.clear();
|
|
}
|
|
var pendingNodeMutations;
|
|
var init_mutation_queue = __esm({
|
|
"src/utils/mutation-queue.ts"() {
|
|
"use strict";
|
|
pendingNodeMutations = /* @__PURE__ */ new Map();
|
|
}
|
|
});
|
|
|
|
// src/core/perf.ts
|
|
function setPerfEnabled(enabled) {
|
|
_enabled = enabled;
|
|
}
|
|
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() {
|
|
}
|
|
function canvasWrap(name, fn) {
|
|
const end = canvasMark(name);
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
end();
|
|
}
|
|
}
|
|
var import_jotai5, perfEnabledAtom, _enabled;
|
|
var init_perf = __esm({
|
|
"src/core/perf.ts"() {
|
|
"use strict";
|
|
import_jotai5 = require("jotai");
|
|
perfEnabledAtom = (0, import_jotai5.atom)(false);
|
|
_enabled = false;
|
|
if (typeof window !== "undefined") {
|
|
window.__canvasPerf = setPerfEnabled;
|
|
}
|
|
}
|
|
});
|
|
|
|
// src/core/graph-position.ts
|
|
function getPositionCache(graph) {
|
|
let cache = _positionCacheByGraph.get(graph);
|
|
if (!cache) {
|
|
cache = /* @__PURE__ */ new Map();
|
|
_positionCacheByGraph.set(graph, cache);
|
|
}
|
|
return cache;
|
|
}
|
|
var import_jotai6, import_jotai_family, import_graphology2, debug3, _positionCacheByGraph, nodePositionUpdateCounterAtom, nodePositionAtomFamily, updateNodePositionAtom, cleanupNodePositionAtom, cleanupAllNodePositionsAtom, clearGraphOnSwitchAtom;
|
|
var init_graph_position = __esm({
|
|
"src/core/graph-position.ts"() {
|
|
"use strict";
|
|
import_jotai6 = require("jotai");
|
|
import_jotai_family = require("jotai-family");
|
|
import_graphology2 = __toESM(require("graphology"));
|
|
init_graph_store();
|
|
init_debug();
|
|
init_mutation_queue();
|
|
init_perf();
|
|
debug3 = createDebug("graph:position");
|
|
_positionCacheByGraph = /* @__PURE__ */ new WeakMap();
|
|
nodePositionUpdateCounterAtom = (0, import_jotai6.atom)(0);
|
|
nodePositionAtomFamily = (0, import_jotai_family.atomFamily)((nodeId) => (0, import_jotai6.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;
|
|
}));
|
|
updateNodePositionAtom = (0, import_jotai6.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();
|
|
});
|
|
cleanupNodePositionAtom = (0, import_jotai6.atom)(null, (get, _set, nodeId) => {
|
|
nodePositionAtomFamily.remove(nodeId);
|
|
const graph = get(graphAtom);
|
|
getPositionCache(graph).delete(nodeId);
|
|
debug3("Removed position atom for node: %s", nodeId);
|
|
});
|
|
cleanupAllNodePositionsAtom = (0, import_jotai6.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);
|
|
});
|
|
clearGraphOnSwitchAtom = (0, import_jotai6.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/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
|
|
};
|
|
}
|
|
var init_history_actions = __esm({
|
|
"src/core/history-actions.ts"() {
|
|
"use strict";
|
|
}
|
|
});
|
|
|
|
// src/core/history-store.ts
|
|
var history_store_exports = {};
|
|
__export(history_store_exports, {
|
|
applyDelta: () => applyDelta,
|
|
canRedoAtom: () => canRedoAtom,
|
|
canUndoAtom: () => canUndoAtom,
|
|
clearHistoryAtom: () => clearHistoryAtom,
|
|
createSnapshot: () => createSnapshot,
|
|
historyLabelsAtom: () => historyLabelsAtom,
|
|
historyStateAtom: () => historyStateAtom,
|
|
invertDelta: () => invertDelta,
|
|
pushDeltaAtom: () => pushDeltaAtom,
|
|
pushHistoryAtom: () => pushHistoryAtom,
|
|
redoAtom: () => redoAtom,
|
|
redoCountAtom: () => redoCountAtom,
|
|
undoAtom: () => undoAtom,
|
|
undoCountAtom: () => undoCountAtom
|
|
});
|
|
var import_jotai7, debug4, MAX_HISTORY_SIZE, historyStateAtom, canUndoAtom, canRedoAtom, undoCountAtom, redoCountAtom, pushDeltaAtom, pushHistoryAtom, undoAtom, redoAtom, clearHistoryAtom, historyLabelsAtom;
|
|
var init_history_store = __esm({
|
|
"src/core/history-store.ts"() {
|
|
"use strict";
|
|
import_jotai7 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_debug();
|
|
init_history_actions();
|
|
init_history_actions();
|
|
debug4 = createDebug("history");
|
|
MAX_HISTORY_SIZE = 50;
|
|
historyStateAtom = (0, import_jotai7.atom)({
|
|
past: [],
|
|
future: [],
|
|
isApplying: false
|
|
});
|
|
canUndoAtom = (0, import_jotai7.atom)((get) => {
|
|
const history = get(historyStateAtom);
|
|
return history.past.length > 0 && !history.isApplying;
|
|
});
|
|
canRedoAtom = (0, import_jotai7.atom)((get) => {
|
|
const history = get(historyStateAtom);
|
|
return history.future.length > 0 && !history.isApplying;
|
|
});
|
|
undoCountAtom = (0, import_jotai7.atom)((get) => get(historyStateAtom).past.length);
|
|
redoCountAtom = (0, import_jotai7.atom)((get) => get(historyStateAtom).future.length);
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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;
|
|
}
|
|
});
|
|
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;
|
|
}
|
|
});
|
|
clearHistoryAtom = (0, import_jotai7.atom)(null, (_get, set) => {
|
|
set(historyStateAtom, {
|
|
past: [],
|
|
future: [],
|
|
isApplying: false
|
|
});
|
|
debug4("History cleared");
|
|
});
|
|
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 group_store_exports = {};
|
|
__export(group_store_exports, {
|
|
autoResizeGroupAtom: () => autoResizeGroupAtom,
|
|
collapseGroupAtom: () => collapseGroupAtom,
|
|
collapsedEdgeRemapAtom: () => collapsedEdgeRemapAtom,
|
|
collapsedGroupsAtom: () => collapsedGroupsAtom,
|
|
expandGroupAtom: () => expandGroupAtom,
|
|
getNodeDescendants: () => getNodeDescendants,
|
|
groupChildCountAtom: () => groupChildCountAtom,
|
|
groupSelectedNodesAtom: () => groupSelectedNodesAtom,
|
|
isGroupNodeAtom: () => isGroupNodeAtom,
|
|
isNodeCollapsed: () => isNodeCollapsed,
|
|
moveNodesToGroupAtom: () => moveNodesToGroupAtom,
|
|
nestNodesOnDropAtom: () => nestNodesOnDropAtom,
|
|
nodeChildrenAtom: () => nodeChildrenAtom,
|
|
nodeParentAtom: () => nodeParentAtom,
|
|
removeFromGroupAtom: () => removeFromGroupAtom,
|
|
setNodeParentAtom: () => setNodeParentAtom,
|
|
toggleGroupCollapseAtom: () => toggleGroupCollapseAtom,
|
|
ungroupNodesAtom: () => ungroupNodesAtom
|
|
});
|
|
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;
|
|
}
|
|
function isNodeCollapsed(nodeId, getParentId, collapsed) {
|
|
let current = nodeId;
|
|
while (true) {
|
|
const parentId = getParentId(current);
|
|
if (!parentId) return false;
|
|
if (collapsed.has(parentId)) return true;
|
|
current = parentId;
|
|
}
|
|
}
|
|
var import_jotai8, collapsedGroupsAtom, toggleGroupCollapseAtom, collapseGroupAtom, expandGroupAtom, nodeChildrenAtom, nodeParentAtom, isGroupNodeAtom, groupChildCountAtom, setNodeParentAtom, moveNodesToGroupAtom, removeFromGroupAtom, groupSelectedNodesAtom, ungroupNodesAtom, nestNodesOnDropAtom, collapsedEdgeRemapAtom, autoResizeGroupAtom;
|
|
var init_group_store = __esm({
|
|
"src/core/group-store.ts"() {
|
|
"use strict";
|
|
import_jotai8 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_history_store();
|
|
collapsedGroupsAtom = (0, import_jotai8.atom)(/* @__PURE__ */ new Set());
|
|
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);
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
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;
|
|
};
|
|
});
|
|
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");
|
|
};
|
|
});
|
|
isGroupNodeAtom = (0, import_jotai8.atom)((get) => {
|
|
const getChildren = get(nodeChildrenAtom);
|
|
return (nodeId) => getChildren(nodeId).length > 0;
|
|
});
|
|
groupChildCountAtom = (0, import_jotai8.atom)((get) => {
|
|
const getChildren = get(nodeChildrenAtom);
|
|
return (groupId) => getChildren(groupId).length;
|
|
});
|
|
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);
|
|
});
|
|
moveNodesToGroupAtom = (0, import_jotai8.atom)(null, (get, set, {
|
|
nodeIds,
|
|
groupId
|
|
}) => {
|
|
for (const nodeId of nodeIds) {
|
|
set(setNodeParentAtom, {
|
|
nodeId,
|
|
parentId: groupId
|
|
});
|
|
}
|
|
});
|
|
removeFromGroupAtom = (0, import_jotai8.atom)(null, (get, set, nodeId) => {
|
|
set(setNodeParentAtom, {
|
|
nodeId,
|
|
parentId: void 0
|
|
});
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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);
|
|
});
|
|
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;
|
|
});
|
|
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
|
|
function getEdgeCache(graph) {
|
|
let cache = _edgeCacheByGraph.get(graph);
|
|
if (!cache) {
|
|
cache = /* @__PURE__ */ new Map();
|
|
_edgeCacheByGraph.set(graph, cache);
|
|
}
|
|
return cache;
|
|
}
|
|
var import_jotai9, import_jotai_family2, highestZIndexAtom, _prevUiNodesByGraph, uiNodesAtom, nodeKeysAtom, nodeFamilyAtom, edgeKeysAtom, edgeKeysWithTempEdgeAtom, _edgeCacheByGraph, edgeFamilyAtom;
|
|
var init_graph_derived = __esm({
|
|
"src/core/graph-derived.ts"() {
|
|
"use strict";
|
|
import_jotai9 = require("jotai");
|
|
import_jotai_family2 = require("jotai-family");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_viewport_store();
|
|
init_group_store();
|
|
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;
|
|
});
|
|
_prevUiNodesByGraph = /* @__PURE__ */ new WeakMap();
|
|
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;
|
|
});
|
|
nodeKeysAtom = (0, import_jotai9.atom)((get) => {
|
|
get(graphUpdateVersionAtom);
|
|
const graph = get(graphAtom);
|
|
return graph.nodes();
|
|
});
|
|
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);
|
|
edgeKeysAtom = (0, import_jotai9.atom)((get) => {
|
|
get(graphUpdateVersionAtom);
|
|
const graph = get(graphAtom);
|
|
return graph.edges();
|
|
});
|
|
edgeKeysWithTempEdgeAtom = (0, import_jotai9.atom)((get) => {
|
|
const keys = get(edgeKeysAtom);
|
|
const edgeCreation = get(edgeCreationAtom);
|
|
if (edgeCreation.isCreating) {
|
|
return [...keys, "temp-creating-edge"];
|
|
}
|
|
return keys;
|
|
});
|
|
_edgeCacheByGraph = /* @__PURE__ */ new WeakMap();
|
|
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/utils/layout.ts
|
|
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;
|
|
}
|
|
var FitToBoundsMode, calculateBounds;
|
|
var init_layout = __esm({
|
|
"src/utils/layout.ts"() {
|
|
"use strict";
|
|
FitToBoundsMode = /* @__PURE__ */ (function(FitToBoundsMode2) {
|
|
FitToBoundsMode2["Graph"] = "graph";
|
|
FitToBoundsMode2["Selection"] = "selection";
|
|
return FitToBoundsMode2;
|
|
})({});
|
|
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
|
|
};
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/core/viewport-store.ts
|
|
var viewport_store_exports = {};
|
|
__export(viewport_store_exports, {
|
|
ZOOM_EXIT_THRESHOLD: () => ZOOM_EXIT_THRESHOLD,
|
|
ZOOM_TRANSITION_THRESHOLD: () => ZOOM_TRANSITION_THRESHOLD,
|
|
animateFitToBoundsAtom: () => animateFitToBoundsAtom,
|
|
animateZoomToNodeAtom: () => animateZoomToNodeAtom,
|
|
centerOnNodeAtom: () => centerOnNodeAtom,
|
|
fitToBoundsAtom: () => fitToBoundsAtom,
|
|
isZoomTransitioningAtom: () => isZoomTransitioningAtom,
|
|
panAtom: () => panAtom,
|
|
resetViewportAtom: () => resetViewportAtom,
|
|
screenToWorldAtom: () => screenToWorldAtom,
|
|
setZoomAtom: () => setZoomAtom,
|
|
viewportRectAtom: () => viewportRectAtom,
|
|
worldToScreenAtom: () => worldToScreenAtom,
|
|
zoomAnimationTargetAtom: () => zoomAnimationTargetAtom,
|
|
zoomAtom: () => zoomAtom,
|
|
zoomFocusNodeIdAtom: () => zoomFocusNodeIdAtom,
|
|
zoomTransitionProgressAtom: () => zoomTransitionProgressAtom
|
|
});
|
|
var import_jotai10, zoomAtom, panAtom, viewportRectAtom, screenToWorldAtom, worldToScreenAtom, setZoomAtom, resetViewportAtom, fitToBoundsAtom, centerOnNodeAtom, ZOOM_TRANSITION_THRESHOLD, ZOOM_EXIT_THRESHOLD, zoomFocusNodeIdAtom, zoomTransitionProgressAtom, isZoomTransitioningAtom, zoomAnimationTargetAtom, animateZoomToNodeAtom, animateFitToBoundsAtom;
|
|
var init_viewport_store = __esm({
|
|
"src/core/viewport-store.ts"() {
|
|
"use strict";
|
|
import_jotai10 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_selection_store();
|
|
init_layout();
|
|
zoomAtom = (0, import_jotai10.atom)(1);
|
|
panAtom = (0, import_jotai10.atom)({
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
viewportRectAtom = (0, import_jotai10.atom)(null);
|
|
screenToWorldAtom = (0, import_jotai10.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
|
|
};
|
|
};
|
|
});
|
|
worldToScreenAtom = (0, import_jotai10.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
|
|
};
|
|
};
|
|
});
|
|
setZoomAtom = (0, import_jotai10.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);
|
|
});
|
|
resetViewportAtom = (0, import_jotai10.atom)(null, (_get, set) => {
|
|
set(zoomAtom, 1);
|
|
set(panAtom, {
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
});
|
|
fitToBoundsAtom = (0, import_jotai10.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
|
|
});
|
|
});
|
|
centerOnNodeAtom = (0, import_jotai10.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
|
|
});
|
|
});
|
|
ZOOM_TRANSITION_THRESHOLD = 3.5;
|
|
ZOOM_EXIT_THRESHOLD = 2;
|
|
zoomFocusNodeIdAtom = (0, import_jotai10.atom)(null);
|
|
zoomTransitionProgressAtom = (0, import_jotai10.atom)(0);
|
|
isZoomTransitioningAtom = (0, import_jotai10.atom)((get) => {
|
|
const progress = get(zoomTransitionProgressAtom);
|
|
return progress > 0 && progress < 1;
|
|
});
|
|
zoomAnimationTargetAtom = (0, import_jotai10.atom)(null);
|
|
animateZoomToNodeAtom = (0, import_jotai10.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()
|
|
});
|
|
});
|
|
animateFitToBoundsAtom = (0, import_jotai10.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/reduced-motion-store.ts
|
|
var import_jotai13, prefersReducedMotionAtom, watchReducedMotionAtom;
|
|
var init_reduced_motion_store = __esm({
|
|
"src/core/reduced-motion-store.ts"() {
|
|
"use strict";
|
|
import_jotai13 = require("jotai");
|
|
prefersReducedMotionAtom = (0, import_jotai13.atom)(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false);
|
|
watchReducedMotionAtom = (0, import_jotai13.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/types.ts
|
|
var init_types = __esm({
|
|
"src/core/types.ts"() {
|
|
"use strict";
|
|
}
|
|
});
|
|
|
|
// src/core/graph-mutations-edges.ts
|
|
var import_jotai19, debug7, addEdgeToLocalGraphAtom, removeEdgeFromLocalGraphAtom, swapEdgeAtomicAtom, departingEdgesAtom, EDGE_ANIMATION_DURATION, removeEdgeWithAnimationAtom, editingEdgeLabelAtom, updateEdgeLabelAtom;
|
|
var init_graph_mutations_edges = __esm({
|
|
"src/core/graph-mutations-edges.ts"() {
|
|
"use strict";
|
|
import_jotai19 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_debug();
|
|
init_reduced_motion_store();
|
|
debug7 = createDebug("graph:mutations:edges");
|
|
addEdgeToLocalGraphAtom = (0, import_jotai19.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 {
|
|
debug7("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) {
|
|
debug7("Failed to add edge %s: %o", newEdge.id, e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
removeEdgeFromLocalGraphAtom = (0, import_jotai19.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);
|
|
}
|
|
});
|
|
swapEdgeAtomicAtom = (0, import_jotai19.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 {
|
|
debug7("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) {
|
|
debug7("Failed to add edge %s: %o", newEdge.id, e);
|
|
}
|
|
}
|
|
}
|
|
set(graphAtom, graph.copy());
|
|
set(graphUpdateVersionAtom, (v) => v + 1);
|
|
});
|
|
departingEdgesAtom = (0, import_jotai19.atom)(/* @__PURE__ */ new Map());
|
|
EDGE_ANIMATION_DURATION = 300;
|
|
removeEdgeWithAnimationAtom = (0, import_jotai19.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);
|
|
}
|
|
});
|
|
editingEdgeLabelAtom = (0, import_jotai19.atom)(null);
|
|
updateEdgeLabelAtom = (0, import_jotai19.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_jotai20, debug8, dropTargetNodeIdAtom, splitNodeAtom, mergeNodesAtom;
|
|
var init_graph_mutations_advanced = __esm({
|
|
"src/core/graph-mutations-advanced.ts"() {
|
|
"use strict";
|
|
import_jotai20 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_history_store();
|
|
init_debug();
|
|
init_graph_mutations_edges();
|
|
init_graph_mutations();
|
|
debug8 = createDebug("graph:mutations:advanced");
|
|
dropTargetNodeIdAtom = (0, import_jotai20.atom)(null);
|
|
splitNodeAtom = (0, import_jotai20.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);
|
|
debug8("Split node %s \u2192 clone %s", nodeId, cloneId);
|
|
});
|
|
mergeNodesAtom = (0, import_jotai20.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);
|
|
debug8("Merged nodes %o \u2192 survivor %s", nodeIds, survivorId);
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/graph-mutations.ts
|
|
var graph_mutations_exports = {};
|
|
__export(graph_mutations_exports, {
|
|
EDGE_ANIMATION_DURATION: () => EDGE_ANIMATION_DURATION,
|
|
addNodeToLocalGraphAtom: () => addNodeToLocalGraphAtom,
|
|
departingEdgesAtom: () => departingEdgesAtom,
|
|
dropTargetNodeIdAtom: () => dropTargetNodeIdAtom,
|
|
editingEdgeLabelAtom: () => editingEdgeLabelAtom,
|
|
endNodeDragAtom: () => endNodeDragAtom,
|
|
loadGraphFromDbAtom: () => loadGraphFromDbAtom,
|
|
mergeNodesAtom: () => mergeNodesAtom,
|
|
optimisticDeleteEdgeAtom: () => optimisticDeleteEdgeAtom,
|
|
optimisticDeleteNodeAtom: () => optimisticDeleteNodeAtom,
|
|
removeEdgeWithAnimationAtom: () => removeEdgeWithAnimationAtom,
|
|
splitNodeAtom: () => splitNodeAtom,
|
|
startNodeDragAtom: () => startNodeDragAtom,
|
|
swapEdgeAtomicAtom: () => swapEdgeAtomicAtom,
|
|
updateEdgeLabelAtom: () => updateEdgeLabelAtom
|
|
});
|
|
var import_jotai21, import_graphology3, debug9, startNodeDragAtom, endNodeDragAtom, optimisticDeleteNodeAtom, optimisticDeleteEdgeAtom, addNodeToLocalGraphAtom, loadGraphFromDbAtom;
|
|
var init_graph_mutations = __esm({
|
|
"src/core/graph-mutations.ts"() {
|
|
"use strict";
|
|
import_jotai21 = require("jotai");
|
|
import_graphology3 = __toESM(require("graphology"));
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_group_store();
|
|
init_debug();
|
|
init_graph_mutations_edges();
|
|
init_graph_mutations_advanced();
|
|
debug9 = createDebug("graph:mutations");
|
|
startNodeDragAtom = (0, import_jotai21.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);
|
|
});
|
|
endNodeDragAtom = (0, import_jotai21.atom)(null, (get, set, _payload) => {
|
|
const currentDraggingId = get(draggingNodeIdAtom);
|
|
if (currentDraggingId) {
|
|
debug9("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);
|
|
});
|
|
optimisticDeleteNodeAtom = (0, import_jotai21.atom)(null, (get, set, {
|
|
nodeId
|
|
}) => {
|
|
const graph = get(graphAtom);
|
|
if (graph.hasNode(nodeId)) {
|
|
graph.dropNode(nodeId);
|
|
set(cleanupNodePositionAtom, nodeId);
|
|
set(graphAtom, graph.copy());
|
|
debug9("Optimistically deleted node %s", nodeId);
|
|
}
|
|
});
|
|
optimisticDeleteEdgeAtom = (0, import_jotai21.atom)(null, (get, set, {
|
|
edgeKey
|
|
}) => {
|
|
const graph = get(graphAtom);
|
|
if (graph.hasEdge(edgeKey)) {
|
|
graph.dropEdge(edgeKey);
|
|
set(graphAtom, graph.copy());
|
|
debug9("Optimistically deleted edge %s", edgeKey);
|
|
}
|
|
});
|
|
addNodeToLocalGraphAtom = (0, import_jotai21.atom)(null, (get, set, newNode) => {
|
|
const graph = get(graphAtom);
|
|
if (graph.hasNode(newNode.id)) {
|
|
debug9("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
|
|
};
|
|
debug9("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);
|
|
});
|
|
loadGraphFromDbAtom = (0, import_jotai21.atom)(null, (get, set, fetchedNodes, fetchedEdges) => {
|
|
debug9("========== START SYNC ==========");
|
|
debug9("Fetched nodes: %d, edges: %d", fetchedNodes.length, fetchedEdges.length);
|
|
const currentGraphId = get(currentGraphIdAtom);
|
|
if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) {
|
|
debug9("Skipping sync - data belongs to different graph");
|
|
return;
|
|
}
|
|
const existingGraph = get(graphAtom);
|
|
const isDragging = get(draggingNodeIdAtom) !== null;
|
|
if (isDragging) {
|
|
debug9("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) {
|
|
debug9("Merging DB data into existing graph");
|
|
graph = existingGraph.copy();
|
|
} else {
|
|
debug9("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)) {
|
|
debug9("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)) {
|
|
debug9("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) {
|
|
debug9("Failed to add edge %s: %o", edge.id, e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
set(graphAtom, graph);
|
|
set(graphUpdateVersionAtom, (v) => v + 1);
|
|
debug9("========== SYNC COMPLETE ==========");
|
|
debug9("Final graph: %d nodes, %d edges", graph.order, graph.size);
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/sync-store.ts
|
|
var import_jotai22, debug10, syncStatusAtom, pendingMutationsCountAtom, isOnlineAtom, lastSyncErrorAtom, lastSyncTimeAtom, mutationQueueAtom, syncStateAtom, startMutationAtom, completeMutationAtom, trackMutationErrorAtom, setOnlineStatusAtom, queueMutationAtom, dequeueMutationAtom, incrementRetryCountAtom, getNextQueuedMutationAtom, clearMutationQueueAtom;
|
|
var init_sync_store = __esm({
|
|
"src/core/sync-store.ts"() {
|
|
"use strict";
|
|
import_jotai22 = require("jotai");
|
|
init_debug();
|
|
debug10 = createDebug("sync");
|
|
syncStatusAtom = (0, import_jotai22.atom)("synced");
|
|
pendingMutationsCountAtom = (0, import_jotai22.atom)(0);
|
|
isOnlineAtom = (0, import_jotai22.atom)(typeof navigator !== "undefined" ? navigator.onLine : true);
|
|
lastSyncErrorAtom = (0, import_jotai22.atom)(null);
|
|
lastSyncTimeAtom = (0, import_jotai22.atom)(Date.now());
|
|
mutationQueueAtom = (0, import_jotai22.atom)([]);
|
|
syncStateAtom = (0, import_jotai22.atom)((get) => ({
|
|
status: get(syncStatusAtom),
|
|
pendingMutations: get(pendingMutationsCountAtom),
|
|
lastError: get(lastSyncErrorAtom),
|
|
lastSyncTime: get(lastSyncTimeAtom),
|
|
isOnline: get(isOnlineAtom),
|
|
queuedMutations: get(mutationQueueAtom).length
|
|
}));
|
|
startMutationAtom = (0, import_jotai22.atom)(null, (get, set) => {
|
|
const currentCount = get(pendingMutationsCountAtom);
|
|
const newCount = currentCount + 1;
|
|
set(pendingMutationsCountAtom, newCount);
|
|
debug10("Mutation started. Pending count: %d -> %d", currentCount, newCount);
|
|
if (newCount > 0 && get(syncStatusAtom) !== "syncing") {
|
|
set(syncStatusAtom, "syncing");
|
|
debug10("Status -> syncing");
|
|
}
|
|
});
|
|
completeMutationAtom = (0, import_jotai22.atom)(null, (get, set, success = true) => {
|
|
const currentCount = get(pendingMutationsCountAtom);
|
|
const newCount = Math.max(0, currentCount - 1);
|
|
set(pendingMutationsCountAtom, newCount);
|
|
debug10("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");
|
|
debug10("Status -> error");
|
|
} else if (!isOnline) {
|
|
set(syncStatusAtom, "offline");
|
|
debug10("Status -> offline");
|
|
} else {
|
|
set(syncStatusAtom, "synced");
|
|
debug10("Status -> synced");
|
|
}
|
|
}
|
|
});
|
|
trackMutationErrorAtom = (0, import_jotai22.atom)(null, (_get, set, error) => {
|
|
set(lastSyncErrorAtom, error);
|
|
debug10("Mutation failed: %s", error);
|
|
});
|
|
setOnlineStatusAtom = (0, import_jotai22.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");
|
|
}
|
|
}
|
|
});
|
|
queueMutationAtom = (0, import_jotai22.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);
|
|
debug10("Queued mutation: %s. Queue size: %d", mutation.type, newQueue.length);
|
|
if (get(pendingMutationsCountAtom) === 0) {
|
|
set(syncStatusAtom, "error");
|
|
}
|
|
return newMutation.id;
|
|
});
|
|
dequeueMutationAtom = (0, import_jotai22.atom)(null, (get, set, mutationId) => {
|
|
const queue = get(mutationQueueAtom);
|
|
const newQueue = queue.filter((m) => m.id !== mutationId);
|
|
set(mutationQueueAtom, newQueue);
|
|
debug10("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");
|
|
}
|
|
});
|
|
incrementRetryCountAtom = (0, import_jotai22.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);
|
|
});
|
|
getNextQueuedMutationAtom = (0, import_jotai22.atom)((get) => {
|
|
const queue = get(mutationQueueAtom);
|
|
return queue.find((m) => m.retryCount < m.maxRetries) ?? null;
|
|
});
|
|
clearMutationQueueAtom = (0, import_jotai22.atom)(null, (get, set) => {
|
|
set(mutationQueueAtom, []);
|
|
debug10("Cleared mutation queue");
|
|
if (get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) {
|
|
set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline");
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/interaction-store.ts
|
|
var import_jotai23, inputModeAtom2, keyboardInteractionModeAtom, interactionFeedbackAtom, pendingInputResolverAtom2, resetInputModeAtom, resetKeyboardInteractionModeAtom, setKeyboardInteractionModeAtom, startPickNodeAtom, startPickNodesAtom, startPickPointAtom, provideInputAtom2, updateInteractionFeedbackAtom, isPickingModeAtom, isPickNodeModeAtom;
|
|
var init_interaction_store = __esm({
|
|
"src/core/interaction-store.ts"() {
|
|
"use strict";
|
|
import_jotai23 = require("jotai");
|
|
inputModeAtom2 = (0, import_jotai23.atom)({
|
|
type: "normal"
|
|
});
|
|
keyboardInteractionModeAtom = (0, import_jotai23.atom)("navigate");
|
|
interactionFeedbackAtom = (0, import_jotai23.atom)(null);
|
|
pendingInputResolverAtom2 = (0, import_jotai23.atom)(null);
|
|
resetInputModeAtom = (0, import_jotai23.atom)(null, (_get, set) => {
|
|
set(inputModeAtom2, {
|
|
type: "normal"
|
|
});
|
|
set(interactionFeedbackAtom, null);
|
|
set(pendingInputResolverAtom2, null);
|
|
});
|
|
resetKeyboardInteractionModeAtom = (0, import_jotai23.atom)(null, (_get, set) => {
|
|
set(keyboardInteractionModeAtom, "navigate");
|
|
});
|
|
setKeyboardInteractionModeAtom = (0, import_jotai23.atom)(null, (_get, set, mode) => {
|
|
set(keyboardInteractionModeAtom, mode);
|
|
});
|
|
startPickNodeAtom = (0, import_jotai23.atom)(null, (_get, set, options) => {
|
|
set(inputModeAtom2, {
|
|
type: "pickNode",
|
|
...options
|
|
});
|
|
});
|
|
startPickNodesAtom = (0, import_jotai23.atom)(null, (_get, set, options) => {
|
|
set(inputModeAtom2, {
|
|
type: "pickNodes",
|
|
...options
|
|
});
|
|
});
|
|
startPickPointAtom = (0, import_jotai23.atom)(null, (_get, set, options) => {
|
|
set(inputModeAtom2, {
|
|
type: "pickPoint",
|
|
...options
|
|
});
|
|
});
|
|
provideInputAtom2 = (0, import_jotai23.atom)(null, (get, set, value) => {
|
|
set(pendingInputResolverAtom2, value);
|
|
});
|
|
updateInteractionFeedbackAtom = (0, import_jotai23.atom)(null, (get, set, feedback) => {
|
|
const current = get(interactionFeedbackAtom);
|
|
set(interactionFeedbackAtom, {
|
|
...current,
|
|
...feedback
|
|
});
|
|
});
|
|
isPickingModeAtom = (0, import_jotai23.atom)((get) => {
|
|
const mode = get(inputModeAtom2);
|
|
return mode.type !== "normal";
|
|
});
|
|
isPickNodeModeAtom = (0, import_jotai23.atom)((get) => {
|
|
const mode = get(inputModeAtom2);
|
|
return mode.type === "pickNode" || mode.type === "pickNodes";
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/locked-node-store.ts
|
|
var import_jotai24, lockedNodeIdAtom, lockedNodeDataAtom, lockedNodePageIndexAtom, lockedNodePageCountAtom, lockNodeAtom, unlockNodeAtom, nextLockedPageAtom, prevLockedPageAtom, goToLockedPageAtom, hasLockedNodeAtom;
|
|
var init_locked_node_store = __esm({
|
|
"src/core/locked-node-store.ts"() {
|
|
"use strict";
|
|
import_jotai24 = require("jotai");
|
|
init_graph_derived();
|
|
lockedNodeIdAtom = (0, import_jotai24.atom)(null);
|
|
lockedNodeDataAtom = (0, import_jotai24.atom)((get) => {
|
|
const id = get(lockedNodeIdAtom);
|
|
if (!id) return null;
|
|
const nodes = get(uiNodesAtom);
|
|
return nodes.find((n) => n.id === id) || null;
|
|
});
|
|
lockedNodePageIndexAtom = (0, import_jotai24.atom)(0);
|
|
lockedNodePageCountAtom = (0, import_jotai24.atom)(1);
|
|
lockNodeAtom = (0, import_jotai24.atom)(null, (_get, set, payload) => {
|
|
set(lockedNodeIdAtom, payload.nodeId);
|
|
set(lockedNodePageIndexAtom, 0);
|
|
});
|
|
unlockNodeAtom = (0, import_jotai24.atom)(null, (_get, set) => {
|
|
set(lockedNodeIdAtom, null);
|
|
});
|
|
nextLockedPageAtom = (0, import_jotai24.atom)(null, (get, set) => {
|
|
const current = get(lockedNodePageIndexAtom);
|
|
const pageCount = get(lockedNodePageCountAtom);
|
|
set(lockedNodePageIndexAtom, (current + 1) % pageCount);
|
|
});
|
|
prevLockedPageAtom = (0, import_jotai24.atom)(null, (get, set) => {
|
|
const current = get(lockedNodePageIndexAtom);
|
|
const pageCount = get(lockedNodePageCountAtom);
|
|
set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount);
|
|
});
|
|
goToLockedPageAtom = (0, import_jotai24.atom)(null, (get, set, index) => {
|
|
const pageCount = get(lockedNodePageCountAtom);
|
|
if (index >= 0 && index < pageCount) {
|
|
set(lockedNodePageIndexAtom, index);
|
|
}
|
|
});
|
|
hasLockedNodeAtom = (0, import_jotai24.atom)((get) => get(lockedNodeIdAtom) !== null);
|
|
}
|
|
});
|
|
|
|
// src/core/node-type-registry.tsx
|
|
function registerNodeType(nodeType, component) {
|
|
nodeTypeRegistry.set(nodeType, component);
|
|
}
|
|
function registerNodeTypes(types) {
|
|
for (const [nodeType, component] of Object.entries(types)) {
|
|
nodeTypeRegistry.set(nodeType, component);
|
|
}
|
|
}
|
|
function unregisterNodeType(nodeType) {
|
|
return nodeTypeRegistry.delete(nodeType);
|
|
}
|
|
function getNodeTypeComponent(nodeType) {
|
|
if (!nodeType) return void 0;
|
|
return nodeTypeRegistry.get(nodeType);
|
|
}
|
|
function hasNodeTypeComponent(nodeType) {
|
|
if (!nodeType) return false;
|
|
return nodeTypeRegistry.has(nodeType);
|
|
}
|
|
function getRegisteredNodeTypes() {
|
|
return Array.from(nodeTypeRegistry.keys());
|
|
}
|
|
function clearNodeTypeRegistry() {
|
|
nodeTypeRegistry.clear();
|
|
}
|
|
var import_compiler_runtime3, import_react6, import_jsx_runtime2, nodeTypeRegistry, FallbackNodeTypeComponent;
|
|
var init_node_type_registry = __esm({
|
|
"src/core/node-type-registry.tsx"() {
|
|
"use strict";
|
|
import_compiler_runtime3 = require("react/compiler-runtime");
|
|
import_react6 = __toESM(require("react"));
|
|
import_jsx_runtime2 = require("react/jsx-runtime");
|
|
nodeTypeRegistry = /* @__PURE__ */ new Map();
|
|
FallbackNodeTypeComponent = (t0) => {
|
|
const $ = (0, import_compiler_runtime3.c)(11);
|
|
const {
|
|
nodeData
|
|
} = t0;
|
|
let t1;
|
|
if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
|
|
t1 = {
|
|
padding: "12px",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
height: "100%",
|
|
color: "#666",
|
|
fontSize: "12px"
|
|
};
|
|
$[0] = t1;
|
|
} else {
|
|
t1 = $[0];
|
|
}
|
|
const t2 = nodeData.dbData.node_type || "none";
|
|
let t3;
|
|
if ($[1] !== t2) {
|
|
t3 = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", {
|
|
children: ["Unknown type: ", t2]
|
|
});
|
|
$[1] = t2;
|
|
$[2] = t3;
|
|
} else {
|
|
t3 = $[2];
|
|
}
|
|
let t4;
|
|
if ($[3] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) {
|
|
t4 = {
|
|
marginTop: "4px",
|
|
opacity: 0.7
|
|
};
|
|
$[3] = t4;
|
|
} else {
|
|
t4 = $[3];
|
|
}
|
|
let t5;
|
|
if ($[4] !== nodeData.id) {
|
|
t5 = nodeData.id.substring(0, 8);
|
|
$[4] = nodeData.id;
|
|
$[5] = t5;
|
|
} else {
|
|
t5 = $[5];
|
|
}
|
|
let t6;
|
|
if ($[6] !== t5) {
|
|
t6 = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", {
|
|
style: t4,
|
|
children: t5
|
|
});
|
|
$[6] = t5;
|
|
$[7] = t6;
|
|
} else {
|
|
t6 = $[7];
|
|
}
|
|
let t7;
|
|
if ($[8] !== t3 || $[9] !== t6) {
|
|
t7 = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", {
|
|
style: t1,
|
|
children: [t3, t6]
|
|
});
|
|
$[8] = t3;
|
|
$[9] = t6;
|
|
$[10] = t7;
|
|
} else {
|
|
t7 = $[10];
|
|
}
|
|
return t7;
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/core/toast-store.ts
|
|
var toast_store_exports = {};
|
|
__export(toast_store_exports, {
|
|
canvasToastAtom: () => canvasToastAtom,
|
|
showToastAtom: () => showToastAtom
|
|
});
|
|
var import_jotai25, canvasToastAtom, showToastAtom;
|
|
var init_toast_store = __esm({
|
|
"src/core/toast-store.ts"() {
|
|
"use strict";
|
|
import_jotai25 = require("jotai");
|
|
canvasToastAtom = (0, import_jotai25.atom)(null);
|
|
showToastAtom = (0, import_jotai25.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/core/snap-store.ts
|
|
function snapToGrid(pos, gridSize) {
|
|
return {
|
|
x: Math.round(pos.x / gridSize) * gridSize,
|
|
y: Math.round(pos.y / gridSize) * gridSize
|
|
};
|
|
}
|
|
function conditionalSnap(pos, gridSize, isActive) {
|
|
return isActive ? snapToGrid(pos, gridSize) : pos;
|
|
}
|
|
function getSnapGuides(pos, gridSize, tolerance = 5) {
|
|
const snappedX = Math.round(pos.x / gridSize) * gridSize;
|
|
const snappedY = Math.round(pos.y / gridSize) * gridSize;
|
|
return {
|
|
x: Math.abs(pos.x - snappedX) < tolerance ? snappedX : null,
|
|
y: Math.abs(pos.y - snappedY) < tolerance ? snappedY : null
|
|
};
|
|
}
|
|
function findAlignmentGuides(dragged, others, tolerance = 5) {
|
|
const verticals = /* @__PURE__ */ new Set();
|
|
const horizontals = /* @__PURE__ */ new Set();
|
|
const dragCX = dragged.x + dragged.width / 2;
|
|
const dragCY = dragged.y + dragged.height / 2;
|
|
const dragRight = dragged.x + dragged.width;
|
|
const dragBottom = dragged.y + dragged.height;
|
|
for (const other of others) {
|
|
const otherCX = other.x + other.width / 2;
|
|
const otherCY = other.y + other.height / 2;
|
|
const otherRight = other.x + other.width;
|
|
const otherBottom = other.y + other.height;
|
|
if (Math.abs(dragCX - otherCX) < tolerance) verticals.add(otherCX);
|
|
if (Math.abs(dragged.x - other.x) < tolerance) verticals.add(other.x);
|
|
if (Math.abs(dragRight - otherRight) < tolerance) verticals.add(otherRight);
|
|
if (Math.abs(dragged.x - otherRight) < tolerance) verticals.add(otherRight);
|
|
if (Math.abs(dragRight - other.x) < tolerance) verticals.add(other.x);
|
|
if (Math.abs(dragCX - other.x) < tolerance) verticals.add(other.x);
|
|
if (Math.abs(dragCX - otherRight) < tolerance) verticals.add(otherRight);
|
|
if (Math.abs(dragCY - otherCY) < tolerance) horizontals.add(otherCY);
|
|
if (Math.abs(dragged.y - other.y) < tolerance) horizontals.add(other.y);
|
|
if (Math.abs(dragBottom - otherBottom) < tolerance) horizontals.add(otherBottom);
|
|
if (Math.abs(dragged.y - otherBottom) < tolerance) horizontals.add(otherBottom);
|
|
if (Math.abs(dragBottom - other.y) < tolerance) horizontals.add(other.y);
|
|
if (Math.abs(dragCY - other.y) < tolerance) horizontals.add(other.y);
|
|
if (Math.abs(dragCY - otherBottom) < tolerance) horizontals.add(otherBottom);
|
|
}
|
|
return {
|
|
verticalGuides: Array.from(verticals),
|
|
horizontalGuides: Array.from(horizontals)
|
|
};
|
|
}
|
|
var import_jotai26, snapEnabledAtom, snapGridSizeAtom, snapTemporaryDisableAtom, isSnappingActiveAtom, toggleSnapAtom, setGridSizeAtom, snapAlignmentEnabledAtom, toggleAlignmentGuidesAtom, alignmentGuidesAtom, clearAlignmentGuidesAtom;
|
|
var init_snap_store = __esm({
|
|
"src/core/snap-store.ts"() {
|
|
"use strict";
|
|
import_jotai26 = require("jotai");
|
|
snapEnabledAtom = (0, import_jotai26.atom)(false);
|
|
snapGridSizeAtom = (0, import_jotai26.atom)(20);
|
|
snapTemporaryDisableAtom = (0, import_jotai26.atom)(false);
|
|
isSnappingActiveAtom = (0, import_jotai26.atom)((get) => {
|
|
return get(snapEnabledAtom) && !get(snapTemporaryDisableAtom);
|
|
});
|
|
toggleSnapAtom = (0, import_jotai26.atom)(null, (get, set) => {
|
|
set(snapEnabledAtom, !get(snapEnabledAtom));
|
|
});
|
|
setGridSizeAtom = (0, import_jotai26.atom)(null, (_get, set, size) => {
|
|
set(snapGridSizeAtom, Math.max(5, Math.min(200, size)));
|
|
});
|
|
snapAlignmentEnabledAtom = (0, import_jotai26.atom)(true);
|
|
toggleAlignmentGuidesAtom = (0, import_jotai26.atom)(null, (get, set) => {
|
|
set(snapAlignmentEnabledAtom, !get(snapAlignmentEnabledAtom));
|
|
});
|
|
alignmentGuidesAtom = (0, import_jotai26.atom)({
|
|
verticalGuides: [],
|
|
horizontalGuides: []
|
|
});
|
|
clearAlignmentGuidesAtom = (0, import_jotai26.atom)(null, (_get, set) => {
|
|
set(alignmentGuidesAtom, {
|
|
verticalGuides: [],
|
|
horizontalGuides: []
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/event-types.ts
|
|
var CanvasEventType, EVENT_TYPE_INFO;
|
|
var init_event_types = __esm({
|
|
"src/core/event-types.ts"() {
|
|
"use strict";
|
|
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;
|
|
})({});
|
|
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, BuiltInActionId;
|
|
var init_action_types = __esm({
|
|
"src/core/action-types.ts"() {
|
|
"use strict";
|
|
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;
|
|
})({});
|
|
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;
|
|
var init_settings_state_types = __esm({
|
|
"src/core/settings-state-types.ts"() {
|
|
"use strict";
|
|
init_event_types();
|
|
init_action_types();
|
|
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-types.ts
|
|
var init_settings_types = __esm({
|
|
"src/core/settings-types.ts"() {
|
|
"use strict";
|
|
init_event_types();
|
|
init_action_types();
|
|
init_settings_state_types();
|
|
}
|
|
});
|
|
|
|
// 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());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
var init_actions_node = __esm({
|
|
"src/core/actions-node.ts"() {
|
|
"use strict";
|
|
init_settings_types();
|
|
init_action_registry();
|
|
}
|
|
});
|
|
|
|
// 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();
|
|
}
|
|
});
|
|
}
|
|
var init_actions_viewport = __esm({
|
|
"src/core/actions-viewport.ts"() {
|
|
"use strict";
|
|
init_settings_types();
|
|
init_action_registry();
|
|
}
|
|
});
|
|
|
|
// 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();
|
|
}
|
|
var init_built_in_actions = __esm({
|
|
"src/core/built-in-actions.ts"() {
|
|
"use strict";
|
|
init_settings_types();
|
|
init_action_registry();
|
|
init_actions_node();
|
|
init_actions_viewport();
|
|
}
|
|
});
|
|
|
|
// src/core/action-registry.ts
|
|
function registerAction(action) {
|
|
actionRegistry.set(action.id, action);
|
|
}
|
|
function getAction(id) {
|
|
return actionRegistry.get(id);
|
|
}
|
|
function hasAction(id) {
|
|
return actionRegistry.has(id);
|
|
}
|
|
function getAllActions() {
|
|
return Array.from(actionRegistry.values());
|
|
}
|
|
function getActionsByCategory(category) {
|
|
return getAllActions().filter((action) => action.category === category);
|
|
}
|
|
function unregisterAction(id) {
|
|
return actionRegistry.delete(id);
|
|
}
|
|
function clearActions() {
|
|
actionRegistry.clear();
|
|
}
|
|
function getActionsByCategories() {
|
|
const categoryLabels = {
|
|
[ActionCategory.None]: "None",
|
|
[ActionCategory.Selection]: "Selection",
|
|
[ActionCategory.Viewport]: "Viewport",
|
|
[ActionCategory.Node]: "Node",
|
|
[ActionCategory.Layout]: "Layout",
|
|
[ActionCategory.History]: "History",
|
|
[ActionCategory.Custom]: "Custom"
|
|
};
|
|
const categoryOrder = [ActionCategory.None, ActionCategory.Selection, ActionCategory.Viewport, ActionCategory.Node, ActionCategory.Layout, ActionCategory.History, ActionCategory.Custom];
|
|
return categoryOrder.map((category) => ({
|
|
category,
|
|
label: categoryLabels[category],
|
|
actions: getActionsByCategory(category)
|
|
})).filter((group) => group.actions.length > 0);
|
|
}
|
|
var actionRegistry;
|
|
var init_action_registry = __esm({
|
|
"src/core/action-registry.ts"() {
|
|
"use strict";
|
|
init_settings_types();
|
|
init_built_in_actions();
|
|
actionRegistry = /* @__PURE__ */ new Map();
|
|
registerBuiltInActions();
|
|
}
|
|
});
|
|
|
|
// src/core/action-executor.ts
|
|
async function executeAction(actionId, context, helpers) {
|
|
if (actionId === BuiltInActionId.None) {
|
|
return {
|
|
success: true,
|
|
actionId
|
|
};
|
|
}
|
|
const action = getAction(actionId);
|
|
if (!action) {
|
|
debug11.warn("Action not found: %s", actionId);
|
|
return {
|
|
success: false,
|
|
actionId,
|
|
error: new Error(`Action not found: ${actionId}`)
|
|
};
|
|
}
|
|
if (action.requiresNode && !context.nodeId) {
|
|
debug11.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) {
|
|
debug11.error("Error executing action %s: %O", actionId, error);
|
|
return {
|
|
success: false,
|
|
actionId,
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
};
|
|
}
|
|
}
|
|
function createActionContext(eventType, screenEvent, worldPosition, options) {
|
|
return {
|
|
eventType,
|
|
nodeId: options?.nodeId,
|
|
nodeData: options?.nodeData,
|
|
edgeId: options?.edgeId,
|
|
edgeData: options?.edgeData,
|
|
worldPosition,
|
|
screenPosition: {
|
|
x: screenEvent.clientX,
|
|
y: screenEvent.clientY
|
|
},
|
|
modifiers: {
|
|
shift: false,
|
|
ctrl: false,
|
|
alt: false,
|
|
meta: false
|
|
}
|
|
};
|
|
}
|
|
function createActionContextFromReactEvent(eventType, event, worldPosition, options) {
|
|
return {
|
|
eventType,
|
|
nodeId: options?.nodeId,
|
|
nodeData: options?.nodeData,
|
|
edgeId: options?.edgeId,
|
|
edgeData: options?.edgeData,
|
|
worldPosition,
|
|
screenPosition: {
|
|
x: event.clientX,
|
|
y: event.clientY
|
|
},
|
|
modifiers: {
|
|
shift: event.shiftKey,
|
|
ctrl: event.ctrlKey,
|
|
alt: event.altKey,
|
|
meta: event.metaKey
|
|
}
|
|
};
|
|
}
|
|
function createActionContextFromTouchEvent(eventType, touch, worldPosition, options) {
|
|
return {
|
|
eventType,
|
|
nodeId: options?.nodeId,
|
|
nodeData: options?.nodeData,
|
|
edgeId: options?.edgeId,
|
|
edgeData: options?.edgeData,
|
|
worldPosition,
|
|
screenPosition: {
|
|
x: touch.clientX,
|
|
y: touch.clientY
|
|
},
|
|
modifiers: {
|
|
shift: false,
|
|
ctrl: false,
|
|
alt: false,
|
|
meta: false
|
|
}
|
|
};
|
|
}
|
|
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 {
|
|
debug11.warn("deleteNode called but onDeleteNode callback not provided");
|
|
}
|
|
},
|
|
isNodeLocked: (nodeId) => store.get(lockedNodeIdAtom) === nodeId,
|
|
applyForceLayout: async () => {
|
|
if (options.onApplyForceLayout) {
|
|
await options.onApplyForceLayout();
|
|
} else {
|
|
debug11.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
|
|
};
|
|
}
|
|
var debug11;
|
|
var init_action_executor = __esm({
|
|
"src/core/action-executor.ts"() {
|
|
"use strict";
|
|
init_action_registry();
|
|
init_settings_types();
|
|
init_selection_store();
|
|
init_viewport_store();
|
|
init_locked_node_store();
|
|
init_history_store();
|
|
init_layout();
|
|
init_debug();
|
|
debug11 = createDebug("actions");
|
|
}
|
|
});
|
|
|
|
// src/core/settings-presets.ts
|
|
function getActionForEvent(mappings, event) {
|
|
return mappings[event] || BuiltInActionId.None;
|
|
}
|
|
var BUILT_IN_PRESETS;
|
|
var init_settings_presets = __esm({
|
|
"src/core/settings-presets.ts"() {
|
|
"use strict";
|
|
init_settings_types();
|
|
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
|
|
}
|
|
}];
|
|
}
|
|
});
|
|
|
|
// src/core/settings-store.ts
|
|
var import_jotai27, import_utils2, debug12, DEFAULT_STATE, canvasSettingsAtom, eventMappingsAtom, activePresetIdAtom, allPresetsAtom, activePresetAtom, isPanelOpenAtom, virtualizationEnabledAtom, hasUnsavedChangesAtom, setEventMappingAtom, applyPresetAtom, saveAsPresetAtom, updatePresetAtom, deletePresetAtom, resetSettingsAtom, togglePanelAtom, setPanelOpenAtom, setVirtualizationEnabledAtom, toggleVirtualizationAtom;
|
|
var init_settings_store = __esm({
|
|
"src/core/settings-store.ts"() {
|
|
"use strict";
|
|
import_jotai27 = require("jotai");
|
|
import_utils2 = require("jotai/utils");
|
|
init_settings_types();
|
|
init_debug();
|
|
init_settings_presets();
|
|
init_settings_presets();
|
|
debug12 = createDebug("settings");
|
|
DEFAULT_STATE = {
|
|
mappings: DEFAULT_MAPPINGS,
|
|
activePresetId: "default",
|
|
customPresets: [],
|
|
isPanelOpen: false,
|
|
virtualizationEnabled: true
|
|
};
|
|
canvasSettingsAtom = (0, import_utils2.atomWithStorage)("@blinksgg/canvas/settings", DEFAULT_STATE);
|
|
eventMappingsAtom = (0, import_jotai27.atom)((get) => get(canvasSettingsAtom).mappings);
|
|
activePresetIdAtom = (0, import_jotai27.atom)((get) => get(canvasSettingsAtom).activePresetId);
|
|
allPresetsAtom = (0, import_jotai27.atom)((get) => {
|
|
const state = get(canvasSettingsAtom);
|
|
return [...BUILT_IN_PRESETS, ...state.customPresets];
|
|
});
|
|
activePresetAtom = (0, import_jotai27.atom)((get) => {
|
|
const presetId = get(activePresetIdAtom);
|
|
if (!presetId) return null;
|
|
const allPresets = get(allPresetsAtom);
|
|
return allPresets.find((p) => p.id === presetId) || null;
|
|
});
|
|
isPanelOpenAtom = (0, import_jotai27.atom)((get) => get(canvasSettingsAtom).isPanelOpen);
|
|
virtualizationEnabledAtom = (0, import_jotai27.atom)((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true);
|
|
hasUnsavedChangesAtom = (0, import_jotai27.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]);
|
|
});
|
|
setEventMappingAtom = (0, import_jotai27.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
|
|
});
|
|
});
|
|
applyPresetAtom = (0, import_jotai27.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
|
|
});
|
|
});
|
|
saveAsPresetAtom = (0, import_jotai27.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;
|
|
});
|
|
updatePresetAtom = (0, import_jotai27.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
|
|
});
|
|
});
|
|
deletePresetAtom = (0, import_jotai27.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
|
|
});
|
|
});
|
|
resetSettingsAtom = (0, import_jotai27.atom)(null, (get, set) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
mappings: DEFAULT_MAPPINGS,
|
|
activePresetId: "default"
|
|
});
|
|
});
|
|
togglePanelAtom = (0, import_jotai27.atom)(null, (get, set) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
isPanelOpen: !current.isPanelOpen
|
|
});
|
|
});
|
|
setPanelOpenAtom = (0, import_jotai27.atom)(null, (get, set, isOpen) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
isPanelOpen: isOpen
|
|
});
|
|
});
|
|
setVirtualizationEnabledAtom = (0, import_jotai27.atom)(null, (get, set, enabled) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
virtualizationEnabled: enabled
|
|
});
|
|
});
|
|
toggleVirtualizationAtom = (0, import_jotai27.atom)(null, (get, set) => {
|
|
const current = get(canvasSettingsAtom);
|
|
set(canvasSettingsAtom, {
|
|
...current,
|
|
virtualizationEnabled: !(current.virtualizationEnabled ?? true)
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/canvas-serializer.ts
|
|
var canvas_serializer_exports = {};
|
|
__export(canvas_serializer_exports, {
|
|
SNAPSHOT_VERSION: () => SNAPSHOT_VERSION,
|
|
exportGraph: () => exportGraph,
|
|
importGraph: () => importGraph,
|
|
validateSnapshot: () => validateSnapshot
|
|
});
|
|
function exportGraph(store, metadata) {
|
|
const graph = store.get(graphAtom);
|
|
const zoom = store.get(zoomAtom);
|
|
const pan = store.get(panAtom);
|
|
const collapsed = store.get(collapsedGroupsAtom);
|
|
const nodes = [];
|
|
const groups = [];
|
|
const seenGroupParents = /* @__PURE__ */ new Set();
|
|
graph.forEachNode((nodeId, attrs) => {
|
|
const a = attrs;
|
|
nodes.push({
|
|
id: nodeId,
|
|
position: {
|
|
x: a.x,
|
|
y: a.y
|
|
},
|
|
dimensions: {
|
|
width: a.width,
|
|
height: a.height
|
|
},
|
|
size: a.size,
|
|
color: a.color,
|
|
zIndex: a.zIndex,
|
|
label: a.label,
|
|
parentId: a.parentId,
|
|
dbData: a.dbData
|
|
});
|
|
if (a.parentId) {
|
|
const key = `${nodeId}:${a.parentId}`;
|
|
if (!seenGroupParents.has(key)) {
|
|
seenGroupParents.add(key);
|
|
groups.push({
|
|
nodeId,
|
|
parentId: a.parentId,
|
|
isCollapsed: collapsed.has(a.parentId)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
const edges = [];
|
|
graph.forEachEdge((key, attrs, source, target) => {
|
|
const a = attrs;
|
|
edges.push({
|
|
key,
|
|
sourceId: source,
|
|
targetId: target,
|
|
attributes: {
|
|
weight: a.weight,
|
|
type: a.type,
|
|
color: a.color,
|
|
label: a.label
|
|
},
|
|
dbData: a.dbData
|
|
});
|
|
});
|
|
return {
|
|
version: SNAPSHOT_VERSION,
|
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
nodes,
|
|
edges,
|
|
groups,
|
|
viewport: {
|
|
zoom,
|
|
pan: {
|
|
...pan
|
|
}
|
|
},
|
|
metadata
|
|
};
|
|
}
|
|
function importGraph(store, snapshot, options = {}) {
|
|
const {
|
|
clearExisting = true,
|
|
offsetPosition,
|
|
remapIds = false
|
|
} = options;
|
|
const idMap = /* @__PURE__ */ new Map();
|
|
if (remapIds) {
|
|
for (const node of snapshot.nodes) {
|
|
idMap.set(node.id, crypto.randomUUID());
|
|
}
|
|
for (const edge of snapshot.edges) {
|
|
idMap.set(edge.key, crypto.randomUUID());
|
|
}
|
|
}
|
|
const remap = (id) => idMap.get(id) ?? id;
|
|
let graph;
|
|
if (clearExisting) {
|
|
graph = new import_graphology4.default(graphOptions);
|
|
} else {
|
|
graph = store.get(graphAtom);
|
|
}
|
|
const ox = offsetPosition?.x ?? 0;
|
|
const oy = offsetPosition?.y ?? 0;
|
|
for (const node of snapshot.nodes) {
|
|
const nodeId = remap(node.id);
|
|
const parentId = node.parentId ? remap(node.parentId) : void 0;
|
|
const dbData = remapIds ? {
|
|
...node.dbData,
|
|
id: nodeId
|
|
} : node.dbData;
|
|
const attrs = {
|
|
x: node.position.x + ox,
|
|
y: node.position.y + oy,
|
|
width: node.dimensions.width,
|
|
height: node.dimensions.height,
|
|
size: node.size,
|
|
color: node.color,
|
|
zIndex: node.zIndex,
|
|
label: node.label,
|
|
parentId,
|
|
dbData
|
|
};
|
|
graph.addNode(nodeId, attrs);
|
|
}
|
|
for (const edge of snapshot.edges) {
|
|
const edgeKey = remap(edge.key);
|
|
const sourceId = remap(edge.sourceId);
|
|
const targetId = remap(edge.targetId);
|
|
if (!graph.hasNode(sourceId) || !graph.hasNode(targetId)) continue;
|
|
const dbData = remapIds ? {
|
|
...edge.dbData,
|
|
id: edgeKey,
|
|
source_node_id: sourceId,
|
|
target_node_id: targetId
|
|
} : edge.dbData;
|
|
const attrs = {
|
|
weight: edge.attributes.weight,
|
|
type: edge.attributes.type,
|
|
color: edge.attributes.color,
|
|
label: edge.attributes.label,
|
|
dbData
|
|
};
|
|
graph.addEdgeWithKey(edgeKey, sourceId, targetId, attrs);
|
|
}
|
|
store.set(graphAtom, graph);
|
|
store.set(graphUpdateVersionAtom, (v) => v + 1);
|
|
store.set(nodePositionUpdateCounterAtom, (c) => c + 1);
|
|
const collapsedSet = /* @__PURE__ */ new Set();
|
|
for (const group of snapshot.groups) {
|
|
if (group.isCollapsed) {
|
|
collapsedSet.add(remap(group.parentId));
|
|
}
|
|
}
|
|
store.set(collapsedGroupsAtom, collapsedSet);
|
|
store.set(zoomAtom, snapshot.viewport.zoom);
|
|
store.set(panAtom, {
|
|
...snapshot.viewport.pan
|
|
});
|
|
}
|
|
function validateSnapshot(data) {
|
|
const errors = [];
|
|
if (!data || typeof data !== "object") {
|
|
return {
|
|
valid: false,
|
|
errors: ["Snapshot must be a non-null object"]
|
|
};
|
|
}
|
|
const obj = data;
|
|
if (obj.version !== SNAPSHOT_VERSION) {
|
|
errors.push(`Expected version ${SNAPSHOT_VERSION}, got ${String(obj.version)}`);
|
|
}
|
|
if (typeof obj.exportedAt !== "string") {
|
|
errors.push('Missing or invalid "exportedAt" (expected ISO string)');
|
|
}
|
|
if (!Array.isArray(obj.nodes)) {
|
|
errors.push('Missing or invalid "nodes" (expected array)');
|
|
} else {
|
|
for (let i = 0; i < obj.nodes.length; i++) {
|
|
const node = obj.nodes[i];
|
|
if (!node || typeof node !== "object") {
|
|
errors.push(`nodes[${i}]: expected object`);
|
|
continue;
|
|
}
|
|
if (typeof node.id !== "string") errors.push(`nodes[${i}]: missing "id"`);
|
|
if (!node.position || typeof node.position !== "object") errors.push(`nodes[${i}]: missing "position"`);
|
|
if (!node.dimensions || typeof node.dimensions !== "object") errors.push(`nodes[${i}]: missing "dimensions"`);
|
|
if (!node.dbData || typeof node.dbData !== "object") errors.push(`nodes[${i}]: missing "dbData"`);
|
|
}
|
|
}
|
|
if (!Array.isArray(obj.edges)) {
|
|
errors.push('Missing or invalid "edges" (expected array)');
|
|
} else {
|
|
for (let i = 0; i < obj.edges.length; i++) {
|
|
const edge = obj.edges[i];
|
|
if (!edge || typeof edge !== "object") {
|
|
errors.push(`edges[${i}]: expected object`);
|
|
continue;
|
|
}
|
|
if (typeof edge.key !== "string") errors.push(`edges[${i}]: missing "key"`);
|
|
if (typeof edge.sourceId !== "string") errors.push(`edges[${i}]: missing "sourceId"`);
|
|
if (typeof edge.targetId !== "string") errors.push(`edges[${i}]: missing "targetId"`);
|
|
if (!edge.dbData || typeof edge.dbData !== "object") errors.push(`edges[${i}]: missing "dbData"`);
|
|
}
|
|
}
|
|
if (!Array.isArray(obj.groups)) {
|
|
errors.push('Missing or invalid "groups" (expected array)');
|
|
}
|
|
if (!obj.viewport || typeof obj.viewport !== "object") {
|
|
errors.push('Missing or invalid "viewport" (expected object)');
|
|
} else {
|
|
const vp = obj.viewport;
|
|
if (typeof vp.zoom !== "number") errors.push('viewport: missing "zoom"');
|
|
if (!vp.pan || typeof vp.pan !== "object") errors.push('viewport: missing "pan"');
|
|
}
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
var import_graphology4, SNAPSHOT_VERSION;
|
|
var init_canvas_serializer = __esm({
|
|
"src/core/canvas-serializer.ts"() {
|
|
"use strict";
|
|
import_graphology4 = __toESM(require("graphology"));
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_viewport_store();
|
|
init_group_store();
|
|
SNAPSHOT_VERSION = 1;
|
|
}
|
|
});
|
|
|
|
// src/core/clipboard-store.ts
|
|
var clipboard_store_exports = {};
|
|
__export(clipboard_store_exports, {
|
|
PASTE_OFFSET: () => PASTE_OFFSET,
|
|
clearClipboardAtom: () => clearClipboardAtom,
|
|
clipboardAtom: () => clipboardAtom,
|
|
clipboardNodeCountAtom: () => clipboardNodeCountAtom,
|
|
copyToClipboardAtom: () => copyToClipboardAtom,
|
|
cutToClipboardAtom: () => cutToClipboardAtom,
|
|
duplicateSelectionAtom: () => duplicateSelectionAtom,
|
|
hasClipboardContentAtom: () => hasClipboardContentAtom,
|
|
pasteFromClipboardAtom: () => pasteFromClipboardAtom
|
|
});
|
|
function calculateBounds2(nodes) {
|
|
if (nodes.length === 0) {
|
|
return {
|
|
minX: 0,
|
|
minY: 0,
|
|
maxX: 0,
|
|
maxY: 0
|
|
};
|
|
}
|
|
let minX = Infinity;
|
|
let minY = Infinity;
|
|
let maxX = -Infinity;
|
|
let maxY = -Infinity;
|
|
for (const node of nodes) {
|
|
minX = Math.min(minX, node.attrs.x);
|
|
minY = Math.min(minY, node.attrs.y);
|
|
maxX = Math.max(maxX, node.attrs.x + node.attrs.width);
|
|
maxY = Math.max(maxY, node.attrs.y + node.attrs.height);
|
|
}
|
|
return {
|
|
minX,
|
|
minY,
|
|
maxX,
|
|
maxY
|
|
};
|
|
}
|
|
function generatePasteId(index) {
|
|
return `paste-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
var import_jotai28, debug13, PASTE_OFFSET, clipboardAtom, hasClipboardContentAtom, clipboardNodeCountAtom, copyToClipboardAtom, cutToClipboardAtom, pasteFromClipboardAtom, duplicateSelectionAtom, clearClipboardAtom;
|
|
var init_clipboard_store = __esm({
|
|
"src/core/clipboard-store.ts"() {
|
|
"use strict";
|
|
import_jotai28 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_mutations();
|
|
init_graph_mutations_edges();
|
|
init_selection_store();
|
|
init_history_store();
|
|
init_debug();
|
|
debug13 = createDebug("clipboard");
|
|
PASTE_OFFSET = {
|
|
x: 50,
|
|
y: 50
|
|
};
|
|
clipboardAtom = (0, import_jotai28.atom)(null);
|
|
hasClipboardContentAtom = (0, import_jotai28.atom)((get) => get(clipboardAtom) !== null);
|
|
clipboardNodeCountAtom = (0, import_jotai28.atom)((get) => {
|
|
const clipboard = get(clipboardAtom);
|
|
return clipboard?.nodes.length ?? 0;
|
|
});
|
|
copyToClipboardAtom = (0, import_jotai28.atom)(null, (get, set, nodeIds) => {
|
|
const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom));
|
|
if (selectedIds.length === 0) {
|
|
debug13("Nothing to copy - no nodes selected");
|
|
return;
|
|
}
|
|
const graph = get(graphAtom);
|
|
const selectedSet = new Set(selectedIds);
|
|
const nodes = [];
|
|
const edges = [];
|
|
for (const nodeId of selectedIds) {
|
|
if (!graph.hasNode(nodeId)) {
|
|
debug13("Node %s not found in graph, skipping", nodeId);
|
|
continue;
|
|
}
|
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
nodes.push({
|
|
attrs: {
|
|
...attrs
|
|
},
|
|
dbData: {
|
|
...attrs.dbData
|
|
}
|
|
});
|
|
}
|
|
graph.forEachEdge((edgeKey, attrs, source, target) => {
|
|
if (selectedSet.has(source) && selectedSet.has(target)) {
|
|
edges.push({
|
|
source,
|
|
target,
|
|
attrs: {
|
|
...attrs
|
|
},
|
|
dbData: {
|
|
...attrs.dbData
|
|
}
|
|
});
|
|
}
|
|
});
|
|
const bounds = calculateBounds2(nodes);
|
|
const clipboardData = {
|
|
nodes,
|
|
edges,
|
|
bounds,
|
|
timestamp: Date.now()
|
|
};
|
|
set(clipboardAtom, clipboardData);
|
|
debug13("Copied %d nodes and %d edges to clipboard", nodes.length, edges.length);
|
|
});
|
|
cutToClipboardAtom = (0, import_jotai28.atom)(null, (get, set, nodeIds) => {
|
|
const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom));
|
|
if (selectedIds.length === 0) return;
|
|
set(copyToClipboardAtom, selectedIds);
|
|
set(pushHistoryAtom, "Cut nodes");
|
|
for (const nodeId of selectedIds) {
|
|
set(optimisticDeleteNodeAtom, {
|
|
nodeId
|
|
});
|
|
}
|
|
set(clearSelectionAtom);
|
|
debug13("Cut %d nodes \u2014 copied to clipboard and deleted from graph", selectedIds.length);
|
|
});
|
|
pasteFromClipboardAtom = (0, import_jotai28.atom)(null, (get, set, offset) => {
|
|
const clipboard = get(clipboardAtom);
|
|
if (!clipboard || clipboard.nodes.length === 0) {
|
|
debug13("Nothing to paste - clipboard empty");
|
|
return [];
|
|
}
|
|
const pasteOffset = offset ?? PASTE_OFFSET;
|
|
const graph = get(graphAtom);
|
|
set(pushHistoryAtom, "Paste nodes");
|
|
const idMap = /* @__PURE__ */ new Map();
|
|
const newNodeIds = [];
|
|
for (let i = 0; i < clipboard.nodes.length; i++) {
|
|
const nodeData = clipboard.nodes[i];
|
|
const newId = generatePasteId(i);
|
|
idMap.set(nodeData.dbData.id, newId);
|
|
newNodeIds.push(newId);
|
|
const newDbNode = {
|
|
...nodeData.dbData,
|
|
id: newId,
|
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
ui_properties: {
|
|
...nodeData.dbData.ui_properties || {},
|
|
x: nodeData.attrs.x + pasteOffset.x,
|
|
y: nodeData.attrs.y + pasteOffset.y
|
|
}
|
|
};
|
|
debug13("Pasting node %s -> %s at (%d, %d)", nodeData.dbData.id, newId, nodeData.attrs.x + pasteOffset.x, nodeData.attrs.y + pasteOffset.y);
|
|
set(addNodeToLocalGraphAtom, newDbNode);
|
|
}
|
|
for (const edgeData of clipboard.edges) {
|
|
const newSourceId = idMap.get(edgeData.source);
|
|
const newTargetId = idMap.get(edgeData.target);
|
|
if (!newSourceId || !newTargetId) {
|
|
debug13("Edge %s: source or target not found in id map, skipping", edgeData.dbData.id);
|
|
continue;
|
|
}
|
|
const newEdgeId = generatePasteId(clipboard.edges.indexOf(edgeData) + clipboard.nodes.length);
|
|
const newDbEdge = {
|
|
...edgeData.dbData,
|
|
id: newEdgeId,
|
|
source_node_id: newSourceId,
|
|
target_node_id: newTargetId,
|
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
};
|
|
debug13("Pasting edge %s -> %s (from %s to %s)", edgeData.dbData.id, newEdgeId, newSourceId, newTargetId);
|
|
set(addEdgeToLocalGraphAtom, newDbEdge);
|
|
}
|
|
set(clearSelectionAtom);
|
|
set(addNodesToSelectionAtom, newNodeIds);
|
|
debug13("Pasted %d nodes and %d edges", newNodeIds.length, clipboard.edges.length);
|
|
return newNodeIds;
|
|
});
|
|
duplicateSelectionAtom = (0, import_jotai28.atom)(null, (get, set) => {
|
|
set(copyToClipboardAtom);
|
|
return set(pasteFromClipboardAtom);
|
|
});
|
|
clearClipboardAtom = (0, import_jotai28.atom)(null, (_get, set) => {
|
|
set(clipboardAtom, null);
|
|
debug13("Clipboard cleared");
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/spatial-index.ts
|
|
var SpatialGrid;
|
|
var init_spatial_index = __esm({
|
|
"src/core/spatial-index.ts"() {
|
|
"use strict";
|
|
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 import_jotai29, VIRTUALIZATION_BUFFER, spatialIndexAtom, visibleBoundsAtom, visibleNodeKeysAtom, visibleEdgeKeysAtom, virtualizationMetricsAtom;
|
|
var init_virtualization_store = __esm({
|
|
"src/core/virtualization-store.ts"() {
|
|
"use strict";
|
|
import_jotai29 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_viewport_store();
|
|
init_settings_store();
|
|
init_group_store();
|
|
init_spatial_index();
|
|
init_perf();
|
|
init_settings_store();
|
|
VIRTUALIZATION_BUFFER = 200;
|
|
spatialIndexAtom = (0, import_jotai29.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;
|
|
});
|
|
visibleBoundsAtom = (0, import_jotai29.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
|
|
};
|
|
});
|
|
visibleNodeKeysAtom = (0, import_jotai29.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;
|
|
});
|
|
visibleEdgeKeysAtom = (0, import_jotai29.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;
|
|
});
|
|
virtualizationMetricsAtom = (0, import_jotai29.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/core/canvas-api.ts
|
|
function createCanvasAPI(store, options = {}) {
|
|
const helpers = buildActionHelpers(store, options);
|
|
const api = {
|
|
// Selection
|
|
selectNode: (id) => store.set(selectSingleNodeAtom, id),
|
|
addToSelection: (ids) => store.set(addNodesToSelectionAtom, ids),
|
|
clearSelection: () => store.set(clearSelectionAtom),
|
|
getSelectedNodeIds: () => Array.from(store.get(selectedNodeIdsAtom)),
|
|
selectEdge: (edgeId) => store.set(selectEdgeAtom, edgeId),
|
|
clearEdgeSelection: () => store.set(clearEdgeSelectionAtom),
|
|
getSelectedEdgeId: () => store.get(selectedEdgeIdAtom),
|
|
// Viewport
|
|
getZoom: () => store.get(zoomAtom),
|
|
setZoom: (zoom) => store.set(zoomAtom, zoom),
|
|
getPan: () => store.get(panAtom),
|
|
setPan: (pan) => store.set(panAtom, pan),
|
|
resetViewport: () => store.set(resetViewportAtom),
|
|
fitToBounds: (mode, padding) => {
|
|
const fitMode = mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection;
|
|
store.set(fitToBoundsAtom, {
|
|
mode: fitMode,
|
|
padding
|
|
});
|
|
},
|
|
centerOnNode: (nodeId) => store.set(centerOnNodeAtom, nodeId),
|
|
// Graph
|
|
addNode: (node) => store.set(addNodeToLocalGraphAtom, node),
|
|
removeNode: (nodeId) => store.set(optimisticDeleteNodeAtom, {
|
|
nodeId
|
|
}),
|
|
addEdge: (edge) => store.set(addEdgeToLocalGraphAtom, edge),
|
|
removeEdge: (edgeKey) => store.set(optimisticDeleteEdgeAtom, {
|
|
edgeKey
|
|
}),
|
|
getNodeKeys: () => store.get(nodeKeysAtom),
|
|
getEdgeKeys: () => store.get(edgeKeysAtom),
|
|
getNodeAttributes: (id) => {
|
|
const graph = store.get(graphAtom);
|
|
return graph.hasNode(id) ? graph.getNodeAttributes(id) : void 0;
|
|
},
|
|
// History
|
|
undo: () => store.set(undoAtom),
|
|
redo: () => store.set(redoAtom),
|
|
canUndo: () => store.get(canUndoAtom),
|
|
canRedo: () => store.get(canRedoAtom),
|
|
recordSnapshot: (label) => store.set(pushHistoryAtom, label),
|
|
clearHistory: () => store.set(clearHistoryAtom),
|
|
// Clipboard
|
|
copy: () => store.set(copyToClipboardAtom),
|
|
cut: () => store.set(cutToClipboardAtom),
|
|
paste: () => store.set(pasteFromClipboardAtom),
|
|
duplicate: () => store.set(duplicateSelectionAtom),
|
|
hasClipboardContent: () => store.get(clipboardAtom) !== null,
|
|
// Snap
|
|
isSnapEnabled: () => store.get(snapEnabledAtom),
|
|
toggleSnap: () => store.set(toggleSnapAtom),
|
|
getSnapGridSize: () => store.get(snapGridSizeAtom),
|
|
// Virtualization
|
|
isVirtualizationEnabled: () => store.get(virtualizationEnabledAtom),
|
|
getVisibleNodeKeys: () => store.get(visibleNodeKeysAtom),
|
|
getVisibleEdgeKeys: () => store.get(visibleEdgeKeysAtom),
|
|
// Actions
|
|
executeAction: (actionId, context) => executeAction(actionId, context, helpers),
|
|
executeEventAction: (event, context) => {
|
|
const mappings = store.get(eventMappingsAtom);
|
|
const actionId = getActionForEvent(mappings, event);
|
|
return executeAction(actionId, context, helpers);
|
|
},
|
|
// Serialization
|
|
exportSnapshot: (metadata) => exportGraph(store, metadata),
|
|
importSnapshot: (snapshot, options2) => importGraph(store, snapshot, options2),
|
|
validateSnapshot: (data) => validateSnapshot(data)
|
|
};
|
|
return api;
|
|
}
|
|
var init_canvas_api = __esm({
|
|
"src/core/canvas-api.ts"() {
|
|
"use strict";
|
|
init_action_executor();
|
|
init_canvas_serializer();
|
|
init_settings_store();
|
|
init_selection_store();
|
|
init_viewport_store();
|
|
init_graph_store();
|
|
init_graph_derived();
|
|
init_graph_mutations();
|
|
init_graph_mutations_edges();
|
|
init_history_store();
|
|
init_clipboard_store();
|
|
init_snap_store();
|
|
init_virtualization_store();
|
|
init_layout();
|
|
}
|
|
});
|
|
|
|
// src/core/port-types.ts
|
|
function calculatePortPosition(nodeX, nodeY, nodeWidth, nodeHeight, port) {
|
|
switch (port.side) {
|
|
case "left":
|
|
return {
|
|
x: nodeX,
|
|
y: nodeY + nodeHeight * port.position
|
|
};
|
|
case "right":
|
|
return {
|
|
x: nodeX + nodeWidth,
|
|
y: nodeY + nodeHeight * port.position
|
|
};
|
|
case "top":
|
|
return {
|
|
x: nodeX + nodeWidth * port.position,
|
|
y: nodeY
|
|
};
|
|
case "bottom":
|
|
return {
|
|
x: nodeX + nodeWidth * port.position,
|
|
y: nodeY + nodeHeight
|
|
};
|
|
}
|
|
}
|
|
function getNodePorts(ports) {
|
|
if (ports && ports.length > 0) {
|
|
return ports;
|
|
}
|
|
return [DEFAULT_PORT];
|
|
}
|
|
function canPortAcceptConnection(port, currentConnections, isSource) {
|
|
if (isSource && port.type === "input") {
|
|
return false;
|
|
}
|
|
if (!isSource && port.type === "output") {
|
|
return false;
|
|
}
|
|
if (port.maxConnections !== void 0 && currentConnections >= port.maxConnections) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function arePortsCompatible(sourcePort, targetPort) {
|
|
if (sourcePort.type === "input") {
|
|
return false;
|
|
}
|
|
if (targetPort.type === "output") {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
var DEFAULT_PORT;
|
|
var init_port_types = __esm({
|
|
"src/core/port-types.ts"() {
|
|
"use strict";
|
|
DEFAULT_PORT = {
|
|
id: "default",
|
|
type: "bidirectional",
|
|
side: "right",
|
|
position: 0.5
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/core/input-classifier.ts
|
|
function classifyPointer(e) {
|
|
const source = pointerTypeToSource(e.pointerType);
|
|
return {
|
|
source,
|
|
pointerId: e.pointerId,
|
|
pressure: e.pressure,
|
|
tiltX: e.tiltX,
|
|
tiltY: e.tiltY,
|
|
isPrimary: e.isPrimary,
|
|
rawPointerType: e.pointerType
|
|
};
|
|
}
|
|
function pointerTypeToSource(pointerType) {
|
|
switch (pointerType) {
|
|
case "pen":
|
|
return "pencil";
|
|
case "touch":
|
|
return "finger";
|
|
case "mouse":
|
|
return "mouse";
|
|
default:
|
|
return "mouse";
|
|
}
|
|
}
|
|
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
|
|
};
|
|
}
|
|
function getGestureThresholds(source) {
|
|
switch (source) {
|
|
case "finger":
|
|
return {
|
|
dragThreshold: 10,
|
|
tapThreshold: 10,
|
|
longPressDuration: 600,
|
|
longPressMoveLimit: 10
|
|
};
|
|
case "pencil":
|
|
return {
|
|
dragThreshold: 2,
|
|
tapThreshold: 3,
|
|
longPressDuration: 500,
|
|
longPressMoveLimit: 5
|
|
};
|
|
case "mouse":
|
|
return {
|
|
dragThreshold: 3,
|
|
tapThreshold: 5,
|
|
longPressDuration: 0,
|
|
// Mouse uses right-click instead
|
|
longPressMoveLimit: 0
|
|
};
|
|
}
|
|
}
|
|
function getHitTargetSize(source) {
|
|
return HIT_TARGET_SIZES[source];
|
|
}
|
|
var HIT_TARGET_SIZES;
|
|
var init_input_classifier = __esm({
|
|
"src/core/input-classifier.ts"() {
|
|
"use strict";
|
|
HIT_TARGET_SIZES = {
|
|
/** Minimum touch target (Apple HIG: 44pt) */
|
|
finger: 44,
|
|
/** Stylus target (precise, can use smaller targets) */
|
|
pencil: 24,
|
|
/** Mouse target (hover-discoverable, smallest) */
|
|
mouse: 16
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/core/input-store.ts
|
|
var import_jotai30, activePointersAtom, primaryInputSourceAtom, inputCapabilitiesAtom, isStylusActiveAtom, isMultiTouchAtom, fingerCountAtom, isTouchDeviceAtom, pointerDownAtom, pointerUpAtom, clearPointersAtom;
|
|
var init_input_store = __esm({
|
|
"src/core/input-store.ts"() {
|
|
"use strict";
|
|
import_jotai30 = require("jotai");
|
|
init_input_classifier();
|
|
activePointersAtom = (0, import_jotai30.atom)(/* @__PURE__ */ new Map());
|
|
primaryInputSourceAtom = (0, import_jotai30.atom)("mouse");
|
|
inputCapabilitiesAtom = (0, import_jotai30.atom)(detectInputCapabilities());
|
|
isStylusActiveAtom = (0, import_jotai30.atom)((get) => {
|
|
const pointers = get(activePointersAtom);
|
|
for (const [, pointer] of pointers) {
|
|
if (pointer.source === "pencil") return true;
|
|
}
|
|
return false;
|
|
});
|
|
isMultiTouchAtom = (0, import_jotai30.atom)((get) => {
|
|
const pointers = get(activePointersAtom);
|
|
let fingerCount = 0;
|
|
for (const [, pointer] of pointers) {
|
|
if (pointer.source === "finger") fingerCount++;
|
|
}
|
|
return fingerCount > 1;
|
|
});
|
|
fingerCountAtom = (0, import_jotai30.atom)((get) => {
|
|
const pointers = get(activePointersAtom);
|
|
let count = 0;
|
|
for (const [, pointer] of pointers) {
|
|
if (pointer.source === "finger") count++;
|
|
}
|
|
return count;
|
|
});
|
|
isTouchDeviceAtom = (0, import_jotai30.atom)((get) => {
|
|
const caps = get(inputCapabilitiesAtom);
|
|
return caps.hasTouch;
|
|
});
|
|
pointerDownAtom = (0, import_jotai30.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
|
|
});
|
|
}
|
|
}
|
|
});
|
|
pointerUpAtom = (0, import_jotai30.atom)(null, (get, set, pointerId) => {
|
|
const pointers = new Map(get(activePointersAtom));
|
|
pointers.delete(pointerId);
|
|
set(activePointersAtom, pointers);
|
|
});
|
|
clearPointersAtom = (0, import_jotai30.atom)(null, (_get, set) => {
|
|
set(activePointersAtom, /* @__PURE__ */ new Map());
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/selection-path-store.ts
|
|
function pointInPolygon(px, py, polygon) {
|
|
let inside = false;
|
|
const n = polygon.length;
|
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
const xi = polygon[i].x;
|
|
const yi = polygon[i].y;
|
|
const xj = polygon[j].x;
|
|
const yj = polygon[j].y;
|
|
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
|
|
inside = !inside;
|
|
}
|
|
}
|
|
return inside;
|
|
}
|
|
var import_jotai31, selectionPathAtom, isSelectingAtom, startSelectionAtom, updateSelectionAtom, cancelSelectionAtom, endSelectionAtom, selectionRectAtom;
|
|
var init_selection_path_store = __esm({
|
|
"src/core/selection-path-store.ts"() {
|
|
"use strict";
|
|
import_jotai31 = require("jotai");
|
|
init_graph_derived();
|
|
init_selection_store();
|
|
selectionPathAtom = (0, import_jotai31.atom)(null);
|
|
isSelectingAtom = (0, import_jotai31.atom)((get) => get(selectionPathAtom) !== null);
|
|
startSelectionAtom = (0, import_jotai31.atom)(null, (_get, set, {
|
|
type,
|
|
point
|
|
}) => {
|
|
set(selectionPathAtom, {
|
|
type,
|
|
points: [point]
|
|
});
|
|
});
|
|
updateSelectionAtom = (0, import_jotai31.atom)(null, (get, set, point) => {
|
|
const current = get(selectionPathAtom);
|
|
if (!current) return;
|
|
if (current.type === "rect") {
|
|
set(selectionPathAtom, {
|
|
...current,
|
|
points: [current.points[0], point]
|
|
});
|
|
} else {
|
|
set(selectionPathAtom, {
|
|
...current,
|
|
points: [...current.points, point]
|
|
});
|
|
}
|
|
});
|
|
cancelSelectionAtom = (0, import_jotai31.atom)(null, (_get, set) => {
|
|
set(selectionPathAtom, null);
|
|
});
|
|
endSelectionAtom = (0, import_jotai31.atom)(null, (get, set) => {
|
|
const path = get(selectionPathAtom);
|
|
if (!path || path.points.length < 2) {
|
|
set(selectionPathAtom, null);
|
|
return;
|
|
}
|
|
const nodes = get(uiNodesAtom);
|
|
const selectedIds = [];
|
|
if (path.type === "rect") {
|
|
const [p1, p2] = [path.points[0], path.points[path.points.length - 1]];
|
|
const minX = Math.min(p1.x, p2.x);
|
|
const maxX = Math.max(p1.x, p2.x);
|
|
const minY = Math.min(p1.y, p2.y);
|
|
const maxY = Math.max(p1.y, p2.y);
|
|
for (const node of nodes) {
|
|
const nodeRight = node.position.x + (node.width ?? 200);
|
|
const nodeBottom = node.position.y + (node.height ?? 100);
|
|
if (node.position.x < maxX && nodeRight > minX && node.position.y < maxY && nodeBottom > minY) {
|
|
selectedIds.push(node.id);
|
|
}
|
|
}
|
|
} else {
|
|
const polygon = path.points;
|
|
for (const node of nodes) {
|
|
const cx = node.position.x + (node.width ?? 200) / 2;
|
|
const cy = node.position.y + (node.height ?? 100) / 2;
|
|
if (pointInPolygon(cx, cy, polygon)) {
|
|
selectedIds.push(node.id);
|
|
}
|
|
}
|
|
}
|
|
set(selectedNodeIdsAtom, new Set(selectedIds));
|
|
set(selectionPathAtom, null);
|
|
});
|
|
selectionRectAtom = (0, import_jotai31.atom)((get) => {
|
|
const path = get(selectionPathAtom);
|
|
if (!path || path.type !== "rect" || path.points.length < 2) return null;
|
|
const [p1, p2] = [path.points[0], path.points[path.points.length - 1]];
|
|
return {
|
|
x: Math.min(p1.x, p2.x),
|
|
y: Math.min(p1.y, p2.y),
|
|
width: Math.abs(p2.x - p1.x),
|
|
height: Math.abs(p2.y - p1.y)
|
|
};
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/search-store.ts
|
|
var search_store_exports = {};
|
|
__export(search_store_exports, {
|
|
clearSearchAtom: () => clearSearchAtom,
|
|
fuzzyMatch: () => fuzzyMatch,
|
|
highlightedSearchIndexAtom: () => highlightedSearchIndexAtom,
|
|
highlightedSearchNodeIdAtom: () => highlightedSearchNodeIdAtom,
|
|
isFilterActiveAtom: () => isFilterActiveAtom,
|
|
nextSearchResultAtom: () => nextSearchResultAtom,
|
|
prevSearchResultAtom: () => prevSearchResultAtom,
|
|
searchEdgeResultCountAtom: () => searchEdgeResultCountAtom,
|
|
searchEdgeResultsAtom: () => searchEdgeResultsAtom,
|
|
searchQueryAtom: () => searchQueryAtom,
|
|
searchResultCountAtom: () => searchResultCountAtom,
|
|
searchResultsArrayAtom: () => searchResultsArrayAtom,
|
|
searchResultsAtom: () => searchResultsAtom,
|
|
searchTotalResultCountAtom: () => searchTotalResultCountAtom,
|
|
setSearchQueryAtom: () => setSearchQueryAtom
|
|
});
|
|
function fuzzyMatch(query, ...haystacks) {
|
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
if (tokens.length === 0) return false;
|
|
const combined = haystacks.join(" ").toLowerCase();
|
|
return tokens.every((token) => combined.includes(token));
|
|
}
|
|
var import_jotai32, searchQueryAtom, setSearchQueryAtom, clearSearchAtom, searchResultsAtom, searchResultsArrayAtom, searchResultCountAtom, searchEdgeResultsAtom, searchEdgeResultCountAtom, isFilterActiveAtom, searchTotalResultCountAtom, highlightedSearchIndexAtom, nextSearchResultAtom, prevSearchResultAtom, highlightedSearchNodeIdAtom;
|
|
var init_search_store = __esm({
|
|
"src/core/search-store.ts"() {
|
|
"use strict";
|
|
import_jotai32 = require("jotai");
|
|
init_graph_derived();
|
|
init_graph_store();
|
|
init_viewport_store();
|
|
init_selection_store();
|
|
searchQueryAtom = (0, import_jotai32.atom)("");
|
|
setSearchQueryAtom = (0, import_jotai32.atom)(null, (_get, set, query) => {
|
|
set(searchQueryAtom, query);
|
|
set(highlightedSearchIndexAtom, 0);
|
|
});
|
|
clearSearchAtom = (0, import_jotai32.atom)(null, (_get, set) => {
|
|
set(searchQueryAtom, "");
|
|
set(highlightedSearchIndexAtom, 0);
|
|
});
|
|
searchResultsAtom = (0, import_jotai32.atom)((get) => {
|
|
const query = get(searchQueryAtom).trim();
|
|
if (!query) return /* @__PURE__ */ new Set();
|
|
const nodes = get(uiNodesAtom);
|
|
const matches = /* @__PURE__ */ new Set();
|
|
for (const node of nodes) {
|
|
if (fuzzyMatch(query, node.label || "", node.dbData.node_type || "", node.id)) {
|
|
matches.add(node.id);
|
|
}
|
|
}
|
|
return matches;
|
|
});
|
|
searchResultsArrayAtom = (0, import_jotai32.atom)((get) => {
|
|
return Array.from(get(searchResultsAtom));
|
|
});
|
|
searchResultCountAtom = (0, import_jotai32.atom)((get) => {
|
|
return get(searchResultsAtom).size;
|
|
});
|
|
searchEdgeResultsAtom = (0, import_jotai32.atom)((get) => {
|
|
const query = get(searchQueryAtom).trim();
|
|
if (!query) return /* @__PURE__ */ new Set();
|
|
get(graphUpdateVersionAtom);
|
|
const graph = get(graphAtom);
|
|
const matches = /* @__PURE__ */ new Set();
|
|
graph.forEachEdge((edgeKey, attrs) => {
|
|
const label = attrs.label || "";
|
|
const edgeType = attrs.dbData?.edge_type || "";
|
|
if (fuzzyMatch(query, label, edgeType, edgeKey)) {
|
|
matches.add(edgeKey);
|
|
}
|
|
});
|
|
return matches;
|
|
});
|
|
searchEdgeResultCountAtom = (0, import_jotai32.atom)((get) => {
|
|
return get(searchEdgeResultsAtom).size;
|
|
});
|
|
isFilterActiveAtom = (0, import_jotai32.atom)((get) => {
|
|
return get(searchQueryAtom).trim().length > 0;
|
|
});
|
|
searchTotalResultCountAtom = (0, import_jotai32.atom)((get) => {
|
|
return get(searchResultCountAtom) + get(searchEdgeResultCountAtom);
|
|
});
|
|
highlightedSearchIndexAtom = (0, import_jotai32.atom)(0);
|
|
nextSearchResultAtom = (0, import_jotai32.atom)(null, (get, set) => {
|
|
const results = get(searchResultsArrayAtom);
|
|
if (results.length === 0) return;
|
|
const currentIndex = get(highlightedSearchIndexAtom);
|
|
const nextIndex = (currentIndex + 1) % results.length;
|
|
set(highlightedSearchIndexAtom, nextIndex);
|
|
const nodeId = results[nextIndex];
|
|
set(centerOnNodeAtom, nodeId);
|
|
set(selectSingleNodeAtom, nodeId);
|
|
});
|
|
prevSearchResultAtom = (0, import_jotai32.atom)(null, (get, set) => {
|
|
const results = get(searchResultsArrayAtom);
|
|
if (results.length === 0) return;
|
|
const currentIndex = get(highlightedSearchIndexAtom);
|
|
const prevIndex = (currentIndex - 1 + results.length) % results.length;
|
|
set(highlightedSearchIndexAtom, prevIndex);
|
|
const nodeId = results[prevIndex];
|
|
set(centerOnNodeAtom, nodeId);
|
|
set(selectSingleNodeAtom, nodeId);
|
|
});
|
|
highlightedSearchNodeIdAtom = (0, import_jotai32.atom)((get) => {
|
|
const results = get(searchResultsArrayAtom);
|
|
if (results.length === 0) return null;
|
|
const index = get(highlightedSearchIndexAtom);
|
|
return results[index] ?? null;
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/gesture-resolver.ts
|
|
var init_gesture_resolver = __esm({
|
|
"src/core/gesture-resolver.ts"() {
|
|
"use strict";
|
|
}
|
|
});
|
|
|
|
// src/core/gesture-rules-defaults.ts
|
|
function formatRuleLabel(pattern) {
|
|
const parts = [];
|
|
if (pattern.modifiers) {
|
|
const mods = MODIFIER_KEYS.filter((k) => pattern.modifiers[k]).map((k) => k.charAt(0).toUpperCase() + k.slice(1));
|
|
if (mods.length) parts.push(mods.join("+"));
|
|
}
|
|
if (pattern.button !== void 0 && pattern.button !== 0) {
|
|
parts.push(BUTTON_LABELS[pattern.button]);
|
|
}
|
|
if (pattern.source) {
|
|
parts.push(SOURCE_LABELS[pattern.source]);
|
|
}
|
|
if (pattern.gesture) {
|
|
parts.push(GESTURE_LABELS[pattern.gesture] ?? pattern.gesture);
|
|
}
|
|
if (pattern.target) {
|
|
parts.push("on " + (TARGET_LABELS[pattern.target] ?? pattern.target));
|
|
}
|
|
if (parts.length === 0) return "Any gesture";
|
|
if (pattern.modifiers) {
|
|
const modCount = MODIFIER_KEYS.filter((k) => pattern.modifiers[k]).length;
|
|
if (modCount > 0 && parts.length > modCount) {
|
|
const modPart = parts.slice(0, 1).join("");
|
|
const rest = parts.slice(1).join(" ").toLowerCase();
|
|
return `${modPart} + ${rest}`;
|
|
}
|
|
}
|
|
return parts.join(" ");
|
|
}
|
|
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 MODIFIER_KEYS, SOURCE_LABELS, GESTURE_LABELS, TARGET_LABELS, BUTTON_LABELS, DEFAULT_GESTURE_RULES;
|
|
var init_gesture_rules_defaults = __esm({
|
|
"src/core/gesture-rules-defaults.ts"() {
|
|
"use strict";
|
|
MODIFIER_KEYS = ["shift", "ctrl", "alt", "meta"];
|
|
SOURCE_LABELS = {
|
|
mouse: "Mouse",
|
|
pencil: "Pencil",
|
|
finger: "Touch"
|
|
};
|
|
GESTURE_LABELS = {
|
|
tap: "Tap",
|
|
"double-tap": "Double-tap",
|
|
"triple-tap": "Triple-tap",
|
|
drag: "Drag",
|
|
"long-press": "Long-press",
|
|
"right-click": "Right-click",
|
|
pinch: "Pinch",
|
|
scroll: "Scroll"
|
|
};
|
|
TARGET_LABELS = {
|
|
node: "node",
|
|
edge: "edge",
|
|
port: "port",
|
|
"resize-handle": "resize handle",
|
|
background: "background"
|
|
};
|
|
BUTTON_LABELS = {
|
|
0: "Left",
|
|
1: "Middle",
|
|
2: "Right"
|
|
};
|
|
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
|
|
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_KEYS2) {
|
|
const required = pattern.modifiers[key];
|
|
if (required === void 0) continue;
|
|
const actual = dm[key] ?? false;
|
|
if (required !== actual) return -1;
|
|
score += 8;
|
|
}
|
|
}
|
|
return score;
|
|
}
|
|
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);
|
|
}
|
|
var MODIFIER_KEYS2, PALM_REJECTION_RULE;
|
|
var init_gesture_rules = __esm({
|
|
"src/core/gesture-rules.ts"() {
|
|
"use strict";
|
|
init_gesture_rules_defaults();
|
|
MODIFIER_KEYS2 = ["shift", "ctrl", "alt", "meta"];
|
|
PALM_REJECTION_RULE = {
|
|
id: "__palm-rejection__",
|
|
pattern: {},
|
|
actionId: "none",
|
|
label: "Palm rejection"
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/core/gesture-rule-store.ts
|
|
var import_jotai33, import_utils3, DEFAULT_RULE_STATE, gestureRuleSettingsAtom, consumerGestureRulesAtom, gestureRulesAtom, gestureRuleIndexAtom, palmRejectionEnabledAtom, addGestureRuleAtom, removeGestureRuleAtom, updateGestureRuleAtom, resetGestureRulesAtom;
|
|
var init_gesture_rule_store = __esm({
|
|
"src/core/gesture-rule-store.ts"() {
|
|
"use strict";
|
|
import_jotai33 = require("jotai");
|
|
import_utils3 = require("jotai/utils");
|
|
init_gesture_rules();
|
|
DEFAULT_RULE_STATE = {
|
|
customRules: [],
|
|
palmRejection: true
|
|
};
|
|
gestureRuleSettingsAtom = (0, import_utils3.atomWithStorage)("canvas-gesture-rules", DEFAULT_RULE_STATE);
|
|
consumerGestureRulesAtom = (0, import_jotai33.atom)([]);
|
|
gestureRulesAtom = (0, import_jotai33.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;
|
|
});
|
|
gestureRuleIndexAtom = (0, import_jotai33.atom)((get) => {
|
|
return buildRuleIndex(get(gestureRulesAtom));
|
|
});
|
|
palmRejectionEnabledAtom = (0, import_jotai33.atom)((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => {
|
|
const current = get(gestureRuleSettingsAtom);
|
|
set(gestureRuleSettingsAtom, {
|
|
...current,
|
|
palmRejection: enabled
|
|
});
|
|
});
|
|
addGestureRuleAtom = (0, import_jotai33.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
|
|
});
|
|
});
|
|
removeGestureRuleAtom = (0, import_jotai33.atom)(null, (get, set, ruleId) => {
|
|
const current = get(gestureRuleSettingsAtom);
|
|
set(gestureRuleSettingsAtom, {
|
|
...current,
|
|
customRules: current.customRules.filter((r) => r.id !== ruleId)
|
|
});
|
|
});
|
|
updateGestureRuleAtom = (0, import_jotai33.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
|
|
});
|
|
});
|
|
resetGestureRulesAtom = (0, import_jotai33.atom)(null, (get, set) => {
|
|
const current = get(gestureRuleSettingsAtom);
|
|
set(gestureRuleSettingsAtom, {
|
|
...current,
|
|
customRules: []
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/external-keyboard-store.ts
|
|
var import_jotai34, hasExternalKeyboardAtom, watchExternalKeyboardAtom;
|
|
var init_external_keyboard_store = __esm({
|
|
"src/core/external-keyboard-store.ts"() {
|
|
"use strict";
|
|
import_jotai34 = require("jotai");
|
|
hasExternalKeyboardAtom = (0, import_jotai34.atom)(false);
|
|
watchExternalKeyboardAtom = (0, import_jotai34.atom)(null, (get, set) => {
|
|
if (typeof window === "undefined") return;
|
|
const handler = (e) => {
|
|
if (e.key && e.key.length === 1 || ["Tab", "Escape", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
set(hasExternalKeyboardAtom, true);
|
|
window.removeEventListener("keydown", handler);
|
|
}
|
|
};
|
|
window.addEventListener("keydown", handler);
|
|
return () => window.removeEventListener("keydown", handler);
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/core/plugin-types.ts
|
|
var PluginError;
|
|
var init_plugin_types = __esm({
|
|
"src/core/plugin-types.ts"() {
|
|
"use strict";
|
|
PluginError = class extends Error {
|
|
constructor(message, pluginId, code) {
|
|
super(`[Plugin "${pluginId}"] ${message}`);
|
|
this.pluginId = pluginId;
|
|
this.code = code;
|
|
this.name = "PluginError";
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/gestures/types.ts
|
|
var NO_MODIFIERS, NO_HELD_KEYS;
|
|
var init_types2 = __esm({
|
|
"src/gestures/types.ts"() {
|
|
"use strict";
|
|
NO_MODIFIERS = Object.freeze({
|
|
shift: false,
|
|
ctrl: false,
|
|
alt: false,
|
|
meta: false
|
|
});
|
|
NO_HELD_KEYS = Object.freeze({
|
|
byKey: Object.freeze({}),
|
|
byCode: Object.freeze({})
|
|
});
|
|
}
|
|
});
|
|
|
|
// src/gestures/dispatcher.ts
|
|
function registerAction2(actionId, handler) {
|
|
handlers.set(actionId, handler);
|
|
}
|
|
function unregisterAction2(actionId) {
|
|
handlers.delete(actionId);
|
|
}
|
|
var handlers;
|
|
var init_dispatcher = __esm({
|
|
"src/gestures/dispatcher.ts"() {
|
|
"use strict";
|
|
init_types2();
|
|
handlers = /* @__PURE__ */ new Map();
|
|
}
|
|
});
|
|
|
|
// src/utils/edge-path-calculators.ts
|
|
var init_edge_path_calculators = __esm({
|
|
"src/utils/edge-path-calculators.ts"() {
|
|
"use strict";
|
|
}
|
|
});
|
|
|
|
// src/utils/edge-path-registry.ts
|
|
function registerEdgePathCalculator(name, calculator) {
|
|
customCalculators.set(name, calculator);
|
|
}
|
|
function unregisterEdgePathCalculator(name) {
|
|
return customCalculators.delete(name);
|
|
}
|
|
var customCalculators;
|
|
var init_edge_path_registry = __esm({
|
|
"src/utils/edge-path-registry.ts"() {
|
|
"use strict";
|
|
init_edge_path_calculators();
|
|
customCalculators = /* @__PURE__ */ new Map();
|
|
}
|
|
});
|
|
|
|
// src/core/plugin-registry.ts
|
|
function registerPlugin(plugin) {
|
|
debug14("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()
|
|
});
|
|
debug14("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);
|
|
debug14("Plugin unregistered: %s", pluginId);
|
|
}
|
|
function getPlugin(id) {
|
|
return plugins.get(id)?.plugin;
|
|
}
|
|
function hasPlugin(id) {
|
|
return plugins.has(id);
|
|
}
|
|
function getAllPlugins() {
|
|
return Array.from(plugins.values()).map((r) => r.plugin);
|
|
}
|
|
function getPluginIds() {
|
|
return Array.from(plugins.keys());
|
|
}
|
|
function getPluginGestureContexts() {
|
|
const contexts = [];
|
|
for (const registration of plugins.values()) {
|
|
if (registration.plugin.gestureContexts) {
|
|
contexts.push(...registration.plugin.gestureContexts);
|
|
}
|
|
}
|
|
return contexts;
|
|
}
|
|
function clearPlugins() {
|
|
const ids = Array.from(plugins.keys()).reverse();
|
|
for (const id of ids) {
|
|
const reg = plugins.get(id);
|
|
if (reg?.cleanup) {
|
|
try {
|
|
reg.cleanup();
|
|
} catch {
|
|
}
|
|
}
|
|
plugins.delete(id);
|
|
}
|
|
debug14("All plugins cleared");
|
|
}
|
|
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
|
|
};
|
|
}
|
|
var debug14, plugins;
|
|
var init_plugin_registry = __esm({
|
|
"src/core/plugin-registry.ts"() {
|
|
"use strict";
|
|
init_plugin_types();
|
|
init_node_type_registry();
|
|
init_action_registry();
|
|
init_dispatcher();
|
|
init_registry();
|
|
init_edge_path_registry();
|
|
init_debug();
|
|
debug14 = createDebug("plugins");
|
|
plugins = /* @__PURE__ */ new Map();
|
|
}
|
|
});
|
|
|
|
// src/core/index.ts
|
|
var core_exports = {};
|
|
__export(core_exports, {
|
|
ActionCategory: () => ActionCategory,
|
|
BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
|
|
BuiltInActionId: () => BuiltInActionId,
|
|
CanvasEventType: () => CanvasEventType,
|
|
DEFAULT_GESTURE_RULES: () => DEFAULT_GESTURE_RULES,
|
|
DEFAULT_MAPPINGS: () => DEFAULT_MAPPINGS,
|
|
DEFAULT_PORT: () => DEFAULT_PORT,
|
|
EDGE_ANIMATION_DURATION: () => EDGE_ANIMATION_DURATION,
|
|
EVENT_TYPE_INFO: () => EVENT_TYPE_INFO,
|
|
FallbackNodeTypeComponent: () => FallbackNodeTypeComponent,
|
|
HIT_TARGET_SIZES: () => HIT_TARGET_SIZES,
|
|
PASTE_OFFSET: () => PASTE_OFFSET,
|
|
PluginError: () => PluginError,
|
|
SNAPSHOT_VERSION: () => SNAPSHOT_VERSION,
|
|
SpatialGrid: () => SpatialGrid,
|
|
VIRTUALIZATION_BUFFER: () => VIRTUALIZATION_BUFFER,
|
|
ZOOM_EXIT_THRESHOLD: () => ZOOM_EXIT_THRESHOLD,
|
|
ZOOM_TRANSITION_THRESHOLD: () => ZOOM_TRANSITION_THRESHOLD,
|
|
activePointersAtom: () => activePointersAtom,
|
|
activePresetAtom: () => activePresetAtom,
|
|
activePresetIdAtom: () => activePresetIdAtom,
|
|
addGestureRuleAtom: () => addGestureRuleAtom,
|
|
addNodeToLocalGraphAtom: () => addNodeToLocalGraphAtom,
|
|
addNodesToSelectionAtom: () => addNodesToSelectionAtom,
|
|
alignmentGuidesAtom: () => alignmentGuidesAtom,
|
|
allPresetsAtom: () => allPresetsAtom,
|
|
animateFitToBoundsAtom: () => animateFitToBoundsAtom,
|
|
animateZoomToNodeAtom: () => animateZoomToNodeAtom,
|
|
applyDelta: () => applyDelta,
|
|
applyPresetAtom: () => applyPresetAtom,
|
|
arePortsCompatible: () => arePortsCompatible,
|
|
autoResizeGroupAtom: () => autoResizeGroupAtom,
|
|
buildActionHelpers: () => buildActionHelpers,
|
|
buildRuleIndex: () => buildRuleIndex,
|
|
calculatePortPosition: () => calculatePortPosition,
|
|
canPortAcceptConnection: () => canPortAcceptConnection,
|
|
canRedoAtom: () => canRedoAtom,
|
|
canUndoAtom: () => canUndoAtom,
|
|
cancelSelectionAtom: () => cancelSelectionAtom,
|
|
canvasMark: () => canvasMark,
|
|
canvasSettingsAtom: () => canvasSettingsAtom,
|
|
canvasToastAtom: () => canvasToastAtom,
|
|
canvasWrap: () => canvasWrap,
|
|
centerOnNodeAtom: () => centerOnNodeAtom,
|
|
classifyPointer: () => classifyPointer,
|
|
cleanupAllNodePositionsAtom: () => cleanupAllNodePositionsAtom,
|
|
cleanupNodePositionAtom: () => cleanupNodePositionAtom,
|
|
clearActions: () => clearActions,
|
|
clearAlignmentGuidesAtom: () => clearAlignmentGuidesAtom,
|
|
clearClipboardAtom: () => clearClipboardAtom,
|
|
clearEdgeSelectionAtom: () => clearEdgeSelectionAtom,
|
|
clearGraphOnSwitchAtom: () => clearGraphOnSwitchAtom,
|
|
clearHistoryAtom: () => clearHistoryAtom,
|
|
clearMutationQueueAtom: () => clearMutationQueueAtom,
|
|
clearNodeTypeRegistry: () => clearNodeTypeRegistry,
|
|
clearPlugins: () => clearPlugins,
|
|
clearPointersAtom: () => clearPointersAtom,
|
|
clearSearchAtom: () => clearSearchAtom,
|
|
clearSelectionAtom: () => clearSelectionAtom,
|
|
clipboardAtom: () => clipboardAtom,
|
|
clipboardNodeCountAtom: () => clipboardNodeCountAtom,
|
|
collapseGroupAtom: () => collapseGroupAtom,
|
|
collapsedEdgeRemapAtom: () => collapsedEdgeRemapAtom,
|
|
collapsedGroupsAtom: () => collapsedGroupsAtom,
|
|
completeMutationAtom: () => completeMutationAtom,
|
|
conditionalSnap: () => conditionalSnap,
|
|
consumerGestureRulesAtom: () => consumerGestureRulesAtom,
|
|
copyToClipboardAtom: () => copyToClipboardAtom,
|
|
createActionContext: () => createActionContext,
|
|
createActionContextFromReactEvent: () => createActionContextFromReactEvent,
|
|
createActionContextFromTouchEvent: () => createActionContextFromTouchEvent,
|
|
createCanvasAPI: () => createCanvasAPI,
|
|
createSnapshot: () => createSnapshot,
|
|
currentGraphIdAtom: () => currentGraphIdAtom,
|
|
cutToClipboardAtom: () => cutToClipboardAtom,
|
|
deletePresetAtom: () => deletePresetAtom,
|
|
departingEdgesAtom: () => departingEdgesAtom,
|
|
dequeueMutationAtom: () => dequeueMutationAtom,
|
|
detectInputCapabilities: () => detectInputCapabilities,
|
|
draggingNodeIdAtom: () => draggingNodeIdAtom,
|
|
dropTargetNodeIdAtom: () => dropTargetNodeIdAtom,
|
|
duplicateSelectionAtom: () => duplicateSelectionAtom,
|
|
edgeCreationAtom: () => edgeCreationAtom,
|
|
edgeFamilyAtom: () => edgeFamilyAtom,
|
|
edgeKeysAtom: () => edgeKeysAtom,
|
|
edgeKeysWithTempEdgeAtom: () => edgeKeysWithTempEdgeAtom,
|
|
editingEdgeLabelAtom: () => editingEdgeLabelAtom,
|
|
endNodeDragAtom: () => endNodeDragAtom,
|
|
endSelectionAtom: () => endSelectionAtom,
|
|
eventMappingsAtom: () => eventMappingsAtom,
|
|
executeAction: () => executeAction,
|
|
expandGroupAtom: () => expandGroupAtom,
|
|
exportGraph: () => exportGraph,
|
|
findAlignmentGuides: () => findAlignmentGuides,
|
|
fingerCountAtom: () => fingerCountAtom,
|
|
fitToBoundsAtom: () => fitToBoundsAtom,
|
|
focusedNodeIdAtom: () => focusedNodeIdAtom,
|
|
formatRuleLabel: () => formatRuleLabel,
|
|
fuzzyMatch: () => fuzzyMatch,
|
|
gestureRuleIndexAtom: () => gestureRuleIndexAtom,
|
|
gestureRuleSettingsAtom: () => gestureRuleSettingsAtom,
|
|
gestureRulesAtom: () => gestureRulesAtom,
|
|
getAction: () => getAction,
|
|
getActionForEvent: () => getActionForEvent,
|
|
getActionsByCategories: () => getActionsByCategories,
|
|
getActionsByCategory: () => getActionsByCategory,
|
|
getAllActions: () => getAllActions,
|
|
getAllPlugins: () => getAllPlugins,
|
|
getGestureThresholds: () => getGestureThresholds,
|
|
getHitTargetSize: () => getHitTargetSize,
|
|
getNextQueuedMutationAtom: () => getNextQueuedMutationAtom,
|
|
getNodeDescendants: () => getNodeDescendants,
|
|
getNodePorts: () => getNodePorts,
|
|
getNodeTypeComponent: () => getNodeTypeComponent,
|
|
getPlugin: () => getPlugin,
|
|
getPluginGestureContexts: () => getPluginGestureContexts,
|
|
getPluginIds: () => getPluginIds,
|
|
getRegisteredNodeTypes: () => getRegisteredNodeTypes,
|
|
getSnapGuides: () => getSnapGuides,
|
|
goToLockedPageAtom: () => goToLockedPageAtom,
|
|
graphAtom: () => graphAtom,
|
|
graphOptions: () => graphOptions,
|
|
graphUpdateVersionAtom: () => graphUpdateVersionAtom,
|
|
groupChildCountAtom: () => groupChildCountAtom,
|
|
groupSelectedNodesAtom: () => groupSelectedNodesAtom,
|
|
handleNodePointerDownSelectionAtom: () => handleNodePointerDownSelectionAtom,
|
|
hasAction: () => hasAction,
|
|
hasClipboardContentAtom: () => hasClipboardContentAtom,
|
|
hasExternalKeyboardAtom: () => hasExternalKeyboardAtom,
|
|
hasFocusedNodeAtom: () => hasFocusedNodeAtom,
|
|
hasLockedNodeAtom: () => hasLockedNodeAtom,
|
|
hasNodeTypeComponent: () => hasNodeTypeComponent,
|
|
hasPlugin: () => hasPlugin,
|
|
hasSelectionAtom: () => hasSelectionAtom,
|
|
hasUnsavedChangesAtom: () => hasUnsavedChangesAtom,
|
|
highestZIndexAtom: () => highestZIndexAtom,
|
|
highlightedSearchIndexAtom: () => highlightedSearchIndexAtom,
|
|
highlightedSearchNodeIdAtom: () => highlightedSearchNodeIdAtom,
|
|
historyLabelsAtom: () => historyLabelsAtom,
|
|
historyStateAtom: () => historyStateAtom,
|
|
importGraph: () => importGraph,
|
|
incrementRetryCountAtom: () => incrementRetryCountAtom,
|
|
inputCapabilitiesAtom: () => inputCapabilitiesAtom,
|
|
inputModeAtom: () => inputModeAtom2,
|
|
interactionFeedbackAtom: () => interactionFeedbackAtom,
|
|
invertDelta: () => invertDelta,
|
|
isFilterActiveAtom: () => isFilterActiveAtom,
|
|
isGroupNodeAtom: () => isGroupNodeAtom,
|
|
isMultiTouchAtom: () => isMultiTouchAtom,
|
|
isNodeCollapsed: () => isNodeCollapsed,
|
|
isOnlineAtom: () => isOnlineAtom,
|
|
isPanelOpenAtom: () => isPanelOpenAtom,
|
|
isPickNodeModeAtom: () => isPickNodeModeAtom,
|
|
isPickingModeAtom: () => isPickingModeAtom,
|
|
isSelectingAtom: () => isSelectingAtom,
|
|
isSnappingActiveAtom: () => isSnappingActiveAtom,
|
|
isStylusActiveAtom: () => isStylusActiveAtom,
|
|
isTouchDeviceAtom: () => isTouchDeviceAtom,
|
|
isZoomTransitioningAtom: () => isZoomTransitioningAtom,
|
|
keyboardInteractionModeAtom: () => keyboardInteractionModeAtom,
|
|
lastSyncErrorAtom: () => lastSyncErrorAtom,
|
|
lastSyncTimeAtom: () => lastSyncTimeAtom,
|
|
loadGraphFromDbAtom: () => loadGraphFromDbAtom,
|
|
lockNodeAtom: () => lockNodeAtom,
|
|
lockedNodeDataAtom: () => lockedNodeDataAtom,
|
|
lockedNodeIdAtom: () => lockedNodeIdAtom,
|
|
lockedNodePageCountAtom: () => lockedNodePageCountAtom,
|
|
lockedNodePageIndexAtom: () => lockedNodePageIndexAtom,
|
|
matchSpecificity: () => matchSpecificity,
|
|
mergeNodesAtom: () => mergeNodesAtom,
|
|
mergeRules: () => mergeRules,
|
|
moveNodesToGroupAtom: () => moveNodesToGroupAtom,
|
|
mutationQueueAtom: () => mutationQueueAtom,
|
|
nestNodesOnDropAtom: () => nestNodesOnDropAtom,
|
|
nextLockedPageAtom: () => nextLockedPageAtom,
|
|
nextSearchResultAtom: () => nextSearchResultAtom,
|
|
nodeChildrenAtom: () => nodeChildrenAtom,
|
|
nodeFamilyAtom: () => nodeFamilyAtom,
|
|
nodeKeysAtom: () => nodeKeysAtom,
|
|
nodeParentAtom: () => nodeParentAtom,
|
|
nodePositionAtomFamily: () => nodePositionAtomFamily,
|
|
nodePositionUpdateCounterAtom: () => nodePositionUpdateCounterAtom,
|
|
optimisticDeleteEdgeAtom: () => optimisticDeleteEdgeAtom,
|
|
optimisticDeleteNodeAtom: () => optimisticDeleteNodeAtom,
|
|
palmRejectionEnabledAtom: () => palmRejectionEnabledAtom,
|
|
panAtom: () => panAtom,
|
|
pasteFromClipboardAtom: () => pasteFromClipboardAtom,
|
|
pendingInputResolverAtom: () => pendingInputResolverAtom2,
|
|
pendingMutationsCountAtom: () => pendingMutationsCountAtom,
|
|
perfEnabledAtom: () => perfEnabledAtom,
|
|
pointInPolygon: () => pointInPolygon,
|
|
pointerDownAtom: () => pointerDownAtom,
|
|
pointerUpAtom: () => pointerUpAtom,
|
|
preDragNodeAttributesAtom: () => preDragNodeAttributesAtom,
|
|
prefersReducedMotionAtom: () => prefersReducedMotionAtom,
|
|
prevLockedPageAtom: () => prevLockedPageAtom,
|
|
prevSearchResultAtom: () => prevSearchResultAtom,
|
|
primaryInputSourceAtom: () => primaryInputSourceAtom,
|
|
provideInputAtom: () => provideInputAtom2,
|
|
pushDeltaAtom: () => pushDeltaAtom,
|
|
pushHistoryAtom: () => pushHistoryAtom,
|
|
queueMutationAtom: () => queueMutationAtom,
|
|
redoAtom: () => redoAtom,
|
|
redoCountAtom: () => redoCountAtom,
|
|
registerAction: () => registerAction,
|
|
registerNodeType: () => registerNodeType,
|
|
registerNodeTypes: () => registerNodeTypes,
|
|
registerPlugin: () => registerPlugin,
|
|
removeEdgeWithAnimationAtom: () => removeEdgeWithAnimationAtom,
|
|
removeFromGroupAtom: () => removeFromGroupAtom,
|
|
removeGestureRuleAtom: () => removeGestureRuleAtom,
|
|
removeNodesFromSelectionAtom: () => removeNodesFromSelectionAtom,
|
|
resetGestureRulesAtom: () => resetGestureRulesAtom,
|
|
resetInputModeAtom: () => resetInputModeAtom,
|
|
resetKeyboardInteractionModeAtom: () => resetKeyboardInteractionModeAtom,
|
|
resetSettingsAtom: () => resetSettingsAtom,
|
|
resetViewportAtom: () => resetViewportAtom,
|
|
resolveGesture: () => resolveGesture,
|
|
resolveGestureIndexed: () => resolveGestureIndexed,
|
|
saveAsPresetAtom: () => saveAsPresetAtom,
|
|
screenToWorldAtom: () => screenToWorldAtom,
|
|
searchEdgeResultCountAtom: () => searchEdgeResultCountAtom,
|
|
searchEdgeResultsAtom: () => searchEdgeResultsAtom,
|
|
searchQueryAtom: () => searchQueryAtom,
|
|
searchResultCountAtom: () => searchResultCountAtom,
|
|
searchResultsArrayAtom: () => searchResultsArrayAtom,
|
|
searchResultsAtom: () => searchResultsAtom,
|
|
searchTotalResultCountAtom: () => searchTotalResultCountAtom,
|
|
selectEdgeAtom: () => selectEdgeAtom,
|
|
selectSingleNodeAtom: () => selectSingleNodeAtom,
|
|
selectedEdgeIdAtom: () => selectedEdgeIdAtom,
|
|
selectedNodeIdsAtom: () => selectedNodeIdsAtom,
|
|
selectedNodesCountAtom: () => selectedNodesCountAtom,
|
|
selectionPathAtom: () => selectionPathAtom,
|
|
selectionRectAtom: () => selectionRectAtom,
|
|
setEventMappingAtom: () => setEventMappingAtom,
|
|
setFocusedNodeAtom: () => setFocusedNodeAtom,
|
|
setGridSizeAtom: () => setGridSizeAtom,
|
|
setKeyboardInteractionModeAtom: () => setKeyboardInteractionModeAtom,
|
|
setNodeParentAtom: () => setNodeParentAtom,
|
|
setOnlineStatusAtom: () => setOnlineStatusAtom,
|
|
setPanelOpenAtom: () => setPanelOpenAtom,
|
|
setPerfEnabled: () => setPerfEnabled,
|
|
setSearchQueryAtom: () => setSearchQueryAtom,
|
|
setVirtualizationEnabledAtom: () => setVirtualizationEnabledAtom,
|
|
setZoomAtom: () => setZoomAtom,
|
|
showToastAtom: () => showToastAtom,
|
|
snapAlignmentEnabledAtom: () => snapAlignmentEnabledAtom,
|
|
snapEnabledAtom: () => snapEnabledAtom,
|
|
snapGridSizeAtom: () => snapGridSizeAtom,
|
|
snapTemporaryDisableAtom: () => snapTemporaryDisableAtom,
|
|
snapToGrid: () => snapToGrid,
|
|
spatialIndexAtom: () => spatialIndexAtom,
|
|
splitNodeAtom: () => splitNodeAtom,
|
|
startMutationAtom: () => startMutationAtom,
|
|
startNodeDragAtom: () => startNodeDragAtom,
|
|
startPickNodeAtom: () => startPickNodeAtom,
|
|
startPickNodesAtom: () => startPickNodesAtom,
|
|
startPickPointAtom: () => startPickPointAtom,
|
|
startSelectionAtom: () => startSelectionAtom,
|
|
swapEdgeAtomicAtom: () => swapEdgeAtomicAtom,
|
|
syncStateAtom: () => syncStateAtom,
|
|
syncStatusAtom: () => syncStatusAtom,
|
|
toggleAlignmentGuidesAtom: () => toggleAlignmentGuidesAtom,
|
|
toggleGroupCollapseAtom: () => toggleGroupCollapseAtom,
|
|
toggleNodeInSelectionAtom: () => toggleNodeInSelectionAtom,
|
|
togglePanelAtom: () => togglePanelAtom,
|
|
toggleSnapAtom: () => toggleSnapAtom,
|
|
toggleVirtualizationAtom: () => toggleVirtualizationAtom,
|
|
trackMutationErrorAtom: () => trackMutationErrorAtom,
|
|
uiNodesAtom: () => uiNodesAtom,
|
|
undoAtom: () => undoAtom,
|
|
undoCountAtom: () => undoCountAtom,
|
|
ungroupNodesAtom: () => ungroupNodesAtom,
|
|
unlockNodeAtom: () => unlockNodeAtom,
|
|
unregisterAction: () => unregisterAction,
|
|
unregisterNodeType: () => unregisterNodeType,
|
|
unregisterPlugin: () => unregisterPlugin,
|
|
updateEdgeLabelAtom: () => updateEdgeLabelAtom,
|
|
updateGestureRuleAtom: () => updateGestureRuleAtom,
|
|
updateInteractionFeedbackAtom: () => updateInteractionFeedbackAtom,
|
|
updateNodePositionAtom: () => updateNodePositionAtom,
|
|
updatePresetAtom: () => updatePresetAtom,
|
|
updateSelectionAtom: () => updateSelectionAtom,
|
|
validateSnapshot: () => validateSnapshot,
|
|
viewportRectAtom: () => viewportRectAtom,
|
|
virtualizationEnabledAtom: () => virtualizationEnabledAtom,
|
|
virtualizationMetricsAtom: () => virtualizationMetricsAtom,
|
|
visibleBoundsAtom: () => visibleBoundsAtom,
|
|
visibleEdgeKeysAtom: () => visibleEdgeKeysAtom,
|
|
visibleNodeKeysAtom: () => visibleNodeKeysAtom,
|
|
watchExternalKeyboardAtom: () => watchExternalKeyboardAtom,
|
|
watchReducedMotionAtom: () => watchReducedMotionAtom,
|
|
worldToScreenAtom: () => worldToScreenAtom,
|
|
zoomAnimationTargetAtom: () => zoomAnimationTargetAtom,
|
|
zoomAtom: () => zoomAtom,
|
|
zoomFocusNodeIdAtom: () => zoomFocusNodeIdAtom,
|
|
zoomTransitionProgressAtom: () => zoomTransitionProgressAtom
|
|
});
|
|
var init_core = __esm({
|
|
"src/core/index.ts"() {
|
|
"use strict";
|
|
init_types();
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_graph_mutations();
|
|
init_viewport_store();
|
|
init_selection_store();
|
|
init_sync_store();
|
|
init_interaction_store();
|
|
init_locked_node_store();
|
|
init_node_type_registry();
|
|
init_history_store();
|
|
init_toast_store();
|
|
init_snap_store();
|
|
init_settings_types();
|
|
init_action_registry();
|
|
init_action_executor();
|
|
init_settings_store();
|
|
init_canvas_api();
|
|
init_virtualization_store();
|
|
init_port_types();
|
|
init_clipboard_store();
|
|
init_input_classifier();
|
|
init_input_store();
|
|
init_selection_path_store();
|
|
init_group_store();
|
|
init_search_store();
|
|
init_gesture_resolver();
|
|
init_gesture_rules();
|
|
init_gesture_rule_store();
|
|
init_reduced_motion_store();
|
|
init_external_keyboard_store();
|
|
init_perf();
|
|
init_spatial_index();
|
|
init_plugin_types();
|
|
init_plugin_registry();
|
|
init_canvas_serializer();
|
|
}
|
|
});
|
|
|
|
// src/commands/index.ts
|
|
var index_exports = {};
|
|
__export(index_exports, {
|
|
CommandProvider: () => CommandProvider,
|
|
DEFAULT_SHORTCUTS: () => DEFAULT_SHORTCUTS,
|
|
cancelCommand: () => cancelCommand,
|
|
clearCommandErrorAtom: () => clearCommandErrorAtom,
|
|
clearSelectionCommand: () => clearSelectionCommand,
|
|
closeCommandLineAtom: () => closeCommandLineAtom,
|
|
collectInput: () => collectInput,
|
|
commandFeedbackAtom: () => commandFeedbackAtom,
|
|
commandHistoryAtom: () => commandHistoryAtom,
|
|
commandLineStateAtom: () => commandLineStateAtom,
|
|
commandLineVisibleAtom: () => commandLineVisibleAtom,
|
|
commandProgressAtom: () => commandProgressAtom,
|
|
commandRegistry: () => commandRegistry,
|
|
copyCommand: () => copyCommand,
|
|
currentInputAtom: () => currentInputAtom,
|
|
cutCommand: () => cutCommand,
|
|
deleteSelectedCommand: () => deleteSelectedCommand,
|
|
duplicateCommand: () => duplicateCommand,
|
|
executeCommandInteractive: () => executeCommandInteractive,
|
|
fitSelectionCommand: () => fitSelectionCommand,
|
|
fitToViewCommand: () => fitToViewCommand,
|
|
forceLayoutCommand: () => forceLayoutCommand,
|
|
goBackInputAtom: () => goBackInputAtom,
|
|
handlePickedNode: () => handlePickedNode,
|
|
handlePickedPoint: () => handlePickedPoint,
|
|
invertSelectionCommand: () => invertSelectionCommand,
|
|
isCommandActiveAtom: () => isCommandActiveAtom,
|
|
openCommandLineAtom: () => openCommandLineAtom,
|
|
pasteCommand: () => pasteCommand,
|
|
redoCommand: () => redoCommand,
|
|
registerBuiltinCommands: () => registerBuiltinCommands,
|
|
registerClipboardCommands: () => registerClipboardCommands,
|
|
registerCommand: () => registerCommand,
|
|
registerHistoryCommands: () => registerHistoryCommands,
|
|
registerLayoutCommands: () => registerLayoutCommands,
|
|
registerSelectionCommands: () => registerSelectionCommands,
|
|
registerViewportCommands: () => registerViewportCommands,
|
|
resetViewportCommand: () => resetViewportCommand,
|
|
selectAllCommand: () => selectAllCommand,
|
|
selectCommandAtom: () => selectCommandAtom,
|
|
selectedSuggestionIndexAtom: () => selectedSuggestionIndexAtom,
|
|
setCommandErrorAtom: () => setCommandErrorAtom,
|
|
skipInputAtom: () => skipInputAtom,
|
|
undoCommand: () => undoCommand,
|
|
updateSearchQueryAtom: () => updateSearchQueryAtom,
|
|
useCommandContext: () => useCommandContext,
|
|
useExecuteCommand: () => useExecuteCommand,
|
|
useGlobalKeyboard: () => useGlobalKeyboard,
|
|
useKeyState: () => useKeyState,
|
|
zoomInCommand: () => zoomInCommand,
|
|
zoomOutCommand: () => zoomOutCommand
|
|
});
|
|
module.exports = __toCommonJS(index_exports);
|
|
init_registry();
|
|
|
|
// src/commands/store.ts
|
|
var import_jotai2 = require("jotai");
|
|
init_registry();
|
|
|
|
// src/commands/store-atoms.ts
|
|
var import_jotai = require("jotai");
|
|
var import_utils = require("jotai/utils");
|
|
var inputModeAtom = (0, import_jotai.atom)({
|
|
type: "normal"
|
|
});
|
|
var commandLineVisibleAtom = (0, import_jotai.atom)(false);
|
|
var commandLineStateAtom = (0, import_jotai.atom)({
|
|
phase: "idle"
|
|
});
|
|
var commandFeedbackAtom = (0, import_jotai.atom)(null);
|
|
var commandHistoryAtom = (0, import_utils.atomWithStorage)("canvas-command-history", []);
|
|
var selectedSuggestionIndexAtom = (0, import_jotai.atom)(0);
|
|
var pendingInputResolverAtom = (0, import_jotai.atom)(null);
|
|
var isCommandActiveAtom = (0, import_jotai.atom)((get) => {
|
|
const state = get(commandLineStateAtom);
|
|
return state.phase === "collecting" || state.phase === "executing";
|
|
});
|
|
var currentInputAtom = (0, import_jotai.atom)((get) => {
|
|
const state = get(commandLineStateAtom);
|
|
if (state.phase !== "collecting") return null;
|
|
return state.command.inputs[state.inputIndex];
|
|
});
|
|
var commandProgressAtom = (0, import_jotai.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_jotai2.atom)(null, (get, set) => {
|
|
set(commandLineVisibleAtom, true);
|
|
set(commandLineStateAtom, {
|
|
phase: "searching",
|
|
query: "",
|
|
suggestions: commandRegistry.all()
|
|
});
|
|
set(selectedSuggestionIndexAtom, 0);
|
|
});
|
|
var closeCommandLineAtom = (0, import_jotai2.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_jotai2.atom)(null, (get, set, query) => {
|
|
const suggestions = commandRegistry.search(query);
|
|
set(commandLineStateAtom, {
|
|
phase: "searching",
|
|
query,
|
|
suggestions
|
|
});
|
|
set(selectedSuggestionIndexAtom, 0);
|
|
});
|
|
var selectCommandAtom = (0, import_jotai2.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_jotai2.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_jotai2.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_jotai2.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_jotai2.atom)(null, (get, set, message) => {
|
|
set(commandLineStateAtom, {
|
|
phase: "error",
|
|
message
|
|
});
|
|
set(inputModeAtom, {
|
|
type: "normal"
|
|
});
|
|
});
|
|
var clearCommandErrorAtom = (0, import_jotai2.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/commands/keyboard.ts
|
|
var DEFAULT_SHORTCUTS = {
|
|
openCommandLine: "/",
|
|
closeCommandLine: "Escape",
|
|
clearSelection: "Escape",
|
|
copy: "ctrl+c",
|
|
cut: "ctrl+x",
|
|
paste: "ctrl+v",
|
|
duplicate: "ctrl+d",
|
|
selectAll: "ctrl+a",
|
|
delete: "Delete",
|
|
search: "ctrl+f",
|
|
nextSearchResult: "Enter",
|
|
prevSearchResult: "shift+Enter",
|
|
nextSearchResultAlt: "ctrl+g",
|
|
prevSearchResultAlt: "ctrl+shift+g",
|
|
mergeNodes: "ctrl+m"
|
|
};
|
|
function useGlobalKeyboard(_options) {
|
|
}
|
|
function useKeyState(_key) {
|
|
return false;
|
|
}
|
|
|
|
// src/commands/executor.ts
|
|
function collectInput(_get, _set, _inputDef, _collected) {
|
|
return Promise.reject(new Error("Interactive input collection is not yet implemented. Pre-fill all inputs via initialInputs."));
|
|
}
|
|
async function executeCommandInteractive(get, set, command, initialInputs) {
|
|
const collected = {
|
|
...initialInputs
|
|
};
|
|
for (let i = 0; i < command.inputs.length; i++) {
|
|
const inputDef = command.inputs[i];
|
|
if (collected[inputDef.name] !== void 0) {
|
|
continue;
|
|
}
|
|
if (inputDef.required === false && inputDef.default !== void 0) {
|
|
collected[inputDef.name] = inputDef.default;
|
|
continue;
|
|
}
|
|
set(commandLineStateAtom, {
|
|
phase: "collecting",
|
|
command,
|
|
inputIndex: i,
|
|
collected
|
|
});
|
|
const value = await collectInput(get, set, inputDef, collected);
|
|
collected[inputDef.name] = value;
|
|
}
|
|
set(commandLineStateAtom, {
|
|
phase: "executing",
|
|
command
|
|
});
|
|
}
|
|
function handlePickedPoint(set, point) {
|
|
set(provideInputAtom, point);
|
|
}
|
|
function handlePickedNode(set, node) {
|
|
set(provideInputAtom, node);
|
|
}
|
|
function cancelCommand(set) {
|
|
set(closeCommandLineAtom);
|
|
}
|
|
|
|
// src/commands/CommandProvider.tsx
|
|
var import_compiler_runtime2 = require("react/compiler-runtime");
|
|
var import_react5 = __toESM(require("react"));
|
|
var import_jotai17 = require("jotai");
|
|
var import_jotai18 = require("jotai");
|
|
init_graph_store();
|
|
init_selection_store();
|
|
init_viewport_store();
|
|
init_history_store();
|
|
init_registry();
|
|
|
|
// src/hooks/useLayout.ts
|
|
var import_compiler_runtime = require("react/compiler-runtime");
|
|
var import_jotai11 = require("jotai");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_viewport_store();
|
|
init_selection_store();
|
|
init_layout();
|
|
init_layout();
|
|
var useFitToBounds = () => {
|
|
const $ = (0, import_compiler_runtime.c)(2);
|
|
const setFitToBounds = (0, import_jotai11.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;
|
|
};
|
|
|
|
// src/hooks/useForceLayout.ts
|
|
var d3 = __toESM(require("d3-force"));
|
|
var import_jotai12 = require("jotai");
|
|
var import_react = require("react");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_graph_derived();
|
|
init_layout();
|
|
init_debug();
|
|
var debug5 = createDebug("force-layout");
|
|
var useForceLayout = (options = {}) => {
|
|
const {
|
|
onPositionsChanged,
|
|
maxIterations = 1e3,
|
|
chargeStrength = -6e3,
|
|
linkStrength = 0.03
|
|
} = options;
|
|
const nodes = (0, import_jotai12.useAtomValue)(uiNodesAtom);
|
|
const graph = (0, import_jotai12.useAtomValue)(graphAtom);
|
|
const updateNodePosition = (0, import_jotai12.useSetAtom)(updateNodePositionAtom);
|
|
const isRunningRef = (0, import_react.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) {
|
|
debug5.warn("Layout already running, ignoring request.");
|
|
return;
|
|
}
|
|
if (nodes.length === 0) {
|
|
debug5.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();
|
|
debug5("Starting simulation...");
|
|
return new Promise((resolve) => {
|
|
let iterations = 0;
|
|
function runSimulationStep() {
|
|
if (iterations >= maxIterations) {
|
|
debug5("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) {
|
|
debug5("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) {
|
|
debug5("Saving %d positions via callback...", positionUpdates.length);
|
|
Promise.resolve(onPositionsChanged(positionUpdates)).then(() => debug5("Positions saved successfully.")).catch((err) => debug5.error("Error saving positions: %O", err));
|
|
}
|
|
debug5("Layout complete.");
|
|
isRunningRef.current = false;
|
|
resolve();
|
|
}
|
|
requestAnimationFrame(runSimulationStep);
|
|
});
|
|
};
|
|
return {
|
|
applyForceLayout,
|
|
isRunning: isRunningRef.current
|
|
};
|
|
};
|
|
|
|
// src/hooks/useTreeLayout.ts
|
|
var import_jotai15 = require("jotai");
|
|
var import_react3 = require("react");
|
|
init_graph_store();
|
|
init_graph_derived();
|
|
|
|
// src/hooks/useAnimatedLayout.ts
|
|
var import_jotai14 = require("jotai");
|
|
var import_react2 = require("react");
|
|
init_graph_store();
|
|
init_graph_position();
|
|
init_history_store();
|
|
init_debug();
|
|
init_reduced_motion_store();
|
|
var debug6 = createDebug("animated-layout");
|
|
function easeInOutCubic(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_jotai14.useAtomValue)(graphAtom);
|
|
const updateNodePosition = (0, import_jotai14.useSetAtom)(updateNodePositionAtom);
|
|
const pushHistory = (0, import_jotai14.useSetAtom)(pushHistoryAtom);
|
|
const setPositionCounter = (0, import_jotai14.useSetAtom)(nodePositionUpdateCounterAtom);
|
|
const reducedMotion = (0, import_jotai14.useAtomValue)(prefersReducedMotionAtom);
|
|
const isAnimatingRef = (0, import_react2.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) => debug6.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 = easeInOutCubic(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) => debug6.error("Position change callback failed: %O", err_0));
|
|
}
|
|
resolve();
|
|
}
|
|
}
|
|
requestAnimationFrame(tick);
|
|
});
|
|
};
|
|
return {
|
|
animate,
|
|
isAnimating: isAnimatingRef.current
|
|
};
|
|
}
|
|
|
|
// src/hooks/useTreeLayout.ts
|
|
function useTreeLayout(options = {}) {
|
|
const {
|
|
direction = "top-down",
|
|
levelGap = 200,
|
|
nodeGap = 100,
|
|
...animateOptions
|
|
} = options;
|
|
const graph = (0, import_jotai15.useAtomValue)(graphAtom);
|
|
const nodes = (0, import_jotai15.useAtomValue)(uiNodesAtom);
|
|
const {
|
|
animate,
|
|
isAnimating
|
|
} = useAnimatedLayout(animateOptions);
|
|
const isRunningRef = (0, import_react3.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_jotai16 = require("jotai");
|
|
var import_react4 = require("react");
|
|
init_graph_store();
|
|
init_graph_derived();
|
|
function useGridLayout(options = {}) {
|
|
const {
|
|
columns,
|
|
gap = 80,
|
|
...animateOptions
|
|
} = options;
|
|
const graph = (0, import_jotai16.useAtomValue)(graphAtom);
|
|
const nodes = (0, import_jotai16.useAtomValue)(uiNodesAtom);
|
|
const {
|
|
animate,
|
|
isAnimating
|
|
} = useAnimatedLayout(animateOptions);
|
|
const isRunningRef = (0, import_react4.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/commands/CommandProvider.tsx
|
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
var CommandContextContext = /* @__PURE__ */ (0, import_react5.createContext)(null);
|
|
function CommandProvider(t0) {
|
|
const $ = (0, import_compiler_runtime2.c)(52);
|
|
const {
|
|
children,
|
|
onCreateNode,
|
|
onUpdateNode,
|
|
onDeleteNode,
|
|
onCreateEdge,
|
|
onDeleteEdge,
|
|
onForceLayoutPersist
|
|
} = t0;
|
|
const store = (0, import_jotai18.useStore)();
|
|
const currentGraphId = (0, import_jotai17.useAtomValue)(currentGraphIdAtom);
|
|
const selectedNodeIds = (0, import_jotai17.useAtomValue)(selectedNodeIdsAtom);
|
|
const zoom = (0, import_jotai17.useAtomValue)(zoomAtom);
|
|
const pan = (0, import_jotai17.useAtomValue)(panAtom);
|
|
const undo = (0, import_jotai17.useSetAtom)(undoAtom);
|
|
const redo = (0, import_jotai17.useSetAtom)(redoAtom);
|
|
const {
|
|
fitToBounds
|
|
} = useFitToBounds();
|
|
let t1;
|
|
if ($[0] !== onForceLayoutPersist) {
|
|
t1 = onForceLayoutPersist ? async (updates) => {
|
|
await onForceLayoutPersist(updates.map(_temp));
|
|
} : void 0;
|
|
$[0] = onForceLayoutPersist;
|
|
$[1] = t1;
|
|
} else {
|
|
t1 = $[1];
|
|
}
|
|
const persistCallback = t1;
|
|
let t2;
|
|
if ($[2] !== persistCallback) {
|
|
t2 = {
|
|
onPositionsChanged: persistCallback
|
|
};
|
|
$[2] = persistCallback;
|
|
$[3] = t2;
|
|
} else {
|
|
t2 = $[3];
|
|
}
|
|
const {
|
|
applyForceLayout
|
|
} = useForceLayout(t2);
|
|
let t3;
|
|
if ($[4] !== persistCallback) {
|
|
t3 = {
|
|
onPositionsChanged: persistCallback
|
|
};
|
|
$[4] = persistCallback;
|
|
$[5] = t3;
|
|
} else {
|
|
t3 = $[5];
|
|
}
|
|
const {
|
|
applyLayout: applyTreeLayoutTopDown
|
|
} = useTreeLayout(t3);
|
|
let t4;
|
|
if ($[6] !== persistCallback) {
|
|
t4 = {
|
|
direction: "left-right",
|
|
onPositionsChanged: persistCallback
|
|
};
|
|
$[6] = persistCallback;
|
|
$[7] = t4;
|
|
} else {
|
|
t4 = $[7];
|
|
}
|
|
const {
|
|
applyLayout: applyTreeLayoutLeftRight
|
|
} = useTreeLayout(t4);
|
|
let t5;
|
|
if ($[8] !== persistCallback) {
|
|
t5 = {
|
|
onPositionsChanged: persistCallback
|
|
};
|
|
$[8] = persistCallback;
|
|
$[9] = t5;
|
|
} else {
|
|
t5 = $[9];
|
|
}
|
|
const {
|
|
applyLayout: applyGridLayoutDefault
|
|
} = useGridLayout(t5);
|
|
const closeCommandLine = (0, import_jotai17.useSetAtom)(closeCommandLineAtom);
|
|
const setCommandError = (0, import_jotai17.useSetAtom)(setCommandErrorAtom);
|
|
let t6;
|
|
if ($[10] !== applyForceLayout || $[11] !== applyGridLayoutDefault || $[12] !== applyTreeLayoutLeftRight || $[13] !== applyTreeLayoutTopDown || $[14] !== currentGraphId || $[15] !== fitToBounds || $[16] !== onCreateEdge || $[17] !== onCreateNode || $[18] !== onDeleteEdge || $[19] !== onDeleteNode || $[20] !== onUpdateNode || $[21] !== pan || $[22] !== redo || $[23] !== selectedNodeIds || $[24] !== store.get || $[25] !== store.set || $[26] !== undo || $[27] !== zoom) {
|
|
t6 = () => ({
|
|
get: store.get,
|
|
set: store.set,
|
|
currentGraphId,
|
|
selectedNodeIds,
|
|
viewport: {
|
|
zoom,
|
|
pan
|
|
},
|
|
mutations: {
|
|
createNode: async (payload) => {
|
|
if (!onCreateNode) {
|
|
throw new Error("onCreateNode callback not provided to CommandProvider");
|
|
}
|
|
return onCreateNode(payload);
|
|
},
|
|
updateNode: async (nodeId, updates_0) => {
|
|
if (!onUpdateNode) {
|
|
throw new Error("onUpdateNode callback not provided to CommandProvider");
|
|
}
|
|
return onUpdateNode(nodeId, updates_0);
|
|
},
|
|
deleteNode: async (nodeId_0) => {
|
|
if (!onDeleteNode) {
|
|
throw new Error("onDeleteNode callback not provided to CommandProvider");
|
|
}
|
|
return onDeleteNode(nodeId_0);
|
|
},
|
|
createEdge: async (payload_0) => {
|
|
if (!onCreateEdge) {
|
|
throw new Error("onCreateEdge callback not provided to CommandProvider");
|
|
}
|
|
return onCreateEdge(payload_0);
|
|
},
|
|
deleteEdge: async (edgeId) => {
|
|
if (!onDeleteEdge) {
|
|
throw new Error("onDeleteEdge callback not provided to CommandProvider");
|
|
}
|
|
return onDeleteEdge(edgeId);
|
|
}
|
|
},
|
|
layout: {
|
|
fitToBounds: (mode, padding) => {
|
|
const fitMode = mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection;
|
|
fitToBounds(fitMode, padding);
|
|
},
|
|
applyForceLayout,
|
|
applyTreeLayout: async (opts) => {
|
|
if (opts?.direction === "left-right") {
|
|
await applyTreeLayoutLeftRight();
|
|
} else {
|
|
await applyTreeLayoutTopDown();
|
|
}
|
|
},
|
|
applyGridLayout: async () => {
|
|
await applyGridLayoutDefault();
|
|
}
|
|
},
|
|
history: {
|
|
undo,
|
|
redo
|
|
}
|
|
});
|
|
$[10] = applyForceLayout;
|
|
$[11] = applyGridLayoutDefault;
|
|
$[12] = applyTreeLayoutLeftRight;
|
|
$[13] = applyTreeLayoutTopDown;
|
|
$[14] = currentGraphId;
|
|
$[15] = fitToBounds;
|
|
$[16] = onCreateEdge;
|
|
$[17] = onCreateNode;
|
|
$[18] = onDeleteEdge;
|
|
$[19] = onDeleteNode;
|
|
$[20] = onUpdateNode;
|
|
$[21] = pan;
|
|
$[22] = redo;
|
|
$[23] = selectedNodeIds;
|
|
$[24] = store.get;
|
|
$[25] = store.set;
|
|
$[26] = undo;
|
|
$[27] = zoom;
|
|
$[28] = t6;
|
|
} else {
|
|
t6 = $[28];
|
|
}
|
|
const getContext = t6;
|
|
let t7;
|
|
if ($[29] !== closeCommandLine || $[30] !== getContext || $[31] !== setCommandError) {
|
|
t7 = async (commandName, inputs) => {
|
|
const command = commandRegistry.get(commandName);
|
|
if (!command) {
|
|
throw new Error(`Unknown command: ${commandName}`);
|
|
}
|
|
for (const inputDef of command.inputs) {
|
|
if (inputDef.required !== false && inputs[inputDef.name] === void 0) {
|
|
throw new Error(`Missing required input: ${inputDef.name}`);
|
|
}
|
|
}
|
|
const ctx = getContext();
|
|
;
|
|
try {
|
|
await command.execute(inputs, ctx);
|
|
closeCommandLine();
|
|
} catch (t82) {
|
|
const error = t82;
|
|
const message = error instanceof Error ? error.message : "Command execution failed";
|
|
setCommandError(message);
|
|
throw error;
|
|
}
|
|
};
|
|
$[29] = closeCommandLine;
|
|
$[30] = getContext;
|
|
$[31] = setCommandError;
|
|
$[32] = t7;
|
|
} else {
|
|
t7 = $[32];
|
|
}
|
|
const executeCommand = t7;
|
|
const hasCommand = _temp2;
|
|
const [commandState, setCommandState] = (0, import_jotai17.useAtom)(commandLineStateAtom);
|
|
const isExecutingRef = (0, import_react5.useRef)(false);
|
|
let t8;
|
|
if ($[33] !== commandState || $[34] !== getContext || $[35] !== setCommandError || $[36] !== setCommandState || $[37] !== store) {
|
|
t8 = () => {
|
|
if (commandState.phase === "executing" && !isExecutingRef.current) {
|
|
const {
|
|
command: command_0
|
|
} = commandState;
|
|
if (command_0.inputs.length === 0) {
|
|
isExecutingRef.current = true;
|
|
const ctx_0 = getContext();
|
|
command_0.execute({}, ctx_0).then(() => {
|
|
setCommandState({
|
|
phase: "searching",
|
|
query: "",
|
|
suggestions: commandRegistry.all()
|
|
});
|
|
}).catch((error_0) => {
|
|
const message_0 = error_0 instanceof Error ? error_0.message : "Command execution failed";
|
|
setCommandError(message_0);
|
|
}).finally(() => {
|
|
isExecutingRef.current = false;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (commandState.phase !== "collecting") {
|
|
isExecutingRef.current = false;
|
|
return;
|
|
}
|
|
const {
|
|
command: command_1,
|
|
inputIndex,
|
|
collected
|
|
} = commandState;
|
|
const lastInputIndex = command_1.inputs.length - 1;
|
|
const lastInput = command_1.inputs[lastInputIndex];
|
|
if (inputIndex === lastInputIndex && collected[lastInput.name] !== void 0 && !isExecutingRef.current) {
|
|
isExecutingRef.current = true;
|
|
setCommandState({
|
|
phase: "executing",
|
|
command: command_1
|
|
});
|
|
const ctx_1 = getContext();
|
|
command_1.execute(collected, ctx_1).then(() => {
|
|
setCommandState({
|
|
phase: "searching",
|
|
query: "",
|
|
suggestions: commandRegistry.all()
|
|
});
|
|
store.set(inputModeAtom, {
|
|
type: "normal"
|
|
});
|
|
store.set(commandFeedbackAtom, null);
|
|
}).catch((error_1) => {
|
|
const message_1 = error_1 instanceof Error ? error_1.message : "Command execution failed";
|
|
setCommandError(message_1);
|
|
}).finally(() => {
|
|
isExecutingRef.current = false;
|
|
});
|
|
}
|
|
};
|
|
$[33] = commandState;
|
|
$[34] = getContext;
|
|
$[35] = setCommandError;
|
|
$[36] = setCommandState;
|
|
$[37] = store;
|
|
$[38] = t8;
|
|
} else {
|
|
t8 = $[38];
|
|
}
|
|
let t9;
|
|
if ($[39] !== closeCommandLine || $[40] !== commandState || $[41] !== getContext || $[42] !== setCommandError || $[43] !== setCommandState || $[44] !== store) {
|
|
t9 = [commandState, getContext, closeCommandLine, setCommandError, setCommandState, store];
|
|
$[39] = closeCommandLine;
|
|
$[40] = commandState;
|
|
$[41] = getContext;
|
|
$[42] = setCommandError;
|
|
$[43] = setCommandState;
|
|
$[44] = store;
|
|
$[45] = t9;
|
|
} else {
|
|
t9 = $[45];
|
|
}
|
|
(0, import_react5.useEffect)(t8, t9);
|
|
let t10;
|
|
if ($[46] !== executeCommand || $[47] !== getContext) {
|
|
t10 = {
|
|
executeCommand,
|
|
getContext,
|
|
hasCommand
|
|
};
|
|
$[46] = executeCommand;
|
|
$[47] = getContext;
|
|
$[48] = t10;
|
|
} else {
|
|
t10 = $[48];
|
|
}
|
|
const value = t10;
|
|
let t11;
|
|
if ($[49] !== children || $[50] !== value) {
|
|
t11 = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandContextContext, {
|
|
value,
|
|
children
|
|
});
|
|
$[49] = children;
|
|
$[50] = value;
|
|
$[51] = t11;
|
|
} else {
|
|
t11 = $[51];
|
|
}
|
|
return t11;
|
|
}
|
|
function _temp2(name) {
|
|
return commandRegistry.has(name);
|
|
}
|
|
function _temp(u) {
|
|
return {
|
|
nodeId: u.nodeId,
|
|
x: u.position.x,
|
|
y: u.position.y
|
|
};
|
|
}
|
|
function useCommandContext() {
|
|
const context = (0, import_react5.useContext)(CommandContextContext);
|
|
if (!context) {
|
|
throw new Error("useCommandContext must be used within a CommandProvider");
|
|
}
|
|
return context;
|
|
}
|
|
function useExecuteCommand() {
|
|
const {
|
|
executeCommand
|
|
} = useCommandContext();
|
|
return executeCommand;
|
|
}
|
|
|
|
// src/commands/builtins/viewport-commands.ts
|
|
init_registry();
|
|
var fitToViewCommand = {
|
|
name: "fitToView",
|
|
description: "Fit all nodes in the viewport",
|
|
aliases: ["fit", "fitAll"],
|
|
category: "viewport",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
ctx.layout.fitToBounds("graph");
|
|
}
|
|
};
|
|
var fitSelectionCommand = {
|
|
name: "fitSelection",
|
|
description: "Fit selected nodes in the viewport",
|
|
aliases: ["fitSel"],
|
|
category: "viewport",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
if (ctx.selectedNodeIds.size === 0) {
|
|
throw new Error("No nodes selected");
|
|
}
|
|
ctx.layout.fitToBounds("selection");
|
|
}
|
|
};
|
|
var resetViewportCommand = {
|
|
name: "resetViewport",
|
|
description: "Reset zoom and pan to default",
|
|
aliases: ["reset", "home"],
|
|
category: "viewport",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
resetViewportAtom: resetViewportAtom2
|
|
} = await Promise.resolve().then(() => (init_viewport_store(), viewport_store_exports));
|
|
ctx.set(resetViewportAtom2);
|
|
}
|
|
};
|
|
var zoomInCommand = {
|
|
name: "zoomIn",
|
|
description: "Zoom in on the canvas",
|
|
aliases: ["+", "in"],
|
|
category: "viewport",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
zoomAtom: zoomAtom2,
|
|
setZoomAtom: setZoomAtom2
|
|
} = await Promise.resolve().then(() => (init_viewport_store(), viewport_store_exports));
|
|
const currentZoom = ctx.get(zoomAtom2);
|
|
ctx.set(setZoomAtom2, {
|
|
zoom: Math.min(5, currentZoom * 1.25)
|
|
});
|
|
}
|
|
};
|
|
var zoomOutCommand = {
|
|
name: "zoomOut",
|
|
description: "Zoom out on the canvas",
|
|
aliases: ["-", "out"],
|
|
category: "viewport",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
zoomAtom: zoomAtom2,
|
|
setZoomAtom: setZoomAtom2
|
|
} = await Promise.resolve().then(() => (init_viewport_store(), viewport_store_exports));
|
|
const currentZoom = ctx.get(zoomAtom2);
|
|
ctx.set(setZoomAtom2, {
|
|
zoom: Math.max(0.1, currentZoom / 1.25)
|
|
});
|
|
}
|
|
};
|
|
function registerViewportCommands() {
|
|
registerCommand(fitToViewCommand);
|
|
registerCommand(fitSelectionCommand);
|
|
registerCommand(resetViewportCommand);
|
|
registerCommand(zoomInCommand);
|
|
registerCommand(zoomOutCommand);
|
|
}
|
|
|
|
// src/commands/builtins/selection-commands.ts
|
|
init_registry();
|
|
var selectAllCommand = {
|
|
name: "selectAll",
|
|
description: "Select all nodes in the graph",
|
|
aliases: ["all"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
nodeKeysAtom: nodeKeysAtom2,
|
|
selectedNodeIdsAtom: selectedNodeIdsAtom2
|
|
} = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
const nodeKeys = ctx.get(nodeKeysAtom2);
|
|
ctx.set(selectedNodeIdsAtom2, new Set(nodeKeys));
|
|
}
|
|
};
|
|
var clearSelectionCommand = {
|
|
name: "clearSelection",
|
|
description: "Clear all selection",
|
|
aliases: ["deselect", "clear"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
clearSelectionAtom: clearSelectionAtom2
|
|
} = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
ctx.set(clearSelectionAtom2);
|
|
}
|
|
};
|
|
var invertSelectionCommand = {
|
|
name: "invertSelection",
|
|
description: "Invert the current selection",
|
|
aliases: ["invert"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
nodeKeysAtom: nodeKeysAtom2,
|
|
selectedNodeIdsAtom: selectedNodeIdsAtom2
|
|
} = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
const allNodeKeys = ctx.get(nodeKeysAtom2);
|
|
const currentSelection = ctx.selectedNodeIds;
|
|
const invertedSelection = allNodeKeys.filter((id) => !currentSelection.has(id));
|
|
ctx.set(selectedNodeIdsAtom2, new Set(invertedSelection));
|
|
}
|
|
};
|
|
function registerSelectionCommands() {
|
|
registerCommand(selectAllCommand);
|
|
registerCommand(clearSelectionCommand);
|
|
registerCommand(invertSelectionCommand);
|
|
}
|
|
|
|
// src/commands/builtins/history-commands.ts
|
|
init_registry();
|
|
var undoCommand = {
|
|
name: "undo",
|
|
description: "Undo the last action",
|
|
aliases: ["z"],
|
|
category: "history",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
ctx.history.undo();
|
|
}
|
|
};
|
|
var redoCommand = {
|
|
name: "redo",
|
|
description: "Redo the last undone action",
|
|
aliases: ["y"],
|
|
category: "history",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
ctx.history.redo();
|
|
}
|
|
};
|
|
function registerHistoryCommands() {
|
|
registerCommand(undoCommand);
|
|
registerCommand(redoCommand);
|
|
}
|
|
|
|
// src/commands/builtins/layout-commands.ts
|
|
init_registry();
|
|
var forceLayoutCommand = {
|
|
name: "forceLayout",
|
|
description: "Apply force-directed layout to all nodes",
|
|
aliases: ["force", "autoLayout"],
|
|
category: "layout",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
await ctx.layout.applyForceLayout();
|
|
}
|
|
};
|
|
var treeLayoutCommand = {
|
|
name: "treeLayout",
|
|
description: "Arrange nodes in a hierarchical tree (top-down)",
|
|
aliases: ["tree"],
|
|
category: "layout",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
await ctx.layout.applyTreeLayout();
|
|
}
|
|
};
|
|
var gridLayoutCommand = {
|
|
name: "gridLayout",
|
|
description: "Arrange nodes in a uniform grid",
|
|
aliases: ["grid"],
|
|
category: "layout",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
await ctx.layout.applyGridLayout();
|
|
}
|
|
};
|
|
var horizontalLayoutCommand = {
|
|
name: "horizontalLayout",
|
|
description: "Arrange nodes in a horizontal tree (left-to-right)",
|
|
aliases: ["horizontal", "hLayout"],
|
|
category: "layout",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
await ctx.layout.applyTreeLayout({
|
|
direction: "left-right"
|
|
});
|
|
}
|
|
};
|
|
function registerLayoutCommands() {
|
|
registerCommand(forceLayoutCommand);
|
|
registerCommand(treeLayoutCommand);
|
|
registerCommand(gridLayoutCommand);
|
|
registerCommand(horizontalLayoutCommand);
|
|
}
|
|
|
|
// src/commands/builtins/clipboard-commands.ts
|
|
init_registry();
|
|
var copyCommand = {
|
|
name: "copy",
|
|
description: "Copy selected nodes to clipboard",
|
|
aliases: ["cp"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
copyToClipboardAtom: copyToClipboardAtom2,
|
|
hasClipboardContentAtom: hasClipboardContentAtom2
|
|
} = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports));
|
|
if (ctx.selectedNodeIds.size === 0) {
|
|
throw new Error("No nodes selected to copy");
|
|
}
|
|
ctx.set(copyToClipboardAtom2, Array.from(ctx.selectedNodeIds));
|
|
const hasContent = ctx.get(hasClipboardContentAtom2);
|
|
if (!hasContent) {
|
|
throw new Error("Failed to copy nodes");
|
|
}
|
|
}
|
|
};
|
|
var cutCommand = {
|
|
name: "cut",
|
|
description: "Cut selected nodes (copy to clipboard)",
|
|
aliases: ["x"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
copyToClipboardAtom: copyToClipboardAtom2,
|
|
hasClipboardContentAtom: hasClipboardContentAtom2
|
|
} = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports));
|
|
if (ctx.selectedNodeIds.size === 0) {
|
|
throw new Error("No nodes selected to cut");
|
|
}
|
|
ctx.set(copyToClipboardAtom2, Array.from(ctx.selectedNodeIds));
|
|
const hasContent = ctx.get(hasClipboardContentAtom2);
|
|
if (!hasContent) {
|
|
throw new Error("Failed to cut nodes");
|
|
}
|
|
for (const nodeId of ctx.selectedNodeIds) {
|
|
await ctx.mutations.deleteNode(nodeId);
|
|
}
|
|
}
|
|
};
|
|
var pasteCommand = {
|
|
name: "paste",
|
|
description: "Paste nodes from clipboard",
|
|
aliases: ["v"],
|
|
category: "selection",
|
|
inputs: [{
|
|
name: "position",
|
|
type: "point",
|
|
prompt: "Click to paste at position (or Enter for default offset)",
|
|
required: false
|
|
}],
|
|
execute: async (inputs, ctx) => {
|
|
const {
|
|
pasteFromClipboardAtom: pasteFromClipboardAtom2,
|
|
clipboardAtom: clipboardAtom2,
|
|
PASTE_OFFSET: PASTE_OFFSET2
|
|
} = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports));
|
|
const clipboard = ctx.get(clipboardAtom2);
|
|
if (!clipboard || clipboard.nodes.length === 0) {
|
|
throw new Error("Clipboard is empty");
|
|
}
|
|
let offset = PASTE_OFFSET2;
|
|
if (inputs.position) {
|
|
const pos = inputs.position;
|
|
offset = {
|
|
x: pos.x - clipboard.bounds.minX,
|
|
y: pos.y - clipboard.bounds.minY
|
|
};
|
|
}
|
|
const newNodeIds = ctx.set(pasteFromClipboardAtom2, offset);
|
|
if (!newNodeIds || newNodeIds.length === 0) {
|
|
throw new Error("Failed to paste nodes");
|
|
}
|
|
},
|
|
feedback: (collected, currentInput) => {
|
|
if (currentInput.name === "position") {
|
|
return {
|
|
crosshair: true
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
var duplicateCommand = {
|
|
name: "duplicate",
|
|
description: "Duplicate selected nodes",
|
|
aliases: ["d", "dup"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
duplicateSelectionAtom: duplicateSelectionAtom2
|
|
} = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports));
|
|
if (ctx.selectedNodeIds.size === 0) {
|
|
throw new Error("No nodes selected to duplicate");
|
|
}
|
|
const newNodeIds = ctx.set(duplicateSelectionAtom2);
|
|
if (!newNodeIds || newNodeIds.length === 0) {
|
|
throw new Error("Failed to duplicate nodes");
|
|
}
|
|
}
|
|
};
|
|
var deleteSelectedCommand = {
|
|
name: "deleteSelected",
|
|
description: "Delete selected nodes",
|
|
aliases: ["del", "delete", "remove"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
if (ctx.selectedNodeIds.size === 0) {
|
|
throw new Error("No nodes selected to delete");
|
|
}
|
|
const {
|
|
pushHistoryAtom: pushHistoryAtom2
|
|
} = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
ctx.set(pushHistoryAtom2, "Delete nodes");
|
|
for (const nodeId of ctx.selectedNodeIds) {
|
|
await ctx.mutations.deleteNode(nodeId);
|
|
}
|
|
}
|
|
};
|
|
function registerClipboardCommands() {
|
|
registerCommand(copyCommand);
|
|
registerCommand(cutCommand);
|
|
registerCommand(pasteCommand);
|
|
registerCommand(duplicateCommand);
|
|
registerCommand(deleteSelectedCommand);
|
|
}
|
|
|
|
// src/commands/builtins/group-commands.ts
|
|
init_registry();
|
|
var groupNodesCommand = {
|
|
name: "groupNodes",
|
|
description: "Group selected nodes into a container",
|
|
aliases: ["group"],
|
|
category: "nodes",
|
|
inputs: [{
|
|
name: "label",
|
|
type: "text",
|
|
prompt: "Group label:",
|
|
required: false,
|
|
default: "Group"
|
|
}],
|
|
execute: async (inputs, ctx) => {
|
|
if (ctx.selectedNodeIds.size < 2) return;
|
|
const {
|
|
addNodeToLocalGraphAtom: addNodeToLocalGraphAtom2
|
|
} = await Promise.resolve().then(() => (init_graph_mutations(), graph_mutations_exports));
|
|
const {
|
|
groupSelectedNodesAtom: groupSelectedNodesAtom2
|
|
} = await Promise.resolve().then(() => (init_group_store(), group_store_exports));
|
|
const label = inputs.label || "Group";
|
|
const groupNodeId = crypto.randomUUID();
|
|
ctx.set(addNodeToLocalGraphAtom2, {
|
|
id: groupNodeId,
|
|
graph_id: ctx.currentGraphId || "",
|
|
label,
|
|
node_type: "group",
|
|
configuration: null,
|
|
ui_properties: {
|
|
x: 0,
|
|
y: 0,
|
|
width: 500,
|
|
height: 500,
|
|
size: 15,
|
|
zIndex: 0
|
|
},
|
|
data: null,
|
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
});
|
|
ctx.set(groupSelectedNodesAtom2, {
|
|
nodeIds: Array.from(ctx.selectedNodeIds),
|
|
groupNodeId
|
|
});
|
|
}
|
|
};
|
|
var ungroupNodesCommand = {
|
|
name: "ungroupNodes",
|
|
description: "Remove grouping from a group node",
|
|
aliases: ["ungroup"],
|
|
category: "nodes",
|
|
inputs: [{
|
|
name: "groupNode",
|
|
type: "node",
|
|
prompt: "Pick a group node to ungroup:"
|
|
}],
|
|
execute: async (inputs, ctx) => {
|
|
const groupId = inputs.groupNode;
|
|
if (!groupId) return;
|
|
const {
|
|
ungroupNodesAtom: ungroupNodesAtom2
|
|
} = await Promise.resolve().then(() => (init_group_store(), group_store_exports));
|
|
ctx.set(ungroupNodesAtom2, groupId);
|
|
}
|
|
};
|
|
var collapseGroupCommand = {
|
|
name: "collapseGroup",
|
|
description: "Collapse a group node to hide its children",
|
|
aliases: ["collapse"],
|
|
category: "nodes",
|
|
inputs: [{
|
|
name: "groupNode",
|
|
type: "node",
|
|
prompt: "Pick a group node to collapse:"
|
|
}],
|
|
execute: async (inputs, ctx) => {
|
|
const groupId = inputs.groupNode;
|
|
if (!groupId) return;
|
|
const {
|
|
collapseGroupAtom: collapseGroupAtom2
|
|
} = await Promise.resolve().then(() => (init_group_store(), group_store_exports));
|
|
ctx.set(collapseGroupAtom2, groupId);
|
|
}
|
|
};
|
|
var expandGroupCommand = {
|
|
name: "expandGroup",
|
|
description: "Expand a collapsed group node",
|
|
aliases: ["expand"],
|
|
category: "nodes",
|
|
inputs: [{
|
|
name: "groupNode",
|
|
type: "node",
|
|
prompt: "Pick a group node to expand:"
|
|
}],
|
|
execute: async (inputs, ctx) => {
|
|
const groupId = inputs.groupNode;
|
|
if (!groupId) return;
|
|
const {
|
|
expandGroupAtom: expandGroupAtom2
|
|
} = await Promise.resolve().then(() => (init_group_store(), group_store_exports));
|
|
ctx.set(expandGroupAtom2, groupId);
|
|
}
|
|
};
|
|
function registerGroupCommands() {
|
|
registerCommand(groupNodesCommand);
|
|
registerCommand(ungroupNodesCommand);
|
|
registerCommand(collapseGroupCommand);
|
|
registerCommand(expandGroupCommand);
|
|
}
|
|
|
|
// src/commands/builtins/search-commands.ts
|
|
init_registry();
|
|
var searchNodesCommand = {
|
|
name: "searchNodes",
|
|
description: "Search nodes by label, type, or ID",
|
|
aliases: ["find", "search"],
|
|
category: "selection",
|
|
inputs: [{
|
|
name: "query",
|
|
type: "text",
|
|
prompt: "Search:",
|
|
required: true
|
|
}],
|
|
execute: async (inputs, ctx) => {
|
|
const query = inputs.query;
|
|
if (!query) return;
|
|
const {
|
|
setSearchQueryAtom: setSearchQueryAtom2
|
|
} = await Promise.resolve().then(() => (init_search_store(), search_store_exports));
|
|
ctx.set(setSearchQueryAtom2, query);
|
|
}
|
|
};
|
|
var clearSearchCommand = {
|
|
name: "clearSearch",
|
|
description: "Clear the active search filter",
|
|
aliases: ["clearsearch"],
|
|
category: "selection",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
clearSearchAtom: clearSearchAtom2
|
|
} = await Promise.resolve().then(() => (init_search_store(), search_store_exports));
|
|
ctx.set(clearSearchAtom2);
|
|
}
|
|
};
|
|
function registerSearchCommands() {
|
|
registerCommand(searchNodesCommand);
|
|
registerCommand(clearSearchCommand);
|
|
}
|
|
|
|
// src/commands/builtins/merge-commands.ts
|
|
init_registry();
|
|
var mergeNodesCommand = {
|
|
name: "mergeNodes",
|
|
description: "Merge selected nodes into one (first selected survives)",
|
|
aliases: ["merge"],
|
|
category: "nodes",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
if (ctx.selectedNodeIds.size < 2) return;
|
|
const {
|
|
mergeNodesAtom: mergeNodesAtom2
|
|
} = await Promise.resolve().then(() => (init_graph_mutations(), graph_mutations_exports));
|
|
const {
|
|
clearSelectionAtom: clearSelectionAtom2,
|
|
addNodesToSelectionAtom: addNodesToSelectionAtom2
|
|
} = await Promise.resolve().then(() => (init_selection_store(), selection_store_exports));
|
|
const nodeIds = Array.from(ctx.selectedNodeIds);
|
|
ctx.set(mergeNodesAtom2, {
|
|
nodeIds
|
|
});
|
|
ctx.set(clearSelectionAtom2);
|
|
ctx.set(addNodesToSelectionAtom2, [nodeIds[0]]);
|
|
}
|
|
};
|
|
function registerMergeCommands() {
|
|
registerCommand(mergeNodesCommand);
|
|
}
|
|
|
|
// src/commands/builtins/serialization-commands.ts
|
|
init_registry();
|
|
var exportCanvasCommand = {
|
|
name: "exportCanvas",
|
|
description: "Export the current canvas to a JSON snapshot (copies to clipboard)",
|
|
aliases: ["export"],
|
|
category: "custom",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
exportGraph: exportGraph2
|
|
} = await Promise.resolve().then(() => (init_canvas_serializer(), canvas_serializer_exports));
|
|
const {
|
|
showToastAtom: showToastAtom2
|
|
} = await Promise.resolve().then(() => (init_toast_store(), toast_store_exports));
|
|
const {
|
|
graphAtom: graphAtom2
|
|
} = await Promise.resolve().then(() => (init_graph_store(), graph_store_exports));
|
|
const graph = ctx.get(graphAtom2);
|
|
const nodeCount = graph.order;
|
|
const edgeCount = graph.size;
|
|
if (nodeCount === 0) {
|
|
throw new Error("Canvas is empty \u2014 nothing to export");
|
|
}
|
|
const store = {
|
|
get: ctx.get,
|
|
set: ctx.set
|
|
};
|
|
const snapshot = exportGraph2(store);
|
|
const json = JSON.stringify(snapshot, null, 2);
|
|
await navigator.clipboard.writeText(json);
|
|
ctx.set(showToastAtom2, `Exported ${nodeCount} nodes, ${edgeCount} edges to clipboard`);
|
|
}
|
|
};
|
|
var importCanvasCommand = {
|
|
name: "importCanvas",
|
|
description: "Import a canvas from a JSON snapshot (reads from clipboard)",
|
|
aliases: ["import"],
|
|
category: "custom",
|
|
inputs: [],
|
|
execute: async (_inputs, ctx) => {
|
|
const {
|
|
importGraph: importGraph2,
|
|
validateSnapshot: validateSnapshot2
|
|
} = await Promise.resolve().then(() => (init_canvas_serializer(), canvas_serializer_exports));
|
|
const {
|
|
showToastAtom: showToastAtom2
|
|
} = await Promise.resolve().then(() => (init_toast_store(), toast_store_exports));
|
|
const {
|
|
pushHistoryAtom: pushHistoryAtom2
|
|
} = await Promise.resolve().then(() => (init_history_store(), history_store_exports));
|
|
let json;
|
|
try {
|
|
json = await navigator.clipboard.readText();
|
|
} catch {
|
|
throw new Error("Could not read clipboard \u2014 paste the snapshot JSON manually");
|
|
}
|
|
let data;
|
|
try {
|
|
data = JSON.parse(json);
|
|
} catch {
|
|
throw new Error("Clipboard does not contain valid JSON");
|
|
}
|
|
const result = validateSnapshot2(data);
|
|
if (!result.valid) {
|
|
throw new Error(`Invalid snapshot: ${result.errors[0]}`);
|
|
}
|
|
ctx.set(pushHistoryAtom2, "Import canvas");
|
|
const store = {
|
|
get: ctx.get,
|
|
set: ctx.set
|
|
};
|
|
const snapshot = data;
|
|
importGraph2(store, snapshot);
|
|
ctx.set(showToastAtom2, `Imported ${snapshot.nodes.length} nodes, ${snapshot.edges.length} edges`);
|
|
}
|
|
};
|
|
function registerSerializationCommands() {
|
|
registerCommand(exportCanvasCommand);
|
|
registerCommand(importCanvasCommand);
|
|
}
|
|
|
|
// src/commands/builtins/index.ts
|
|
function registerBuiltinCommands() {
|
|
registerViewportCommands();
|
|
registerSelectionCommands();
|
|
registerHistoryCommands();
|
|
registerLayoutCommands();
|
|
registerClipboardCommands();
|
|
registerGroupCommands();
|
|
registerSearchCommands();
|
|
registerMergeCommands();
|
|
registerSerializationCommands();
|
|
}
|
|
//# sourceMappingURL=index.js.map
|