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.
@@ -1,211 +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
- * @param {string} startNodeId - ID of the node to start from
58
- * @param {number} dt - Delta time
59
- */
60
- runOnce(startNodeId, dt = 0) {
61
- console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
62
-
63
- const executedNodes = [];
64
- const allConnectedNodes = new Set();
65
- let currentNodeId = startNodeId;
66
-
67
- // Follow exec flow
68
- while (currentNodeId) {
69
- const node = this.graph.nodes.get(currentNodeId);
70
- if (!node) {
71
- console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
72
- break;
73
- }
74
-
75
- executedNodes.push(currentNodeId);
76
- allConnectedNodes.add(currentNodeId);
77
- console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
78
-
79
- // Find and add data dependency nodes (nodes providing input data)
80
- for (const input of node.inputs) {
81
- if (input.portType === "data") {
82
- // Find edge feeding this data input
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
- // Execute data source node before current node
89
- this.executeNode(edge.fromNode, dt);
90
- }
91
- }
92
- }
93
- }
94
- }
95
-
96
- // Execute current node
97
- this.executeNode(currentNodeId, dt);
98
-
99
- // Find next node via exec output
100
- currentNodeId = this.findNextExecNode(currentNodeId);
101
- }
102
-
103
- console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
104
-
105
- // Find all edges involved (both exec and data)
106
- const connectedEdges = new Set();
107
- for (const edge of this.graph.edges.values()) {
108
- if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
109
- connectedEdges.add(edge.id);
110
- }
111
- }
112
-
113
- console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
114
- return { connectedNodes: allConnectedNodes, connectedEdges };
115
- }
116
-
117
- /**
118
- * Find the next node to execute by following exec output
119
- * @param {string} nodeId - Current node ID
120
- * @returns {string|null} Next node ID or null
121
- */
122
- findNextExecNode(nodeId) {
123
- const node = this.graph.nodes.get(nodeId);
124
- if (!node) return null;
125
-
126
- // Find exec output port
127
- const execOutput = node.outputs.find(p => p.portType === "exec");
128
- if (!execOutput) return null;
129
-
130
- // Find edge from exec output
131
- for (const edge of this.graph.edges.values()) {
132
- if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
133
- return edge.toNode;
134
- }
135
- }
136
-
137
- return null;
138
- }
139
-
140
- /**
141
- * Execute a single node
142
- * @param {string} nodeId - Node ID to execute
143
- * @param {number} dt - Delta time
144
- */
145
- executeNode(nodeId, dt) {
146
- const node = this.graph.nodes.get(nodeId);
147
- if (!node) return;
148
-
149
- const def = this.registry.types.get(node.type);
150
- if (!def?.onExecute) return;
151
-
152
- try {
153
- def.onExecute(node, {
154
- dt,
155
- graph: this.graph,
156
- getInput: (portName) => {
157
- const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
158
- return p ? this.graph.getInput(node.id, p.id) : undefined;
159
- },
160
- setOutput: (portName, value) => {
161
- const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
162
- if (p) {
163
- // Write directly to current buffer so other nodes can read it immediately
164
- const key = `${node.id}:${p.id}`;
165
- this.graph._curBuf().set(key, value);
166
- }
167
- },
168
- });
169
- } catch (err) {
170
- this.hooks?.emit?.("error", err);
171
- }
172
- }
173
-
174
- start() {
175
- if (this.running) return;
176
- this.running = true;
177
- this._last = 0;
178
- this.hooks?.emit?.("runner:start");
179
-
180
- const loop = (t) => {
181
- if (!this.running) return;
182
- const dtMs = this._last ? t - this._last : 0;
183
- this._last = t;
184
- const dt = dtMs / 1000; // seconds
185
-
186
- // 1) 스텝 실행
187
- this.step(this.cyclesPerFrame, dt);
188
-
189
- // 2) 프레임 훅 (렌더러/컨트롤러는 여기서 running, time, dt를 받아 표현 업데이트)
190
- this.hooks?.emit?.("runner:tick", {
191
- time: t,
192
- dt,
193
- running: true,
194
- cps: this.cyclesPerFrame,
195
- });
196
-
197
- this._raf = requestAnimationFrame(loop);
198
- };
199
-
200
- this._raf = requestAnimationFrame(loop);
201
- }
202
-
203
- stop() {
204
- if (!this.running) return;
205
- this.running = false;
206
- if (this._raf) cancelAnimationFrame(this._raf);
207
- this._raf = null;
208
- this._last = 0;
209
- this.hooks?.emit?.("runner:stop");
210
- }
211
- }
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
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Default Context Menu Setup
3
+ *
4
+ * This module provides the default context menu configuration.
5
+ * Users can import and use this directly, modify it, or create their own.
6
+ *
7
+ * @example
8
+ * import { setupDefaultContextMenu } from "html-overlay-node/defaults";
9
+ * setupDefaultContextMenu(editor.contextMenu, { controller, graph, hooks });
10
+ */
11
+
12
+ import { RemoveNodeCmd, ChangeGroupColorCmd } from "../core/commands.js";
13
+
14
+ /**
15
+ * Setup default context menu items
16
+ * @param {ContextMenu} contextMenu - Context menu instance
17
+ * @param {Object} options - Configuration options
18
+ * @param {Controller} options.controller - Controller instance
19
+ * @param {Graph} options.graph - Graph instance
20
+ * @param {Hooks} options.hooks - Hooks instance
21
+ */
22
+ export function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
23
+ // Add Node submenu (canvas background only)
24
+ // Use a function to dynamically generate node types when menu is shown
25
+ const getNodeTypes = () => {
26
+ const nodeTypes = [];
27
+ for (const [key, typeDef] of graph.registry.types.entries()) {
28
+ nodeTypes.push({
29
+ id: `add-${key}`,
30
+ label: typeDef.title || key,
31
+ action: () => {
32
+ // Get world position from context menu
33
+ const worldPos = contextMenu.worldPosition || { x: 100, y: 100 };
34
+
35
+ // Add node at click position
36
+ const node = graph.addNode(key, {
37
+ x: worldPos.x,
38
+ y: worldPos.y,
39
+ });
40
+
41
+ hooks?.emit("node:updated", node);
42
+ controller.render(); // Update minimap and canvas
43
+ },
44
+ });
45
+ }
46
+ return nodeTypes;
47
+ };
48
+
49
+ contextMenu.addItem("add-node", "Add Node", {
50
+ condition: (target) => !target,
51
+ submenu: getNodeTypes, // Pass function instead of array
52
+ order: 5,
53
+ });
54
+
55
+ // Delete Node (for all nodes except groups)
56
+ contextMenu.addItem("delete-node", "Delete Node", {
57
+ condition: (target) => target && target.type !== "core/Group",
58
+ action: (target) => {
59
+ const cmd = RemoveNodeCmd(graph, target);
60
+ controller.stack.exec(cmd);
61
+ hooks?.emit("node:updated", target);
62
+ },
63
+ order: 10,
64
+ });
65
+
66
+ // Change Group Color (for groups only) - with submenu
67
+ const colors = [
68
+ { name: "Default", color: "#39424e" },
69
+ { name: "Slate", color: "#4a5568" },
70
+ { name: "Gray", color: "#2d3748" },
71
+ { name: "Blue", color: "#1a365d" },
72
+ { name: "Green", color: "#22543d" },
73
+ { name: "Red", color: "#742a2a" },
74
+ { name: "Purple", color: "#44337a" },
75
+ ];
76
+
77
+ contextMenu.addItem("change-group-color", "Change Color", {
78
+ condition: (target) => target && target.type === "core/Group",
79
+ submenu: colors.map((colorInfo) => ({
80
+ id: `color-${colorInfo.color}`,
81
+ label: colorInfo.name,
82
+ color: colorInfo.color,
83
+ action: (target) => {
84
+ const currentColor = target.state.color || "#39424e";
85
+ const cmd = ChangeGroupColorCmd(target, currentColor, colorInfo.color);
86
+ controller.stack.exec(cmd);
87
+ hooks?.emit("node:updated", target);
88
+ },
89
+ })),
90
+ order: 20,
91
+ });
92
+
93
+ contextMenu.addItem("delete-group", "Delete Group", {
94
+ condition: (target) => target && target.type === "core/Group",
95
+ action: (target) => {
96
+ const cmd = RemoveNodeCmd(graph, target);
97
+ controller.stack.exec(cmd);
98
+ hooks?.emit("node:updated", target);
99
+ },
100
+ order: 20,
101
+ });
102
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Default Configurations
3
+ * Export all default setup functions
4
+ */
5
+
6
+ export { setupDefaultContextMenu } from "./contextMenu.js";