html-overlay-node 0.1.6 → 0.1.10
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/dist/example.json +3 -3
- package/dist/html-overlay-node.es.js +997 -1014
- package/dist/html-overlay-node.es.js.map +1 -1
- package/dist/html-overlay-node.umd.js +1 -1
- package/dist/html-overlay-node.umd.js.map +1 -1
- package/index.css +391 -232
- package/package.json +9 -8
- package/readme.md +58 -364
- package/src/core/Edge.js +4 -2
- package/src/core/Graph.js +29 -5
- package/src/core/Node.js +27 -11
- package/src/core/Runner.js +201 -211
- package/src/defaults/contextMenu.js +102 -0
- package/src/defaults/index.js +6 -0
- package/src/index.js +85 -793
- package/src/interact/ContextMenu.js +5 -1
- package/src/interact/Controller.js +73 -46
- package/src/nodes/core.js +266 -0
- package/src/nodes/index.js +42 -0
- package/src/nodes/logic.js +60 -0
- package/src/nodes/math.js +99 -0
- package/src/nodes/util.js +176 -0
- package/src/nodes/value.js +100 -0
- package/src/render/CanvasRenderer.js +784 -604
- package/src/render/HtmlOverlay.js +15 -5
- package/src/render/hitTest.js +18 -9
- package/src/ui/HelpOverlay.js +158 -0
- package/src/ui/PropertyPanel.css +58 -27
- package/src/ui/PropertyPanel.js +441 -268
- package/src/utils/utils.js +4 -4
package/readme.md
CHANGED
|
@@ -3,419 +3,113 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/html-overlay-node)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
**HTML-overlay-Node
|
|
6
|
+
**HTML-overlay-Node**는 Canvas의 정밀함과 HTML의 유연함을 결합한 전문가용 노드 에디터 라이브러리입니다. 장난감 같은 디자인에서 벗어나 실제 프로덕션 환경에 적합한 날카롭고 세련된 UI/UX를 지향합니다.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
- 🎨 **Production-quality design** - Professional dark theme with refined aesthetics
|
|
13
|
-
- 🔧 **Type registration system** - Easily create custom node types
|
|
14
|
-
- 🔌 **Dual port system** - Exec ports (flow control) and data ports (values)
|
|
15
|
-
- ⚡ **Flexible execution** - Automatic or manual execution modes with trigger nodes
|
|
16
|
-
- 🔗 **Multiple edge styles** - Curved, orthogonal, or straight connections
|
|
17
|
-
- 💾 **Serialization** - Save and load graphs with `toJSON`/`fromJSON`
|
|
18
|
-
- 🖱️ **Rich interactions** - Zoom, pan, drag, box select, and snap-to-grid
|
|
19
|
-
- ⌨️ **Keyboard shortcuts** - Undo/redo, align, group, and more
|
|
20
|
-
- 🎯 **Custom drawing** - Per-node custom rendering with `onDraw`
|
|
21
|
-
- 🌐 **HTML overlays** - Embed interactive HTML UIs with proper port layering
|
|
22
|
-
- 📦 **Group nodes** - Organize nodes in hierarchical groups
|
|
23
|
-
- 🪝 **Event hooks** - Subscribe to graph events for extensibility
|
|
24
|
-
- 🎥 **Visual feedback** - Smooth animations and hover states
|
|
10
|
+
## ⚡ 주요 특징
|
|
25
11
|
|
|
26
|
-
|
|
12
|
+
- **전문가급 디자인**: 2px의 날카로운 라운딩 처리와 깊이감 있는 다크 테마로 도구의 전문성을 극대화했습니다.
|
|
13
|
+
- **카테고리별 컬러 시스템**: Math, Logic, Data 등 노드 성격에 따른 정교한 컬러 코딩으로 복잡한 그래프의 가독성을 높였습니다.
|
|
14
|
+
- **강력한 듀얼 포트**: 실행 제어(Flow)를 위한 Exec 포트와 데이터 전송(Data) 포트를 분리하여 복잡한 로직 설계가 가능합니다.
|
|
15
|
+
- **인터랙티브 HTML 오버레이**: 노드 내부의 복잡한 UI는 HTML로, 전체 연결은 Canvas로 처리하여 성능과 확장성을 모두 잡았습니다.
|
|
16
|
+
- **파워 유저 생산성**: 정렬(Align), 그룹화(Group), Undo/Redo 등 실제 작업 시간을 단축해주는 풍부한 단축키를 지원합니다.
|
|
17
|
+
- **정밀한 시각 릴레이**: 노드 실행 상태를 보여주는 완벽하게 밀착된 테두리 애니메이션을 통해 실시간 피드백을 제공합니다.
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
---
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
## 🚀 시작하기
|
|
31
22
|
|
|
32
|
-
|
|
23
|
+
Vite나 Webpack 환경에서 바로 시작할 수 있습니다.
|
|
33
24
|
|
|
34
25
|
```javascript
|
|
35
26
|
import { createGraphEditor } from "html-overlay-node";
|
|
27
|
+
import "html-overlay-node/index.css";
|
|
36
28
|
|
|
37
|
-
//
|
|
38
|
-
// Pass a selector or HTMLElement. Canvas and overlays are created automatically.
|
|
29
|
+
// 단 한 줄로 에디터 초기화
|
|
39
30
|
const editor = createGraphEditor("#editor-container", {
|
|
40
31
|
autorun: true,
|
|
41
|
-
enablePropertyPanel: true
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const { graph, registry, addGroup, start } = editor;
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 💅 Styles (Required)
|
|
48
|
-
|
|
49
|
-
Make sure to import the necessary CSS files for the editor and Property Panel to look correctly:
|
|
50
|
-
|
|
51
|
-
```javascript
|
|
52
|
-
import "html-overlay-node/index.css";
|
|
53
|
-
import "html-overlay-node/src/ui/PropertyPanel.css";
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
// Register and add nodes
|
|
57
|
-
registry.register("math/Add", {
|
|
58
|
-
title: "Add",
|
|
59
|
-
size: { w: 180, h: 80 },
|
|
60
|
-
inputs: [
|
|
61
|
-
{ name: "a", datatype: "number" },
|
|
62
|
-
{ name: "b", datatype: "number" },
|
|
63
|
-
],
|
|
64
|
-
outputs: [{ name: "result", datatype: "number" }],
|
|
65
|
-
onCreate(node) {
|
|
66
|
-
node.state.a = 0;
|
|
67
|
-
node.state.b = 0;
|
|
68
|
-
},
|
|
69
|
-
onExecute(node, { getInput, setOutput }) {
|
|
70
|
-
const a = getInput("a") ?? node.state.a;
|
|
71
|
-
const b = getInput("b") ?? node.state.b;
|
|
72
|
-
setOutput("result", a + b);
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Add nodes
|
|
77
|
-
const node1 = graph.addNode("math/Add", { x: 100, y: 100 });
|
|
78
|
-
const node2 = graph.addNode("math/Add", { x: 100, y: 200 });
|
|
79
|
-
|
|
80
|
-
// Create a group
|
|
81
|
-
addGroup({
|
|
82
|
-
title: "Math Operations",
|
|
83
|
-
x: 50,
|
|
84
|
-
y: 50,
|
|
85
|
-
width: 300,
|
|
86
|
-
height: 300,
|
|
87
|
-
color: "#4a5568",
|
|
88
|
-
members: [node1.id, node2.id],
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
start();
|
|
92
|
-
|
|
93
|
-
````
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## 📦 Group Management
|
|
98
|
-
|
|
99
|
-
HTML-overlay-Node supports organizing nodes into hierarchical groups for better organization.
|
|
100
|
-
|
|
101
|
-
### Creating Groups
|
|
102
|
-
|
|
103
|
-
```javascript
|
|
104
|
-
const { addGroup } = editor;
|
|
105
|
-
|
|
106
|
-
// Create a group
|
|
107
|
-
const group = addGroup({
|
|
108
|
-
title: "My Group", // Group name
|
|
109
|
-
x: 0, // X position
|
|
110
|
-
y: 0, // Y position
|
|
111
|
-
width: 400, // Width (min: 100)
|
|
112
|
-
height: 300, // Height (min: 60)
|
|
113
|
-
color: "#2d3748", // Background color
|
|
114
|
-
members: [node1.id, node2.id], // Nodes to include
|
|
115
|
-
});
|
|
116
|
-
````
|
|
117
|
-
|
|
118
|
-
### Group Features
|
|
119
|
-
|
|
120
|
-
- **Hierarchical Structure**: Groups can contain multiple nodes
|
|
121
|
-
- **Automatic Movement**: Nodes inside the group move with the group
|
|
122
|
-
- **Resizable**: Resize groups using the handle in the bottom-right corner
|
|
123
|
-
- **Custom Colors**: Set group colors for visual organization
|
|
124
|
-
- **Local Coordinates**: World and local coordinates automatically managed
|
|
125
|
-
|
|
126
|
-
### Advanced Group Operations
|
|
127
|
-
|
|
128
|
-
```javascript
|
|
129
|
-
// Access GroupManager
|
|
130
|
-
const groupManager = graph.groupManager;
|
|
131
|
-
|
|
132
|
-
// Reparent a node to a group
|
|
133
|
-
graph.reparent(node, group);
|
|
134
|
-
|
|
135
|
-
// Remove node from group (reparent to root)
|
|
136
|
-
graph.reparent(node, null);
|
|
137
|
-
|
|
138
|
-
// Resize a group
|
|
139
|
-
groupManager.resizeGroup(group.id, 50, 50); // add 50px to width and height
|
|
140
|
-
|
|
141
|
-
// Remove a group (children are un-grouped)
|
|
142
|
-
groupManager.removeGroup(group.id);
|
|
143
|
-
|
|
144
|
-
// Listen to group events
|
|
145
|
-
hooks.on("group:change", () => {
|
|
146
|
-
console.log("Group structure changed");
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
## 🌐 HTML Overlays
|
|
153
|
-
|
|
154
|
-
Create interactive HTML UIs inside nodes.
|
|
155
|
-
|
|
156
|
-
### Basic Example
|
|
157
|
-
|
|
158
|
-
```javascript
|
|
159
|
-
registry.register("ui/TextInput", {
|
|
160
|
-
title: "Text Input",
|
|
161
|
-
size: { w: 220, h: 100 },
|
|
162
|
-
outputs: [{ name: "text", datatype: "string" }],
|
|
163
|
-
|
|
164
|
-
html: {
|
|
165
|
-
init(node, el, { header, body }) {
|
|
166
|
-
el.style.backgroundColor = "#1a1a1a";
|
|
167
|
-
el.style.borderRadius = "8px";
|
|
168
|
-
|
|
169
|
-
const input = document.createElement("input");
|
|
170
|
-
input.type = "text";
|
|
171
|
-
input.placeholder = "Enter text...";
|
|
172
|
-
Object.assign(input.style, {
|
|
173
|
-
width: "100%",
|
|
174
|
-
padding: "8px",
|
|
175
|
-
background: "#111",
|
|
176
|
-
border: "1px solid #444",
|
|
177
|
-
color: "#fff",
|
|
178
|
-
pointerEvents: "auto", // IMPORTANT: Enable interaction
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
input.addEventListener("input", (e) => {
|
|
182
|
-
node.state.text = e.target.value;
|
|
183
|
-
hooks.emit("node:updated", node);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
input.addEventListener("mousedown", (e) => e.stopPropagation()); // Prevent drag
|
|
187
|
-
|
|
188
|
-
body.appendChild(input);
|
|
189
|
-
el._input = input;
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
update(node, el, { selected }) {
|
|
193
|
-
el.style.borderColor = selected ? "#3b82f6" : "#333";
|
|
194
|
-
if (el._input.value !== (node.state.text || "")) {
|
|
195
|
-
el._input.value = node.state.text || "";
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
onCreate(node) {
|
|
201
|
-
node.state.text = "";
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
onExecute(node, { setOutput }) {
|
|
205
|
-
setOutput("text", node.state.text || "");
|
|
206
|
-
},
|
|
32
|
+
enablePropertyPanel: true
|
|
207
33
|
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Best Practices
|
|
211
|
-
|
|
212
|
-
1. **Enable Interaction**: Set `pointerEvents: "auto"` on interactive elements
|
|
213
|
-
2. **Stop Propagation**: Prevent canvas drag with `e.stopPropagation()` on `mousedown`
|
|
214
|
-
3. **Update State**: Emit `"node:updated"` when state changes
|
|
215
|
-
4. **Store References**: Cache DOM elements in `el._refs` for performance
|
|
216
|
-
5. **Port Visibility**: HTML overlays are rendered below ports for proper visibility
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## 🔌 Port Types
|
|
221
|
-
|
|
222
|
-
HTML-overlay-Node supports two types of ports for different purposes:
|
|
223
|
-
|
|
224
|
-
### Exec Ports (Flow Control)
|
|
225
|
-
|
|
226
|
-
Exec ports control the execution flow between nodes.
|
|
227
|
-
|
|
228
|
-
```javascript
|
|
229
|
-
registry.register("util/Print", {
|
|
230
|
-
title: "Print",
|
|
231
|
-
inputs: [
|
|
232
|
-
{ name: "exec", portType: "exec" }, // Execution input
|
|
233
|
-
{ name: "value", portType: "data", datatype: "any" },
|
|
234
|
-
],
|
|
235
|
-
outputs: [
|
|
236
|
-
{ name: "exec", portType: "exec" }, // Execution output
|
|
237
|
-
],
|
|
238
|
-
onExecute(node, { getInput }) {
|
|
239
|
-
console.log("[Print]", getInput("value"));
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
```
|
|
243
34
|
|
|
244
|
-
|
|
35
|
+
const { graph, registry, start } = editor;
|
|
245
36
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
```javascript
|
|
37
|
+
// 노드 등록 예시 (컬러 코드 포함)
|
|
249
38
|
registry.register("math/Add", {
|
|
250
39
|
title: "Add",
|
|
40
|
+
color: "#f43f5e", // 연산 노드는 로즈 핑크
|
|
41
|
+
size: { w: 180, h: 80 },
|
|
251
42
|
inputs: [
|
|
252
|
-
{ name: "
|
|
253
|
-
{ name: "
|
|
254
|
-
{ name: "b", portType: "data", datatype: "number" },
|
|
255
|
-
],
|
|
256
|
-
outputs: [
|
|
257
|
-
{ name: "exec", portType: "exec" },
|
|
258
|
-
{ name: "result", portType: "data", datatype: "number" },
|
|
43
|
+
{ name: "a", datatype: "number" },
|
|
44
|
+
{ name: "b", datatype: "number" },
|
|
259
45
|
],
|
|
46
|
+
outputs: [{ name: "result", datatype: "number" }],
|
|
260
47
|
onExecute(node, { getInput, setOutput }) {
|
|
261
|
-
const
|
|
262
|
-
|
|
48
|
+
const a = getInput("a") ?? 0;
|
|
49
|
+
const b = getInput("b") ?? 0;
|
|
50
|
+
setOutput("result", a + b);
|
|
263
51
|
},
|
|
264
52
|
});
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Visual Style
|
|
268
|
-
|
|
269
|
-
- **Exec ports**: Emerald green rounded squares (8×8px)
|
|
270
|
-
- **Data ports**: Indigo blue circles (10px diameter)
|
|
271
|
-
- Both have subtle outlines for depth
|
|
272
|
-
|
|
273
|
-
---
|
|
274
|
-
|
|
275
|
-
## ⌨️ Keyboard Shortcuts
|
|
276
|
-
|
|
277
|
-
| Shortcut | Action |
|
|
278
|
-
| ------------------------------- | --------------------------- |
|
|
279
|
-
| **Selection** | |
|
|
280
|
-
| `Click` | Select node |
|
|
281
|
-
| `Shift + Click` | Add to selection |
|
|
282
|
-
| `Ctrl + Drag` | Box select |
|
|
283
|
-
| **Editing** | |
|
|
284
|
-
| `Delete` | Delete selected nodes |
|
|
285
|
-
| `Ctrl + Z` | Undo |
|
|
286
|
-
| `Ctrl + Y` / `Ctrl + Shift + Z` | Redo |
|
|
287
|
-
| **Grouping** | |
|
|
288
|
-
| `Ctrl + G` | Create group from selection |
|
|
289
|
-
| **Alignment** | |
|
|
290
|
-
| `A` | Align nodes horizontally |
|
|
291
|
-
| `Shift + A` | Align nodes vertically |
|
|
292
|
-
| **Tools** | |
|
|
293
|
-
| `G` | Toggle snap-to-grid |
|
|
294
|
-
| `?` | Toggle shortcuts help |
|
|
295
|
-
| **Navigation** | |
|
|
296
|
-
| `Middle Click + Drag` | Pan canvas |
|
|
297
|
-
| `Mouse Wheel` | Zoom in/out |
|
|
298
|
-
| `Right Click` | Context menu |
|
|
299
53
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
## 📚 Complete API
|
|
303
|
-
|
|
304
|
-
For full API documentation, see the comments in [src/index.js](src/index.js).
|
|
305
|
-
|
|
306
|
-
### Editor API
|
|
307
|
-
|
|
308
|
-
| Property | Description |
|
|
309
|
-
| ------------------- | --------------------- |
|
|
310
|
-
| `graph` | Graph instance |
|
|
311
|
-
| `registry` | Node type registry |
|
|
312
|
-
| `hooks` | Event system |
|
|
313
|
-
| `render()` | Trigger manual render |
|
|
314
|
-
| `start()` | Start execution loop |
|
|
315
|
-
| `stop()` | Stop execution loop |
|
|
316
|
-
| `addGroup(options)` | Create a group |
|
|
317
|
-
| `destroy()` | Cleanup |
|
|
318
|
-
|
|
319
|
-
### Key Methods
|
|
320
|
-
|
|
321
|
-
- `registry.register(type, definition)` - Register node type
|
|
322
|
-
- `graph.addNode(type, options)` - Create node
|
|
323
|
-
- `graph.addEdge(from, fromPort, to, toPort)` - Connect nodes
|
|
324
|
-
- `graph.toJSON()` / `graph.fromJSON(json)` - Serialize/deserialize
|
|
325
|
-
- `hooks.on(event, callback)` - Subscribe to events
|
|
326
|
-
|
|
327
|
-
### Available Events
|
|
328
|
-
|
|
329
|
-
- `node:create` | `node:move` | `node:resize` | `node:updated`
|
|
330
|
-
- `edge:create` | `edge:delete`
|
|
331
|
-
- `group:change`
|
|
332
|
-
- `runner:start` | `runner:stop` | `runner:tick`
|
|
333
|
-
- `error`
|
|
54
|
+
start();
|
|
55
|
+
```
|
|
334
56
|
|
|
335
57
|
---
|
|
336
58
|
|
|
337
|
-
## 🎨
|
|
59
|
+
## 🎨 디자인 커스터마이징
|
|
338
60
|
|
|
339
|
-
|
|
61
|
+
다크 모드를 기본으로 하지만, 프로젝트 테마에 맞춰 모든 색상을 조정할 수 있습니다.
|
|
340
62
|
|
|
341
63
|
```javascript
|
|
342
|
-
const editor = createGraphEditor(
|
|
64
|
+
const editor = createGraphEditor("#container", {
|
|
343
65
|
theme: {
|
|
344
|
-
bg: "#0d0d0f",
|
|
345
|
-
|
|
346
|
-
node: "#16161a",
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
text: "#e4e4e7", // Primary text
|
|
350
|
-
textMuted: "#a1a1aa", // Secondary text
|
|
351
|
-
port: "#6366f1", // Data port color (indigo)
|
|
352
|
-
portExec: "#10b981", // Exec port color (emerald)
|
|
353
|
-
edge: "#52525b", // Edge color
|
|
354
|
-
edgeActive: "#8b5cf6", // Active edge (purple)
|
|
355
|
-
accent: "#6366f1", // Accent color
|
|
356
|
-
accentBright: "#818cf8", // Bright accent
|
|
357
|
-
},
|
|
66
|
+
bg: "#0d0d0f",
|
|
67
|
+
accent: "#6366f1", // 메인 포인트 컬러
|
|
68
|
+
node: "#16161a", // 노드 배경
|
|
69
|
+
title: "#1f1f24" // 노드 헤더
|
|
70
|
+
}
|
|
358
71
|
});
|
|
359
72
|
```
|
|
360
73
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
```javascript
|
|
364
|
-
// Set edge style
|
|
365
|
-
editor.renderer.setEdgeStyle("orthogonal"); // or "curved", "line"
|
|
366
|
-
```
|
|
74
|
+
---
|
|
367
75
|
|
|
368
|
-
|
|
76
|
+
## ⌨️ 생산성을 높여주는 단축키
|
|
369
77
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
ctx.beginPath();
|
|
381
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
382
|
-
ctx.fillStyle = theme.wire;
|
|
383
|
-
ctx.fill();
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
```
|
|
78
|
+
| 기능 | 단축키 |
|
|
79
|
+
| :--- | :--- |
|
|
80
|
+
| **노드 삭제** | `Delete` |
|
|
81
|
+
| **수평 정렬** | `A` |
|
|
82
|
+
| **수직 정렬** | `Shift + A` |
|
|
83
|
+
| **그룹 생성** | `Ctrl + G` |
|
|
84
|
+
| **그리드 스냅**| `G` |
|
|
85
|
+
| **되돌리기** | `Ctrl + Z` / `Ctrl + Y` |
|
|
86
|
+
| **영역 선택** | `Ctrl + Drag` |
|
|
387
87
|
|
|
388
88
|
---
|
|
389
89
|
|
|
390
|
-
## 💾
|
|
90
|
+
## 💾 데이터 다루기
|
|
91
|
+
|
|
92
|
+
그래프 통째로 JSON으로 뽑거나, 저장된 데이터를 불러오는 것도 간단합니다.
|
|
391
93
|
|
|
392
94
|
```javascript
|
|
393
|
-
//
|
|
394
|
-
const
|
|
395
|
-
localStorage.setItem("myGraph", JSON.stringify(json));
|
|
95
|
+
// 현재 그래프 저장
|
|
96
|
+
const data = graph.toJSON();
|
|
396
97
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
graph.fromJSON(saved);
|
|
98
|
+
// 데이터 불러오기
|
|
99
|
+
graph.fromJSON(data);
|
|
400
100
|
```
|
|
401
101
|
|
|
402
102
|
---
|
|
403
103
|
|
|
404
|
-
##
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
| ---------------------------- | --------------------------------------------------- |
|
|
408
|
-
| Canvas not rendering | Ensure canvas has explicit width/height |
|
|
409
|
-
| Nodes not executing | Call `start()` or set `autorun: true` |
|
|
410
|
-
| Type errors | Register node types before using them |
|
|
411
|
-
| HTML overlay not interactive | Set `pointerEvents: "auto"` on elements |
|
|
412
|
-
| Performance issues | Limit to <1000 nodes, optimize `onExecute`/`onDraw` |
|
|
104
|
+
## 🔗 관련 링크
|
|
105
|
+
- [GitHub Repository](https://github.com/cheonghakim/html-overlay-node)
|
|
106
|
+
- [Issue Tracker](https://github.com/cheonghakim/html-overlay-node/issues)
|
|
413
107
|
|
|
414
108
|
---
|
|
415
109
|
|
|
416
|
-
##
|
|
417
|
-
|
|
418
|
-
|
|
110
|
+
## 📄 라이선스
|
|
111
|
+
[MIT](LICENSE) © cheonghakim
|
|
112
|
+
G.md).
|
|
419
113
|
|
|
420
114
|
```bash
|
|
421
115
|
npm install # Install dependencies
|
package/src/core/Edge.js
CHANGED
|
@@ -16,8 +16,10 @@ export class Edge {
|
|
|
16
16
|
* @param {string} options.toPort - Target port ID
|
|
17
17
|
*/
|
|
18
18
|
constructor({ id, fromNode, fromPort, toNode, toPort }) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Allow empty strings for port names (exec ports use empty names)
|
|
20
|
+
// Only check for null/undefined
|
|
21
|
+
if (fromNode == null || fromPort == null || toNode == null || toPort == null) {
|
|
22
|
+
throw new Error("Edge requires fromNode, fromPort, toNode, and toPort (null/undefined not allowed)");
|
|
21
23
|
}
|
|
22
24
|
this.id = id ?? randomUUID();
|
|
23
25
|
this.fromNode = fromNode;
|
package/src/core/Graph.js
CHANGED
|
@@ -51,11 +51,13 @@ export class Graph {
|
|
|
51
51
|
const available = Array.from(this.registry.types.keys()).join(", ") || "none";
|
|
52
52
|
throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
|
|
53
53
|
}
|
|
54
|
+
const height = opts.height || def.size?.h || this._calculateDefaultNodeHeight(def);
|
|
55
|
+
|
|
54
56
|
const node = new Node({
|
|
55
57
|
type,
|
|
56
58
|
title: def.title,
|
|
57
|
-
width: def.size?.w,
|
|
58
|
-
height
|
|
59
|
+
width: opts.width || def.size?.w || 140,
|
|
60
|
+
height,
|
|
59
61
|
...opts,
|
|
60
62
|
});
|
|
61
63
|
for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
|
|
@@ -212,11 +214,17 @@ export class Graph {
|
|
|
212
214
|
this.hooks?.emit("graph:serialize", json);
|
|
213
215
|
return json;
|
|
214
216
|
}
|
|
217
|
+
|
|
215
218
|
fromJSON(json) {
|
|
216
|
-
this.clear();
|
|
219
|
+
this.nodes.clear();
|
|
220
|
+
this.edges.clear();
|
|
217
221
|
|
|
218
222
|
// Restore nodes first
|
|
219
223
|
for (const nd of json.nodes) {
|
|
224
|
+
const def = this.registry?.types?.get(nd.type);
|
|
225
|
+
const minH = def ? this._calculateDefaultNodeHeight(def) : 60;
|
|
226
|
+
const height = nd.h !== undefined ? nd.h : minH;
|
|
227
|
+
|
|
220
228
|
const node = new Node({
|
|
221
229
|
id: nd.id,
|
|
222
230
|
type: nd.type,
|
|
@@ -224,10 +232,10 @@ export class Graph {
|
|
|
224
232
|
x: nd.x,
|
|
225
233
|
y: nd.y,
|
|
226
234
|
width: nd.w,
|
|
227
|
-
height:
|
|
235
|
+
height: height,
|
|
228
236
|
});
|
|
237
|
+
|
|
229
238
|
// Call onCreate to initialize node with defaults first
|
|
230
|
-
const def = this.registry?.types?.get(nd.type);
|
|
231
239
|
if (def?.onCreate) {
|
|
232
240
|
def.onCreate(node);
|
|
233
241
|
}
|
|
@@ -264,4 +272,20 @@ export class Graph {
|
|
|
264
272
|
|
|
265
273
|
return this;
|
|
266
274
|
}
|
|
275
|
+
|
|
276
|
+
_calculateDefaultNodeHeight(def) {
|
|
277
|
+
const inCount = def.inputs?.length || 0;
|
|
278
|
+
const outCount = def.outputs?.length || 0;
|
|
279
|
+
const maxPorts = Math.max(inCount, outCount);
|
|
280
|
+
const headerHeight = 26;
|
|
281
|
+
const padding = 8; // tighter buffer
|
|
282
|
+
const portSpacing = 20; // compact spacing
|
|
283
|
+
|
|
284
|
+
let h = headerHeight + padding + (maxPorts * portSpacing) + padding;
|
|
285
|
+
|
|
286
|
+
// Add extra space if it has HTML overlay to prevent overlap with ports
|
|
287
|
+
if (def.html) h += 16;
|
|
288
|
+
|
|
289
|
+
return Math.max(h, 40); // Minimum height 40
|
|
290
|
+
}
|
|
267
291
|
}
|
package/src/core/Node.js
CHANGED
|
@@ -43,28 +43,44 @@ export class Node {
|
|
|
43
43
|
* @param {string} [portType="data"] - Port type: "exec" or "data"
|
|
44
44
|
* @returns {Object} The created port
|
|
45
45
|
*/
|
|
46
|
+
/**
|
|
47
|
+
* Recalculate minimum size based on ports
|
|
48
|
+
*/
|
|
49
|
+
_updateMinSize() {
|
|
50
|
+
const HEADER_HEIGHT = 28;
|
|
51
|
+
const PORT_SPACING = 24;
|
|
52
|
+
const BOTTOM_PADDING = 10;
|
|
53
|
+
|
|
54
|
+
// Calculate required height for inputs and outputs
|
|
55
|
+
const inHeight = HEADER_HEIGHT + 10 + this.inputs.length * PORT_SPACING + BOTTOM_PADDING;
|
|
56
|
+
const outHeight = HEADER_HEIGHT + 10 + this.outputs.length * PORT_SPACING + BOTTOM_PADDING;
|
|
57
|
+
|
|
58
|
+
const minHeight = Math.max(inHeight, outHeight, 60); // Minimum 60px base
|
|
59
|
+
|
|
60
|
+
if (this.size.height < minHeight) {
|
|
61
|
+
this.size.height = minHeight;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
addInput(name, datatype = "any", portType = "data") {
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
// ... existing validation ...
|
|
67
|
+
if (typeof name !== "string" || (portType === "data" && !name)) {
|
|
68
|
+
throw new Error("Input port name must be a string (non-empty for data ports)");
|
|
49
69
|
}
|
|
50
70
|
const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
|
|
51
71
|
this.inputs.push(port);
|
|
72
|
+
this._updateMinSize();
|
|
52
73
|
return port;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
|
-
/**
|
|
56
|
-
* Add an output port to this node
|
|
57
|
-
* @param {string} name - Port name
|
|
58
|
-
* @param {string} [datatype="any"] - Data type for the port
|
|
59
|
-
* @param {string} [portType="data"] - Port type: "exec" or "data"
|
|
60
|
-
* @returns {Object} The created port
|
|
61
|
-
*/
|
|
62
76
|
addOutput(name, datatype = "any", portType = "data") {
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
// ... existing validation ...
|
|
78
|
+
if (typeof name !== "string" || (portType === "data" && !name)) {
|
|
79
|
+
throw new Error("Output port name must be a string (non-empty for data ports)");
|
|
65
80
|
}
|
|
66
81
|
const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
|
|
67
82
|
this.outputs.push(port);
|
|
83
|
+
this._updateMinSize();
|
|
68
84
|
return port;
|
|
69
85
|
}
|
|
70
86
|
}
|