project-graph-mcp 2.2.6 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +81 -0
- package/CHANGELOG.md +57 -0
- package/README.md +9 -4
- package/package.json +4 -13
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/src/compact/expand.js +1 -1
- package/src/core/graph-builder.js +2 -2
- package/src/core/parser.js +2 -2
- package/src/network/server.js +1 -2
- package/src/network/web-server.js +1 -1
- package/vendor/symbiote-node/CHANGELOG.md +31 -0
- package/vendor/symbiote-node/LICENSE +21 -0
- package/vendor/symbiote-node/README.md +206 -0
- package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
- package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
- package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
- package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
- package/vendor/symbiote-node/canvas/LODManager.js +88 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
- package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
- package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
- package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
- package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
- package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
- package/vendor/symbiote-node/core/Connection.js +45 -0
- package/vendor/symbiote-node/core/Editor.js +451 -0
- package/vendor/symbiote-node/core/Frame.js +31 -0
- package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
- package/vendor/symbiote-node/core/GraphText.js +210 -0
- package/vendor/symbiote-node/core/Node.js +143 -0
- package/vendor/symbiote-node/core/Portal.js +104 -0
- package/vendor/symbiote-node/core/Socket.js +185 -0
- package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
- package/vendor/symbiote-node/index.js +103 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
- package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
- package/vendor/symbiote-node/interactions/Drag.js +102 -0
- package/vendor/symbiote-node/interactions/Selector.js +132 -0
- package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
- package/vendor/symbiote-node/interactions/Zoom.js +140 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
- package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
- package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
- package/vendor/symbiote-node/layout/index.js +16 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
- package/vendor/symbiote-node/package.json +59 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
- package/vendor/symbiote-node/plugins/History.js +384 -0
- package/vendor/symbiote-node/plugins/Readonly.js +59 -0
- package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
- package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
- package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
- package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
- package/vendor/symbiote-node/shapes/PillShape.js +91 -0
- package/vendor/symbiote-node/shapes/RectShape.js +72 -0
- package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
- package/vendor/symbiote-node/shapes/index.js +53 -0
- package/vendor/symbiote-node/themes/Palette.js +32 -0
- package/vendor/symbiote-node/themes/Skin.js +113 -0
- package/vendor/symbiote-node/themes/Theme.js +84 -0
- package/vendor/symbiote-node/themes/carbon.js +137 -0
- package/vendor/symbiote-node/themes/dark.js +137 -0
- package/vendor/symbiote-node/themes/ebook.js +138 -0
- package/vendor/symbiote-node/themes/grey.js +137 -0
- package/vendor/symbiote-node/themes/light.js +137 -0
- package/vendor/symbiote-node/themes/neon.js +138 -0
- package/vendor/symbiote-node/themes/pcb.js +273 -0
- package/vendor/symbiote-node/themes/synthwave.js +137 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
- package/web/app.js +9 -5
- package/web/components/canvas-graph.js +1705 -0
- package/web/components/code-block.js +1 -1
- package/web/components/event-feed/CodeWidget.js +32 -0
- package/web/components/event-feed/EventWidget.js +97 -0
- package/web/components/event-feed/ListWidget.js +57 -0
- package/web/components/event-feed/MiniGraphWidget.js +159 -0
- package/web/components/follow-ribbon.js +134 -0
- package/web/dashboard.js +1 -1
- package/web/follow-controller.js +241 -0
- package/web/index.html +4 -0
- package/web/panels/ActionBoard/ActionBoard.js +1 -1
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
- package/web/panels/code-viewer.js +50 -15
- package/web/panels/dep-graph.js +2691 -7
- package/web/panels/file-tree.js +5 -2
- package/web/panels/live-monitor.js +75 -3
- package/web/style.css +39 -0
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/src/.contextignore +0 -22
- package/src/.project-graph-cache.json +0 -1
- package/src/compact/.project-graph-cache.json +0 -1
- package/web/.project-graph-cache.json +0 -1
- package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History — undo/redo action stack for NodeEditor
|
|
3
|
+
*
|
|
4
|
+
* Records editor actions (addNode, removeNode, move, connect, disconnect)
|
|
5
|
+
* and provides undo()/redo() with Ctrl+Z / Ctrl+Shift+Z keyboard bindings.
|
|
6
|
+
* Plugin pattern: attach to editor via history.listen(editor).
|
|
7
|
+
*
|
|
8
|
+
* @module symbiote-node/plugins/History
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} HistoryAction
|
|
13
|
+
* @property {'addNode'|'removeNode'|'moveNode'|'addConnection'|'removeConnection'|'addFrame'|'removeFrame'} type
|
|
14
|
+
* @property {object} data - Serialized action data for undo/redo
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MAX_STACK = 200;
|
|
18
|
+
|
|
19
|
+
export class History {
|
|
20
|
+
|
|
21
|
+
/** @type {HistoryAction[]} */
|
|
22
|
+
#undoStack = [];
|
|
23
|
+
|
|
24
|
+
/** @type {HistoryAction[]} */
|
|
25
|
+
#redoStack = [];
|
|
26
|
+
|
|
27
|
+
/** @type {import('../core/Editor.js').NodeEditor|null} */
|
|
28
|
+
#editor = null;
|
|
29
|
+
|
|
30
|
+
/** @type {function|null} */
|
|
31
|
+
#getCanvas = null;
|
|
32
|
+
|
|
33
|
+
/** @type {boolean} - prevent recording actions triggered by undo/redo itself */
|
|
34
|
+
#isApplying = false;
|
|
35
|
+
|
|
36
|
+
/** @type {function[]} - unsubscribe functions */
|
|
37
|
+
#unsubs = [];
|
|
38
|
+
|
|
39
|
+
/** @type {object} - injected class constructors */
|
|
40
|
+
#classes = {};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Attach history tracking to an editor
|
|
44
|
+
* @param {import('../core/Editor.js').NodeEditor} editor
|
|
45
|
+
* @param {object} [options]
|
|
46
|
+
* @param {function} [options.getCanvas] - returns canvas element for position tracking
|
|
47
|
+
* @param {object} [options.classes] - class constructors: { Node, Connection, Frame, Socket, Input, Output, InputControl }
|
|
48
|
+
*/
|
|
49
|
+
listen(editor, options = {}) {
|
|
50
|
+
this.#editor = editor;
|
|
51
|
+
this.#getCanvas = options.getCanvas || null;
|
|
52
|
+
this.#classes = options.classes || {};
|
|
53
|
+
|
|
54
|
+
// Track node additions
|
|
55
|
+
this.#unsubs.push(editor.on('nodecreated', (node) => {
|
|
56
|
+
if (this.#isApplying) return;
|
|
57
|
+
const canvas = this.#getCanvas?.();
|
|
58
|
+
const pos = canvas ? this.#getNodePosition(canvas, node.id) : [0, 0];
|
|
59
|
+
this.#push({
|
|
60
|
+
type: 'addNode',
|
|
61
|
+
data: { node: this.#serializeNode(node), position: pos },
|
|
62
|
+
});
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Track node removals
|
|
66
|
+
this.#unsubs.push(editor.on('noderemove', (node) => {
|
|
67
|
+
if (this.#isApplying) return;
|
|
68
|
+
const canvas = this.#getCanvas?.();
|
|
69
|
+
const pos = canvas ? this.#getNodePosition(canvas, node.id) : [0, 0];
|
|
70
|
+
// Capture connections that will be removed with this node
|
|
71
|
+
const conns = editor.getNodeConnections(node.id).map(c => this.#serializeConnection(c));
|
|
72
|
+
this.#push({
|
|
73
|
+
type: 'removeNode',
|
|
74
|
+
data: { node: this.#serializeNode(node), position: pos, connections: conns },
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// Track node moves
|
|
79
|
+
this.#unsubs.push(editor.on('nodepicked', (node) => {
|
|
80
|
+
if (this.#isApplying) return;
|
|
81
|
+
const canvas = this.#getCanvas?.();
|
|
82
|
+
if (!canvas) return;
|
|
83
|
+
const pos = this.#getNodePosition(canvas, node.id);
|
|
84
|
+
node._historyStartPos = pos;
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
this.#unsubs.push(editor.on('nodedragged', ({ id }) => {
|
|
88
|
+
if (this.#isApplying) return;
|
|
89
|
+
const node = editor.getNode(id);
|
|
90
|
+
if (!node?._historyStartPos) return;
|
|
91
|
+
const canvas = this.#getCanvas?.();
|
|
92
|
+
const endPos = canvas ? this.#getNodePosition(canvas, id) : [0, 0];
|
|
93
|
+
const startPos = node._historyStartPos;
|
|
94
|
+
// Only record if position actually changed
|
|
95
|
+
if (startPos[0] !== endPos[0] || startPos[1] !== endPos[1]) {
|
|
96
|
+
this.#push({
|
|
97
|
+
type: 'moveNode',
|
|
98
|
+
data: { nodeId: id, from: startPos, to: endPos },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
delete node._historyStartPos;
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// Track connections
|
|
105
|
+
this.#unsubs.push(editor.on('connectioncreated', (conn) => {
|
|
106
|
+
if (this.#isApplying) return;
|
|
107
|
+
this.#push({
|
|
108
|
+
type: 'addConnection',
|
|
109
|
+
data: { connection: this.#serializeConnection(conn) },
|
|
110
|
+
});
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
this.#unsubs.push(editor.on('connectionremove', (conn) => {
|
|
114
|
+
if (this.#isApplying) return;
|
|
115
|
+
this.#push({
|
|
116
|
+
type: 'removeConnection',
|
|
117
|
+
data: { connection: this.#serializeConnection(conn) },
|
|
118
|
+
});
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// Track frames
|
|
122
|
+
this.#unsubs.push(editor.on('framecreated', (frame) => {
|
|
123
|
+
if (this.#isApplying) return;
|
|
124
|
+
this.#push({
|
|
125
|
+
type: 'addFrame',
|
|
126
|
+
data: { frame: { ...frame } },
|
|
127
|
+
});
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
this.#unsubs.push(editor.on('frameremove', (frame) => {
|
|
131
|
+
if (this.#isApplying) return;
|
|
132
|
+
this.#push({
|
|
133
|
+
type: 'removeFrame',
|
|
134
|
+
data: { frame: { ...frame } },
|
|
135
|
+
});
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Bind Ctrl+Z / Ctrl+Shift+Z keyboard shortcuts
|
|
141
|
+
* @param {HTMLElement} target - element to listen for keydown
|
|
142
|
+
*/
|
|
143
|
+
bindKeyboard(target) {
|
|
144
|
+
const handler = (e) => {
|
|
145
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
if (e.shiftKey) this.redo();
|
|
148
|
+
else this.undo();
|
|
149
|
+
}
|
|
150
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
this.redo();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
target.addEventListener('keydown', handler);
|
|
156
|
+
this.#unsubs.push(() => target.removeEventListener('keydown', handler));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Undo last action */
|
|
160
|
+
undo() {
|
|
161
|
+
const action = this.#undoStack.pop();
|
|
162
|
+
if (!action) return;
|
|
163
|
+
this.#isApplying = true;
|
|
164
|
+
try {
|
|
165
|
+
this.#applyReverse(action);
|
|
166
|
+
this.#redoStack.push(action);
|
|
167
|
+
} finally {
|
|
168
|
+
this.#isApplying = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Redo last undone action */
|
|
173
|
+
redo() {
|
|
174
|
+
const action = this.#redoStack.pop();
|
|
175
|
+
if (!action) return;
|
|
176
|
+
this.#isApplying = true;
|
|
177
|
+
try {
|
|
178
|
+
this.#applyForward(action);
|
|
179
|
+
this.#undoStack.push(action);
|
|
180
|
+
} finally {
|
|
181
|
+
this.#isApplying = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** @returns {number} */
|
|
186
|
+
get undoCount() { return this.#undoStack.length; }
|
|
187
|
+
|
|
188
|
+
/** @returns {number} */
|
|
189
|
+
get redoCount() { return this.#redoStack.length; }
|
|
190
|
+
|
|
191
|
+
/** Clear all history */
|
|
192
|
+
clear() {
|
|
193
|
+
this.#undoStack = [];
|
|
194
|
+
this.#redoStack = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Destroy and unsubscribe */
|
|
198
|
+
destroy() {
|
|
199
|
+
for (const unsub of this.#unsubs) {
|
|
200
|
+
if (typeof unsub === 'function') unsub();
|
|
201
|
+
}
|
|
202
|
+
this.#unsubs = [];
|
|
203
|
+
this.clear();
|
|
204
|
+
this.#editor = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Private ---
|
|
208
|
+
|
|
209
|
+
#push(action) {
|
|
210
|
+
this.#undoStack.push(action);
|
|
211
|
+
if (this.#undoStack.length > MAX_STACK) this.#undoStack.shift();
|
|
212
|
+
this.#redoStack = []; // new action invalidates redo
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#applyReverse(action) {
|
|
216
|
+
const editor = this.#editor;
|
|
217
|
+
const canvas = this.#getCanvas?.();
|
|
218
|
+
if (!editor) return;
|
|
219
|
+
|
|
220
|
+
switch (action.type) {
|
|
221
|
+
case 'addNode':
|
|
222
|
+
editor.removeNode(action.data.node.id);
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'removeNode': {
|
|
226
|
+
const { node: nodeData, position, connections } = action.data;
|
|
227
|
+
const restoredNode = this.#deserializeNode(nodeData);
|
|
228
|
+
editor.addNode(restoredNode);
|
|
229
|
+
if (canvas && position) {
|
|
230
|
+
canvas.setNodePosition(restoredNode.id, position[0], position[1]);
|
|
231
|
+
}
|
|
232
|
+
// Restore connections
|
|
233
|
+
for (const connData of connections) {
|
|
234
|
+
const { Connection } = this.#classes;
|
|
235
|
+
const conn = new Connection(connData.from, connData.out, connData.to, connData.in);
|
|
236
|
+
conn.id = connData.id;
|
|
237
|
+
try { editor.addConnection(conn); } catch { /* node may not exist */ }
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case 'moveNode':
|
|
243
|
+
if (canvas) {
|
|
244
|
+
canvas.setNodePosition(action.data.nodeId, action.data.from[0], action.data.from[1]);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
case 'addConnection':
|
|
249
|
+
editor.removeConnection(action.data.connection.id);
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case 'removeConnection': {
|
|
253
|
+
const connData = action.data.connection;
|
|
254
|
+
const { Connection } = this.#classes;
|
|
255
|
+
const conn = new Connection(connData.from, connData.out, connData.to, connData.in);
|
|
256
|
+
conn.id = connData.id;
|
|
257
|
+
try { editor.addConnection(conn); } catch { /* already exists */ }
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case 'addFrame':
|
|
262
|
+
editor.removeFrame(action.data.frame.id);
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'removeFrame': {
|
|
266
|
+
const { Frame } = this.#classes;
|
|
267
|
+
const frame = new Frame(action.data.frame.label, action.data.frame);
|
|
268
|
+
frame.id = action.data.frame.id;
|
|
269
|
+
editor.addFrame(frame);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#applyForward(action) {
|
|
276
|
+
const editor = this.#editor;
|
|
277
|
+
const canvas = this.#getCanvas?.();
|
|
278
|
+
if (!editor) return;
|
|
279
|
+
|
|
280
|
+
switch (action.type) {
|
|
281
|
+
case 'addNode': {
|
|
282
|
+
const restoredNode = this.#deserializeNode(action.data.node);
|
|
283
|
+
editor.addNode(restoredNode);
|
|
284
|
+
if (canvas && action.data.position) {
|
|
285
|
+
canvas.setNodePosition(restoredNode.id, action.data.position[0], action.data.position[1]);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'removeNode':
|
|
291
|
+
editor.removeNode(action.data.node.id);
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case 'moveNode':
|
|
295
|
+
if (canvas) {
|
|
296
|
+
canvas.setNodePosition(action.data.nodeId, action.data.to[0], action.data.to[1]);
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
case 'addConnection': {
|
|
301
|
+
const connData = action.data.connection;
|
|
302
|
+
const { Connection } = this.#classes;
|
|
303
|
+
const conn = new Connection(connData.from, connData.out, connData.to, connData.in);
|
|
304
|
+
conn.id = connData.id;
|
|
305
|
+
try { editor.addConnection(conn); } catch { /* already exists */ }
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'removeConnection':
|
|
310
|
+
editor.removeConnection(action.data.connection.id);
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case 'addFrame': {
|
|
314
|
+
const { Frame } = this.#classes;
|
|
315
|
+
const frame = new Frame(action.data.frame.label, action.data.frame);
|
|
316
|
+
frame.id = action.data.frame.id;
|
|
317
|
+
editor.addFrame(frame);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
case 'removeFrame':
|
|
322
|
+
editor.removeFrame(action.data.frame.id);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#serializeNode(node) {
|
|
328
|
+
return {
|
|
329
|
+
id: node.id,
|
|
330
|
+
label: node.label,
|
|
331
|
+
type: node.type,
|
|
332
|
+
category: node.category,
|
|
333
|
+
shape: node.shape,
|
|
334
|
+
params: { ...node.params },
|
|
335
|
+
inputs: Object.fromEntries(Object.entries(node.inputs).map(([k, v]) => [k, {
|
|
336
|
+
socket: v.socket ? { type: v.socket.type, color: v.socket.color } : null,
|
|
337
|
+
label: v.label,
|
|
338
|
+
}])),
|
|
339
|
+
outputs: Object.fromEntries(Object.entries(node.outputs).map(([k, v]) => [k, {
|
|
340
|
+
socket: v.socket ? { type: v.socket.type, color: v.socket.color } : null,
|
|
341
|
+
label: v.label,
|
|
342
|
+
}])),
|
|
343
|
+
controls: Object.fromEntries(Object.entries(node.controls).map(([k, v]) => [k, {
|
|
344
|
+
label: v.label,
|
|
345
|
+
value: v.value,
|
|
346
|
+
type: v.type,
|
|
347
|
+
}])),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#serializeConnection(conn) {
|
|
352
|
+
return { id: conn.id, from: conn.from, out: conn.out, to: conn.to, in: conn.in };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#deserializeNode(data) {
|
|
356
|
+
const { Node, Socket, Input, Output, InputControl } = this.#classes;
|
|
357
|
+
const node = new Node(data.label, {
|
|
358
|
+
type: data.type,
|
|
359
|
+
category: data.category,
|
|
360
|
+
shape: data.shape,
|
|
361
|
+
});
|
|
362
|
+
node.id = data.id;
|
|
363
|
+
node.params = { ...data.params };
|
|
364
|
+
|
|
365
|
+
for (const [key, inp] of Object.entries(data.inputs)) {
|
|
366
|
+
const socket = inp.socket ? new Socket(inp.socket.type, { color: inp.socket.color }) : new Socket('any');
|
|
367
|
+
node.addInput(key, new Input(socket, inp.label));
|
|
368
|
+
}
|
|
369
|
+
for (const [key, out] of Object.entries(data.outputs)) {
|
|
370
|
+
const socket = out.socket ? new Socket(out.socket.type, { color: out.socket.color }) : new Socket('any');
|
|
371
|
+
node.addOutput(key, new Output(socket, out.label));
|
|
372
|
+
}
|
|
373
|
+
for (const [key, ctrl] of Object.entries(data.controls)) {
|
|
374
|
+
node.addControl(key, new InputControl(ctrl.type || 'text', { label: ctrl.label, initial: ctrl.value }));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return node;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#getNodePosition(canvas, nodeId) {
|
|
381
|
+
const positions = canvas.getPositions();
|
|
382
|
+
return positions[nodeId] || [0, 0];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readonly — toggle readonly mode for node editor
|
|
3
|
+
*
|
|
4
|
+
* When enabled, blocks node creation, deletion, connection
|
|
5
|
+
* creation/removal, and node dragging.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from Rete.js readonly plugin (63 LOC).
|
|
8
|
+
* @module symbiote-node/plugins/Readonly
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class Readonly {
|
|
12
|
+
|
|
13
|
+
/** @type {boolean} */
|
|
14
|
+
#enabled = false;
|
|
15
|
+
|
|
16
|
+
/** @type {import('../core/Editor.js').NodeEditor|null} */
|
|
17
|
+
#editor = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('../core/Editor.js').NodeEditor} editor
|
|
21
|
+
*/
|
|
22
|
+
constructor(editor) {
|
|
23
|
+
this.#editor = editor;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Enable readonly mode */
|
|
27
|
+
enable() {
|
|
28
|
+
this.#enabled = true;
|
|
29
|
+
this.#editor.emit('readonlychanged', true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Disable readonly mode */
|
|
33
|
+
disable() {
|
|
34
|
+
this.#enabled = false;
|
|
35
|
+
this.#editor.emit('readonlychanged', false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Toggle readonly mode */
|
|
39
|
+
toggle() {
|
|
40
|
+
this.#enabled ? this.disable() : this.enable();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether readonly is currently enabled
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
get isEnabled() {
|
|
48
|
+
return this.#enabled;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Guard check — throws if readonly
|
|
53
|
+
* Use before mutation operations
|
|
54
|
+
* @returns {boolean} true if operation should be blocked
|
|
55
|
+
*/
|
|
56
|
+
shouldBlock() {
|
|
57
|
+
return this.#enabled;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CircleShape — circular hub node
|
|
3
|
+
*
|
|
4
|
+
* Sockets distributed around the perimeter of a circle.
|
|
5
|
+
* Inputs on left semicircle, outputs on right semicircle.
|
|
6
|
+
*
|
|
7
|
+
* @module symbiote-node/shapes/CircleShape
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NodeShape } from './NodeShape.js';
|
|
11
|
+
|
|
12
|
+
export class CircleShape extends NodeShape {
|
|
13
|
+
name = 'circle';
|
|
14
|
+
|
|
15
|
+
getSocketPosition(side, index, total, { width, height }) {
|
|
16
|
+
const r = Math.min(width, height) / 2;
|
|
17
|
+
const cx = width / 2;
|
|
18
|
+
const cy = height / 2;
|
|
19
|
+
|
|
20
|
+
// Inputs: left semicircle (90° to 270°), top to bottom
|
|
21
|
+
// Outputs: right semicircle (-90° to 90°), top to bottom
|
|
22
|
+
const arcSpan = Math.PI * 0.8; // 144 degrees
|
|
23
|
+
const centerAngle = side === 'input' ? Math.PI : 0;
|
|
24
|
+
const startAngle = centerAngle - arcSpan / 2;
|
|
25
|
+
const step = total > 1 ? arcSpan / (total - 1) : 0;
|
|
26
|
+
const angle = startAngle + step * index;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
x: cx + r * Math.cos(angle),
|
|
30
|
+
y: cy + r * Math.sin(angle),
|
|
31
|
+
angle: (angle * 180) / Math.PI,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get pin position on a specific side of the circle.
|
|
37
|
+
* Pins follow an arc in the corresponding quadrant.
|
|
38
|
+
*
|
|
39
|
+
* @param {'top'|'right'|'bottom'|'left'} side
|
|
40
|
+
* @param {number} t - position along the side (0..1)
|
|
41
|
+
* @param {{ width: number, height: number }} size
|
|
42
|
+
* @returns {{ x: number, y: number, angle: number }}
|
|
43
|
+
*/
|
|
44
|
+
getSidePosition(side, t, size) {
|
|
45
|
+
const r = Math.min(size.width, size.height) / 2;
|
|
46
|
+
const cx = size.width / 2;
|
|
47
|
+
const cy = size.height / 2;
|
|
48
|
+
|
|
49
|
+
// Each side spans a 90° arc centered on the cardinal direction
|
|
50
|
+
const CENTERS = { right: 0, bottom: Math.PI / 2, left: Math.PI, top: -Math.PI / 2 };
|
|
51
|
+
const ARC = Math.PI * 0.8; // 144° arc to avoid exact corners
|
|
52
|
+
const MARGIN = 0.2;
|
|
53
|
+
const effectiveT = MARGIN + t * (1 - 2 * MARGIN);
|
|
54
|
+
|
|
55
|
+
const center = CENTERS[side];
|
|
56
|
+
const a = center - ARC / 2 + ARC * effectiveT;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
x: cx + r * Math.cos(a),
|
|
60
|
+
y: cy + r * Math.sin(a),
|
|
61
|
+
angle: (a * 180) / Math.PI,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getBorderRadius() {
|
|
66
|
+
return '50%';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get hasHeader() {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get hasControls() {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getMinSize() {
|
|
78
|
+
return { minWidth: 80, minHeight: 80 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommentShape — wide annotation banner node
|
|
3
|
+
*
|
|
4
|
+
* No sockets, no header, no controls.
|
|
5
|
+
* Just text content with semi-transparent background.
|
|
6
|
+
* Used for annotations and documentation on the canvas.
|
|
7
|
+
*
|
|
8
|
+
* @module symbiote-node/shapes/CommentShape
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NodeShape } from './NodeShape.js';
|
|
12
|
+
|
|
13
|
+
export class CommentShape extends NodeShape {
|
|
14
|
+
name = 'comment';
|
|
15
|
+
|
|
16
|
+
getSocketPosition() {
|
|
17
|
+
return { x: 0, y: 0, angle: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getBorderRadius() {
|
|
21
|
+
return 'var(--sn-comment-radius, 6px)';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get hasHeader() {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get hasControls() {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getMinSize() {
|
|
33
|
+
return { minWidth: 200, minHeight: 40 };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiamondShape — condition/decision rhombus node
|
|
3
|
+
*
|
|
4
|
+
* Input at top vertex, outputs distributed along bottom edges.
|
|
5
|
+
* Classic if/else, switch/case pattern.
|
|
6
|
+
*
|
|
7
|
+
* @module symbiote-node/shapes/DiamondShape
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { NodeShape } from './NodeShape.js';
|
|
11
|
+
|
|
12
|
+
export class DiamondShape extends NodeShape {
|
|
13
|
+
name = 'diamond';
|
|
14
|
+
|
|
15
|
+
getSocketPosition(side, index, total, { width, height }) {
|
|
16
|
+
const cx = width / 2;
|
|
17
|
+
const cy = height / 2;
|
|
18
|
+
|
|
19
|
+
if (side === 'input') {
|
|
20
|
+
// Inputs distributed along top edges
|
|
21
|
+
if (total === 1) {
|
|
22
|
+
return { x: cx, y: 0, angle: 270 };
|
|
23
|
+
}
|
|
24
|
+
// Multiple inputs: spread along top-left and top-right edges
|
|
25
|
+
const t = (index + 1) / (total + 1);
|
|
26
|
+
return {
|
|
27
|
+
x: cx * (1 - t),
|
|
28
|
+
y: cy * t,
|
|
29
|
+
angle: 225 + 90 * (index / (total - 1)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Outputs along bottom edges
|
|
34
|
+
if (total === 1) {
|
|
35
|
+
return { x: cx, y: height, angle: 90 };
|
|
36
|
+
}
|
|
37
|
+
if (total === 2) {
|
|
38
|
+
// True: bottom-left, False: bottom-right
|
|
39
|
+
return index === 0
|
|
40
|
+
? { x: cx * 0.35, y: cy + cy * 0.65, angle: 225 }
|
|
41
|
+
: { x: width - cx * 0.35, y: cy + cy * 0.65, angle: 315 };
|
|
42
|
+
}
|
|
43
|
+
// 3+ outputs: spread along bottom edges
|
|
44
|
+
const t = (index + 1) / (total + 1);
|
|
45
|
+
return {
|
|
46
|
+
x: t < 0.5 ? cx * (1 - 2 * t) : cx + cx * (2 * t - 1),
|
|
47
|
+
y: t < 0.5 ? cy + cy * 2 * t : cy + cy * (2 - 2 * t),
|
|
48
|
+
angle: 135 + 90 * (index / (total - 1)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get pin position on a specific side of the diamond.
|
|
54
|
+
* Each side maps to two diagonal edges meeting at that cardinal vertex.
|
|
55
|
+
*
|
|
56
|
+
* @param {'top'|'right'|'bottom'|'left'} side
|
|
57
|
+
* @param {number} t - position along the side (0..1)
|
|
58
|
+
* @param {{ width: number, height: number }} size
|
|
59
|
+
* @returns {{ x: number, y: number, angle: number }}
|
|
60
|
+
*/
|
|
61
|
+
getSidePosition(side, t, size) {
|
|
62
|
+
const cx = size.width / 2;
|
|
63
|
+
const cy = size.height / 2;
|
|
64
|
+
const NORMALS = { top: -90, right: 0, bottom: 90, left: 180 };
|
|
65
|
+
const MARGIN = 0.2;
|
|
66
|
+
const effectiveT = MARGIN + t * (1 - 2 * MARGIN);
|
|
67
|
+
|
|
68
|
+
// Diamond vertices: top(cx,0), right(w,cy), bottom(cx,h), left(0,cy)
|
|
69
|
+
// Each "side" spans two edges meeting at the cardinal vertex
|
|
70
|
+
const vertices = {
|
|
71
|
+
top: [{ x: 0, y: cy }, { x: cx, y: 0 }, { x: size.width, y: cy }],
|
|
72
|
+
right: [{ x: cx, y: 0 }, { x: size.width, y: cy }, { x: cx, y: size.height }],
|
|
73
|
+
bottom: [{ x: 0, y: cy }, { x: cx, y: size.height }, { x: size.width, y: cy }],
|
|
74
|
+
left: [{ x: cx, y: 0 }, { x: 0, y: cy }, { x: cx, y: size.height }],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const [p0, p1, p2] = vertices[side];
|
|
78
|
+
|
|
79
|
+
// effectiveT spans from p0 → p1 → p2 (two edges)
|
|
80
|
+
let x, y;
|
|
81
|
+
if (effectiveT <= 0.5) {
|
|
82
|
+
const segT = effectiveT * 2;
|
|
83
|
+
x = p0.x + (p1.x - p0.x) * segT;
|
|
84
|
+
y = p0.y + (p1.y - p0.y) * segT;
|
|
85
|
+
} else {
|
|
86
|
+
const segT = (effectiveT - 0.5) * 2;
|
|
87
|
+
x = p1.x + (p2.x - p1.x) * segT;
|
|
88
|
+
y = p1.y + (p2.y - p1.y) * segT;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { x, y, angle: NORMALS[side] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getBorderRadius() {
|
|
95
|
+
return '0';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getClipPath({ width, height }) {
|
|
99
|
+
const cx = width / 2;
|
|
100
|
+
const cy = height / 2;
|
|
101
|
+
return `polygon(${cx}px 0, ${width}px ${cy}px, ${cx}px ${height}px, 0 ${cy}px)`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get hasHeader() {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get hasControls() {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getMinSize() {
|
|
113
|
+
return { minWidth: 100, minHeight: 100 };
|
|
114
|
+
}
|
|
115
|
+
}
|