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.
- package/dist/example.json +3 -3
- package/dist/html-overlay-node.es.js +997 -1014
- 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 +9 -8
- package/readme.md +58 -364
- package/src/core/Edge.js +4 -2
- package/src/core/Graph.js +29 -5
- package/src/core/Node.js +27 -11
- package/src/core/Runner.js +201 -211
- package/src/defaults/contextMenu.js +102 -0
- package/src/defaults/index.js +6 -0
- package/src/index.js +85 -793
- package/src/interact/ContextMenu.js +5 -1
- package/src/interact/Controller.js +73 -46
- package/src/nodes/core.js +266 -0
- package/src/nodes/index.js +42 -0
- package/src/nodes/logic.js +60 -0
- package/src/nodes/math.js +99 -0
- package/src/nodes/util.js +176 -0
- package/src/nodes/value.js +100 -0
- package/src/render/CanvasRenderer.js +784 -604
- package/src/render/HtmlOverlay.js +15 -5
- package/src/render/hitTest.js +18 -9
- package/src/ui/HelpOverlay.js +158 -0
- package/src/ui/PropertyPanel.css +58 -27
- package/src/ui/PropertyPanel.js +441 -268
- package/src/utils/utils.js +4 -4
package/src/core/Runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|