html-overlay-node 0.1.6 → 0.1.9

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/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "html-overlay-node",
3
- "version": "0.1.6",
4
- "description": "A customizable, LiteGraph-style node editor library with canvas rendering, HTML overlays, and flexible execution control",
5
- "main": "dist/html-overlay-node.umd.js",
6
- "module": "dist/html-overlay-node.es.js",
3
+ "version": "0.1.9",
4
+ "description": "LiteGraph-style node editor with HTML overlay support",
5
+ "main": "./src/index.js",
6
+ "module": "./src/index.js",
7
7
  "type": "module",
8
8
  "exports": {
9
- ".": {
10
- "import": "./dist/html-overlay-node.es.js",
11
- "require": "./dist/html-overlay-node.umd.js"
12
- }
9
+ ".": "./src/index.js",
10
+ "./nodes": "./src/nodes/index.js",
11
+ "./defaults": "./src/defaults/index.js",
12
+ "./index.css": "./index.css",
13
+ "./src/ui/PropertyPanel.css": "./src/ui/PropertyPanel.css"
13
14
  },
14
15
  "files": [
15
16
  "dist",
package/src/core/Edge.js CHANGED
@@ -16,8 +16,10 @@ export class Edge {
16
16
  * @param {string} options.toPort - Target port ID
17
17
  */
18
18
  constructor({ id, fromNode, fromPort, toNode, toPort }) {
19
- if (!fromNode || !fromPort || !toNode || !toPort) {
20
- throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
19
+ // Allow empty strings for port names (exec ports use empty names)
20
+ // Only check for null/undefined
21
+ if (fromNode == null || fromPort == null || toNode == null || toPort == null) {
22
+ throw new Error("Edge requires fromNode, fromPort, toNode, and toPort (null/undefined not allowed)");
21
23
  }
22
24
  this.id = id ?? randomUUID();
23
25
  this.fromNode = fromNode;
package/src/core/Node.js CHANGED
@@ -43,28 +43,44 @@ export class Node {
43
43
  * @param {string} [portType="data"] - Port type: "exec" or "data"
44
44
  * @returns {Object} The created port
45
45
  */
46
+ /**
47
+ * Recalculate minimum size based on ports
48
+ */
49
+ _updateMinSize() {
50
+ const HEADER_HEIGHT = 28;
51
+ const PORT_SPACING = 24;
52
+ const BOTTOM_PADDING = 10;
53
+
54
+ // Calculate required height for inputs and outputs
55
+ const inHeight = HEADER_HEIGHT + 10 + this.inputs.length * PORT_SPACING + BOTTOM_PADDING;
56
+ const outHeight = HEADER_HEIGHT + 10 + this.outputs.length * PORT_SPACING + BOTTOM_PADDING;
57
+
58
+ const minHeight = Math.max(inHeight, outHeight, 60); // Minimum 60px base
59
+
60
+ if (this.size.height < minHeight) {
61
+ this.size.height = minHeight;
62
+ }
63
+ }
64
+
46
65
  addInput(name, datatype = "any", portType = "data") {
47
- if (!name || typeof name !== "string") {
48
- throw new Error("Input port name must be a non-empty string");
66
+ // ... existing validation ...
67
+ if (typeof name !== "string" || (portType === "data" && !name)) {
68
+ throw new Error("Input port name must be a string (non-empty for data ports)");
49
69
  }
50
70
  const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
51
71
  this.inputs.push(port);
72
+ this._updateMinSize();
52
73
  return port;
53
74
  }
54
75
 
55
- /**
56
- * Add an output port to this node
57
- * @param {string} name - Port name
58
- * @param {string} [datatype="any"] - Data type for the port
59
- * @param {string} [portType="data"] - Port type: "exec" or "data"
60
- * @returns {Object} The created port
61
- */
62
76
  addOutput(name, datatype = "any", portType = "data") {
63
- if (!name || typeof name !== "string") {
64
- throw new Error("Output port name must be a non-empty string");
77
+ // ... existing validation ...
78
+ if (typeof name !== "string" || (portType === "data" && !name)) {
79
+ throw new Error("Output port name must be a string (non-empty for data ports)");
65
80
  }
66
81
  const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
67
82
  this.outputs.push(port);
83
+ this._updateMinSize();
68
84
  return port;
69
85
  }
70
86
  }
@@ -54,6 +54,7 @@ export class Runner {
54
54
 
55
55
  /**
56
56
  * Execute connected nodes once from a starting node
57
+ * Uses queue-based traversal to support branching exec flows
57
58
  * @param {string} startNodeId - ID of the node to start from
58
59
  * @param {number} dt - Delta time
59
60
  */
@@ -62,14 +63,21 @@ export class Runner {
62
63
 
63
64
  const executedNodes = [];
64
65
  const allConnectedNodes = new Set();
65
- let currentNodeId = startNodeId;
66
+ const queue = [startNodeId];
67
+ const visited = new Set(); // Prevent infinite loops
68
+
69
+ // Queue-based traversal for branching execution
70
+ while (queue.length > 0) {
71
+ const currentNodeId = queue.shift();
72
+
73
+ // Skip if already executed (prevents cycles)
74
+ if (visited.has(currentNodeId)) continue;
75
+ visited.add(currentNodeId);
66
76
 
67
- // Follow exec flow
68
- while (currentNodeId) {
69
77
  const node = this.graph.nodes.get(currentNodeId);
70
78
  if (!node) {
71
79
  console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
72
- break;
80
+ continue;
73
81
  }
74
82
 
75
83
  executedNodes.push(currentNodeId);
@@ -96,8 +104,9 @@ export class Runner {
96
104
  // Execute current node
97
105
  this.executeNode(currentNodeId, dt);
98
106
 
99
- // Find next node via exec output
100
- currentNodeId = this.findNextExecNode(currentNodeId);
107
+ // Find all next nodes via exec outputs and add to queue
108
+ const nextNodes = this.findAllNextExecNodes(currentNodeId);
109
+ queue.push(...nextNodes);
101
110
  }
102
111
 
103
112
  console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
@@ -115,26 +124,31 @@ export class Runner {
115
124
  }
116
125
 
117
126
  /**
118
- * Find the next node to execute by following exec output
127
+ * Find all nodes connected via exec outputs
128
+ * Supports multiple connections from a single exec output
119
129
  * @param {string} nodeId - Current node ID
120
- * @returns {string|null} Next node ID or null
130
+ * @returns {string[]} Array of next node IDs
121
131
  */
122
- findNextExecNode(nodeId) {
132
+ findAllNextExecNodes(nodeId) {
123
133
  const node = this.graph.nodes.get(nodeId);
124
- if (!node) return null;
134
+ if (!node) return [];
125
135
 
126
- // Find exec output port
127
- const execOutput = node.outputs.find(p => p.portType === "exec");
128
- if (!execOutput) return null;
136
+ // Find all exec output ports
137
+ const execOutputs = node.outputs.filter(p => p.portType === "exec");
138
+ if (execOutputs.length === 0) return [];
129
139
 
130
- // Find edge from exec output
131
- for (const edge of this.graph.edges.values()) {
132
- if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
133
- return edge.toNode;
140
+ const nextNodes = [];
141
+
142
+ // Find all edges from exec outputs
143
+ for (const execOutput of execOutputs) {
144
+ for (const edge of this.graph.edges.values()) {
145
+ if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
146
+ nextNodes.push(edge.toNode);
147
+ }
134
148
  }
135
149
  }
136
150
 
137
- return null;
151
+ return nextNodes;
138
152
  }
139
153
 
140
154
  /**
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Default Configurations
3
+ * Export all default setup functions
4
+ */
5
+
6
+ export { setupDefaultContextMenu } from "./contextMenu.js";