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
|
@@ -232,7 +232,11 @@ export class ContextMenu {
|
|
|
232
232
|
|
|
233
233
|
// Show submenu if exists
|
|
234
234
|
if (item.submenu) {
|
|
235
|
-
|
|
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 =
|
|
445
|
-
|
|
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,
|
|
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:
|
|
769
|
+
tempEdge: null, // Don't draw temp edge on background
|
|
772
770
|
boxSelecting: this.boxSelecting,
|
|
773
|
-
activeEdges: this.activeEdges || new Set(),
|
|
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
|
|
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.
|
|
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(
|
|
798
|
-
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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
|
+
}
|