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.
@@ -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
- isRunning() {
14
- return this.running;
15
- }
16
-
17
- // 실행 도중에도 CPS 변경 가능
18
- setCyclesPerFrame(n) {
19
- this.cyclesPerFrame = Math.max(1, n | 0);
20
- }
21
-
22
- step(cycles = 1, dt = 0) {
23
- const nCycles = Math.max(1, cycles | 0);
24
- for (let c = 0; c < nCycles; c++) {
25
- for (const node of this.graph.nodes.values()) {
26
- const def = this.registry.types.get(node.type);
27
- if (def?.onExecute) {
28
- try {
29
- def.onExecute(node, {
30
- dt,
31
- graph: this.graph,
32
- getInput: (portName) => {
33
- const p =
34
- node.inputs.find((i) => i.name === portName) ||
35
- node.inputs[0];
36
- return p ? this.graph.getInput(node.id, p.id) : undefined;
37
- },
38
- setOutput: (portName, value) => {
39
- const p =
40
- node.outputs.find((o) => o.name === portName) ||
41
- node.outputs[0];
42
- if (p) this.graph.setOutput(node.id, p.id, value);
43
- },
44
- });
45
- } catch (err) {
46
- this.hooks?.emit?.("error", err);
47
- }
48
- }
49
- }
50
- // commit writes for this cycle
51
- this.graph.swapBuffers();
52
- }
53
- }
54
-
55
- /**
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
60
- */
61
- runOnce(startNodeId, dt = 0) {
62
- console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
63
-
64
- const executedNodes = [];
65
- const allConnectedNodes = new Set();
66
- const queue = [startNodeId];
67
- const visited = new Set(); // Prevent infinite loops
68
-
69
- // Queue-based traversal for branching execution
70
- while (queue.length > 0) {
71
- const currentNodeId = queue.shift();
72
-
73
- // Skip if already executed (prevents cycles)
74
- if (visited.has(currentNodeId)) continue;
75
- visited.add(currentNodeId);
76
-
77
- const node = this.graph.nodes.get(currentNodeId);
78
- if (!node) {
79
- console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
80
- continue;
81
- }
82
-
83
- executedNodes.push(currentNodeId);
84
- allConnectedNodes.add(currentNodeId);
85
- console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
86
-
87
- // Find and add data dependency nodes (nodes providing input data)
88
- for (const input of node.inputs) {
89
- if (input.portType === "data") {
90
- // Find edge feeding this data input
91
- for (const edge of this.graph.edges.values()) {
92
- if (edge.toNode === currentNodeId && edge.toPort === input.id) {
93
- const sourceNode = this.graph.nodes.get(edge.fromNode);
94
- if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
95
- allConnectedNodes.add(edge.fromNode);
96
- // Execute data source node before current node
97
- this.executeNode(edge.fromNode, dt);
98
- }
99
- }
100
- }
101
- }
102
- }
103
-
104
- // Execute current node
105
- this.executeNode(currentNodeId, dt);
106
-
107
- // Find all next nodes via exec outputs and add to queue
108
- const nextNodes = this.findAllNextExecNodes(currentNodeId);
109
- queue.push(...nextNodes);
110
- }
111
-
112
- console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
113
-
114
- // Find all edges involved (both exec and data)
115
- const connectedEdges = new Set();
116
- for (const edge of this.graph.edges.values()) {
117
- if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
118
- connectedEdges.add(edge.id);
119
- }
120
- }
121
-
122
- console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
123
- return { connectedNodes: allConnectedNodes, connectedEdges };
124
- }
125
-
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
- findAllNextExecNodes(nodeId) {
133
- const node = this.graph.nodes.get(nodeId);
134
- if (!node) return [];
135
-
136
- // Find all exec output ports
137
- const execOutputs = node.outputs.filter(p => p.portType === "exec");
138
- if (execOutputs.length === 0) return [];
139
-
140
- const nextNodes = [];
141
-
142
- // Find all edges from exec outputs
143
- for (const execOutput of execOutputs) {
144
- for (const edge of this.graph.edges.values()) {
145
- if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
146
- nextNodes.push(edge.toNode);
147
- }
148
- }
149
- }
150
-
151
- return nextNodes;
152
- }
153
-
154
- /**
155
- * Execute a single node
156
- * @param {string} nodeId - Node ID to execute
157
- * @param {number} dt - Delta time
158
- */
159
- executeNode(nodeId, dt) {
160
- const node = this.graph.nodes.get(nodeId);
161
- if (!node) return;
162
-
163
- const def = this.registry.types.get(node.type);
164
- if (!def?.onExecute) return;
165
-
166
- try {
167
- def.onExecute(node, {
168
- dt,
169
- graph: this.graph,
170
- getInput: (portName) => {
171
- const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
172
- return p ? this.graph.getInput(node.id, p.id) : undefined;
173
- },
174
- setOutput: (portName, value) => {
175
- const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
176
- if (p) {
177
- // Write directly to current buffer so other nodes can read it immediately
178
- const key = `${node.id}:${p.id}`;
179
- this.graph._curBuf().set(key, value);
180
- }
181
- },
182
- });
183
- } catch (err) {
184
- this.hooks?.emit?.("error", err);
185
- }
186
- }
187
-
188
- start() {
189
- if (this.running) return;
190
- this.running = true;
191
- this._last = 0;
192
- this.hooks?.emit?.("runner:start");
193
-
194
- const loop = (t) => {
195
- if (!this.running) return;
196
- const dtMs = this._last ? t - this._last : 0;
197
- this._last = t;
198
- const dt = dtMs / 1000; // seconds
199
-
200
- // 1) 스텝 실행
201
- this.step(this.cyclesPerFrame, dt);
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 || new Set(),
783
- running: false,
787
+ activeEdges: this.activeEdges,
788
+ activeEdgeTimes: this.activeEdgeTimes,
789
+ activeNodes: this.activeNodes,
790
+ selection: this.selection,
784
791
  time: performance.now(),
785
- tempEdge: tEdge, // Draw temp edge on edge layer
792
+ tempEdge: tEdge,
786
793
  });
787
794
 
788
795
  this.edgeRenderer._resetTransform();