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.
@@ -232,7 +232,11 @@ export class ContextMenu {
232
232
 
233
233
  // Show submenu if exists
234
234
  if (item.submenu) {
235
- this._showSubmenu(item.submenu, itemEl);
235
+ // Support function-based submenus for dynamic content
236
+ const submenuItems = typeof item.submenu === 'function'
237
+ ? item.submenu()
238
+ : item.submenu;
239
+ this._showSubmenu(submenuItems, itemEl);
236
240
  }
237
241
  });
238
242
 
@@ -1,23 +1,18 @@
1
1
  import { portRect } from "../render/hitTest.js";
2
- import {
3
- AddEdgeCmd,
4
- RemoveEdgeCmd,
5
- RemoveNodeCmd,
6
- ResizeNodeCmd,
7
- } from "../core/commands.js";
2
+ import { AddEdgeCmd, RemoveEdgeCmd, RemoveNodeCmd, ResizeNodeCmd } from "../core/commands.js";
8
3
  import { CommandStack } from "../core/CommandStack.js";
9
4
 
10
5
  export class Controller {
11
-
12
6
  static MIN_NODE_WIDTH = 80;
13
7
  static MIN_NODE_HEIGHT = 60;
14
8
 
15
- constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, portRenderer }) {
9
+ constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, edgeRenderer, portRenderer }) {
16
10
  this.graph = graph;
17
11
  this.renderer = renderer;
18
12
  this.hooks = hooks;
19
13
  this.htmlOverlay = htmlOverlay;
20
14
  this.contextMenu = contextMenu;
15
+ this.edgeRenderer = edgeRenderer; // Separate renderer for edges/animations above HTML
21
16
  this.portRenderer = portRenderer; // Separate renderer for ports above HTML
22
17
 
23
18
  this.stack = new CommandStack();
@@ -30,6 +25,11 @@ export class Controller {
30
25
  this.gResizing = null;
31
26
  this.boxSelecting = null; // { startX, startY, currentX, currentY } - world coords
32
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
+
33
33
  // Feature flags
34
34
  this.snapToGrid = true; // Snap nodes to grid (toggle with G key)
35
35
  this.gridSize = 20; // Grid size for snapping
@@ -207,13 +207,11 @@ export class Controller {
207
207
  for (const n of this.graph.nodes.values()) {
208
208
  for (let i = 0; i < n.inputs.length; i++) {
209
209
  const r = portRect(n, n.inputs[i], i, "in");
210
- if (rectHas(r, x, y))
211
- return { node: n, port: n.inputs[i], dir: "in", idx: i };
210
+ if (rectHas(r, x, y)) return { node: n, port: n.inputs[i], dir: "in", idx: i };
212
211
  }
213
212
  for (let i = 0; i < n.outputs.length; i++) {
214
213
  const r = portRect(n, n.outputs[i], i, "out");
215
- if (rectHas(r, x, y))
216
- return { node: n, port: n.outputs[i], dir: "out", idx: i };
214
+ if (rectHas(r, x, y)) return { node: n, port: n.outputs[i], dir: "out", idx: i };
217
215
  }
218
216
  }
219
217
  return null;
@@ -441,8 +439,10 @@ export class Controller {
441
439
  }
442
440
 
443
441
  // Calculate delta from original position
444
- const deltaX = targetWx - this.dragging.selectedNodes.find(sn => sn.node.id === n.id).startWorldX;
445
- const deltaY = targetWy - this.dragging.selectedNodes.find(sn => sn.node.id === n.id).startWorldY;
442
+ const deltaX =
443
+ targetWx - this.dragging.selectedNodes.find((sn) => sn.node.id === n.id).startWorldX;
444
+ const deltaY =
445
+ targetWy - this.dragging.selectedNodes.find((sn) => sn.node.id === n.id).startWorldY;
446
446
 
447
447
  // Update world transforms
448
448
  this.graph.updateWorldTransforms();
@@ -532,13 +532,7 @@ export class Controller {
532
532
  const portIn = this._findPortAtWorld(w.x, w.y);
533
533
  if (portIn && portIn.dir === "in") {
534
534
  this.stack.exec(
535
- AddEdgeCmd(
536
- this.graph,
537
- from.fromNode,
538
- from.fromPort,
539
- portIn.node.id,
540
- portIn.port.id
541
- )
535
+ AddEdgeCmd(this.graph, from.fromNode, from.fromPort, portIn.node.id, portIn.port.id)
542
536
  );
543
537
  }
544
538
  this.connecting = null;
@@ -694,10 +688,13 @@ export class Controller {
694
688
  }
695
689
 
696
690
  // Get selected nodes
697
- const selectedNodes = Array.from(this.selection).map(id => this.graph.getNodeById(id));
691
+ const selectedNodes = Array.from(this.selection).map((id) => this.graph.getNodeById(id));
698
692
 
699
693
  // Calculate bounding box
700
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
694
+ let minX = Infinity,
695
+ minY = Infinity,
696
+ maxX = -Infinity,
697
+ maxY = -Infinity;
701
698
  for (const node of selectedNodes) {
702
699
  const { x, y, w, h } = node.computed;
703
700
  minX = Math.min(minX, x);
@@ -733,7 +730,7 @@ export class Controller {
733
730
  _alignNodesHorizontal() {
734
731
  if (this.selection.size < 2) return;
735
732
 
736
- const nodes = Array.from(this.selection).map(id => this.graph.getNodeById(id));
733
+ const nodes = Array.from(this.selection).map((id) => this.graph.getNodeById(id));
737
734
  const avgY = nodes.reduce((sum, n) => sum + n.computed.y, 0) / nodes.length;
738
735
 
739
736
  for (const node of nodes) {
@@ -751,7 +748,7 @@ export class Controller {
751
748
  _alignNodesVertical() {
752
749
  if (this.selection.size < 2) return;
753
750
 
754
- const nodes = Array.from(this.selection).map(id => this.graph.getNodeById(id));
751
+ const nodes = Array.from(this.selection).map((id) => this.graph.getNodeById(id));
755
752
  const avgX = nodes.reduce((sum, n) => sum + n.computed.x, 0) / nodes.length;
756
753
 
757
754
  for (const node of nodes) {
@@ -766,16 +763,39 @@ export class Controller {
766
763
  render() {
767
764
  const tEdge = this.renderTempEdge();
768
765
 
766
+ // 1. Draw background (grid, canvas-only nodes) on main canvas
769
767
  this.renderer.draw(this.graph, {
770
768
  selection: this.selection,
771
- tempEdge: tEdge,
769
+ tempEdge: null, // Don't draw temp edge on background
772
770
  boxSelecting: this.boxSelecting,
773
- activeEdges: this.activeEdges || new Set(), // For animation
771
+ activeEdges: this.activeEdges || new Set(),
772
+ drawEdges: !this.edgeRenderer, // Only draw edges here if no separate edge renderer
774
773
  });
775
774
 
775
+ // 2. HTML Overlay layer (HTML nodes at z-index 10)
776
776
  this.htmlOverlay?.draw(this.graph, this.selection);
777
777
 
778
- // Draw box selection rectangle on top of everything
778
+ // 3. Draw edges and animations on edge canvas (above HTML overlay at z-index 15)
779
+ if (this.edgeRenderer) {
780
+ const edgeCtx = this.edgeRenderer.ctx;
781
+ edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
782
+
783
+ // Edges use shared transform (via property getters)
784
+ this.edgeRenderer._applyTransform();
785
+
786
+ this.edgeRenderer.drawEdgesOnly(this.graph, {
787
+ activeEdges: this.activeEdges,
788
+ activeEdgeTimes: this.activeEdgeTimes,
789
+ activeNodes: this.activeNodes,
790
+ selection: this.selection,
791
+ time: performance.now(),
792
+ tempEdge: tEdge,
793
+ });
794
+
795
+ this.edgeRenderer._resetTransform();
796
+ }
797
+
798
+ // 4. Draw box selection rectangle on top of edges
779
799
  if (this.boxSelecting) {
780
800
  const { startX, startY, currentX, currentY } = this.boxSelecting;
781
801
  const minX = Math.min(startX, currentX);
@@ -786,23 +806,36 @@ export class Controller {
786
806
  const screenStart = this.renderer.worldToScreen(minX, minY);
787
807
  const screenEnd = this.renderer.worldToScreen(minX + width, minY + height);
788
808
 
789
- const ctx = this.renderer.ctx;
809
+ const ctx = this.edgeRenderer ? this.edgeRenderer.ctx : this.renderer.ctx;
790
810
  ctx.save();
791
- this.renderer._resetTransform();
811
+ if (this.edgeRenderer) {
812
+ this.edgeRenderer._resetTransform();
813
+ } else {
814
+ this.renderer._resetTransform();
815
+ }
792
816
 
793
817
  // Draw selection box
794
818
  ctx.strokeStyle = "#6cf";
795
819
  ctx.fillStyle = "rgba(102, 204, 255, 0.1)";
796
820
  ctx.lineWidth = 2;
797
- ctx.strokeRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
798
- ctx.fillRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
821
+ ctx.strokeRect(
822
+ screenStart.x,
823
+ screenStart.y,
824
+ screenEnd.x - screenStart.x,
825
+ screenEnd.y - screenStart.y
826
+ );
827
+ ctx.fillRect(
828
+ screenStart.x,
829
+ screenStart.y,
830
+ screenEnd.x - screenStart.x,
831
+ screenEnd.y - screenStart.y
832
+ );
799
833
 
800
834
  ctx.restore();
801
835
  }
802
836
 
803
- // Draw ports for HTML overlay nodes on separate canvas (above HTML)
837
+ // 5. Draw ports on port canvas (above edges at z-index 20)
804
838
  if (this.portRenderer) {
805
- // Clear port canvas
806
839
  const portCtx = this.portRenderer.ctx;
807
840
  portCtx.clearRect(0, 0, this.portRenderer.canvas.width, this.portRenderer.canvas.height);
808
841
 
@@ -811,16 +844,13 @@ export class Controller {
811
844
  this.portRenderer.offsetX = this.renderer.offsetX;
812
845
  this.portRenderer.offsetY = this.renderer.offsetY;
813
846
 
814
- // Draw ports for HTML overlay nodes
815
847
  this.portRenderer._applyTransform();
848
+
849
+ // Draw ports for HTML overlay nodes only
850
+ // Draw ports for ALL nodes to ensure they are above edges
816
851
  for (const n of this.graph.nodes.values()) {
817
852
  if (n.type !== "core/Group") {
818
- const def = this.portRenderer.registry?.types?.get(n.type);
819
- const hasHtmlOverlay = !!(def?.html);
820
-
821
- if (hasHtmlOverlay) {
822
- this.portRenderer._drawPorts(n);
823
- }
853
+ this.portRenderer._drawPorts(n);
824
854
  }
825
855
  }
826
856
  this.portRenderer._resetTransform();
@@ -829,10 +859,7 @@ export class Controller {
829
859
 
830
860
  renderTempEdge() {
831
861
  if (!this.connecting) return null;
832
- const a = this._portAnchorScreen(
833
- this.connecting.fromNode,
834
- this.connecting.fromPort
835
- ); // {x,y}
862
+ const a = this._portAnchorScreen(this.connecting.fromNode, this.connecting.fromPort); // {x,y}
836
863
  return {
837
864
  x1: a.x,
838
865
  y1: a.y,
@@ -845,7 +872,7 @@ export class Controller {
845
872
  const n = this.graph.nodes.get(nodeId);
846
873
  const iOut = n.outputs.findIndex((p) => p.id === portId);
847
874
  const r = portRect(n, null, iOut, "out"); // world rect
848
- return this.renderer.worldToScreen(r.x, r.y + 7); // -> screen point
875
+ return this.renderer.worldToScreen(r.x + r.w / 2, r.y + r.h / 2); // -> screen point (CENTER)
849
876
  }
850
877
  }
851
878
 
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Core Nodes Package
3
+ * Provides core example nodes: Note, HtmlNote, TodoNode, and Group
4
+ */
5
+
6
+ export function registerCoreNodes(registry, hooks) {
7
+ // Note Node
8
+ registry.register("core/Note", {
9
+ title: "Note",
10
+ color: "#10b981", // info (emerald)
11
+ size: { w: 180 },
12
+ inputs: [{ name: "in", datatype: "any" }],
13
+ outputs: [{ name: "out", datatype: "any" }],
14
+ onCreate(node) {
15
+ node.state.text = "hello";
16
+ },
17
+ onExecute(node, { getInput, setOutput }) {
18
+ const incoming = getInput("in");
19
+ const out = (incoming ?? node.state.text ?? "").toString().toUpperCase();
20
+ setOutput(
21
+ "out",
22
+ out + ` · ${Math.floor((performance.now() / 1000) % 100)}`
23
+ );
24
+ },
25
+ });
26
+
27
+ // HTML Note Node
28
+ registry.register("core/HtmlNote", {
29
+ title: "HTML Note",
30
+ color: "#3b82f6", // data (blue)
31
+ size: { w: 220 },
32
+ inputs: [{ name: "in", datatype: "any" }],
33
+ outputs: [{ name: "out", datatype: "any" }],
34
+
35
+ html: {
36
+ init(node, el, { body }) {
37
+ el.classList.add("node-overlay");
38
+
39
+ body.style.display = "flex";
40
+ body.style.flexDirection = "column";
41
+ body.style.gap = "8px";
42
+
43
+ const label = document.createElement("label");
44
+ label.className = "premium-label";
45
+ label.textContent = "Data Input";
46
+ body.appendChild(label);
47
+
48
+ const input = document.createElement("input");
49
+ input.className = "premium-input";
50
+ input.placeholder = "Type message...";
51
+ input.addEventListener("input", (e) => {
52
+ node.state.text = e.target.value;
53
+ });
54
+ input.addEventListener("mousedown", (e) => e.stopPropagation());
55
+
56
+ body.appendChild(input);
57
+ el._input = input;
58
+ },
59
+
60
+ update(node, el, _opts) {
61
+ // Selection is handled by the canvas renderer
62
+ if (el._input.value !== (node.state.text || "")) {
63
+ el._input.value = node.state.text || "";
64
+ }
65
+ }
66
+ },
67
+
68
+ onCreate(node) {
69
+ node.state.text = "";
70
+ },
71
+ onExecute(node, { getInput, setOutput }) {
72
+ const incoming = getInput("in");
73
+ setOutput("out", incoming);
74
+ },
75
+ });
76
+
77
+ // Todo List Node (HTML Overlay)
78
+ registry.register("core/TodoNode", {
79
+ title: "Task list",
80
+ color: "#10b981", // info (emerald)
81
+ size: { w: 240, h: 300 },
82
+ inputs: [{ name: "in", datatype: "any" }],
83
+ outputs: [{ name: "out", datatype: "any" }],
84
+ html: {
85
+ init(node, el, { body }) {
86
+ el.classList.add("node-overlay");
87
+
88
+ body.style.display = "flex";
89
+ body.style.flexDirection = "column";
90
+
91
+ const label = document.createElement("label");
92
+ label.className = "premium-label";
93
+ label.textContent = "New Task";
94
+ body.appendChild(label);
95
+
96
+ const inputRow = document.createElement("div");
97
+ Object.assign(inputRow.style, { display: "flex", gap: "6px", marginBottom: "12px" });
98
+
99
+ const input = document.createElement("input");
100
+ input.className = "premium-input";
101
+ input.placeholder = "What needs to be done?";
102
+
103
+ const addBtn = document.createElement("button");
104
+ addBtn.className = "premium-button";
105
+ addBtn.textContent = "Add";
106
+
107
+ inputRow.append(input, addBtn);
108
+
109
+ const list = document.createElement("ul");
110
+ Object.assign(list.style, {
111
+ listStyle: "none", padding: "0", margin: "0",
112
+ overflow: "hidden", flex: "1"
113
+ });
114
+
115
+ body.append(inputRow, list);
116
+
117
+ const addTodo = () => {
118
+ const text = input.value.trim();
119
+ if (!text) return;
120
+ const todos = node.state.todos || [];
121
+ node.state.todos = [...todos, { id: Date.now(), text, done: false }];
122
+ input.value = "";
123
+ hooks.emit("node:updated", node);
124
+ };
125
+
126
+ addBtn.onclick = addTodo;
127
+ input.onkeydown = (e) => {
128
+ if (e.key === "Enter") addTodo();
129
+ e.stopPropagation();
130
+ };
131
+ input.onmousedown = (e) => e.stopPropagation();
132
+
133
+ el._refs = { list };
134
+ },
135
+ update(node, el, _opts) {
136
+ // Selection is handled by the canvas renderer
137
+ const { list } = el._refs;
138
+ const todos = node.state.todos || [];
139
+
140
+ list.innerHTML = "";
141
+ todos.forEach((todo) => {
142
+ const li = document.createElement("li");
143
+ Object.assign(li.style, {
144
+ display: "flex", alignItems: "center", padding: "6px 0",
145
+ borderBottom: "1px solid rgba(255,255,255,0.03)"
146
+ });
147
+
148
+ const chk = document.createElement("input");
149
+ chk.type = "checkbox";
150
+ chk.checked = todo.done;
151
+ Object.assign(chk.style, {
152
+ marginRight: "8px",
153
+ accentColor: "#5568d0",
154
+ pointerEvents: "auto",
155
+ });
156
+ chk.onchange = () => {
157
+ todo.done = chk.checked;
158
+ hooks.emit("node:updated", node);
159
+ };
160
+ chk.onmousedown = (e) => e.stopPropagation();
161
+
162
+ const span = document.createElement("span");
163
+ span.textContent = todo.text;
164
+ span.style.flex = "1";
165
+ span.style.fontSize = "11px";
166
+ span.style.textDecoration = todo.done ? "line-through" : "none";
167
+ span.style.color = todo.done ? "#404060" : "#8888a8";
168
+
169
+ const del = document.createElement("button");
170
+ del.textContent = "×";
171
+ Object.assign(del.style, {
172
+ background: "none", border: "none", color: "#4a3a4a",
173
+ cursor: "pointer", fontSize: "14px",
174
+ pointerEvents: "auto",
175
+ transition: "color 0.12s ease",
176
+ });
177
+ del.addEventListener("mouseover", () => { del.style.color = "#ff4d4d"; });
178
+ del.addEventListener("mouseout", () => { del.style.color = "#4a3a4a"; });
179
+ del.onclick = () => {
180
+ node.state.todos = node.state.todos.filter((t) => t.id !== todo.id);
181
+ hooks.emit("node:updated", node);
182
+ };
183
+ del.onmousedown = (e) => e.stopPropagation();
184
+
185
+ li.append(chk, span, del);
186
+ list.appendChild(li);
187
+ });
188
+ }
189
+ },
190
+ onCreate(node) {
191
+ node.state.todos = [
192
+ { id: 1, text: "Welcome to Free Node", done: false },
193
+ { id: 2, text: "Try adding a task", done: true },
194
+ ];
195
+ },
196
+ });
197
+
198
+ // Group Node
199
+ registry.register("core/Group", {
200
+ title: "Group",
201
+ color: "#475569", // group (slate)
202
+ size: { w: 240, h: 160 },
203
+ onDraw(node, { ctx, theme, renderer }) {
204
+ const { x, y, w, h } = node.computed;
205
+ const headerH = 24;
206
+ const color = node.state.color || node.color || "#39424e";
207
+ const bgAlpha = 0.4;
208
+ const textColor = theme.text || "#e9e9ef";
209
+ const r = 4; // Groups can be slightly softer but still sharp
210
+
211
+ const rgba = (hex, a) => {
212
+ const c = hex.replace("#", "");
213
+ const n = parseInt(
214
+ c.length === 3
215
+ ? c
216
+ .split("")
217
+ .map((x) => x + x)
218
+ .join("")
219
+ : c,
220
+ 16
221
+ );
222
+ const r = (n >> 16) & 255,
223
+ g = (n >> 8) & 255,
224
+ b = n & 255;
225
+ return `rgba(${r},${g},${b},${a})`;
226
+ };
227
+
228
+ const roundRect = (ctx, x, y, w, h, r) => {
229
+ if (w < 2 * r) r = w / 2;
230
+ if (h < 2 * r) r = h / 2;
231
+ ctx.beginPath();
232
+ ctx.moveTo(x + r, y);
233
+ ctx.arcTo(x + w, y, x + w, y + h, r);
234
+ ctx.arcTo(x + w, y + h, x, y + h, r);
235
+ ctx.arcTo(x, y + h, x, y, r);
236
+ ctx.arcTo(x, y, x + w, y, r);
237
+ ctx.closePath();
238
+ };
239
+
240
+ ctx.fillStyle = rgba(color, bgAlpha);
241
+ roundRect(ctx, x, y, w, h, r);
242
+ ctx.fill();
243
+
244
+ ctx.fillStyle = rgba(color, 0.2);
245
+ ctx.beginPath();
246
+ ctx.roundRect(x, y, w, headerH, [r, r, 0, 0]);
247
+ ctx.fill();
248
+
249
+ // Use screen-coordinate text rendering for consistent scale
250
+ if (renderer && renderer._drawScreenText) {
251
+ renderer._drawScreenText(node.title, x + 12, y + 13, {
252
+ fontPx: 13,
253
+ color: textColor,
254
+ baseline: "middle",
255
+ align: "left"
256
+ });
257
+ } else {
258
+ // Fallback to world coordinates if renderer not available
259
+ ctx.fillStyle = textColor;
260
+ ctx.font = "600 13px system-ui";
261
+ ctx.textBaseline = "top";
262
+ ctx.fillText(node.title, x + 12, y + 6);
263
+ }
264
+ },
265
+ });
266
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Node Packages - Main Export
3
+ *
4
+ * This module exports all node registration functions.
5
+ * Users can import individual packages or registerAllNodes for convenience.
6
+ *
7
+ * @example
8
+ * // Import specific packages
9
+ * import { registerMathNodes, registerLogicNodes } from "html-overlay-node/nodes";
10
+ * registerMathNodes(editor.registry);
11
+ * registerLogicNodes(editor.registry);
12
+ *
13
+ * @example
14
+ * // Import all nodes at once
15
+ * import { registerAllNodes } from "html-overlay-node/nodes";
16
+ * registerAllNodes(editor.registry, editor.hooks);
17
+ */
18
+
19
+ import { registerMathNodes } from "./math.js";
20
+ import { registerLogicNodes } from "./logic.js";
21
+ import { registerValueNodes } from "./value.js";
22
+ import { registerUtilNodes } from "./util.js";
23
+ import { registerCoreNodes } from "./core.js";
24
+
25
+ export { registerMathNodes } from "./math.js";
26
+ export { registerLogicNodes } from "./logic.js";
27
+ export { registerValueNodes } from "./value.js";
28
+ export { registerUtilNodes } from "./util.js";
29
+ export { registerCoreNodes } from "./core.js";
30
+
31
+ /**
32
+ * Register all example nodes at once
33
+ * @param {Registry} registry - Node registry instance
34
+ * @param {Hooks} hooks - Hooks instance (required for TodoNode)
35
+ */
36
+ export function registerAllNodes(registry, hooks) {
37
+ registerMathNodes(registry);
38
+ registerLogicNodes(registry);
39
+ registerValueNodes(registry);
40
+ registerUtilNodes(registry);
41
+ registerCoreNodes(registry, hooks);
42
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Logic Nodes Package
3
+ * Provides boolean logic operation nodes
4
+ */
5
+
6
+ export function registerLogicNodes(registry) {
7
+ // AND Node
8
+ registry.register("logic/AND", {
9
+ title: "AND",
10
+ color: "#a855f7", // logic (purple)
11
+ size: { w: 120 },
12
+ inputs: [
13
+ { name: "exec", portType: "exec" },
14
+ { name: "a", portType: "data", datatype: "boolean" },
15
+ { name: "b", portType: "data", datatype: "boolean" },
16
+ ],
17
+ outputs: [
18
+ { name: "exec", portType: "exec" },
19
+ { name: "result", portType: "data", datatype: "boolean" },
20
+ ],
21
+ onExecute(node, { getInput, setOutput }) {
22
+ const a = getInput("a") ?? false;
23
+ const b = getInput("b") ?? false;
24
+ console.log("[AND] Inputs - a:", a, "b:", b);
25
+ const result = a && b;
26
+ console.log("[AND] Result:", result);
27
+ setOutput("result", result);
28
+ },
29
+ });
30
+
31
+ // OR Node
32
+ registry.register("logic/OR", {
33
+ title: "OR",
34
+ color: "#a855f7", // logic (purple)
35
+ size: { w: 120 },
36
+ inputs: [
37
+ { name: "a", datatype: "boolean" },
38
+ { name: "b", datatype: "boolean" },
39
+ ],
40
+ outputs: [{ name: "result", datatype: "boolean" }],
41
+ onExecute(node, { getInput, setOutput }) {
42
+ const a = getInput("a") ?? false;
43
+ const b = getInput("b") ?? false;
44
+ setOutput("result", a || b);
45
+ },
46
+ });
47
+
48
+ // NOT Node
49
+ registry.register("logic/NOT", {
50
+ title: "NOT",
51
+ color: "#a855f7", // logic (purple)
52
+ size: { w: 120 },
53
+ inputs: [{ name: "in", datatype: "boolean" }],
54
+ outputs: [{ name: "out", datatype: "boolean" }],
55
+ onExecute(node, { getInput, setOutput }) {
56
+ const val = getInput("in") ?? false;
57
+ setOutput("out", !val);
58
+ },
59
+ });
60
+ }