html-overlay-node 0.1.9 → 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 +704 -298
- 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 +1 -1
- package/readme.md +58 -364
- package/src/core/Graph.js +29 -5
- package/src/core/Runner.js +201 -225
- package/src/index.js +17 -0
- package/src/interact/Controller.js +10 -3
- package/src/nodes/core.js +55 -77
- package/src/nodes/logic.js +51 -48
- package/src/nodes/math.js +21 -8
- package/src/nodes/util.js +176 -134
- package/src/nodes/value.js +86 -102
- package/src/render/CanvasRenderer.js +784 -704
- package/src/render/HtmlOverlay.js +1 -1
- package/src/render/hitTest.js +5 -2
- package/src/ui/HelpOverlay.js +158 -0
- package/src/ui/PropertyPanel.css +58 -27
- package/src/ui/PropertyPanel.js +441 -268
package/src/core/Runner.js
CHANGED
|
@@ -1,225 +1,201 @@
|
|
|
1
|
-
export class Runner {
|
|
2
|
-
constructor({ graph, registry, hooks, cyclesPerFrame = 1 }) {
|
|
3
|
-
this.graph = graph;
|
|
4
|
-
this.registry = registry;
|
|
5
|
-
this.hooks = hooks;
|
|
6
|
-
this.running = false;
|
|
7
|
-
this._raf = null;
|
|
8
|
-
this._last = 0;
|
|
9
|
-
this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// 2) 프레임 훅 (렌더러/컨트롤러는 여기서 running, time, dt를 받아 표현 업데이트)
|
|
204
|
-
this.hooks?.emit?.("runner:tick", {
|
|
205
|
-
time: t,
|
|
206
|
-
dt,
|
|
207
|
-
running: true,
|
|
208
|
-
cps: this.cyclesPerFrame,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
this._raf = requestAnimationFrame(loop);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
this._raf = requestAnimationFrame(loop);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
stop() {
|
|
218
|
-
if (!this.running) return;
|
|
219
|
-
this.running = false;
|
|
220
|
-
if (this._raf) cancelAnimationFrame(this._raf);
|
|
221
|
-
this._raf = null;
|
|
222
|
-
this._last = 0;
|
|
223
|
-
this.hooks?.emit?.("runner:stop");
|
|
224
|
-
}
|
|
225
|
-
}
|
|
1
|
+
export class Runner {
|
|
2
|
+
constructor({ graph, registry, hooks, cyclesPerFrame = 1 }) {
|
|
3
|
+
this.graph = graph;
|
|
4
|
+
this.registry = registry;
|
|
5
|
+
this.hooks = hooks;
|
|
6
|
+
this.running = false;
|
|
7
|
+
this._raf = null;
|
|
8
|
+
this._last = 0;
|
|
9
|
+
this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
isRunning() {
|
|
13
|
+
return this.running;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setCyclesPerFrame(n) {
|
|
17
|
+
this.cyclesPerFrame = Math.max(1, n | 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
step(cycles = 1, dt = 0) {
|
|
21
|
+
const nCycles = Math.max(1, cycles | 0);
|
|
22
|
+
for (let c = 0; c < nCycles; c++) {
|
|
23
|
+
for (const node of this.graph.nodes.values()) {
|
|
24
|
+
const def = this.registry.types.get(node.type);
|
|
25
|
+
if (def?.onExecute) {
|
|
26
|
+
try {
|
|
27
|
+
def.onExecute(node, {
|
|
28
|
+
dt,
|
|
29
|
+
graph: this.graph,
|
|
30
|
+
getInput: (portName) => {
|
|
31
|
+
const p =
|
|
32
|
+
node.inputs.find((i) => i.name === portName) ||
|
|
33
|
+
node.inputs[0];
|
|
34
|
+
return p ? this.graph.getInput(node.id, p.id) : undefined;
|
|
35
|
+
},
|
|
36
|
+
setOutput: (portName, value) => {
|
|
37
|
+
const p =
|
|
38
|
+
node.outputs.find((o) => o.name === portName) ||
|
|
39
|
+
node.outputs[0];
|
|
40
|
+
if (p) this.graph.setOutput(node.id, p.id, value);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
this.hooks?.emit?.("error", err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.graph.swapBuffers();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute connected nodes once from a starting node.
|
|
54
|
+
* Returns execEdgeOrder: exec edges in the order they were traversed.
|
|
55
|
+
*/
|
|
56
|
+
runOnce(startNodeId, dt = 0) {
|
|
57
|
+
const executedNodes = [];
|
|
58
|
+
const allConnectedNodes = new Set();
|
|
59
|
+
const execEdgeOrder = []; // exec edge IDs in traversal order
|
|
60
|
+
|
|
61
|
+
// Queue items: { nodeId, fromEdgeId }
|
|
62
|
+
const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
|
|
67
|
+
|
|
68
|
+
if (visited.has(currentNodeId)) continue;
|
|
69
|
+
visited.add(currentNodeId);
|
|
70
|
+
|
|
71
|
+
// Record the exec edge that led to this node
|
|
72
|
+
if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
|
|
73
|
+
|
|
74
|
+
const node = this.graph.nodes.get(currentNodeId);
|
|
75
|
+
if (!node) continue;
|
|
76
|
+
|
|
77
|
+
executedNodes.push(currentNodeId);
|
|
78
|
+
allConnectedNodes.add(currentNodeId);
|
|
79
|
+
|
|
80
|
+
// Execute data dependency nodes first
|
|
81
|
+
for (const input of node.inputs) {
|
|
82
|
+
if (input.portType === "data") {
|
|
83
|
+
for (const edge of this.graph.edges.values()) {
|
|
84
|
+
if (edge.toNode === currentNodeId && edge.toPort === input.id) {
|
|
85
|
+
const sourceNode = this.graph.nodes.get(edge.fromNode);
|
|
86
|
+
if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
|
|
87
|
+
allConnectedNodes.add(edge.fromNode);
|
|
88
|
+
this.executeNode(edge.fromNode, dt);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Execute this node
|
|
96
|
+
this.executeNode(currentNodeId, dt);
|
|
97
|
+
|
|
98
|
+
// Find exec output edges and enqueue next nodes
|
|
99
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
100
|
+
for (const execOutput of execOutputs) {
|
|
101
|
+
for (const edge of this.graph.edges.values()) {
|
|
102
|
+
if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
|
|
103
|
+
queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Collect all edges involved (both exec and data)
|
|
110
|
+
const connectedEdges = new Set();
|
|
111
|
+
for (const edge of this.graph.edges.values()) {
|
|
112
|
+
if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
|
|
113
|
+
connectedEdges.add(edge.id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
findAllNextExecNodes(nodeId) {
|
|
121
|
+
const node = this.graph.nodes.get(nodeId);
|
|
122
|
+
if (!node) return [];
|
|
123
|
+
|
|
124
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
125
|
+
if (execOutputs.length === 0) return [];
|
|
126
|
+
|
|
127
|
+
const nextNodes = [];
|
|
128
|
+
for (const execOutput of execOutputs) {
|
|
129
|
+
for (const edge of this.graph.edges.values()) {
|
|
130
|
+
if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
|
|
131
|
+
nextNodes.push(edge.toNode);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return nextNodes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
executeNode(nodeId, dt) {
|
|
139
|
+
const node = this.graph.nodes.get(nodeId);
|
|
140
|
+
if (!node) return;
|
|
141
|
+
|
|
142
|
+
const def = this.registry.types.get(node.type);
|
|
143
|
+
if (!def?.onExecute) return;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
def.onExecute(node, {
|
|
147
|
+
dt,
|
|
148
|
+
graph: this.graph,
|
|
149
|
+
getInput: (portName) => {
|
|
150
|
+
const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
|
|
151
|
+
return p ? this.graph.getInput(node.id, p.id) : undefined;
|
|
152
|
+
},
|
|
153
|
+
setOutput: (portName, value) => {
|
|
154
|
+
const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
|
|
155
|
+
if (p) {
|
|
156
|
+
const key = `${node.id}:${p.id}`;
|
|
157
|
+
this.graph._curBuf().set(key, value);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.hooks?.emit?.("error", err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
start() {
|
|
167
|
+
if (this.running) return;
|
|
168
|
+
this.running = true;
|
|
169
|
+
this._last = 0;
|
|
170
|
+
this.hooks?.emit?.("runner:start");
|
|
171
|
+
|
|
172
|
+
const loop = (t) => {
|
|
173
|
+
if (!this.running) return;
|
|
174
|
+
const dtMs = this._last ? t - this._last : 0;
|
|
175
|
+
this._last = t;
|
|
176
|
+
const dt = dtMs / 1000;
|
|
177
|
+
|
|
178
|
+
this.step(this.cyclesPerFrame, dt);
|
|
179
|
+
|
|
180
|
+
this.hooks?.emit?.("runner:tick", {
|
|
181
|
+
time: t,
|
|
182
|
+
dt,
|
|
183
|
+
running: true,
|
|
184
|
+
cps: this.cyclesPerFrame,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this._raf = requestAnimationFrame(loop);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
this._raf = requestAnimationFrame(loop);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
stop() {
|
|
194
|
+
if (!this.running) return;
|
|
195
|
+
this.running = false;
|
|
196
|
+
if (this._raf) cancelAnimationFrame(this._raf);
|
|
197
|
+
this._raf = null;
|
|
198
|
+
this._last = 0;
|
|
199
|
+
this.hooks?.emit?.("runner:stop");
|
|
200
|
+
}
|
|
201
|
+
}
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { HtmlOverlay } from "./render/HtmlOverlay.js";
|
|
|
10
10
|
import { RemoveNodeCmd, ChangeGroupColorCmd } from "./core/commands.js";
|
|
11
11
|
import { Minimap } from "./minimap/Minimap.js";
|
|
12
12
|
import { PropertyPanel } from "./ui/PropertyPanel.js";
|
|
13
|
+
import { HelpOverlay } from "./ui/HelpOverlay.js";
|
|
13
14
|
import { setupDefaultContextMenu as defaultContextMenuSetup } from "./defaults/contextMenu.js";
|
|
14
15
|
|
|
15
16
|
|
|
@@ -23,6 +24,8 @@ export function createGraphEditor(
|
|
|
23
24
|
showMinimap = true,
|
|
24
25
|
enablePropertyPanel = true,
|
|
25
26
|
propertyPanelContainer = null,
|
|
27
|
+
enableHelp = true,
|
|
28
|
+
helpShortcuts = null,
|
|
26
29
|
setupDefaultContextMenu = true,
|
|
27
30
|
setupContextMenu = null,
|
|
28
31
|
plugins = [],
|
|
@@ -171,6 +174,14 @@ export function createGraphEditor(
|
|
|
171
174
|
});
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
// Initialize Help Overlay if enabled
|
|
178
|
+
let helpOverlay = null;
|
|
179
|
+
if (enableHelp) {
|
|
180
|
+
helpOverlay = new HelpOverlay(container, {
|
|
181
|
+
shortcuts: helpShortcuts,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
174
185
|
const runner = new Runner({ graph, registry, hooks });
|
|
175
186
|
|
|
176
187
|
// Attach runner and controller to graph for node access
|
|
@@ -215,6 +226,11 @@ export function createGraphEditor(
|
|
|
215
226
|
controller.render();
|
|
216
227
|
});
|
|
217
228
|
|
|
229
|
+
hooks.on("graph:deserialize", () => {
|
|
230
|
+
renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
|
|
231
|
+
controller.render();
|
|
232
|
+
});
|
|
233
|
+
|
|
218
234
|
// Note: Example nodes have been moved to src/nodes/
|
|
219
235
|
// Users can import and register them selectively:
|
|
220
236
|
// import { registerAllNodes } from "html-overlay-node/nodes";
|
|
@@ -297,6 +313,7 @@ export function createGraphEditor(
|
|
|
297
313
|
contextMenu.destroy();
|
|
298
314
|
if (propertyPanel) propertyPanel.destroy();
|
|
299
315
|
if (minimap) minimap.destroy();
|
|
316
|
+
if (helpOverlay) helpOverlay.destroy();
|
|
300
317
|
},
|
|
301
318
|
};
|
|
302
319
|
|
|
@@ -25,6 +25,11 @@ export class Controller {
|
|
|
25
25
|
this.gResizing = null;
|
|
26
26
|
this.boxSelecting = null; // { startX, startY, currentX, currentY } - world coords
|
|
27
27
|
|
|
28
|
+
// Edge / node animation state
|
|
29
|
+
this.activeEdges = new Set();
|
|
30
|
+
this.activeEdgeTimes = new Map(); // edge.id → activation timestamp
|
|
31
|
+
this.activeNodes = new Set(); // node IDs currently executing
|
|
32
|
+
|
|
28
33
|
// Feature flags
|
|
29
34
|
this.snapToGrid = true; // Snap nodes to grid (toggle with G key)
|
|
30
35
|
this.gridSize = 20; // Grid size for snapping
|
|
@@ -779,10 +784,12 @@ export class Controller {
|
|
|
779
784
|
this.edgeRenderer._applyTransform();
|
|
780
785
|
|
|
781
786
|
this.edgeRenderer.drawEdgesOnly(this.graph, {
|
|
782
|
-
activeEdges: this.activeEdges
|
|
783
|
-
|
|
787
|
+
activeEdges: this.activeEdges,
|
|
788
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
789
|
+
activeNodes: this.activeNodes,
|
|
790
|
+
selection: this.selection,
|
|
784
791
|
time: performance.now(),
|
|
785
|
-
tempEdge: tEdge,
|
|
792
|
+
tempEdge: tEdge,
|
|
786
793
|
});
|
|
787
794
|
|
|
788
795
|
this.edgeRenderer._resetTransform();
|