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.
@@ -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
- 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;
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: performance.now(),
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
- registry.register("util/Trigger", {
6
- title: "Trigger",
7
- color: "#f59e0b", // event (amber)
8
- size: { w: 140 },
9
- outputs: [{ name: "triggered", 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
-
25
- button.addEventListener("click", (e) => {
26
- e.stopPropagation();
27
- e.preventDefault();
28
-
29
- if (node.state._firing) return;
30
-
31
- const editor = window.editor;
32
- if (!editor?.controller || !editor?.runner) return;
33
-
34
- const controller = editor.controller;
35
- const runner = editor.runner;
36
-
37
- node.state.triggered = true;
38
- node.state._firing = true;
39
-
40
- // Active state styling
41
- button.style.borderColor = "#4f62c0";
42
- button.style.color = "#7080d8";
43
- button.style.background = "rgba(79,98,192,0.12)";
44
-
45
- const { connectedEdges, execEdgeOrder } = runner.runOnce(node.id, 0);
46
-
47
- controller.activeEdgeTimes = new Map();
48
-
49
- if (execEdgeOrder.length > 0) {
50
- // Sequential: animate one exec edge at a time
51
- const startTime = performance.now();
52
- const totalDuration = execEdgeOrder.length * STEP_DURATION + 80;
53
-
54
- const animate = () => {
55
- const now = performance.now();
56
- const elapsed = now - startTime;
57
- const step = Math.floor(elapsed / STEP_DURATION);
58
-
59
- const activeNow = new Set();
60
- const activeNodeNow = new Set();
61
- if (step < execEdgeOrder.length) {
62
- const edgeId = execEdgeOrder[step];
63
- activeNow.add(edgeId);
64
- if (!controller.activeEdgeTimes.has(edgeId)) {
65
- controller.activeEdgeTimes.set(edgeId, startTime + step * STEP_DURATION);
66
- }
67
- // Highlight the destination node of this exec edge
68
- const edge = runner.graph.edges.get(edgeId);
69
- if (edge?.toNode) activeNodeNow.add(edge.toNode);
70
- }
71
-
72
- controller.activeEdges = activeNow;
73
- controller.activeNodes = activeNodeNow;
74
- controller.render();
75
-
76
- if (elapsed < totalDuration) {
77
- requestAnimationFrame(animate);
78
- } else {
79
- _resetTrigger(controller, node, button);
80
- }
81
- };
82
- requestAnimationFrame(animate);
83
- } else if (connectedEdges.size > 0) {
84
- // Fallback: all data edges at once
85
- const startTime = performance.now();
86
- const totalDuration = STEP_DURATION;
87
- const now = performance.now();
88
- for (const id of connectedEdges) {
89
- controller.activeEdgeTimes.set(id, now);
90
- }
91
-
92
- const animate = () => {
93
- controller.activeEdges = connectedEdges;
94
- controller.render();
95
- if (performance.now() - startTime < totalDuration) {
96
- requestAnimationFrame(animate);
97
- } else {
98
- _resetTrigger(controller, node, button);
99
- }
100
- };
101
- requestAnimationFrame(animate);
102
- } else {
103
- _resetTrigger(controller, node, button);
104
- }
105
- });
106
-
107
- body.appendChild(button);
108
- el._btn = button;
109
- },
110
- },
111
- onExecute(node, { setOutput }) {
112
- if (node.state.triggered) {
113
- setOutput("triggered", true);
114
- }
115
- },
116
- });
117
-
118
- registry.register("util/Watch", {
119
- title: "Watch",
120
- color: "#10b981", // info (emerald)
121
- size: { w: 180 },
122
- inputs: [{ name: "value", portType: "data", datatype: "any" }],
123
- onExecute(node, { getInput }) {
124
- const value = getInput("value");
125
- console.log("[Watch] onExecute called, value:", value);
126
- },
127
- });
128
-
129
- registry.register("util/Print", {
130
- title: "Print",
131
- color: "#10b981", // info (emerald)
132
- size: { w: 140 },
133
- inputs: [
134
- { name: "", portType: "exec" },
135
- { name: "value", portType: "data", datatype: "any" },
136
- ],
137
- outputs: [{ name: "", portType: "exec" }],
138
- onExecute(node, { getInput, setOutput }) {
139
- const value = getInput("value");
140
- console.log("[Print]", value);
141
- setOutput("", true);
142
- },
143
- });
144
-
145
- registry.register("util/Timer", {
146
- title: "Timer",
147
- color: "#f59e0b", // event (amber)
148
- size: { w: 140 },
149
- inputs: [
150
- { name: "", portType: "exec" },
151
- { name: "delay (ms)", portType: "data", datatype: "number" },
152
- ],
153
- outputs: [{ name: "", portType: "exec" }],
154
- async onExecute(node, { getInput, setOutput }) {
155
- const delay = getInput("delay (ms)") || 0;
156
- await new Promise((resolve) => {
157
- setTimeout(() => {
158
- resolve();
159
- }, delay);
160
- });
161
- setOutput("", true);
162
- },
163
- });
164
- }
165
-
166
- function _resetTrigger(controller, node, button) {
167
- controller.activeEdges = new Set();
168
- controller.activeEdgeTimes = new Map();
169
- controller.activeNodes = new Set();
170
- controller.render();
171
- node.state.triggered = false;
172
- node.state._firing = false;
173
- button.style.borderColor = "#383858";
174
- button.style.color = "#8888aa";
175
- button.style.background = "transparent";
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
+ }
@@ -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 = "center";
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";