html-overlay-node 0.1.0

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.
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createHooks } from "./Hooks.js";
3
+
4
+ describe("Hooks", () => {
5
+ describe("createHooks", () => {
6
+ it("should create a hooks system with specified events", () => {
7
+ const hooks = createHooks(["test:event", "another:event"]);
8
+ expect(hooks).toBeDefined();
9
+ expect(typeof hooks.on).toBe("function");
10
+ expect(typeof hooks.off).toBe("function");
11
+ expect(typeof hooks.emit).toBe("function");
12
+ });
13
+ });
14
+
15
+ describe("on", () => {
16
+ it("should register an event listener", () => {
17
+ const hooks = createHooks(["test:event"]);
18
+ let called = false;
19
+ hooks.on("test:event", () => {
20
+ called = true;
21
+ });
22
+ hooks.emit("test:event");
23
+ expect(called).toBe(true);
24
+ });
25
+
26
+ it("should register multiple listeners for same event", () => {
27
+ const hooks = createHooks(["test:event"]);
28
+ let count = 0;
29
+ hooks.on("test:event", () => count++);
30
+ hooks.on("test:event", () => count++);
31
+ hooks.emit("test:event");
32
+ expect(count).toBe(2);
33
+ });
34
+
35
+ it("should pass data to listeners", () => {
36
+ const hooks = createHooks(["test:event"]);
37
+ let receivedData = null;
38
+ hooks.on("test:event", (data) => {
39
+ receivedData = data;
40
+ });
41
+ hooks.emit("test:event", { value: 42 });
42
+ expect(receivedData).toEqual({ value: 42 });
43
+ });
44
+ });
45
+
46
+ describe("off", () => {
47
+ it("should unregister a specific listener", () => {
48
+ const hooks = createHooks(["test:event"]);
49
+ let count = 0;
50
+ const listener = () => count++;
51
+
52
+ hooks.on("test:event", listener);
53
+ hooks.emit("test:event");
54
+ expect(count).toBe(1);
55
+
56
+ hooks.off("test:event", listener);
57
+ hooks.emit("test:event");
58
+ expect(count).toBe(1); // Still 1, not 2
59
+ });
60
+
61
+ it("should not affect other listeners", () => {
62
+ const hooks = createHooks(["test:event"]);
63
+ let count1 = 0;
64
+ let count2 = 0;
65
+ const listener1 = () => count1++;
66
+ const listener2 = () => count2++;
67
+
68
+ hooks.on("test:event", listener1);
69
+ hooks.on("test:event", listener2);
70
+
71
+ hooks.off("test:event", listener1);
72
+ hooks.emit("test:event");
73
+
74
+ expect(count1).toBe(0);
75
+ expect(count2).toBe(1);
76
+ });
77
+ });
78
+
79
+ describe("emit", () => {
80
+ it("should call all registered listeners", () => {
81
+ const hooks = createHooks(["test:event"]);
82
+ const calls = [];
83
+ hooks.on("test:event", () => calls.push(1));
84
+ hooks.on("test:event", () => calls.push(2));
85
+ hooks.on("test:event", () => calls.push(3));
86
+
87
+ hooks.emit("test:event");
88
+ expect(calls).toEqual([1, 2, 3]);
89
+ });
90
+
91
+ it("should not throw when emitting event with no listeners", () => {
92
+ const hooks = createHooks(["test:event"]);
93
+ expect(() => {
94
+ hooks.emit("test:event");
95
+ }).not.toThrow();
96
+ });
97
+
98
+ it("should pass multiple arguments to listeners", () => {
99
+ const hooks = createHooks(["test:event"]);
100
+ let receivedArgs = null;
101
+ hooks.on("test:event", (...args) => {
102
+ receivedArgs = args;
103
+ });
104
+ hooks.emit("test:event", "arg1", "arg2", 123);
105
+ expect(receivedArgs).toEqual(["arg1", "arg2", 123]);
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from "../utils/utils.js";
2
+
3
+ // src/core/Node.js
4
+
5
+ /**
6
+ * Node represents a single node in the graph
7
+ */
8
+ export class Node {
9
+ /**
10
+ * Create a new Node
11
+ * @param {Object} options - Node configuration
12
+ * @param {string} [options.id] - Unique identifier (auto-generated if not provided)
13
+ * @param {string} options.type - Node type identifier
14
+ * @param {string} [options.title] - Display title (defaults to type)
15
+ * @param {number} [options.x=0] - X position
16
+ * @param {number} [options.y=0] - Y position
17
+ * @param {number} [options.width=160] - Node width
18
+ * @param {number} [options.height=60] - Node height
19
+ */
20
+ constructor({ id, type, title, x = 0, y = 0, width = 160, height = 60 }) {
21
+ if (!type) {
22
+ throw new Error("Node type is required");
23
+ }
24
+ this.id = id ?? randomUUID();
25
+ this.type = type;
26
+ this.title = title ?? type;
27
+ this.pos = { x, y };
28
+ this.size = { width, height };
29
+ this.inputs = []; // {id,name,datatype,portType,dir}
30
+ this.outputs = []; // {id,name,datatype,portType,dir}
31
+ this.state = {}; // User state data
32
+
33
+ // Tree Structure
34
+ this.parent = null; // Parent Node (or null if root)
35
+ this.children = new Set(); // Set<Node>
36
+ this.computed = { x: 0, y: 0, w: 0, h: 0 }; // World Transform
37
+ }
38
+
39
+ /**
40
+ * Add an input port to this node
41
+ * @param {string} name - Port name
42
+ * @param {string} [datatype="any"] - Data type for the port
43
+ * @param {string} [portType="data"] - Port type: "exec" or "data"
44
+ * @returns {Object} The created port
45
+ */
46
+ 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");
49
+ }
50
+ const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
51
+ this.inputs.push(port);
52
+ return port;
53
+ }
54
+
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
+ 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");
65
+ }
66
+ const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
67
+ this.outputs.push(port);
68
+ return port;
69
+ }
70
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Node } from "./Node.js";
3
+
4
+ describe("Node", () => {
5
+ describe("constructor", () => {
6
+ it("should create a node with default values", () => {
7
+ const node = new Node({ type: "test/node" });
8
+ expect(node.type).toBe("test/node");
9
+ expect(node.title).toBe("test/node");
10
+ expect(node.pos).toEqual({ x: 0, y: 0 });
11
+ expect(node.size).toEqual({ width: 160, height: 60 });
12
+ expect(node.inputs).toEqual([]);
13
+ expect(node.outputs).toEqual([]);
14
+ expect(node.state).toEqual({});
15
+ expect(node.id).toBeDefined();
16
+ });
17
+
18
+ it("should create a node with custom values", () => {
19
+ const node = new Node({
20
+ id: "custom-id",
21
+ type: "test/node",
22
+ title: "Custom Title",
23
+ x: 100,
24
+ y: 200,
25
+ width: 300,
26
+ height: 400,
27
+ });
28
+ expect(node.id).toBe("custom-id");
29
+ expect(node.type).toBe("test/node");
30
+ expect(node.title).toBe("Custom Title");
31
+ expect(node.pos).toEqual({ x: 100, y: 200 });
32
+ expect(node.size).toEqual({ width: 300, height: 400 });
33
+ });
34
+
35
+ it("should throw error when type is missing", () => {
36
+ expect(() => {
37
+ new Node({});
38
+ }).toThrow(/type is required/);
39
+ });
40
+
41
+ it("should initialize tree structure", () => {
42
+ const node = new Node({ type: "test/node" });
43
+ expect(node.parent).toBe(null);
44
+ expect(node.children).toBeInstanceOf(Set);
45
+ expect(node.children.size).toBe(0);
46
+ expect(node.computed).toEqual({ x: 0, y: 0, w: 0, h: 0 });
47
+ });
48
+ });
49
+
50
+ describe("addInput", () => {
51
+ let node;
52
+
53
+ beforeEach(() => {
54
+ node = new Node({ type: "test/node" });
55
+ });
56
+
57
+ it("should add an input port", () => {
58
+ const port = node.addInput("test", "number");
59
+ expect(port.name).toBe("test");
60
+ expect(port.datatype).toBe("number");
61
+ expect(port.dir).toBe("in");
62
+ expect(port.id).toBeDefined();
63
+ expect(node.inputs).toContain(port);
64
+ expect(node.inputs.length).toBe(1);
65
+ });
66
+
67
+ it("should default datatype to 'any'", () => {
68
+ const port = node.addInput("test");
69
+ expect(port.datatype).toBe("any");
70
+ });
71
+
72
+ it("should throw error when name is missing", () => {
73
+ expect(() => {
74
+ node.addInput("");
75
+ }).toThrow(/must be a non-empty string/);
76
+ });
77
+
78
+ it("should throw error when name is not a string", () => {
79
+ expect(() => {
80
+ node.addInput(null);
81
+ }).toThrow(/must be a non-empty string/);
82
+ });
83
+ });
84
+
85
+ describe("addOutput", () => {
86
+ let node;
87
+
88
+ beforeEach(() => {
89
+ node = new Node({ type: "test/node" });
90
+ });
91
+
92
+ it("should add an output port", () => {
93
+ const port = node.addOutput("result", "string");
94
+ expect(port.name).toBe("result");
95
+ expect(port.datatype).toBe("string");
96
+ expect(port.dir).toBe("out");
97
+ expect(port.id).toBeDefined();
98
+ expect(node.outputs).toContain(port);
99
+ expect(node.outputs.length).toBe(1);
100
+ });
101
+
102
+ it("should default datatype to 'any'", () => {
103
+ const port = node.addOutput("result");
104
+ expect(port.datatype).toBe("any");
105
+ });
106
+
107
+ it("should throw error when name is missing", () => {
108
+ expect(() => {
109
+ node.addOutput("");
110
+ }).toThrow(/must be a non-empty string/);
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,71 @@
1
+ // src/core/Registry.js
2
+
3
+ /**
4
+ * Registry for managing node type definitions
5
+ */
6
+ export class Registry {
7
+ constructor() {
8
+ this.types = new Map();
9
+ }
10
+
11
+ /**
12
+ * Register a new node type
13
+ * @param {string} type - Unique type identifier (e.g., "core/Note")
14
+ * @param {Object} def - Node definition
15
+ * @param {string} [def.title] - Display title
16
+ * @param {Object} [def.size] - Default size {w, h}
17
+ * @param {Array} [def.inputs] - Input port definitions
18
+ * @param {Array} [def.outputs] - Output port definitions
19
+ * @param {Function} [def.onCreate] - Called when node is created
20
+ * @param {Function} [def.onExecute] - Called each execution cycle
21
+ * @param {Function} [def.onDraw] - Custom drawing function
22
+ * @param {Object} [def.html] - HTML overlay configuration
23
+ * @throws {Error} If type is already registered or invalid
24
+ */
25
+ register(type, def) {
26
+ if (!type || typeof type !== 'string') {
27
+ throw new Error(`Invalid node type: type must be a non-empty string, got ${typeof type}`);
28
+ }
29
+ if (!def || typeof def !== 'object') {
30
+ throw new Error(`Invalid definition for type "${type}": definition must be an object`);
31
+ }
32
+ if (this.types.has(type)) {
33
+ throw new Error(`Node type "${type}" is already registered. Use unregister() first to replace it.`);
34
+ }
35
+ this.types.set(type, def);
36
+ }
37
+
38
+ /**
39
+ * Unregister a node type
40
+ * @param {string} type - Type identifier to unregister
41
+ * @throws {Error} If type doesn't exist
42
+ */
43
+ unregister(type) {
44
+ if (!this.types.has(type)) {
45
+ throw new Error(`Cannot unregister type "${type}": type is not registered`);
46
+ }
47
+ this.types.delete(type);
48
+ }
49
+
50
+ /**
51
+ * Remove all registered node types
52
+ */
53
+ removeAll() {
54
+ this.types.clear();
55
+ }
56
+
57
+ /**
58
+ * Get the definition for a registered node type
59
+ * @param {string} type - Type identifier
60
+ * @returns {Object} Node definition
61
+ * @throws {Error} If type is not registered
62
+ */
63
+ createInstance(type) {
64
+ const def = this.types.get(type);
65
+ if (!def) {
66
+ const available = Array.from(this.types.keys()).join(', ') || 'none';
67
+ throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
68
+ }
69
+ return def;
70
+ }
71
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Registry } from "./Registry.js";
3
+
4
+ describe("Registry", () => {
5
+ let registry;
6
+
7
+ beforeEach(() => {
8
+ registry = new Registry();
9
+ });
10
+
11
+ describe("register", () => {
12
+ it("should register a new node type", () => {
13
+ const def = { title: "Test Node", size: { w: 100, h: 50 } };
14
+ registry.register("test/node", def);
15
+ expect(registry.types.has("test/node")).toBe(true);
16
+ expect(registry.types.get("test/node")).toBe(def);
17
+ });
18
+
19
+ it("should throw error when registering duplicate type", () => {
20
+ const def = { title: "Test" };
21
+ registry.register("test/node", def);
22
+ expect(() => {
23
+ registry.register("test/node", def);
24
+ }).toThrow(/already registered/);
25
+ });
26
+
27
+ it("should throw error when type is not a string", () => {
28
+ expect(() => {
29
+ registry.register(null, {});
30
+ }).toThrow(/must be a non-empty string/);
31
+ });
32
+
33
+ it("should throw error when definition is not an object", () => {
34
+ expect(() => {
35
+ registry.register("test/node", null);
36
+ }).toThrow(/must be an object/);
37
+ });
38
+ });
39
+
40
+ describe("unregister", () => {
41
+ it("should unregister an existing type", () => {
42
+ const def = { title: "Test" };
43
+ registry.register("test/node", def);
44
+ registry.unregister("test/node");
45
+ expect(registry.types.has("test/node")).toBe(false);
46
+ });
47
+
48
+ it("should throw error when unregistering non-existent type", () => {
49
+ expect(() => {
50
+ registry.unregister("non/existent");
51
+ }).toThrow(/is not registered/);
52
+ });
53
+ });
54
+
55
+ describe("removeAll", () => {
56
+ it("should remove all registered types", () => {
57
+ registry.register("test/a", { title: "A" });
58
+ registry.register("test/b", { title: "B" });
59
+ expect(registry.types.size).toBe(2);
60
+
61
+ registry.removeAll();
62
+ expect(registry.types.size).toBe(0);
63
+ });
64
+ });
65
+
66
+ describe("createInstance", () => {
67
+ it("should return the definition for a registered type", () => {
68
+ const def = { title: "Test" };
69
+ registry.register("test/node", def);
70
+ const instance = registry.createInstance("test/node");
71
+ expect(instance).toBe(def);
72
+ });
73
+
74
+ it("should throw error for unknown type with available types listed", () => {
75
+ registry.register("test/a", { title: "A" });
76
+ registry.register("test/b", { title: "B" });
77
+ expect(() => {
78
+ registry.createInstance("unknown");
79
+ }).toThrow(/Available types: test\/a, test\/b/);
80
+ });
81
+
82
+ it("should show 'none' when no types are available", () => {
83
+ expect(() => {
84
+ registry.createInstance("unknown");
85
+ }).toThrow(/Available types: none/);
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,211 @@
1
+ export class Runner {
2
+ constructor({ graph, registry, hooks, cyclesPerFrame = 1 }) {
3
+ this.graph = graph;
4
+ this.registry = registry;
5
+ this.hooks = hooks;
6
+ this.running = false;
7
+ this._raf = null;
8
+ this._last = 0;
9
+ this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
10
+ }
11
+
12
+ // 외부에서 실행 중인지 확인
13
+ isRunning() {
14
+ return this.running;
15
+ }
16
+
17
+ // 실행 도중에도 CPS 변경 가능
18
+ setCyclesPerFrame(n) {
19
+ this.cyclesPerFrame = Math.max(1, n | 0);
20
+ }
21
+
22
+ step(cycles = 1, dt = 0) {
23
+ const nCycles = Math.max(1, cycles | 0);
24
+ for (let c = 0; c < nCycles; c++) {
25
+ for (const node of this.graph.nodes.values()) {
26
+ const def = this.registry.types.get(node.type);
27
+ if (def?.onExecute) {
28
+ try {
29
+ def.onExecute(node, {
30
+ dt,
31
+ graph: this.graph,
32
+ getInput: (portName) => {
33
+ const p =
34
+ node.inputs.find((i) => i.name === portName) ||
35
+ node.inputs[0];
36
+ return p ? this.graph.getInput(node.id, p.id) : undefined;
37
+ },
38
+ setOutput: (portName, value) => {
39
+ const p =
40
+ node.outputs.find((o) => o.name === portName) ||
41
+ node.outputs[0];
42
+ if (p) this.graph.setOutput(node.id, p.id, value);
43
+ },
44
+ });
45
+ } catch (err) {
46
+ this.hooks?.emit?.("error", err);
47
+ }
48
+ }
49
+ }
50
+ // commit writes for this cycle
51
+ this.graph.swapBuffers();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute connected nodes once from a starting node
57
+ * @param {string} startNodeId - ID of the node to start from
58
+ * @param {number} dt - Delta time
59
+ */
60
+ runOnce(startNodeId, dt = 0) {
61
+ console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
62
+
63
+ const executedNodes = [];
64
+ const allConnectedNodes = new Set();
65
+ let currentNodeId = startNodeId;
66
+
67
+ // Follow exec flow
68
+ while (currentNodeId) {
69
+ const node = this.graph.nodes.get(currentNodeId);
70
+ if (!node) {
71
+ console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
72
+ break;
73
+ }
74
+
75
+ executedNodes.push(currentNodeId);
76
+ allConnectedNodes.add(currentNodeId);
77
+ console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
78
+
79
+ // Find and add data dependency nodes (nodes providing input data)
80
+ for (const input of node.inputs) {
81
+ if (input.portType === "data") {
82
+ // Find edge feeding this data input
83
+ for (const edge of this.graph.edges.values()) {
84
+ if (edge.toNode === currentNodeId && edge.toPort === input.id) {
85
+ const sourceNode = this.graph.nodes.get(edge.fromNode);
86
+ if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
87
+ allConnectedNodes.add(edge.fromNode);
88
+ // Execute data source node before current node
89
+ this.executeNode(edge.fromNode, dt);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ // Execute current node
97
+ this.executeNode(currentNodeId, dt);
98
+
99
+ // Find next node via exec output
100
+ currentNodeId = this.findNextExecNode(currentNodeId);
101
+ }
102
+
103
+ console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
104
+
105
+ // Find all edges involved (both exec and data)
106
+ const connectedEdges = new Set();
107
+ for (const edge of this.graph.edges.values()) {
108
+ if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
109
+ connectedEdges.add(edge.id);
110
+ }
111
+ }
112
+
113
+ console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
114
+ return { connectedNodes: allConnectedNodes, connectedEdges };
115
+ }
116
+
117
+ /**
118
+ * Find the next node to execute by following exec output
119
+ * @param {string} nodeId - Current node ID
120
+ * @returns {string|null} Next node ID or null
121
+ */
122
+ findNextExecNode(nodeId) {
123
+ const node = this.graph.nodes.get(nodeId);
124
+ if (!node) return null;
125
+
126
+ // Find exec output port
127
+ const execOutput = node.outputs.find(p => p.portType === "exec");
128
+ if (!execOutput) return null;
129
+
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;
134
+ }
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Execute a single node
142
+ * @param {string} nodeId - Node ID to execute
143
+ * @param {number} dt - Delta time
144
+ */
145
+ executeNode(nodeId, dt) {
146
+ const node = this.graph.nodes.get(nodeId);
147
+ if (!node) return;
148
+
149
+ const def = this.registry.types.get(node.type);
150
+ if (!def?.onExecute) return;
151
+
152
+ try {
153
+ def.onExecute(node, {
154
+ dt,
155
+ graph: this.graph,
156
+ getInput: (portName) => {
157
+ const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
158
+ return p ? this.graph.getInput(node.id, p.id) : undefined;
159
+ },
160
+ setOutput: (portName, value) => {
161
+ const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
162
+ if (p) {
163
+ // Write directly to current buffer so other nodes can read it immediately
164
+ const key = `${node.id}:${p.id}`;
165
+ this.graph._curBuf().set(key, value);
166
+ }
167
+ },
168
+ });
169
+ } catch (err) {
170
+ this.hooks?.emit?.("error", err);
171
+ }
172
+ }
173
+
174
+ start() {
175
+ if (this.running) return;
176
+ this.running = true;
177
+ this._last = 0;
178
+ this.hooks?.emit?.("runner:start");
179
+
180
+ const loop = (t) => {
181
+ if (!this.running) return;
182
+ const dtMs = this._last ? t - this._last : 0;
183
+ this._last = t;
184
+ const dt = dtMs / 1000; // seconds
185
+
186
+ // 1) 스텝 실행
187
+ this.step(this.cyclesPerFrame, dt);
188
+
189
+ // 2) 프레임 훅 (렌더러/컨트롤러는 여기서 running, time, dt를 받아 표현 업데이트)
190
+ this.hooks?.emit?.("runner:tick", {
191
+ time: t,
192
+ dt,
193
+ running: true,
194
+ cps: this.cyclesPerFrame,
195
+ });
196
+
197
+ this._raf = requestAnimationFrame(loop);
198
+ };
199
+
200
+ this._raf = requestAnimationFrame(loop);
201
+ }
202
+
203
+ stop() {
204
+ if (!this.running) return;
205
+ this.running = false;
206
+ if (this._raf) cancelAnimationFrame(this._raf);
207
+ this._raf = null;
208
+ this._last = 0;
209
+ this.hooks?.emit?.("runner:stop");
210
+ }
211
+ }