html-overlay-node 0.1.0
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/LICENSE +21 -0
- package/dist/example.json +522 -0
- package/dist/html-overlay-node.es.js +3596 -0
- package/dist/html-overlay-node.es.js.map +1 -0
- package/dist/html-overlay-node.umd.js +2 -0
- package/dist/html-overlay-node.umd.js.map +1 -0
- package/index.css +232 -0
- package/package.json +65 -0
- package/readme.md +437 -0
- package/src/core/CommandStack.js +26 -0
- package/src/core/Edge.js +28 -0
- package/src/core/Edge.test.js +73 -0
- package/src/core/Graph.js +267 -0
- package/src/core/Graph.test.js +256 -0
- package/src/core/Group.js +77 -0
- package/src/core/Hooks.js +12 -0
- package/src/core/Hooks.test.js +108 -0
- package/src/core/Node.js +70 -0
- package/src/core/Node.test.js +113 -0
- package/src/core/Registry.js +71 -0
- package/src/core/Registry.test.js +88 -0
- package/src/core/Runner.js +211 -0
- package/src/core/commands.js +125 -0
- package/src/groups/GroupManager.js +116 -0
- package/src/index.js +1030 -0
- package/src/interact/ContextMenu.js +400 -0
- package/src/interact/Controller.js +856 -0
- package/src/minimap/Minimap.js +146 -0
- package/src/render/CanvasRenderer.js +606 -0
- package/src/render/HtmlOverlay.js +161 -0
- package/src/render/hitTest.js +38 -0
- package/src/ui/PropertyPanel.css +277 -0
- package/src/ui/PropertyPanel.js +269 -0
- package/src/utils/utils.js +75 -0
package/readme.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# HTML-overlay-Node
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cheonghakim/html-overlay-node/actions)
|
|
4
|
+
[](https://www.npmjs.com/package/html-overlay-node)
|
|
5
|
+
[](https://opensource.org/licenses/ISC)
|
|
6
|
+
|
|
7
|
+
**HTML-overlay-Node** is a customizable, LiteGraph-style node editor library for building visual programming interfaces. It uses **Canvas rendering** for fast performance and supports node type registration, execution cycle control, custom drawing, HTML overlays, and group management.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- 🎨 **Production-quality design** - Professional dark theme with refined aesthetics
|
|
14
|
+
- 🔧 **Type registration system** - Easily create custom node types
|
|
15
|
+
- 🔌 **Dual port system** - Exec ports (flow control) and data ports (values)
|
|
16
|
+
- ⚡ **Flexible execution** - Automatic or manual execution modes with trigger nodes
|
|
17
|
+
- 🔗 **Multiple edge styles** - Curved, orthogonal, or straight connections
|
|
18
|
+
- 💾 **Serialization** - Save and load graphs with `toJSON`/`fromJSON`
|
|
19
|
+
- 🖱️ **Rich interactions** - Zoom, pan, drag, box select, and snap-to-grid
|
|
20
|
+
- ⌨️ **Keyboard shortcuts** - Undo/redo, align, group, and more
|
|
21
|
+
- 🎯 **Custom drawing** - Per-node custom rendering with `onDraw`
|
|
22
|
+
- 🌐 **HTML overlays** - Embed interactive HTML UIs with proper port layering
|
|
23
|
+
- 📦 **Group nodes** - Organize nodes in hierarchical groups
|
|
24
|
+
- 🪝 **Event hooks** - Subscribe to graph events for extensibility
|
|
25
|
+
- 🎥 **Visual feedback** - Smooth animations and hover states
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Quick Start
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
import { createGraphEditor } from "html-overlay-node";
|
|
33
|
+
|
|
34
|
+
// One-liner Initialization!
|
|
35
|
+
// Pass a selector or HTMLElement. Canvas and overlays are created automatically.
|
|
36
|
+
const editor = createGraphEditor("#editor-container", {
|
|
37
|
+
autorun: true,
|
|
38
|
+
enablePropertyPanel: true, // Integrated property panel (default: true)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const { graph, registry, addGroup, start } = editor;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 💅 Styles (Required)
|
|
45
|
+
|
|
46
|
+
Make sure to import the necessary CSS files for the editor and Property Panel to look correctly:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
import "html-overlay-node/index.css";
|
|
50
|
+
import "html-overlay-node/src/ui/PropertyPanel.css";
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
// Register and add nodes
|
|
54
|
+
registry.register("math/Add", {
|
|
55
|
+
title: "Add",
|
|
56
|
+
size: { w: 180, h: 80 },
|
|
57
|
+
inputs: [
|
|
58
|
+
{ name: "a", datatype: "number" },
|
|
59
|
+
{ name: "b", datatype: "number" },
|
|
60
|
+
],
|
|
61
|
+
outputs: [{ name: "result", datatype: "number" }],
|
|
62
|
+
onCreate(node) {
|
|
63
|
+
node.state.a = 0;
|
|
64
|
+
node.state.b = 0;
|
|
65
|
+
},
|
|
66
|
+
onExecute(node, { getInput, setOutput }) {
|
|
67
|
+
const a = getInput("a") ?? node.state.a;
|
|
68
|
+
const b = getInput("b") ?? node.state.b;
|
|
69
|
+
setOutput("result", a + b);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Add nodes
|
|
74
|
+
const node1 = graph.addNode("math/Add", { x: 100, y: 100 });
|
|
75
|
+
const node2 = graph.addNode("math/Add", { x: 100, y: 200 });
|
|
76
|
+
|
|
77
|
+
// Create a group
|
|
78
|
+
addGroup({
|
|
79
|
+
title: "Math Operations",
|
|
80
|
+
x: 50,
|
|
81
|
+
y: 50,
|
|
82
|
+
width: 300,
|
|
83
|
+
height: 300,
|
|
84
|
+
color: "#4a5568",
|
|
85
|
+
members: [node1.id, node2.id],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
start();
|
|
89
|
+
|
|
90
|
+
````
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 📦 Group Management
|
|
95
|
+
|
|
96
|
+
HTML-overlay-Node supports organizing nodes into hierarchical groups for better organization.
|
|
97
|
+
|
|
98
|
+
### Creating Groups
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
const { addGroup } = editor;
|
|
102
|
+
|
|
103
|
+
// Create a group
|
|
104
|
+
const group = addGroup({
|
|
105
|
+
title: "My Group", // Group name
|
|
106
|
+
x: 0, // X position
|
|
107
|
+
y: 0, // Y position
|
|
108
|
+
width: 400, // Width (min: 100)
|
|
109
|
+
height: 300, // Height (min: 60)
|
|
110
|
+
color: "#2d3748", // Background color
|
|
111
|
+
members: [node1.id, node2.id], // Nodes to include
|
|
112
|
+
});
|
|
113
|
+
````
|
|
114
|
+
|
|
115
|
+
### Group Features
|
|
116
|
+
|
|
117
|
+
- **Hierarchical Structure**: Groups can contain multiple nodes
|
|
118
|
+
- **Automatic Movement**: Nodes inside the group move with the group
|
|
119
|
+
- **Resizable**: Resize groups using the handle in the bottom-right corner
|
|
120
|
+
- **Custom Colors**: Set group colors for visual organization
|
|
121
|
+
- **Local Coordinates**: World and local coordinates automatically managed
|
|
122
|
+
|
|
123
|
+
### Advanced Group Operations
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// Access GroupManager
|
|
127
|
+
const groupManager = graph.groupManager;
|
|
128
|
+
|
|
129
|
+
// Reparent a node to a group
|
|
130
|
+
graph.reparent(node, group);
|
|
131
|
+
|
|
132
|
+
// Remove node from group (reparent to root)
|
|
133
|
+
graph.reparent(node, null);
|
|
134
|
+
|
|
135
|
+
// Resize a group
|
|
136
|
+
groupManager.resizeGroup(group.id, 50, 50); // add 50px to width and height
|
|
137
|
+
|
|
138
|
+
// Remove a group (children are un-grouped)
|
|
139
|
+
groupManager.removeGroup(group.id);
|
|
140
|
+
|
|
141
|
+
// Listen to group events
|
|
142
|
+
hooks.on("group:change", () => {
|
|
143
|
+
console.log("Group structure changed");
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🌐 HTML Overlays
|
|
150
|
+
|
|
151
|
+
Create interactive HTML UIs inside nodes.
|
|
152
|
+
|
|
153
|
+
### Basic Example
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
registry.register("ui/TextInput", {
|
|
157
|
+
title: "Text Input",
|
|
158
|
+
size: { w: 220, h: 100 },
|
|
159
|
+
outputs: [{ name: "text", datatype: "string" }],
|
|
160
|
+
|
|
161
|
+
html: {
|
|
162
|
+
init(node, el, { header, body }) {
|
|
163
|
+
el.style.backgroundColor = "#1a1a1a";
|
|
164
|
+
el.style.borderRadius = "8px";
|
|
165
|
+
|
|
166
|
+
const input = document.createElement("input");
|
|
167
|
+
input.type = "text";
|
|
168
|
+
input.placeholder = "Enter text...";
|
|
169
|
+
Object.assign(input.style, {
|
|
170
|
+
width: "100%",
|
|
171
|
+
padding: "8px",
|
|
172
|
+
background: "#111",
|
|
173
|
+
border: "1px solid #444",
|
|
174
|
+
color: "#fff",
|
|
175
|
+
pointerEvents: "auto", // IMPORTANT: Enable interaction
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
input.addEventListener("input", (e) => {
|
|
179
|
+
node.state.text = e.target.value;
|
|
180
|
+
hooks.emit("node:updated", node);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
input.addEventListener("mousedown", (e) => e.stopPropagation()); // Prevent drag
|
|
184
|
+
|
|
185
|
+
body.appendChild(input);
|
|
186
|
+
el._input = input;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
update(node, el, { selected }) {
|
|
190
|
+
el.style.borderColor = selected ? "#3b82f6" : "#333";
|
|
191
|
+
if (el._input.value !== (node.state.text || "")) {
|
|
192
|
+
el._input.value = node.state.text || "";
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
onCreate(node) {
|
|
198
|
+
node.state.text = "";
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
onExecute(node, { setOutput }) {
|
|
202
|
+
setOutput("text", node.state.text || "");
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Best Practices
|
|
208
|
+
|
|
209
|
+
1. **Enable Interaction**: Set `pointerEvents: "auto"` on interactive elements
|
|
210
|
+
2. **Stop Propagation**: Prevent canvas drag with `e.stopPropagation()` on `mousedown`
|
|
211
|
+
3. **Update State**: Emit `"node:updated"` when state changes
|
|
212
|
+
4. **Store References**: Cache DOM elements in `el._refs` for performance
|
|
213
|
+
5. **Port Visibility**: HTML overlays are rendered below ports for proper visibility
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 🔌 Port Types
|
|
218
|
+
|
|
219
|
+
HTML-overlay-Node supports two types of ports for different purposes:
|
|
220
|
+
|
|
221
|
+
### Exec Ports (Flow Control)
|
|
222
|
+
|
|
223
|
+
Exec ports control the execution flow between nodes.
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
registry.register("util/Print", {
|
|
227
|
+
title: "Print",
|
|
228
|
+
inputs: [
|
|
229
|
+
{ name: "exec", portType: "exec" }, // Execution input
|
|
230
|
+
{ name: "value", portType: "data", datatype: "any" },
|
|
231
|
+
],
|
|
232
|
+
outputs: [
|
|
233
|
+
{ name: "exec", portType: "exec" }, // Execution output
|
|
234
|
+
],
|
|
235
|
+
onExecute(node, { getInput }) {
|
|
236
|
+
console.log("[Print]", getInput("value"));
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Data Ports (Values)
|
|
242
|
+
|
|
243
|
+
Data ports transfer values between nodes.
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
registry.register("math/Add", {
|
|
247
|
+
title: "Add",
|
|
248
|
+
inputs: [
|
|
249
|
+
{ name: "exec", portType: "exec" },
|
|
250
|
+
{ name: "a", portType: "data", datatype: "number" },
|
|
251
|
+
{ name: "b", portType: "data", datatype: "number" },
|
|
252
|
+
],
|
|
253
|
+
outputs: [
|
|
254
|
+
{ name: "exec", portType: "exec" },
|
|
255
|
+
{ name: "result", portType: "data", datatype: "number" },
|
|
256
|
+
],
|
|
257
|
+
onExecute(node, { getInput, setOutput }) {
|
|
258
|
+
const result = (getInput("a") ?? 0) + (getInput("b") ?? 0);
|
|
259
|
+
setOutput("result", result);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Visual Style
|
|
265
|
+
|
|
266
|
+
- **Exec ports**: Emerald green rounded squares (8×8px)
|
|
267
|
+
- **Data ports**: Indigo blue circles (10px diameter)
|
|
268
|
+
- Both have subtle outlines for depth
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## ⌨️ Keyboard Shortcuts
|
|
273
|
+
|
|
274
|
+
| Shortcut | Action |
|
|
275
|
+
| ------------------------------- | --------------------------- |
|
|
276
|
+
| **Selection** | |
|
|
277
|
+
| `Click` | Select node |
|
|
278
|
+
| `Shift + Click` | Add to selection |
|
|
279
|
+
| `Ctrl + Drag` | Box select |
|
|
280
|
+
| **Editing** | |
|
|
281
|
+
| `Delete` | Delete selected nodes |
|
|
282
|
+
| `Ctrl + Z` | Undo |
|
|
283
|
+
| `Ctrl + Y` / `Ctrl + Shift + Z` | Redo |
|
|
284
|
+
| **Grouping** | |
|
|
285
|
+
| `Ctrl + G` | Create group from selection |
|
|
286
|
+
| **Alignment** | |
|
|
287
|
+
| `A` | Align nodes horizontally |
|
|
288
|
+
| `Shift + A` | Align nodes vertically |
|
|
289
|
+
| **Tools** | |
|
|
290
|
+
| `G` | Toggle snap-to-grid |
|
|
291
|
+
| `?` | Toggle shortcuts help |
|
|
292
|
+
| **Navigation** | |
|
|
293
|
+
| `Middle Click + Drag` | Pan canvas |
|
|
294
|
+
| `Mouse Wheel` | Zoom in/out |
|
|
295
|
+
| `Right Click` | Context menu |
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 📚 Complete API
|
|
300
|
+
|
|
301
|
+
For full API documentation, see the comments in [src/index.js](src/index.js).
|
|
302
|
+
|
|
303
|
+
### Editor API
|
|
304
|
+
|
|
305
|
+
| Property | Description |
|
|
306
|
+
| ------------------- | --------------------- |
|
|
307
|
+
| `graph` | Graph instance |
|
|
308
|
+
| `registry` | Node type registry |
|
|
309
|
+
| `hooks` | Event system |
|
|
310
|
+
| `render()` | Trigger manual render |
|
|
311
|
+
| `start()` | Start execution loop |
|
|
312
|
+
| `stop()` | Stop execution loop |
|
|
313
|
+
| `addGroup(options)` | Create a group |
|
|
314
|
+
| `destroy()` | Cleanup |
|
|
315
|
+
|
|
316
|
+
### Key Methods
|
|
317
|
+
|
|
318
|
+
- `registry.register(type, definition)` - Register node type
|
|
319
|
+
- `graph.addNode(type, options)` - Create node
|
|
320
|
+
- `graph.addEdge(from, fromPort, to, toPort)` - Connect nodes
|
|
321
|
+
- `graph.toJSON()` / `graph.fromJSON(json)` - Serialize/deserialize
|
|
322
|
+
- `hooks.on(event, callback)` - Subscribe to events
|
|
323
|
+
|
|
324
|
+
### Available Events
|
|
325
|
+
|
|
326
|
+
- `node:create` | `node:move` | `node:resize` | `node:updated`
|
|
327
|
+
- `edge:create` | `edge:delete`
|
|
328
|
+
- `group:change`
|
|
329
|
+
- `runner:start` | `runner:stop` | `runner:tick`
|
|
330
|
+
- `error`
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 🎨 Customization
|
|
335
|
+
|
|
336
|
+
### Theme Colors
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
const editor = createGraphEditor(canvas, {
|
|
340
|
+
theme: {
|
|
341
|
+
bg: "#0d0d0f", // Canvas background
|
|
342
|
+
grid: "#1a1a1d", // Grid lines
|
|
343
|
+
node: "#16161a", // Node background
|
|
344
|
+
nodeBorder: "#2a2a2f", // Node border
|
|
345
|
+
title: "#1f1f24", // Node header
|
|
346
|
+
text: "#e4e4e7", // Primary text
|
|
347
|
+
textMuted: "#a1a1aa", // Secondary text
|
|
348
|
+
port: "#6366f1", // Data port color (indigo)
|
|
349
|
+
portExec: "#10b981", // Exec port color (emerald)
|
|
350
|
+
edge: "#52525b", // Edge color
|
|
351
|
+
edgeActive: "#8b5cf6", // Active edge (purple)
|
|
352
|
+
accent: "#6366f1", // Accent color
|
|
353
|
+
accentBright: "#818cf8", // Bright accent
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Edge Styles
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
// Set edge style
|
|
362
|
+
editor.renderer.setEdgeStyle("orthogonal"); // or "curved", "line"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Custom Node Drawing
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
registry.register("visual/Circle", {
|
|
369
|
+
title: "Circle",
|
|
370
|
+
size: { w: 120, h: 120 },
|
|
371
|
+
onDraw(node, { ctx, theme }) {
|
|
372
|
+
const { x, y, width, height } = node.computed;
|
|
373
|
+
const centerX = x + width / 2;
|
|
374
|
+
const centerY = y + height / 2;
|
|
375
|
+
const radius = Math.min(width, height) / 3;
|
|
376
|
+
|
|
377
|
+
ctx.beginPath();
|
|
378
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
379
|
+
ctx.fillStyle = theme.wire;
|
|
380
|
+
ctx.fill();
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 💾 Serialization
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
// Save
|
|
391
|
+
const json = graph.toJSON();
|
|
392
|
+
localStorage.setItem("myGraph", JSON.stringify(json));
|
|
393
|
+
|
|
394
|
+
// Load
|
|
395
|
+
const saved = JSON.parse(localStorage.getItem("myGraph"));
|
|
396
|
+
graph.fromJSON(saved);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## 🐛 Troubleshooting
|
|
402
|
+
|
|
403
|
+
| Issue | Solution |
|
|
404
|
+
| ---------------------------- | --------------------------------------------------- |
|
|
405
|
+
| Canvas not rendering | Ensure canvas has explicit width/height |
|
|
406
|
+
| Nodes not executing | Call `start()` or set `autorun: true` |
|
|
407
|
+
| Type errors | Register node types before using them |
|
|
408
|
+
| HTML overlay not interactive | Set `pointerEvents: "auto"` on elements |
|
|
409
|
+
| Performance issues | Limit to <1000 nodes, optimize `onExecute`/`onDraw` |
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 🤝 Contributing
|
|
414
|
+
|
|
415
|
+
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
npm install # Install dependencies
|
|
419
|
+
npm run dev # Start dev server
|
|
420
|
+
npm test # Run tests
|
|
421
|
+
npm run lint # Check code quality
|
|
422
|
+
npm run build # Build library
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 📄 License
|
|
428
|
+
|
|
429
|
+
[MIT](LICENSE) © cheonghakim
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 🔗 Links
|
|
434
|
+
|
|
435
|
+
- [GitHub Repository](https://github.com/cheonghakim/html-overlay-node)
|
|
436
|
+
- [Issue Tracker](https://github.com/cheonghakim/html-overlay-node/issues)
|
|
437
|
+
- [Changelog](CHANGELOG.md)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/core/CommandStack.js
|
|
2
|
+
export class CommandStack {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.undoStack = [];
|
|
5
|
+
this.redoStack = [];
|
|
6
|
+
}
|
|
7
|
+
exec(cmd) {
|
|
8
|
+
cmd.do();
|
|
9
|
+
this.undoStack.push(cmd);
|
|
10
|
+
this.redoStack.length = 0;
|
|
11
|
+
}
|
|
12
|
+
undo() {
|
|
13
|
+
const c = this.undoStack.pop();
|
|
14
|
+
if (c) {
|
|
15
|
+
c.undo();
|
|
16
|
+
this.redoStack.push(c);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
redo() {
|
|
20
|
+
const c = this.redoStack.pop();
|
|
21
|
+
if (c) {
|
|
22
|
+
c.do();
|
|
23
|
+
this.undoStack.push(c);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/core/Edge.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { randomUUID } from "../utils/utils.js";
|
|
2
|
+
|
|
3
|
+
// src/core/Edge.js
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Edge represents a connection between two node ports
|
|
7
|
+
*/
|
|
8
|
+
export class Edge {
|
|
9
|
+
/**
|
|
10
|
+
* Create a new Edge
|
|
11
|
+
* @param {Object} options - Edge configuration
|
|
12
|
+
* @param {string} [options.id] - Unique identifier (auto-generated if not provided)
|
|
13
|
+
* @param {string} options.fromNode - Source node ID
|
|
14
|
+
* @param {string} options.fromPort - Source port ID
|
|
15
|
+
* @param {string} options.toNode - Target node ID
|
|
16
|
+
* @param {string} options.toPort - Target port ID
|
|
17
|
+
*/
|
|
18
|
+
constructor({ id, fromNode, fromPort, toNode, toPort }) {
|
|
19
|
+
if (!fromNode || !fromPort || !toNode || !toPort) {
|
|
20
|
+
throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
|
|
21
|
+
}
|
|
22
|
+
this.id = id ?? randomUUID();
|
|
23
|
+
this.fromNode = fromNode;
|
|
24
|
+
this.fromPort = fromPort;
|
|
25
|
+
this.toNode = toNode;
|
|
26
|
+
this.toPort = toPort;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Edge } from "./Edge.js";
|
|
3
|
+
|
|
4
|
+
describe("Edge", () => {
|
|
5
|
+
describe("constructor", () => {
|
|
6
|
+
it("should create an edge with all required fields", () => {
|
|
7
|
+
const edge = new Edge({
|
|
8
|
+
fromNode: "node-1",
|
|
9
|
+
fromPort: "port-1",
|
|
10
|
+
toNode: "node-2",
|
|
11
|
+
toPort: "port-2",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(edge.fromNode).toBe("node-1");
|
|
15
|
+
expect(edge.fromPort).toBe("port-1");
|
|
16
|
+
expect(edge.toNode).toBe("node-2");
|
|
17
|
+
expect(edge.toPort).toBe("port-2");
|
|
18
|
+
expect(edge.id).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should use custom ID if provided", () => {
|
|
22
|
+
const edge = new Edge({
|
|
23
|
+
id: "custom-edge-id",
|
|
24
|
+
fromNode: "node-1",
|
|
25
|
+
fromPort: "port-1",
|
|
26
|
+
toNode: "node-2",
|
|
27
|
+
toPort: "port-2",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(edge.id).toBe("custom-edge-id");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should throw error when fromNode is missing", () => {
|
|
34
|
+
expect(() => {
|
|
35
|
+
new Edge({
|
|
36
|
+
fromPort: "port-1",
|
|
37
|
+
toNode: "node-2",
|
|
38
|
+
toPort: "port-2",
|
|
39
|
+
});
|
|
40
|
+
}).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should throw error when fromPort is missing", () => {
|
|
44
|
+
expect(() => {
|
|
45
|
+
new Edge({
|
|
46
|
+
fromNode: "node-1",
|
|
47
|
+
toNode: "node-2",
|
|
48
|
+
toPort: "port-2",
|
|
49
|
+
});
|
|
50
|
+
}).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should throw error when toNode is missing", () => {
|
|
54
|
+
expect(() => {
|
|
55
|
+
new Edge({
|
|
56
|
+
fromNode: "node-1",
|
|
57
|
+
fromPort: "port-1",
|
|
58
|
+
toPort: "port-2",
|
|
59
|
+
});
|
|
60
|
+
}).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should throw error when toPort is missing", () => {
|
|
64
|
+
expect(() => {
|
|
65
|
+
new Edge({
|
|
66
|
+
fromNode: "node-1",
|
|
67
|
+
fromPort: "port-1",
|
|
68
|
+
toNode: "node-2",
|
|
69
|
+
});
|
|
70
|
+
}).toThrow(/requires fromNode, fromPort, toNode, and toPort/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|