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.
@@ -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
- * Uses queue-based traversal to support branching exec flows
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 queue = [startNodeId];
67
- const visited = new Set(); // Prevent infinite loops
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
- // Find and add data dependency nodes (nodes providing input data)
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
- // Execute data source node before current node
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 current node
105
- this.executeNode(currentNodeId, dt);
103
+ // Execute this node
104
+ this._executeNodeWithCache(currentNodeId, dt, runCache);
106
105
 
107
- // Find all next nodes via exec outputs and add to queue
108
- const nextNodes = this.findAllNextExecNodes(currentNodeId);
109
- queue.push(...nextNodes);
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
- console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
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
- console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
123
- return { connectedNodes: allConnectedNodes, connectedEdges };
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
- // Find all exec output ports
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; // seconds
330
+ const dt = dtMs / 1000;
199
331
 
200
- // 1) 스텝 실행
201
- this.step(this.cyclesPerFrame, dt);
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
- renderer.draw(graph, {
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
- const minH = Controller.MIN_NODE_HEIGHT;
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 || new Set(),
783
- running: false,
784
- time: performance.now(),
785
- tempEdge: tEdge, // Draw temp edge on edge layer
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();