html-overlay-node 0.1.9 → 0.1.11
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 +9 -9
- package/dist/html-overlay-node.es.js +1000 -321
- 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/dist/img/favicon.svg +1 -0
- package/index.css +436 -232
- package/package.json +1 -1
- package/readme.md +143 -440
- package/src/core/Graph.js +34 -5
- package/src/core/Runner.js +188 -54
- package/src/index.js +29 -26
- package/src/interact/Controller.js +35 -6
- package/src/nodes/core.js +55 -77
- package/src/nodes/logic.js +51 -48
- package/src/nodes/math.js +23 -8
- package/src/nodes/util.js +238 -131
- package/src/nodes/value.js +87 -102
- package/src/render/CanvasRenderer.js +465 -285
- package/src/render/HtmlOverlay.js +65 -3
- 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
|
@@ -7,14 +7,16 @@ export class Runner {
|
|
|
7
7
|
this._raf = null;
|
|
8
8
|
this._last = 0;
|
|
9
9
|
this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
|
|
10
|
+
this.executionMode = "run"; // "run" or "step"
|
|
11
|
+
this.activePlan = null;
|
|
12
|
+
this.activeStepIndex = -1;
|
|
13
|
+
this.stepCache = new Map();
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
// 외부에서 실행 중인지 확인
|
|
13
16
|
isRunning() {
|
|
14
17
|
return this.running;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
// 실행 도중에도 CPS 변경 가능
|
|
18
20
|
setCyclesPerFrame(n) {
|
|
19
21
|
this.cyclesPerFrame = Math.max(1, n | 0);
|
|
20
22
|
}
|
|
@@ -47,71 +49,72 @@ export class Runner {
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
|
-
// commit writes for this cycle
|
|
51
52
|
this.graph.swapBuffers();
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Execute connected nodes once from a starting node
|
|
57
|
-
*
|
|
58
|
-
* @param {string} startNodeId - ID of the node to start from
|
|
59
|
-
* @param {number} dt - Delta time
|
|
57
|
+
* Execute connected nodes once from a starting node.
|
|
58
|
+
* Returns execEdgeOrder: exec edges in the order they were traversed.
|
|
60
59
|
*/
|
|
61
60
|
runOnce(startNodeId, dt = 0) {
|
|
62
|
-
console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
|
|
63
|
-
|
|
64
61
|
const executedNodes = [];
|
|
65
62
|
const allConnectedNodes = new Set();
|
|
66
|
-
const
|
|
67
|
-
|
|
63
|
+
const execEdgeOrder = []; // exec edge IDs in traversal order
|
|
64
|
+
|
|
65
|
+
// Local output cache: nodeId:portId → value
|
|
66
|
+
// Ensures outputs written by executeNode are immediately readable by subsequent nodes
|
|
67
|
+
const runCache = new Map();
|
|
68
|
+
|
|
69
|
+
// Queue items: { nodeId, fromEdgeId }
|
|
70
|
+
const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
|
|
71
|
+
const visited = new Set();
|
|
68
72
|
|
|
69
|
-
// Queue-based traversal for branching execution
|
|
70
73
|
while (queue.length > 0) {
|
|
71
|
-
const currentNodeId = queue.shift();
|
|
74
|
+
const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
|
|
72
75
|
|
|
73
|
-
// Skip if already executed (prevents cycles)
|
|
74
76
|
if (visited.has(currentNodeId)) continue;
|
|
75
77
|
visited.add(currentNodeId);
|
|
76
78
|
|
|
79
|
+
// Record the exec edge that led to this node
|
|
80
|
+
if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
|
|
81
|
+
|
|
77
82
|
const node = this.graph.nodes.get(currentNodeId);
|
|
78
|
-
if (!node)
|
|
79
|
-
console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
83
|
+
if (!node) continue;
|
|
82
84
|
|
|
83
85
|
executedNodes.push(currentNodeId);
|
|
84
86
|
allConnectedNodes.add(currentNodeId);
|
|
85
|
-
console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
|
|
86
87
|
|
|
87
|
-
//
|
|
88
|
+
// Execute data dependency nodes first
|
|
88
89
|
for (const input of node.inputs) {
|
|
89
90
|
if (input.portType === "data") {
|
|
90
|
-
// Find edge feeding this data input
|
|
91
91
|
for (const edge of this.graph.edges.values()) {
|
|
92
92
|
if (edge.toNode === currentNodeId && edge.toPort === input.id) {
|
|
93
93
|
const sourceNode = this.graph.nodes.get(edge.fromNode);
|
|
94
94
|
if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
|
|
95
95
|
allConnectedNodes.add(edge.fromNode);
|
|
96
|
-
|
|
97
|
-
this.executeNode(edge.fromNode, dt);
|
|
96
|
+
this._executeNodeWithCache(edge.fromNode, dt, runCache);
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
// Execute
|
|
105
|
-
this.
|
|
103
|
+
// Execute this node
|
|
104
|
+
this._executeNodeWithCache(currentNodeId, dt, runCache);
|
|
106
105
|
|
|
107
|
-
// Find
|
|
108
|
-
const
|
|
109
|
-
|
|
106
|
+
// Find exec output edges and enqueue next nodes
|
|
107
|
+
const execOutputPorts = node.outputs.filter((p) => p.portType === "exec");
|
|
108
|
+
for (const execOutput of execOutputPorts) {
|
|
109
|
+
for (const edge of this.graph.edges.values()) {
|
|
110
|
+
if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
|
|
111
|
+
queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// Find all edges involved (both exec and data)
|
|
117
|
+
// Collect all edges involved (both exec and data)
|
|
115
118
|
const connectedEdges = new Set();
|
|
116
119
|
for (const edge of this.graph.edges.values()) {
|
|
117
120
|
if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
|
|
@@ -119,27 +122,163 @@ export class Runner {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setExecutionMode(mode) {
|
|
129
|
+
this.executionMode = mode;
|
|
130
|
+
if (mode === "run") this.resetStepping();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
resetStepping() {
|
|
134
|
+
this.activePlan = null;
|
|
135
|
+
this.activeStepIndex = -1;
|
|
136
|
+
this.stepCache.clear();
|
|
137
|
+
this.hooks?.emit?.("runner:step-updated", { activeNodeId: null });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
buildPlan(startNodeId) {
|
|
141
|
+
const plan = [];
|
|
142
|
+
const allConnectedNodes = new Set();
|
|
143
|
+
const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
|
|
144
|
+
const visited = new Set();
|
|
145
|
+
|
|
146
|
+
while (queue.length > 0) {
|
|
147
|
+
const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
|
|
148
|
+
if (visited.has(currentNodeId)) continue;
|
|
149
|
+
visited.add(currentNodeId);
|
|
150
|
+
allConnectedNodes.add(currentNodeId);
|
|
151
|
+
|
|
152
|
+
const node = this.graph.nodes.get(currentNodeId);
|
|
153
|
+
if (!node) continue;
|
|
154
|
+
|
|
155
|
+
// Collect data dependency nodes (in order) for this exec node
|
|
156
|
+
const dataDeps = [];
|
|
157
|
+
for (const input of node.inputs) {
|
|
158
|
+
if (input.portType === "data") {
|
|
159
|
+
for (const edge of this.graph.edges.values()) {
|
|
160
|
+
if (edge.toNode === currentNodeId && edge.toPort === input.id) {
|
|
161
|
+
const srcId = edge.fromNode;
|
|
162
|
+
if (!allConnectedNodes.has(srcId)) {
|
|
163
|
+
allConnectedNodes.add(srcId);
|
|
164
|
+
dataDeps.push(srcId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Find ALL incoming edges (both exec and data) to highlight them in Step Mode
|
|
172
|
+
const incomingEdges = [];
|
|
173
|
+
for (const edge of this.graph.edges.values()) {
|
|
174
|
+
if (edge.toNode === currentNodeId) {
|
|
175
|
+
incomingEdges.push(edge.id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
plan.push({ nodeId: currentNodeId, fromEdgeId, incomingEdges, dataDeps });
|
|
180
|
+
|
|
181
|
+
// Enqueue next exec nodes
|
|
182
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
183
|
+
for (const execOutput of execOutputs) {
|
|
184
|
+
for (const edge of this.graph.edges.values()) {
|
|
185
|
+
if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
|
|
186
|
+
queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return plan;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
startStepping(startNodeId) {
|
|
196
|
+
this.stepCache.clear();
|
|
197
|
+
this.activePlan = this.buildPlan(startNodeId);
|
|
198
|
+
this.activeStepIndex = 0;
|
|
199
|
+
const step = this.activePlan[0];
|
|
200
|
+
this.hooks?.emit?.("runner:step-updated", {
|
|
201
|
+
activeNodeId: step?.nodeId,
|
|
202
|
+
activeEdgeIds: step?.incomingEdges || [],
|
|
203
|
+
});
|
|
204
|
+
this.start(); // Start the loop to driving animations
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
executeNextStep() {
|
|
208
|
+
if (!this.activePlan || this.activeStepIndex < 0 || this.activeStepIndex >= this.activePlan.length) {
|
|
209
|
+
this.resetStepping();
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const step = this.activePlan[this.activeStepIndex];
|
|
214
|
+
|
|
215
|
+
// Execute data deps
|
|
216
|
+
for (const depId of step.dataDeps) {
|
|
217
|
+
this._executeNodeWithCache(depId, 0, this.stepCache);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Execute main node
|
|
221
|
+
this._executeNodeWithCache(step.nodeId, 0, this.stepCache);
|
|
222
|
+
|
|
223
|
+
this.activeStepIndex++;
|
|
224
|
+
|
|
225
|
+
if (this.activeStepIndex < this.activePlan.length) {
|
|
226
|
+
const nextStep = this.activePlan[this.activeStepIndex];
|
|
227
|
+
this.hooks?.emit?.("runner:step-updated", {
|
|
228
|
+
activeNodeId: nextStep.nodeId,
|
|
229
|
+
activeEdgeIds: nextStep.incomingEdges || [],
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
this.hooks?.emit?.("runner:step-updated", { activeNodeId: null });
|
|
233
|
+
this.resetStepping();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return step.nodeId;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Execute a node using a shared run-local output cache for reliable data passing. */
|
|
240
|
+
_executeNodeWithCache(nodeId, dt, runCache) {
|
|
241
|
+
const node = this.graph.nodes.get(nodeId);
|
|
242
|
+
if (!node) return;
|
|
243
|
+
const def = this.registry.types.get(node.type);
|
|
244
|
+
if (!def?.onExecute) return;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
def.onExecute(node, {
|
|
248
|
+
dt,
|
|
249
|
+
graph: this.graph,
|
|
250
|
+
getInput: (portName) => {
|
|
251
|
+
const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
|
|
252
|
+
if (!p) return undefined;
|
|
253
|
+
for (const edge of this.graph.edges.values()) {
|
|
254
|
+
if (edge.toNode === nodeId && edge.toPort === p.id) {
|
|
255
|
+
const key = `${edge.fromNode}:${edge.fromPort}`;
|
|
256
|
+
// Check run-local cache first, then fall back to graph buffer
|
|
257
|
+
return runCache.has(key) ? runCache.get(key) : this.graph._curBuf().get(key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
},
|
|
262
|
+
setOutput: (portName, value) => {
|
|
263
|
+
const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
|
|
264
|
+
if (p) {
|
|
265
|
+
runCache.set(`${node.id}:${p.id}`, value);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
this.hooks?.emit?.("error", err);
|
|
271
|
+
}
|
|
124
272
|
}
|
|
125
273
|
|
|
126
|
-
/**
|
|
127
|
-
* Find all nodes connected via exec outputs
|
|
128
|
-
* Supports multiple connections from a single exec output
|
|
129
|
-
* @param {string} nodeId - Current node ID
|
|
130
|
-
* @returns {string[]} Array of next node IDs
|
|
131
|
-
*/
|
|
132
274
|
findAllNextExecNodes(nodeId) {
|
|
133
275
|
const node = this.graph.nodes.get(nodeId);
|
|
134
276
|
if (!node) return [];
|
|
135
277
|
|
|
136
|
-
|
|
137
|
-
const execOutputs = node.outputs.filter(p => p.portType === "exec");
|
|
278
|
+
const execOutputs = node.outputs.filter((p) => p.portType === "exec");
|
|
138
279
|
if (execOutputs.length === 0) return [];
|
|
139
280
|
|
|
140
281
|
const nextNodes = [];
|
|
141
|
-
|
|
142
|
-
// Find all edges from exec outputs
|
|
143
282
|
for (const execOutput of execOutputs) {
|
|
144
283
|
for (const edge of this.graph.edges.values()) {
|
|
145
284
|
if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
|
|
@@ -147,15 +286,9 @@ export class Runner {
|
|
|
147
286
|
}
|
|
148
287
|
}
|
|
149
288
|
}
|
|
150
|
-
|
|
151
289
|
return nextNodes;
|
|
152
290
|
}
|
|
153
291
|
|
|
154
|
-
/**
|
|
155
|
-
* Execute a single node
|
|
156
|
-
* @param {string} nodeId - Node ID to execute
|
|
157
|
-
* @param {number} dt - Delta time
|
|
158
|
-
*/
|
|
159
292
|
executeNode(nodeId, dt) {
|
|
160
293
|
const node = this.graph.nodes.get(nodeId);
|
|
161
294
|
if (!node) return;
|
|
@@ -174,7 +307,6 @@ export class Runner {
|
|
|
174
307
|
setOutput: (portName, value) => {
|
|
175
308
|
const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
|
|
176
309
|
if (p) {
|
|
177
|
-
// Write directly to current buffer so other nodes can read it immediately
|
|
178
310
|
const key = `${node.id}:${p.id}`;
|
|
179
311
|
this.graph._curBuf().set(key, value);
|
|
180
312
|
}
|
|
@@ -195,12 +327,14 @@ export class Runner {
|
|
|
195
327
|
if (!this.running) return;
|
|
196
328
|
const dtMs = this._last ? t - this._last : 0;
|
|
197
329
|
this._last = t;
|
|
198
|
-
const dt = dtMs / 1000;
|
|
330
|
+
const dt = dtMs / 1000;
|
|
199
331
|
|
|
200
|
-
//
|
|
201
|
-
|
|
332
|
+
// Only execute nodes automatically in "run" mode.
|
|
333
|
+
// In "step" mode, we only want the loop to fire for animations (via tick event).
|
|
334
|
+
if (this.executionMode === "run") {
|
|
335
|
+
this.step(this.cyclesPerFrame, dt);
|
|
336
|
+
}
|
|
202
337
|
|
|
203
|
-
// 2) 프레임 훅 (렌더러/컨트롤러는 여기서 running, time, dt를 받아 표현 업데이트)
|
|
204
338
|
this.hooks?.emit?.("runner:tick", {
|
|
205
339
|
time: t,
|
|
206
340
|
dt,
|
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
|
|
@@ -179,42 +190,24 @@ export function createGraphEditor(
|
|
|
179
190
|
graph.controller = controller;
|
|
180
191
|
|
|
181
192
|
hooks.on("runner:tick", ({ time, dt }) => {
|
|
182
|
-
|
|
183
|
-
selection: controller.selection,
|
|
184
|
-
tempEdge: controller.connecting ? controller.renderTempEdge() : null, // 필요시 helper
|
|
185
|
-
running: true,
|
|
186
|
-
time,
|
|
187
|
-
dt,
|
|
188
|
-
});
|
|
189
|
-
htmlOverlay.draw(graph, controller.selection);
|
|
193
|
+
controller.render(time);
|
|
190
194
|
});
|
|
191
195
|
hooks.on("runner:start", () => {
|
|
192
|
-
|
|
193
|
-
renderer.draw(graph, {
|
|
194
|
-
selection: controller.selection,
|
|
195
|
-
tempEdge: controller.connecting ? controller.renderTempEdge() : null,
|
|
196
|
-
running: true,
|
|
197
|
-
time: performance.now(),
|
|
198
|
-
dt: 0,
|
|
199
|
-
});
|
|
200
|
-
htmlOverlay.draw(graph, controller.selection);
|
|
196
|
+
controller.render(performance.now());
|
|
201
197
|
});
|
|
202
198
|
hooks.on("runner:stop", () => {
|
|
203
|
-
|
|
204
|
-
renderer.draw(graph, {
|
|
205
|
-
selection: controller.selection,
|
|
206
|
-
tempEdge: controller.connecting ? controller.renderTempEdge() : null,
|
|
207
|
-
running: false,
|
|
208
|
-
time: performance.now(),
|
|
209
|
-
dt: 0,
|
|
210
|
-
});
|
|
211
|
-
htmlOverlay.draw(graph, controller.selection);
|
|
199
|
+
controller.render(performance.now());
|
|
212
200
|
});
|
|
213
201
|
|
|
214
202
|
hooks.on("node:updated", () => {
|
|
215
203
|
controller.render();
|
|
216
204
|
});
|
|
217
205
|
|
|
206
|
+
hooks.on("graph:deserialize", () => {
|
|
207
|
+
renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
|
|
208
|
+
controller.render();
|
|
209
|
+
});
|
|
210
|
+
|
|
218
211
|
// Note: Example nodes have been moved to src/nodes/
|
|
219
212
|
// Users can import and register them selectively:
|
|
220
213
|
// import { registerAllNodes } from "html-overlay-node/nodes";
|
|
@@ -278,6 +271,7 @@ export function createGraphEditor(
|
|
|
278
271
|
},
|
|
279
272
|
graph,
|
|
280
273
|
renderer,
|
|
274
|
+
edgeRenderer, // Expose edge renderer for style changes
|
|
281
275
|
controller, // Expose controller for snap-to-grid access
|
|
282
276
|
runner, // Expose runner for trigger
|
|
283
277
|
minimap, // Expose minimap
|
|
@@ -289,6 +283,14 @@ export function createGraphEditor(
|
|
|
289
283
|
render: () => controller.render(),
|
|
290
284
|
start: () => runner.start(),
|
|
291
285
|
stop: () => runner.stop(),
|
|
286
|
+
setEdgeStyle: (style) => {
|
|
287
|
+
renderer.setEdgeStyle(style);
|
|
288
|
+
edgeRenderer.setEdgeStyle(style);
|
|
289
|
+
},
|
|
290
|
+
setExecutionMode: (mode) => {
|
|
291
|
+
runner.setExecutionMode(mode);
|
|
292
|
+
controller.render(); // Redraw to update overlays
|
|
293
|
+
},
|
|
292
294
|
destroy: () => {
|
|
293
295
|
runner.stop();
|
|
294
296
|
ro.disconnect();
|
|
@@ -297,6 +299,7 @@ export function createGraphEditor(
|
|
|
297
299
|
contextMenu.destroy();
|
|
298
300
|
if (propertyPanel) propertyPanel.destroy();
|
|
299
301
|
if (minimap) minimap.destroy();
|
|
302
|
+
if (helpOverlay) helpOverlay.destroy();
|
|
300
303
|
},
|
|
301
304
|
};
|
|
302
305
|
|
|
@@ -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
|
|
@@ -40,6 +45,18 @@ export class Controller {
|
|
|
40
45
|
this._onDblClickEvt = this._onDblClick.bind(this);
|
|
41
46
|
|
|
42
47
|
this._bindEvents();
|
|
48
|
+
|
|
49
|
+
// Listen for stepping updates from runner
|
|
50
|
+
this.hooks.on("runner:step-updated", ({ activeNodeId, activeEdgeIds = [] }) => {
|
|
51
|
+
this.activeNodes = activeNodeId ? new Set([activeNodeId]) : new Set();
|
|
52
|
+
this.activeEdges = new Set(activeEdgeIds);
|
|
53
|
+
this.activeEdgeTimes.clear(); // Clear previous times to ensure fresh animation
|
|
54
|
+
const now = performance.now();
|
|
55
|
+
for (const edgeId of activeEdgeIds) {
|
|
56
|
+
this.activeEdgeTimes.set(edgeId, now);
|
|
57
|
+
}
|
|
58
|
+
this.render();
|
|
59
|
+
});
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
destroy() {
|
|
@@ -401,7 +418,11 @@ export class Controller {
|
|
|
401
418
|
const dy = w.y - this.resizing.startY;
|
|
402
419
|
|
|
403
420
|
const minW = Controller.MIN_NODE_WIDTH;
|
|
404
|
-
|
|
421
|
+
// Minimum height must fit all port rows
|
|
422
|
+
const maxPorts = Math.max(n.inputs.length, n.outputs.length);
|
|
423
|
+
const minH = maxPorts > 0
|
|
424
|
+
? Math.max(Controller.MIN_NODE_HEIGHT, 42 + maxPorts * 20)
|
|
425
|
+
: Controller.MIN_NODE_HEIGHT;
|
|
405
426
|
n.size.width = Math.max(minW, this.resizing.startW + dx);
|
|
406
427
|
n.size.height = Math.max(minH, this.resizing.startH + dy);
|
|
407
428
|
|
|
@@ -755,8 +776,10 @@ export class Controller {
|
|
|
755
776
|
this.render();
|
|
756
777
|
}
|
|
757
778
|
|
|
758
|
-
render() {
|
|
779
|
+
render(time = performance.now()) {
|
|
759
780
|
const tEdge = this.renderTempEdge();
|
|
781
|
+
const runner = this.graph.runner;
|
|
782
|
+
const isStepMode = !!runner && runner.executionMode === "step";
|
|
760
783
|
|
|
761
784
|
// 1. Draw background (grid, canvas-only nodes) on main canvas
|
|
762
785
|
this.renderer.draw(this.graph, {
|
|
@@ -764,7 +787,10 @@ export class Controller {
|
|
|
764
787
|
tempEdge: null, // Don't draw temp edge on background
|
|
765
788
|
boxSelecting: this.boxSelecting,
|
|
766
789
|
activeEdges: this.activeEdges || new Set(),
|
|
790
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
767
791
|
drawEdges: !this.edgeRenderer, // Only draw edges here if no separate edge renderer
|
|
792
|
+
time,
|
|
793
|
+
loopActiveEdges: isStepMode,
|
|
768
794
|
});
|
|
769
795
|
|
|
770
796
|
// 2. HTML Overlay layer (HTML nodes at z-index 10)
|
|
@@ -779,10 +805,13 @@ export class Controller {
|
|
|
779
805
|
this.edgeRenderer._applyTransform();
|
|
780
806
|
|
|
781
807
|
this.edgeRenderer.drawEdgesOnly(this.graph, {
|
|
782
|
-
activeEdges: this.activeEdges
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
808
|
+
activeEdges: this.activeEdges,
|
|
809
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
810
|
+
activeNodes: this.activeNodes,
|
|
811
|
+
selection: this.selection,
|
|
812
|
+
time,
|
|
813
|
+
tempEdge: tEdge,
|
|
814
|
+
loopActiveEdges: isStepMode,
|
|
786
815
|
});
|
|
787
816
|
|
|
788
817
|
this.edgeRenderer._resetTransform();
|