html-overlay-node 0.1.10 → 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 +351 -78
- 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 +45 -0
- package/package.json +1 -1
- package/readme.md +143 -134
- package/src/core/Graph.js +13 -8
- package/src/core/Runner.js +359 -201
- package/src/index.js +12 -26
- package/src/interact/Controller.js +25 -3
- package/src/nodes/math.js +2 -0
- package/src/nodes/util.js +241 -176
- package/src/nodes/value.js +3 -2
- package/src/render/CanvasRenderer.js +884 -784
- package/src/render/HtmlOverlay.js +64 -2
- package/src/ui/HelpOverlay.js +1 -1
|
@@ -45,6 +45,18 @@ export class Controller {
|
|
|
45
45
|
this._onDblClickEvt = this._onDblClick.bind(this);
|
|
46
46
|
|
|
47
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
|
+
});
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
destroy() {
|
|
@@ -406,7 +418,11 @@ export class Controller {
|
|
|
406
418
|
const dy = w.y - this.resizing.startY;
|
|
407
419
|
|
|
408
420
|
const minW = Controller.MIN_NODE_WIDTH;
|
|
409
|
-
|
|
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;
|
|
410
426
|
n.size.width = Math.max(minW, this.resizing.startW + dx);
|
|
411
427
|
n.size.height = Math.max(minH, this.resizing.startH + dy);
|
|
412
428
|
|
|
@@ -760,8 +776,10 @@ export class Controller {
|
|
|
760
776
|
this.render();
|
|
761
777
|
}
|
|
762
778
|
|
|
763
|
-
render() {
|
|
779
|
+
render(time = performance.now()) {
|
|
764
780
|
const tEdge = this.renderTempEdge();
|
|
781
|
+
const runner = this.graph.runner;
|
|
782
|
+
const isStepMode = !!runner && runner.executionMode === "step";
|
|
765
783
|
|
|
766
784
|
// 1. Draw background (grid, canvas-only nodes) on main canvas
|
|
767
785
|
this.renderer.draw(this.graph, {
|
|
@@ -769,7 +787,10 @@ export class Controller {
|
|
|
769
787
|
tempEdge: null, // Don't draw temp edge on background
|
|
770
788
|
boxSelecting: this.boxSelecting,
|
|
771
789
|
activeEdges: this.activeEdges || new Set(),
|
|
790
|
+
activeEdgeTimes: this.activeEdgeTimes,
|
|
772
791
|
drawEdges: !this.edgeRenderer, // Only draw edges here if no separate edge renderer
|
|
792
|
+
time,
|
|
793
|
+
loopActiveEdges: isStepMode,
|
|
773
794
|
});
|
|
774
795
|
|
|
775
796
|
// 2. HTML Overlay layer (HTML nodes at z-index 10)
|
|
@@ -788,8 +809,9 @@ export class Controller {
|
|
|
788
809
|
activeEdgeTimes: this.activeEdgeTimes,
|
|
789
810
|
activeNodes: this.activeNodes,
|
|
790
811
|
selection: this.selection,
|
|
791
|
-
time
|
|
812
|
+
time,
|
|
792
813
|
tempEdge: tEdge,
|
|
814
|
+
loopActiveEdges: isStepMode,
|
|
793
815
|
});
|
|
794
816
|
|
|
795
817
|
this.edgeRenderer._resetTransform();
|
package/src/nodes/math.js
CHANGED
|
@@ -32,6 +32,7 @@ export function registerMathNodes(registry) {
|
|
|
32
32
|
node.state.result = result;
|
|
33
33
|
console.log("[Add] a:", a, "b:", b, "result:", result);
|
|
34
34
|
setOutput("result", result);
|
|
35
|
+
setOutput("exec", true);
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
38
|
|
|
@@ -77,6 +78,7 @@ export function registerMathNodes(registry) {
|
|
|
77
78
|
}
|
|
78
79
|
console.log("[Multiply] a:", a, "b:", b, "result:", result);
|
|
79
80
|
setOutput("result", result);
|
|
81
|
+
setOutput("exec", true);
|
|
80
82
|
},
|
|
81
83
|
});
|
|
82
84
|
|
package/src/nodes/util.js
CHANGED
|
@@ -1,176 +1,241 @@
|
|
|
1
|
-
// Duration each exec edge stays active during sequential animation (ms)
|
|
2
|
-
const STEP_DURATION = 620;
|
|
3
|
-
|
|
4
|
-
export function registerUtilNodes(registry) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1
|
+
// Duration each exec edge stays active during sequential animation (ms)
|
|
2
|
+
const STEP_DURATION = 620;
|
|
3
|
+
|
|
4
|
+
export function registerUtilNodes(registry) {
|
|
5
|
+
registry.register("util/Trigger", {
|
|
6
|
+
title: "Trigger",
|
|
7
|
+
color: "#f7cb4d", // event (amber)
|
|
8
|
+
size: { w: 140, h: 100 },
|
|
9
|
+
outputs: [{ name: "exec", portType: "exec" }],
|
|
10
|
+
html: {
|
|
11
|
+
init(node, el, { body }) {
|
|
12
|
+
el.classList.add("node-overlay");
|
|
13
|
+
|
|
14
|
+
body.style.display = "flex";
|
|
15
|
+
body.style.alignItems = "center";
|
|
16
|
+
body.style.justifyContent = "center";
|
|
17
|
+
|
|
18
|
+
const button = document.createElement("button");
|
|
19
|
+
button.className = "premium-button";
|
|
20
|
+
button.textContent = "Execute";
|
|
21
|
+
button.style.width = "100%";
|
|
22
|
+
button.style.textTransform = "uppercase";
|
|
23
|
+
button.style.letterSpacing = "1px";
|
|
24
|
+
button.style.marginTop = "22px"; // Push below exec port label (port bottom ~y=50)
|
|
25
|
+
|
|
26
|
+
button.addEventListener("click", (e) => {
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
|
|
30
|
+
if (node.state._firing) return;
|
|
31
|
+
|
|
32
|
+
const editor = window.editor;
|
|
33
|
+
if (!editor?.controller || !editor?.runner) return;
|
|
34
|
+
|
|
35
|
+
const controller = editor.controller;
|
|
36
|
+
const runner = editor.runner;
|
|
37
|
+
|
|
38
|
+
node.state.triggered = true;
|
|
39
|
+
node.state._firing = true;
|
|
40
|
+
|
|
41
|
+
// If in Step Mode, just prepare the plan and wait for manual clicks
|
|
42
|
+
if (runner.executionMode === "step") {
|
|
43
|
+
runner.startStepping(node.id);
|
|
44
|
+
node.state.triggered = false;
|
|
45
|
+
node.state._firing = false;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Active state styling for Run mode
|
|
50
|
+
button.style.borderColor = "#4f62c0";
|
|
51
|
+
button.style.color = "#7080d8";
|
|
52
|
+
button.style.background = "rgba(79,98,192,0.12)";
|
|
53
|
+
|
|
54
|
+
const { connectedEdges, execEdgeOrder } = runner.runOnce(node.id, 0);
|
|
55
|
+
|
|
56
|
+
controller.activeEdgeTimes = new Map();
|
|
57
|
+
|
|
58
|
+
if (execEdgeOrder.length > 0) {
|
|
59
|
+
// Sequential: animate one exec edge at a time
|
|
60
|
+
const startTime = performance.now();
|
|
61
|
+
const totalDuration = execEdgeOrder.length * STEP_DURATION + 80;
|
|
62
|
+
|
|
63
|
+
const animate = () => {
|
|
64
|
+
const now = performance.now();
|
|
65
|
+
const elapsed = now - startTime;
|
|
66
|
+
const step = Math.floor(elapsed / STEP_DURATION);
|
|
67
|
+
|
|
68
|
+
const activeNow = new Set();
|
|
69
|
+
const activeNodeNow = new Set();
|
|
70
|
+
if (step < execEdgeOrder.length) {
|
|
71
|
+
const edgeId = execEdgeOrder[step];
|
|
72
|
+
activeNow.add(edgeId);
|
|
73
|
+
if (!controller.activeEdgeTimes.has(edgeId)) {
|
|
74
|
+
controller.activeEdgeTimes.set(edgeId, startTime + step * STEP_DURATION);
|
|
75
|
+
}
|
|
76
|
+
// Highlight the destination node of this exec edge
|
|
77
|
+
const edge = runner.graph.edges.get(edgeId);
|
|
78
|
+
if (edge?.toNode) activeNodeNow.add(edge.toNode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
controller.activeEdges = activeNow;
|
|
82
|
+
controller.activeNodes = activeNodeNow;
|
|
83
|
+
controller.render();
|
|
84
|
+
|
|
85
|
+
if (elapsed < totalDuration) {
|
|
86
|
+
requestAnimationFrame(animate);
|
|
87
|
+
} else {
|
|
88
|
+
_resetTrigger(controller, node, button);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
requestAnimationFrame(animate);
|
|
92
|
+
} else if (connectedEdges.size > 0) {
|
|
93
|
+
// Fallback: all data edges at once
|
|
94
|
+
const startTime = performance.now();
|
|
95
|
+
const totalDuration = STEP_DURATION;
|
|
96
|
+
const now = performance.now();
|
|
97
|
+
for (const id of connectedEdges) {
|
|
98
|
+
controller.activeEdgeTimes.set(id, now);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const animate = () => {
|
|
102
|
+
controller.activeEdges = connectedEdges;
|
|
103
|
+
controller.render();
|
|
104
|
+
if (performance.now() - startTime < totalDuration) {
|
|
105
|
+
requestAnimationFrame(animate);
|
|
106
|
+
} else {
|
|
107
|
+
_resetTrigger(controller, node, button);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
requestAnimationFrame(animate);
|
|
111
|
+
} else {
|
|
112
|
+
_resetTrigger(controller, node, button);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
body.appendChild(button);
|
|
117
|
+
el._btn = button;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
onExecute(node, { setOutput }) {
|
|
121
|
+
if (node.state.triggered) {
|
|
122
|
+
setOutput("exec", true);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
registry.register("util/Watch", {
|
|
128
|
+
title: "Watch",
|
|
129
|
+
color: "#10b981", // info (emerald)
|
|
130
|
+
size: { w: 180, h: 130 },
|
|
131
|
+
inputs: [
|
|
132
|
+
{ name: "exec", portType: "exec" },
|
|
133
|
+
{ name: "value", portType: "data", datatype: "any" },
|
|
134
|
+
],
|
|
135
|
+
outputs: [
|
|
136
|
+
{ name: "exec", portType: "exec" },
|
|
137
|
+
{ name: "value", portType: "data", datatype: "any" },
|
|
138
|
+
],
|
|
139
|
+
onCreate(node) {
|
|
140
|
+
node.state.displayValue = "---";
|
|
141
|
+
},
|
|
142
|
+
html: {
|
|
143
|
+
init(node, el, { body }) {
|
|
144
|
+
el.classList.add("node-overlay");
|
|
145
|
+
body.style.display = "flex";
|
|
146
|
+
body.style.alignItems = "center";
|
|
147
|
+
body.style.justifyContent = "center";
|
|
148
|
+
body.style.paddingTop = "44px"; // Push display below both port rows (y=44, y=64)
|
|
149
|
+
|
|
150
|
+
const container = document.createElement("div");
|
|
151
|
+
container.className = "watch-display";
|
|
152
|
+
container.style.width = "90%";
|
|
153
|
+
container.style.padding = "10px";
|
|
154
|
+
container.style.background = "rgba(0,0,0,0.2)";
|
|
155
|
+
container.style.borderRadius = "4px";
|
|
156
|
+
container.style.border = "1px solid rgba(255,255,255,0.05)";
|
|
157
|
+
container.style.textAlign = "center";
|
|
158
|
+
container.style.fontFamily = "'JetBrains Mono', 'Fira Code', monospace";
|
|
159
|
+
container.style.fontSize = "12px";
|
|
160
|
+
container.style.color = "#10b981";
|
|
161
|
+
container.style.textShadow = "0 0 10px rgba(16,185,129,0.3)";
|
|
162
|
+
container.textContent = node.state.displayValue || "---";
|
|
163
|
+
|
|
164
|
+
body.appendChild(container);
|
|
165
|
+
node._display = container;
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
onExecute(node, { getInput, setOutput }) {
|
|
169
|
+
const val = getInput("value");
|
|
170
|
+
console.log("[Watch] onExecute called, value:", val);
|
|
171
|
+
|
|
172
|
+
// Format value for display
|
|
173
|
+
let display = "---";
|
|
174
|
+
if (val !== undefined && val !== null) {
|
|
175
|
+
display = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
node.state.displayValue = display;
|
|
179
|
+
if (node._display) {
|
|
180
|
+
node._display.textContent = display;
|
|
181
|
+
// Add a brief glow effect
|
|
182
|
+
node._display.style.color = "#ffffff";
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
if (node._display) node._display.style.color = "#10b981";
|
|
185
|
+
}, 100);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Pass through
|
|
189
|
+
setOutput("exec", true); // exec signal
|
|
190
|
+
setOutput("value", val); // data value
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
registry.register("util/Print", {
|
|
195
|
+
title: "Print",
|
|
196
|
+
color: "#10b981", // info (emerald)
|
|
197
|
+
size: { w: 140 },
|
|
198
|
+
inputs: [
|
|
199
|
+
{ name: "", portType: "exec" },
|
|
200
|
+
{ name: "value", portType: "data", datatype: "any" },
|
|
201
|
+
],
|
|
202
|
+
outputs: [{ name: "", portType: "exec" }],
|
|
203
|
+
onExecute(node, { getInput, setOutput }) {
|
|
204
|
+
const value = getInput("value");
|
|
205
|
+
console.log("[Print]", value);
|
|
206
|
+
setOutput("", true);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
registry.register("util/Timer", {
|
|
211
|
+
title: "Timer",
|
|
212
|
+
color: "#7baaf7", // event (amber)
|
|
213
|
+
size: { w: 140 },
|
|
214
|
+
inputs: [
|
|
215
|
+
{ name: "", portType: "exec" },
|
|
216
|
+
{ name: "delay (ms)", portType: "data", datatype: "number" },
|
|
217
|
+
],
|
|
218
|
+
outputs: [{ name: "", portType: "exec" }],
|
|
219
|
+
async onExecute(node, { getInput, setOutput }) {
|
|
220
|
+
const delay = getInput("delay (ms)") || 0;
|
|
221
|
+
await new Promise((resolve) => {
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
resolve();
|
|
224
|
+
}, delay);
|
|
225
|
+
});
|
|
226
|
+
setOutput("", true);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _resetTrigger(controller, node, button) {
|
|
232
|
+
controller.activeEdges = new Set();
|
|
233
|
+
controller.activeEdgeTimes = new Map();
|
|
234
|
+
controller.activeNodes = new Set();
|
|
235
|
+
controller.render();
|
|
236
|
+
node.state.triggered = false;
|
|
237
|
+
node.state._firing = false;
|
|
238
|
+
button.style.borderColor = "#383858";
|
|
239
|
+
button.style.color = "#8888aa";
|
|
240
|
+
button.style.background = "transparent";
|
|
241
|
+
}
|
package/src/nodes/value.js
CHANGED
|
@@ -8,7 +8,7 @@ export function registerValueNodes(registry) {
|
|
|
8
8
|
registry.register("value/Number", {
|
|
9
9
|
title: "Number",
|
|
10
10
|
color: "#3b82f6", // data (blue)
|
|
11
|
-
size: { w: 140 },
|
|
11
|
+
size: { w: 140, h: 90 },
|
|
12
12
|
outputs: [{ name: "value", portType: "data", datatype: "number" }],
|
|
13
13
|
onCreate(node) {
|
|
14
14
|
node.state.value = 0;
|
|
@@ -22,8 +22,9 @@ export function registerValueNodes(registry) {
|
|
|
22
22
|
el.classList.add("node-overlay");
|
|
23
23
|
|
|
24
24
|
body.style.display = "flex";
|
|
25
|
-
body.style.alignItems = "
|
|
25
|
+
body.style.alignItems = "flex-start"; // Don't center vertically — push to top of padded area
|
|
26
26
|
body.style.justifyContent = "center";
|
|
27
|
+
body.style.paddingTop = "26px"; // Push input below value port (port bottom ~y=50)
|
|
27
28
|
|
|
28
29
|
const input = document.createElement("input");
|
|
29
30
|
input.className = "premium-input";
|