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,3596 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ class Registry {
5
+ constructor() {
6
+ this.types = /* @__PURE__ */ new Map();
7
+ }
8
+ /**
9
+ * Register a new node type
10
+ * @param {string} type - Unique type identifier (e.g., "core/Note")
11
+ * @param {Object} def - Node definition
12
+ * @param {string} [def.title] - Display title
13
+ * @param {Object} [def.size] - Default size {w, h}
14
+ * @param {Array} [def.inputs] - Input port definitions
15
+ * @param {Array} [def.outputs] - Output port definitions
16
+ * @param {Function} [def.onCreate] - Called when node is created
17
+ * @param {Function} [def.onExecute] - Called each execution cycle
18
+ * @param {Function} [def.onDraw] - Custom drawing function
19
+ * @param {Object} [def.html] - HTML overlay configuration
20
+ * @throws {Error} If type is already registered or invalid
21
+ */
22
+ register(type, def) {
23
+ if (!type || typeof type !== "string") {
24
+ throw new Error(`Invalid node type: type must be a non-empty string, got ${typeof type}`);
25
+ }
26
+ if (!def || typeof def !== "object") {
27
+ throw new Error(`Invalid definition for type "${type}": definition must be an object`);
28
+ }
29
+ if (this.types.has(type)) {
30
+ throw new Error(`Node type "${type}" is already registered. Use unregister() first to replace it.`);
31
+ }
32
+ this.types.set(type, def);
33
+ }
34
+ /**
35
+ * Unregister a node type
36
+ * @param {string} type - Type identifier to unregister
37
+ * @throws {Error} If type doesn't exist
38
+ */
39
+ unregister(type) {
40
+ if (!this.types.has(type)) {
41
+ throw new Error(`Cannot unregister type "${type}": type is not registered`);
42
+ }
43
+ this.types.delete(type);
44
+ }
45
+ /**
46
+ * Remove all registered node types
47
+ */
48
+ removeAll() {
49
+ this.types.clear();
50
+ }
51
+ /**
52
+ * Get the definition for a registered node type
53
+ * @param {string} type - Type identifier
54
+ * @returns {Object} Node definition
55
+ * @throws {Error} If type is not registered
56
+ */
57
+ createInstance(type) {
58
+ const def = this.types.get(type);
59
+ if (!def) {
60
+ const available = Array.from(this.types.keys()).join(", ") || "none";
61
+ throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
62
+ }
63
+ return def;
64
+ }
65
+ }
66
+ function createHooks(names) {
67
+ const map = Object.fromEntries(names.map((n) => [n, /* @__PURE__ */ new Set()]));
68
+ return {
69
+ on(name, fn) {
70
+ map[name].add(fn);
71
+ return () => map[name].delete(fn);
72
+ },
73
+ async emit(name, ...args) {
74
+ for (const fn of map[name]) await fn(...args);
75
+ }
76
+ };
77
+ }
78
+ function randomUUID() {
79
+ const g = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {};
80
+ const c = g.crypto || g.msCrypto;
81
+ if (c && typeof c.randomUUID === "function") {
82
+ return c.randomUUID();
83
+ }
84
+ if (c && typeof c.getRandomValues === "function") {
85
+ const bytes2 = new Uint8Array(16);
86
+ c.getRandomValues(bytes2);
87
+ bytes2[6] = bytes2[6] & 15 | 64;
88
+ bytes2[8] = bytes2[8] & 63 | 128;
89
+ const hex2 = Array.from(bytes2, (b) => b.toString(16).padStart(2, "0"));
90
+ return hex2.slice(0, 4).join("") + "-" + hex2.slice(4, 6).join("") + "-" + hex2.slice(6, 8).join("") + "-" + hex2.slice(8, 10).join("") + "-" + hex2.slice(10, 16).join("");
91
+ }
92
+ try {
93
+ const req = Function('return typeof require === "function" ? require : null')();
94
+ if (req) {
95
+ const nodeCrypto = req("crypto");
96
+ if (typeof nodeCrypto.randomUUID === "function") {
97
+ return nodeCrypto.randomUUID();
98
+ }
99
+ const bytes2 = nodeCrypto.randomBytes(16);
100
+ bytes2[6] = bytes2[6] & 15 | 64;
101
+ bytes2[8] = bytes2[8] & 63 | 128;
102
+ const hex2 = Array.from(bytes2, (b) => b.toString(16).padStart(2, "0"));
103
+ return hex2.slice(0, 4).join("") + "-" + hex2.slice(4, 6).join("") + "-" + hex2.slice(6, 8).join("") + "-" + hex2.slice(8, 10).join("") + "-" + hex2.slice(10, 16).join("");
104
+ }
105
+ } catch {
106
+ }
107
+ const bytes = new Uint8Array(16);
108
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
109
+ bytes[6] = bytes[6] & 15 | 64;
110
+ bytes[8] = bytes[8] & 63 | 128;
111
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
112
+ return hex.slice(0, 4).join("") + "-" + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 16).join("");
113
+ }
114
+ class Node {
115
+ /**
116
+ * Create a new Node
117
+ * @param {Object} options - Node configuration
118
+ * @param {string} [options.id] - Unique identifier (auto-generated if not provided)
119
+ * @param {string} options.type - Node type identifier
120
+ * @param {string} [options.title] - Display title (defaults to type)
121
+ * @param {number} [options.x=0] - X position
122
+ * @param {number} [options.y=0] - Y position
123
+ * @param {number} [options.width=160] - Node width
124
+ * @param {number} [options.height=60] - Node height
125
+ */
126
+ constructor({ id, type, title, x = 0, y = 0, width = 160, height = 60 }) {
127
+ if (!type) {
128
+ throw new Error("Node type is required");
129
+ }
130
+ this.id = id ?? randomUUID();
131
+ this.type = type;
132
+ this.title = title ?? type;
133
+ this.pos = { x, y };
134
+ this.size = { width, height };
135
+ this.inputs = [];
136
+ this.outputs = [];
137
+ this.state = {};
138
+ this.parent = null;
139
+ this.children = /* @__PURE__ */ new Set();
140
+ this.computed = { x: 0, y: 0, w: 0, h: 0 };
141
+ }
142
+ /**
143
+ * Add an input port to this node
144
+ * @param {string} name - Port name
145
+ * @param {string} [datatype="any"] - Data type for the port
146
+ * @param {string} [portType="data"] - Port type: "exec" or "data"
147
+ * @returns {Object} The created port
148
+ */
149
+ addInput(name, datatype = "any", portType = "data") {
150
+ if (!name || typeof name !== "string") {
151
+ throw new Error("Input port name must be a non-empty string");
152
+ }
153
+ const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
154
+ this.inputs.push(port);
155
+ return port;
156
+ }
157
+ /**
158
+ * Add an output port to this node
159
+ * @param {string} name - Port name
160
+ * @param {string} [datatype="any"] - Data type for the port
161
+ * @param {string} [portType="data"] - Port type: "exec" or "data"
162
+ * @returns {Object} The created port
163
+ */
164
+ addOutput(name, datatype = "any", portType = "data") {
165
+ if (!name || typeof name !== "string") {
166
+ throw new Error("Output port name must be a non-empty string");
167
+ }
168
+ const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
169
+ this.outputs.push(port);
170
+ return port;
171
+ }
172
+ }
173
+ class Edge {
174
+ /**
175
+ * Create a new Edge
176
+ * @param {Object} options - Edge configuration
177
+ * @param {string} [options.id] - Unique identifier (auto-generated if not provided)
178
+ * @param {string} options.fromNode - Source node ID
179
+ * @param {string} options.fromPort - Source port ID
180
+ * @param {string} options.toNode - Target node ID
181
+ * @param {string} options.toPort - Target port ID
182
+ */
183
+ constructor({ id, fromNode, fromPort, toNode, toPort }) {
184
+ if (!fromNode || !fromPort || !toNode || !toPort) {
185
+ throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
186
+ }
187
+ this.id = id ?? randomUUID();
188
+ this.fromNode = fromNode;
189
+ this.fromPort = fromPort;
190
+ this.toNode = toNode;
191
+ this.toPort = toPort;
192
+ }
193
+ }
194
+ class GroupManager {
195
+ constructor({ graph, hooks }) {
196
+ this.graph = graph;
197
+ this.hooks = hooks;
198
+ this._groups = [];
199
+ }
200
+ // ---------- CRUD ----------
201
+ addGroup({
202
+ title = "Group",
203
+ x = 0,
204
+ y = 0,
205
+ width = 240,
206
+ height = 160,
207
+ color = "#39424e",
208
+ members = []
209
+ } = {}) {
210
+ var _a;
211
+ if (width < 100 || height < 60) {
212
+ console.warn("Group size too small, using minimum size");
213
+ width = Math.max(100, width);
214
+ height = Math.max(60, height);
215
+ }
216
+ const groupNode = this.graph.addNode("core/Group", {
217
+ title,
218
+ x,
219
+ y,
220
+ width,
221
+ height
222
+ });
223
+ groupNode.state.color = color;
224
+ for (const memberId of members) {
225
+ const node = this.graph.getNodeById(memberId);
226
+ if (node) {
227
+ if (node.type === "core/Group") {
228
+ console.warn(`Cannot add group ${memberId} as member of another group`);
229
+ continue;
230
+ }
231
+ this.graph.reparent(node, groupNode);
232
+ } else {
233
+ console.warn(`Member node ${memberId} not found, skipping`);
234
+ }
235
+ }
236
+ this._groups.push(groupNode);
237
+ (_a = this.hooks) == null ? void 0 : _a.emit("group:change");
238
+ return groupNode;
239
+ }
240
+ addGroupFromSelection({ title = "Group", margin = { x: 12, y: 12 } } = {}) {
241
+ return null;
242
+ }
243
+ removeGroup(id) {
244
+ var _a;
245
+ const groupNode = this.graph.getNodeById(id);
246
+ if (!groupNode || groupNode.type !== "core/Group") return;
247
+ const children = [...groupNode.children];
248
+ for (const child of children) {
249
+ this.graph.reparent(child, groupNode.parent);
250
+ }
251
+ this.graph.removeNode(id);
252
+ (_a = this.hooks) == null ? void 0 : _a.emit("group:change");
253
+ }
254
+ // ---------- 이동/리사이즈 ----------
255
+ // 이제 Node의 이동/리사이즈 로직을 따름.
256
+ // Controller에서 Node 이동 시 updateWorldTransforms가 호출되므로 자동 처리됨.
257
+ resizeGroup(id, dw, dh) {
258
+ var _a;
259
+ const g = this.graph.getNodeById(id);
260
+ if (!g || g.type !== "core/Group") return;
261
+ const minW = 100;
262
+ const minH = 60;
263
+ g.size.width = Math.max(minW, g.size.width + dw);
264
+ g.size.height = Math.max(minH, g.size.height + dh);
265
+ this.graph.updateWorldTransforms();
266
+ (_a = this.hooks) == null ? void 0 : _a.emit("group:change");
267
+ }
268
+ // ---------- 히트테스트 & 드래그 ----------
269
+ // 이제 Group도 Node이므로 Controller의 Node 히트테스트 로직을 따름.
270
+ // 단, Resize Handle은 별도 처리가 필요할 수 있음.
271
+ hitTestResizeHandle(x, y) {
272
+ const handleSize = 10;
273
+ const nodes = [...this.graph.nodes.values()].reverse();
274
+ for (const node of nodes) {
275
+ if (node.type !== "core/Group") continue;
276
+ const { x: gx, y: gy, w: gw, h: gh } = node.computed;
277
+ if (x >= gx + gw - handleSize && x <= gx + gw && y >= gy + gh - handleSize && y <= gy + gh) {
278
+ return { group: node, handle: "se" };
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+ }
284
+ class Graph {
285
+ /**
286
+ * Create a new Graph
287
+ * @param {Object} options - Graph configuration
288
+ * @param {Object} options.hooks - Event hooks system
289
+ * @param {Object} options.registry - Node type registry
290
+ */
291
+ constructor({ hooks, registry }) {
292
+ if (!registry) {
293
+ throw new Error("Graph requires a registry");
294
+ }
295
+ this.nodes = /* @__PURE__ */ new Map();
296
+ this.edges = /* @__PURE__ */ new Map();
297
+ this.hooks = hooks;
298
+ this.registry = registry;
299
+ this._valuesA = /* @__PURE__ */ new Map();
300
+ this._valuesB = /* @__PURE__ */ new Map();
301
+ this._useAasCurrent = true;
302
+ this.groupManager = new GroupManager({
303
+ graph: this,
304
+ hooks: this.hooks
305
+ });
306
+ }
307
+ /**
308
+ * Get a node by its ID
309
+ * @param {string} id - Node ID
310
+ * @returns {Node|null} The node or null if not found
311
+ */
312
+ getNodeById(id) {
313
+ return this.nodes.get(id) || null;
314
+ }
315
+ /**
316
+ * Add a node to the graph
317
+ * @param {string} type - Node type identifier
318
+ * @param {Object} [opts={}] - Additional node options (x, y, width, height, etc.)
319
+ * @returns {Node} The created node
320
+ * @throws {Error} If node type is not registered
321
+ */
322
+ addNode(type, opts = {}) {
323
+ var _a, _b, _c, _d;
324
+ const def = this.registry.types.get(type);
325
+ if (!def) {
326
+ const available = Array.from(this.registry.types.keys()).join(", ") || "none";
327
+ throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
328
+ }
329
+ const node = new Node({
330
+ type,
331
+ title: def.title,
332
+ width: (_a = def.size) == null ? void 0 : _a.w,
333
+ height: (_b = def.size) == null ? void 0 : _b.h,
334
+ ...opts
335
+ });
336
+ for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
337
+ for (const o of def.outputs || []) node.addOutput(o.name, o.datatype, o.portType || "data");
338
+ (_c = def.onCreate) == null ? void 0 : _c.call(def, node);
339
+ this.nodes.set(node.id, node);
340
+ (_d = this.hooks) == null ? void 0 : _d.emit("node:create", node);
341
+ return node;
342
+ }
343
+ /**
344
+ * Remove a node and its connected edges from the graph
345
+ * @param {string} nodeId - ID of the node to remove
346
+ */
347
+ removeNode(nodeId) {
348
+ for (const [eid, e] of this.edges) {
349
+ if (e.fromNode === nodeId || e.toNode === nodeId) {
350
+ this.edges.delete(eid);
351
+ }
352
+ }
353
+ this.nodes.delete(nodeId);
354
+ }
355
+ /**
356
+ * Add an edge connecting two node ports
357
+ * @param {string} fromNode - Source node ID
358
+ * @param {string} fromPort - Source port ID
359
+ * @param {string} toNode - Target node ID
360
+ * @param {string} toPort - Target port ID
361
+ * @returns {Edge} The created edge
362
+ * @throws {Error} If nodes don't exist
363
+ */
364
+ addEdge(fromNode, fromPort, toNode, toPort) {
365
+ var _a;
366
+ if (!this.nodes.has(fromNode)) {
367
+ throw new Error(`Cannot create edge: source node "${fromNode}" not found`);
368
+ }
369
+ if (!this.nodes.has(toNode)) {
370
+ throw new Error(`Cannot create edge: target node "${toNode}" not found`);
371
+ }
372
+ const e = new Edge({ fromNode, fromPort, toNode, toPort });
373
+ this.edges.set(e.id, e);
374
+ (_a = this.hooks) == null ? void 0 : _a.emit("edge:create", e);
375
+ return e;
376
+ }
377
+ /**
378
+ * Clear all nodes and edges from the graph
379
+ */
380
+ clear() {
381
+ this.nodes.clear();
382
+ this.edges.clear();
383
+ }
384
+ updateWorldTransforms() {
385
+ const roots = [];
386
+ for (const n of this.nodes.values()) {
387
+ if (!n.parent) roots.push(n);
388
+ }
389
+ const stack = roots.map((n) => ({ node: n, px: 0, py: 0 }));
390
+ while (stack.length > 0) {
391
+ const { node, px, py } = stack.pop();
392
+ node.computed.x = px + node.pos.x;
393
+ node.computed.y = py + node.pos.y;
394
+ node.computed.w = node.size.width;
395
+ node.computed.h = node.size.height;
396
+ for (const child of node.children) {
397
+ stack.push({ node: child, px: node.computed.x, py: node.computed.y });
398
+ }
399
+ }
400
+ }
401
+ reparent(node, newParent) {
402
+ if (node.parent === newParent) return;
403
+ const wx = node.computed.x;
404
+ const wy = node.computed.y;
405
+ if (node.parent) {
406
+ node.parent.children.delete(node);
407
+ }
408
+ node.parent = newParent;
409
+ if (newParent) {
410
+ newParent.children.add(node);
411
+ node.pos.x = wx - newParent.computed.x;
412
+ node.pos.y = wy - newParent.computed.y;
413
+ } else {
414
+ node.pos.x = wx;
415
+ node.pos.y = wy;
416
+ }
417
+ this.updateWorldTransforms();
418
+ }
419
+ // buffer helpers
420
+ _curBuf() {
421
+ return this._useAasCurrent ? this._valuesA : this._valuesB;
422
+ }
423
+ _nextBuf() {
424
+ return this._useAasCurrent ? this._valuesB : this._valuesA;
425
+ }
426
+ swapBuffers() {
427
+ this._useAasCurrent = !this._useAasCurrent;
428
+ this._nextBuf().clear();
429
+ }
430
+ // data helpers
431
+ setOutput(nodeId, portId, value) {
432
+ console.log(`[Graph.setOutput] nodeId: ${nodeId}, portId: ${portId}, value:`, value);
433
+ const key = `${nodeId}:${portId}`;
434
+ this._nextBuf().set(key, value);
435
+ }
436
+ getInput(nodeId, portId) {
437
+ for (const edge of this.edges.values()) {
438
+ if (edge.toNode === nodeId && edge.toPort === portId) {
439
+ const key = `${edge.fromNode}:${edge.fromPort}`;
440
+ const value = this._curBuf().get(key);
441
+ console.log(`[Graph.getInput] nodeId: ${nodeId}, portId: ${portId}, reading from ${edge.fromNode}:${edge.fromPort}, value:`, value);
442
+ return value;
443
+ }
444
+ }
445
+ console.log(`[Graph.getInput] nodeId: ${nodeId}, portId: ${portId}, no edge found, returning undefined`);
446
+ return void 0;
447
+ }
448
+ toJSON() {
449
+ var _a;
450
+ const json = {
451
+ nodes: [...this.nodes.values()].map((n) => {
452
+ var _a2;
453
+ return {
454
+ id: n.id,
455
+ type: n.type,
456
+ title: n.title,
457
+ x: n.pos.x,
458
+ y: n.pos.y,
459
+ w: n.size.width,
460
+ h: n.size.height,
461
+ inputs: n.inputs,
462
+ outputs: n.outputs,
463
+ state: n.state,
464
+ parentId: ((_a2 = n.parent) == null ? void 0 : _a2.id) || null
465
+ // Save parent relationship
466
+ };
467
+ }),
468
+ edges: [...this.edges.values()]
469
+ };
470
+ (_a = this.hooks) == null ? void 0 : _a.emit("graph:serialize", json);
471
+ return json;
472
+ }
473
+ fromJSON(json) {
474
+ var _a, _b, _c;
475
+ this.clear();
476
+ for (const nd of json.nodes) {
477
+ const node = new Node({
478
+ id: nd.id,
479
+ type: nd.type,
480
+ title: nd.title,
481
+ x: nd.x,
482
+ y: nd.y,
483
+ width: nd.w,
484
+ height: nd.h
485
+ });
486
+ const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
487
+ if (def == null ? void 0 : def.onCreate) {
488
+ def.onCreate(node);
489
+ }
490
+ node.inputs = nd.inputs;
491
+ node.outputs = nd.outputs;
492
+ node.state = { ...node.state, ...nd.state || {} };
493
+ this.nodes.set(node.id, node);
494
+ }
495
+ for (const nd of json.nodes) {
496
+ if (nd.parentId) {
497
+ const node = this.nodes.get(nd.id);
498
+ const parent = this.nodes.get(nd.parentId);
499
+ if (node && parent) {
500
+ node.parent = parent;
501
+ parent.children.add(node);
502
+ }
503
+ }
504
+ }
505
+ for (const ed of json.edges) {
506
+ this.edges.set(ed.id, new Edge(ed));
507
+ }
508
+ this.updateWorldTransforms();
509
+ (_c = this.hooks) == null ? void 0 : _c.emit("graph:deserialize", json);
510
+ return this;
511
+ }
512
+ }
513
+ function portRect(node, port, idx, dir) {
514
+ const { x: nx, y: ny, w: width, h: height } = node.computed || {
515
+ x: node.pos.x,
516
+ y: node.pos.y,
517
+ w: node.size.width,
518
+ h: node.size.height
519
+ };
520
+ const portCount = dir === "in" ? node.inputs.length : node.outputs.length;
521
+ const headerHeight = 28;
522
+ const availableHeight = (height || node.size.height) - headerHeight - 16;
523
+ const spacing = availableHeight / (portCount + 1);
524
+ const y = ny + headerHeight + spacing * (idx + 1);
525
+ const portWidth = 12;
526
+ const portHeight = 12;
527
+ if (dir === "in") {
528
+ return { x: nx - portWidth / 2, y: y - portHeight / 2, w: portWidth, h: portHeight };
529
+ }
530
+ if (dir === "out") {
531
+ return { x: nx + width - portWidth / 2, y: y - portHeight / 2, w: portWidth, h: portHeight };
532
+ }
533
+ }
534
+ const _CanvasRenderer = class _CanvasRenderer {
535
+ constructor(canvas, { theme = {}, registry, edgeStyle = "orthogonal" } = {}) {
536
+ this.canvas = canvas;
537
+ this.ctx = canvas.getContext("2d");
538
+ this.registry = registry;
539
+ this.scale = 1;
540
+ this.minScale = 0.25;
541
+ this.maxScale = 3;
542
+ this.offsetX = 0;
543
+ this.offsetY = 0;
544
+ this.edgeStyle = edgeStyle;
545
+ this.theme = Object.assign(
546
+ {
547
+ bg: "#0d0d0f",
548
+ // Darker background
549
+ grid: "#1a1a1d",
550
+ // Subtle grid
551
+ node: "#16161a",
552
+ // Darker nodes
553
+ nodeBorder: "#2a2a2f",
554
+ // Subtle border
555
+ title: "#1f1f24",
556
+ // Darker header
557
+ text: "#e4e4e7",
558
+ // Softer white
559
+ textMuted: "#a1a1aa",
560
+ // Muted text
561
+ port: "#6366f1",
562
+ // Indigo for data ports
563
+ portExec: "#10b981",
564
+ // Emerald for exec ports
565
+ edge: "#52525b",
566
+ // Neutral edge color
567
+ edgeActive: "#8b5cf6",
568
+ // Purple for active
569
+ accent: "#6366f1",
570
+ // Indigo accent
571
+ accentBright: "#818cf8"
572
+ // Brighter accent
573
+ },
574
+ theme
575
+ );
576
+ }
577
+ setEdgeStyle(style) {
578
+ this.edgeStyle = style === "line" || style === "orthogonal" ? style : "bezier";
579
+ }
580
+ setRegistry(reg) {
581
+ this.registry = reg;
582
+ }
583
+ resize(w, h) {
584
+ this.canvas.width = w;
585
+ this.canvas.height = h;
586
+ }
587
+ setTransform({
588
+ scale = this.scale,
589
+ offsetX = this.offsetX,
590
+ offsetY = this.offsetY
591
+ } = {}) {
592
+ this.scale = Math.min(this.maxScale, Math.max(this.minScale, scale));
593
+ this.offsetX = offsetX;
594
+ this.offsetY = offsetY;
595
+ }
596
+ panBy(dx, dy) {
597
+ this.offsetX += dx;
598
+ this.offsetY += dy;
599
+ }
600
+ zoomAt(factor, cx, cy) {
601
+ const prev = this.scale;
602
+ const next = Math.min(
603
+ this.maxScale,
604
+ Math.max(this.minScale, prev * factor)
605
+ );
606
+ if (next === prev) return;
607
+ const wx = (cx - this.offsetX) / prev;
608
+ const wy = (cy - this.offsetY) / prev;
609
+ this.offsetX = cx - wx * next;
610
+ this.offsetY = cy - wy * next;
611
+ this.scale = next;
612
+ }
613
+ screenToWorld(x, y) {
614
+ return {
615
+ x: (x - this.offsetX) / this.scale,
616
+ y: (y - this.offsetY) / this.scale
617
+ };
618
+ }
619
+ worldToScreen(x, y) {
620
+ return {
621
+ x: x * this.scale + this.offsetX,
622
+ y: y * this.scale + this.offsetY
623
+ };
624
+ }
625
+ _applyTransform() {
626
+ const { ctx } = this;
627
+ ctx.setTransform(this.scale, 0, 0, this.scale, this.offsetX, this.offsetY);
628
+ }
629
+ _resetTransform() {
630
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
631
+ }
632
+ // ── Drawing ────────────────────────────────────────────────────────────────
633
+ _drawArrowhead(x1, y1, x2, y2, size = 10) {
634
+ const { ctx } = this;
635
+ const s = size / this.scale;
636
+ const ang = Math.atan2(y2 - y1, x2 - x1);
637
+ ctx.beginPath();
638
+ ctx.moveTo(x2, y2);
639
+ ctx.lineTo(
640
+ x2 - s * Math.cos(ang - Math.PI / 6),
641
+ y2 - s * Math.sin(ang - Math.PI / 6)
642
+ );
643
+ ctx.lineTo(
644
+ x2 - s * Math.cos(ang + Math.PI / 6),
645
+ y2 - s * Math.sin(ang + Math.PI / 6)
646
+ );
647
+ ctx.closePath();
648
+ ctx.fill();
649
+ }
650
+ _drawScreenText(text, lx, ly, {
651
+ fontPx = 12,
652
+ color = this.theme.text,
653
+ align = "left",
654
+ baseline = "alphabetic",
655
+ dpr = 1
656
+ // 추후 devicePixelRatio 도입
657
+ } = {}) {
658
+ const { ctx } = this;
659
+ const { x: sx, y: sy } = this.worldToScreen(lx, ly);
660
+ ctx.save();
661
+ this._resetTransform();
662
+ const px = Math.round(sx) + 0.5;
663
+ const py = Math.round(sy) + 0.5;
664
+ ctx.font = `${fontPx * this.scale}px system-ui`;
665
+ ctx.fillStyle = color;
666
+ ctx.textAlign = align;
667
+ ctx.textBaseline = baseline;
668
+ ctx.fillText(text, px, py);
669
+ ctx.restore();
670
+ }
671
+ drawGrid() {
672
+ const { ctx, canvas, theme, scale, offsetX, offsetY } = this;
673
+ this._resetTransform();
674
+ ctx.fillStyle = theme.bg;
675
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
676
+ this._applyTransform();
677
+ ctx.strokeStyle = this._rgba(theme.grid, 0.35);
678
+ ctx.lineWidth = 1 / scale;
679
+ const base = 20;
680
+ const step = base;
681
+ const x0 = -offsetX / scale;
682
+ const y0 = -offsetY / scale;
683
+ const x1 = (canvas.width - offsetX) / scale;
684
+ const y1 = (canvas.height - offsetY) / scale;
685
+ const startX = Math.floor(x0 / step) * step;
686
+ const startY = Math.floor(y0 / step) * step;
687
+ ctx.beginPath();
688
+ for (let x = startX; x <= x1; x += step) {
689
+ ctx.moveTo(x, y0);
690
+ ctx.lineTo(x, y1);
691
+ }
692
+ for (let y = startY; y <= y1; y += step) {
693
+ ctx.moveTo(x0, y);
694
+ ctx.lineTo(x1, y);
695
+ }
696
+ ctx.stroke();
697
+ this._resetTransform();
698
+ }
699
+ draw(graph, {
700
+ selection = /* @__PURE__ */ new Set(),
701
+ tempEdge = null,
702
+ running = false,
703
+ time = performance.now(),
704
+ dt = 0,
705
+ groups = null,
706
+ activeEdges = /* @__PURE__ */ new Set()
707
+ } = {}) {
708
+ var _a, _b, _c, _d, _e, _f;
709
+ graph.updateWorldTransforms();
710
+ this.drawGrid();
711
+ const { ctx, theme } = this;
712
+ this._applyTransform();
713
+ ctx.save();
714
+ for (const n of graph.nodes.values()) {
715
+ if (n.type === "core/Group") {
716
+ const sel = selection.has(n.id);
717
+ const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(n.type);
718
+ if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
719
+ else this._drawNode(n, sel);
720
+ }
721
+ }
722
+ ctx.lineWidth = 1.5 / this.scale;
723
+ let dashArray = null;
724
+ let dashOffset = 0;
725
+ if (running) {
726
+ const speed = 120;
727
+ const phase = time / 1e3 * speed / this.scale % _CanvasRenderer.FONT_SIZE;
728
+ dashArray = [6 / this.scale, 6 / this.scale];
729
+ dashOffset = -phase;
730
+ }
731
+ for (const e of graph.edges.values()) {
732
+ const shouldAnimate = activeEdges && activeEdges.size > 0 && activeEdges.has(e.id);
733
+ if (running && shouldAnimate && dashArray) {
734
+ ctx.setLineDash(dashArray);
735
+ ctx.lineDashOffset = dashOffset;
736
+ } else {
737
+ ctx.setLineDash([]);
738
+ ctx.lineDashOffset = 0;
739
+ }
740
+ const isActive = activeEdges && activeEdges.has(e.id);
741
+ if (isActive) {
742
+ ctx.strokeStyle = "#00ffff";
743
+ ctx.lineWidth = 3 * this.scale;
744
+ } else {
745
+ ctx.strokeStyle = theme.edge;
746
+ ctx.lineWidth = 1.5 / this.scale;
747
+ }
748
+ this._drawEdge(graph, e);
749
+ }
750
+ if (tempEdge) {
751
+ const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
752
+ const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
753
+ const prevDash = this.ctx.getLineDash();
754
+ this.ctx.setLineDash([6 / this.scale, 6 / this.scale]);
755
+ let ptsForArrow = null;
756
+ if (this.edgeStyle === "line") {
757
+ this._drawLine(a.x, a.y, b.x, b.y);
758
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
759
+ } else if (this.edgeStyle === "orthogonal") {
760
+ ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
761
+ } else {
762
+ this._drawCurve(a.x, a.y, b.x, b.y);
763
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
764
+ }
765
+ this.ctx.setLineDash(prevDash);
766
+ if (ptsForArrow && ptsForArrow.length >= 2) {
767
+ const p1 = ptsForArrow[ptsForArrow.length - 2];
768
+ const p2 = ptsForArrow[ptsForArrow.length - 1];
769
+ this.ctx.fillStyle = this.theme.edge;
770
+ this.ctx.strokeStyle = this.theme.edge;
771
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
772
+ }
773
+ }
774
+ for (const n of graph.nodes.values()) {
775
+ if (n.type !== "core/Group") {
776
+ const sel = selection.has(n.id);
777
+ const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
778
+ const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
779
+ this._drawNode(n, sel, hasHtmlOverlay);
780
+ if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
781
+ }
782
+ }
783
+ for (const n of graph.nodes.values()) {
784
+ if (n.type !== "core/Group") {
785
+ const def = (_f = (_e = this.registry) == null ? void 0 : _e.types) == null ? void 0 : _f.get(n.type);
786
+ const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
787
+ if (hasHtmlOverlay) {
788
+ this._drawPorts(n);
789
+ }
790
+ }
791
+ }
792
+ this._resetTransform();
793
+ }
794
+ _rgba(hex, a) {
795
+ const c = hex.replace("#", "");
796
+ const n = parseInt(
797
+ c.length === 3 ? c.split("").map((x) => x + x).join("") : c,
798
+ 16
799
+ );
800
+ const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
801
+ return `rgba(${r},${g},${b},${a})`;
802
+ }
803
+ _drawNode(node, selected, skipPorts = false) {
804
+ const { ctx, theme } = this;
805
+ const r = 8;
806
+ const { x, y, w, h } = node.computed;
807
+ if (!selected) {
808
+ ctx.save();
809
+ ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
810
+ ctx.shadowBlur = 8 / this.scale;
811
+ ctx.shadowOffsetY = 2 / this.scale;
812
+ ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
813
+ roundRect(ctx, x, y, w, h, r);
814
+ ctx.fill();
815
+ ctx.restore();
816
+ }
817
+ ctx.fillStyle = theme.node;
818
+ ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
819
+ ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
820
+ roundRect(ctx, x, y, w, h, r);
821
+ ctx.fill();
822
+ ctx.stroke();
823
+ ctx.fillStyle = theme.title;
824
+ roundRect(ctx, x, y, w, 24, { tl: r, tr: r, br: 0, bl: 0 });
825
+ ctx.fill();
826
+ ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
827
+ ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
828
+ ctx.beginPath();
829
+ ctx.moveTo(x + r, y);
830
+ ctx.lineTo(x + w - r, y);
831
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
832
+ ctx.lineTo(x + w, y + 24);
833
+ ctx.moveTo(x, y + 24);
834
+ ctx.lineTo(x, y + r);
835
+ ctx.quadraticCurveTo(x, y, x + r, y);
836
+ ctx.stroke();
837
+ this._drawScreenText(node.title, x + 8, y + _CanvasRenderer.FONT_SIZE, {
838
+ fontPx: _CanvasRenderer.FONT_SIZE,
839
+ color: theme.text,
840
+ baseline: "middle",
841
+ align: "left"
842
+ });
843
+ if (skipPorts) return;
844
+ node.inputs.forEach((p, i) => {
845
+ const rct = portRect(node, p, i, "in");
846
+ const cx = rct.x + rct.w / 2;
847
+ const cy = rct.y + rct.h / 2;
848
+ if (p.portType === "exec") {
849
+ const portSize = 8;
850
+ ctx.fillStyle = theme.portExec;
851
+ ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
852
+ ctx.lineWidth = 2 / this.scale;
853
+ ctx.beginPath();
854
+ ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
855
+ ctx.fill();
856
+ ctx.stroke();
857
+ } else {
858
+ ctx.fillStyle = theme.port;
859
+ ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
860
+ ctx.lineWidth = 2 / this.scale;
861
+ ctx.beginPath();
862
+ ctx.arc(cx, cy, 5, 0, Math.PI * 2);
863
+ ctx.fill();
864
+ ctx.stroke();
865
+ }
866
+ });
867
+ node.outputs.forEach((p, i) => {
868
+ const rct = portRect(node, p, i, "out");
869
+ const cx = rct.x + rct.w / 2;
870
+ const cy = rct.y + rct.h / 2;
871
+ if (p.portType === "exec") {
872
+ const portSize = 8;
873
+ ctx.fillStyle = theme.portExec;
874
+ ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
875
+ ctx.lineWidth = 2 / this.scale;
876
+ ctx.beginPath();
877
+ ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
878
+ ctx.fill();
879
+ ctx.stroke();
880
+ } else {
881
+ ctx.fillStyle = theme.port;
882
+ ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
883
+ ctx.lineWidth = 2 / this.scale;
884
+ ctx.beginPath();
885
+ ctx.arc(cx, cy, 5, 0, Math.PI * 2);
886
+ ctx.fill();
887
+ ctx.stroke();
888
+ }
889
+ });
890
+ }
891
+ _drawPorts(node) {
892
+ const { ctx, theme } = this;
893
+ node.inputs.forEach((p, i) => {
894
+ const rct = portRect(node, p, i, "in");
895
+ const cx = rct.x + rct.w / 2;
896
+ const cy = rct.y + rct.h / 2;
897
+ if (p.portType === "exec") {
898
+ const portSize = 8;
899
+ ctx.fillStyle = theme.portExec;
900
+ ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
901
+ ctx.lineWidth = 2 / this.scale;
902
+ ctx.beginPath();
903
+ ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
904
+ ctx.fill();
905
+ ctx.stroke();
906
+ } else {
907
+ ctx.fillStyle = theme.port;
908
+ ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
909
+ ctx.lineWidth = 2 / this.scale;
910
+ ctx.beginPath();
911
+ ctx.arc(cx, cy, 5, 0, Math.PI * 2);
912
+ ctx.fill();
913
+ }
914
+ });
915
+ node.outputs.forEach((p, i) => {
916
+ const rct = portRect(node, p, i, "out");
917
+ const cx = rct.x + rct.w / 2;
918
+ const cy = rct.y + rct.h / 2;
919
+ if (p.portType === "exec") {
920
+ const portSize = 8;
921
+ ctx.fillStyle = theme.portExec;
922
+ ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
923
+ ctx.lineWidth = 2 / this.scale;
924
+ ctx.beginPath();
925
+ ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
926
+ ctx.fill();
927
+ ctx.stroke();
928
+ } else {
929
+ ctx.fillStyle = theme.port;
930
+ ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
931
+ ctx.lineWidth = 2 / this.scale;
932
+ ctx.beginPath();
933
+ ctx.arc(cx, cy, 5, 0, Math.PI * 2);
934
+ ctx.fill();
935
+ ctx.stroke();
936
+ }
937
+ });
938
+ }
939
+ _drawEdge(graph, e) {
940
+ const from = graph.nodes.get(e.fromNode);
941
+ const to = graph.nodes.get(e.toNode);
942
+ if (!from || !to) return;
943
+ const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
944
+ const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
945
+ const pr1 = portRect(from, null, iOut, "out");
946
+ const pr2 = portRect(to, null, iIn, "in");
947
+ const x1 = pr1.x, y1 = pr1.y + 7, x2 = pr2.x, y2 = pr2.y + 7;
948
+ if (this.edgeStyle === "line") {
949
+ this._drawLine(x1, y1, x2, y2);
950
+ } else if (this.edgeStyle === "orthogonal") {
951
+ this._drawOrthogonal(x1, y1, x2, y2);
952
+ } else {
953
+ this._drawCurve(x1, y1, x2, y2);
954
+ }
955
+ }
956
+ _drawLine(x1, y1, x2, y2) {
957
+ const { ctx } = this;
958
+ ctx.beginPath();
959
+ ctx.moveTo(x1, y1);
960
+ ctx.lineTo(x2, y2);
961
+ ctx.stroke();
962
+ }
963
+ _drawPolyline(points) {
964
+ const { ctx } = this;
965
+ ctx.beginPath();
966
+ ctx.moveTo(points[0].x, points[0].y);
967
+ for (let i = 1; i < points.length; i++)
968
+ ctx.lineTo(points[i].x, points[i].y);
969
+ ctx.stroke();
970
+ }
971
+ _drawOrthogonal(x1, y1, x2, y2) {
972
+ const midX = (x1 + x2) / 2;
973
+ let pts;
974
+ {
975
+ pts = [
976
+ { x: x1, y: y1 },
977
+ { x: midX, y: y1 },
978
+ { x: midX, y: y2 },
979
+ { x: x2, y: y2 }
980
+ ];
981
+ }
982
+ const { ctx } = this;
983
+ const prevJoin = ctx.lineJoin, prevCap = ctx.lineCap;
984
+ ctx.lineJoin = "round";
985
+ ctx.lineCap = "round";
986
+ this._drawPolyline(pts);
987
+ ctx.lineJoin = prevJoin;
988
+ ctx.lineCap = prevCap;
989
+ return pts;
990
+ }
991
+ _drawCurve(x1, y1, x2, y2) {
992
+ const { ctx } = this;
993
+ const dx = Math.max(40, Math.abs(x2 - x1) * 0.4);
994
+ ctx.beginPath();
995
+ ctx.moveTo(x1, y1);
996
+ ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
997
+ ctx.stroke();
998
+ }
999
+ };
1000
+ __publicField(_CanvasRenderer, "FONT_SIZE", 12);
1001
+ __publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
1002
+ let CanvasRenderer = _CanvasRenderer;
1003
+ function roundRect(ctx, x, y, w, h, r = 6) {
1004
+ if (typeof r === "number") r = { tl: r, tr: r, br: r, bl: r };
1005
+ ctx.beginPath();
1006
+ ctx.moveTo(x + r.tl, y);
1007
+ ctx.lineTo(x + w - r.tr, y);
1008
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr);
1009
+ ctx.lineTo(x + w, y + h - r.br);
1010
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
1011
+ ctx.lineTo(x + r.bl, y + h);
1012
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl);
1013
+ ctx.lineTo(x, y + r.tl);
1014
+ ctx.quadraticCurveTo(x, y, x + r.tl, y);
1015
+ ctx.closePath();
1016
+ }
1017
+ function findEdgeId(graph, a, b, c, d) {
1018
+ for (const [id, e] of graph.edges) {
1019
+ if (e.fromNode === a && e.fromPort === b && e.toNode === c && e.toPort === d)
1020
+ return id;
1021
+ }
1022
+ return null;
1023
+ }
1024
+ function AddEdgeCmd(graph, fromNode, fromPort, toNode, toPort) {
1025
+ let addedId = null;
1026
+ return {
1027
+ do() {
1028
+ graph.addEdge(fromNode, fromPort, toNode, toPort);
1029
+ addedId = findEdgeId(graph, fromNode, fromPort, toNode, toPort);
1030
+ },
1031
+ undo() {
1032
+ const id = addedId ?? findEdgeId(graph, fromNode, fromPort, toNode, toPort);
1033
+ if (id != null) graph.edges.delete(id);
1034
+ }
1035
+ };
1036
+ }
1037
+ function RemoveEdgeCmd(graph, edgeId) {
1038
+ const e = graph.edges.get(edgeId);
1039
+ if (!e) return null;
1040
+ const { fromNode, fromPort, toNode, toPort } = e;
1041
+ return {
1042
+ do() {
1043
+ graph.edges.delete(edgeId);
1044
+ },
1045
+ undo() {
1046
+ graph.addEdge(fromNode, fromPort, toNode, toPort);
1047
+ }
1048
+ };
1049
+ }
1050
+ function RemoveNodeCmd(graph, node) {
1051
+ let removedNode = null;
1052
+ let removedEdges = [];
1053
+ return {
1054
+ do() {
1055
+ removedNode = node;
1056
+ removedEdges = graph.edges ? [...graph.edges.values()].filter((e) => {
1057
+ return e.fromNode === node.id || e.toNode === node.id;
1058
+ }) : [];
1059
+ for (const edge of removedEdges) {
1060
+ graph.edges.delete(edge.id);
1061
+ }
1062
+ graph.nodes.delete(node.id);
1063
+ },
1064
+ undo() {
1065
+ if (removedNode) {
1066
+ graph.nodes.set(removedNode.id, removedNode);
1067
+ }
1068
+ for (const edge of removedEdges) {
1069
+ graph.edges.set(edge.id, edge);
1070
+ }
1071
+ }
1072
+ };
1073
+ }
1074
+ function ResizeNodeCmd(node, fromSize, toSize) {
1075
+ return {
1076
+ do() {
1077
+ node.size.width = toSize.w;
1078
+ node.size.height = toSize.h;
1079
+ },
1080
+ undo() {
1081
+ node.size.width = fromSize.w;
1082
+ node.size.height = fromSize.h;
1083
+ }
1084
+ };
1085
+ }
1086
+ function ChangeGroupColorCmd(node, fromColor, toColor) {
1087
+ return {
1088
+ do() {
1089
+ node.state.color = toColor;
1090
+ },
1091
+ undo() {
1092
+ node.state.color = fromColor;
1093
+ }
1094
+ };
1095
+ }
1096
+ class CommandStack {
1097
+ constructor() {
1098
+ this.undoStack = [];
1099
+ this.redoStack = [];
1100
+ }
1101
+ exec(cmd) {
1102
+ cmd.do();
1103
+ this.undoStack.push(cmd);
1104
+ this.redoStack.length = 0;
1105
+ }
1106
+ undo() {
1107
+ const c = this.undoStack.pop();
1108
+ if (c) {
1109
+ c.undo();
1110
+ this.redoStack.push(c);
1111
+ }
1112
+ }
1113
+ redo() {
1114
+ const c = this.redoStack.pop();
1115
+ if (c) {
1116
+ c.do();
1117
+ this.undoStack.push(c);
1118
+ }
1119
+ }
1120
+ }
1121
+ const _Controller = class _Controller {
1122
+ constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, portRenderer }) {
1123
+ this.graph = graph;
1124
+ this.renderer = renderer;
1125
+ this.hooks = hooks;
1126
+ this.htmlOverlay = htmlOverlay;
1127
+ this.contextMenu = contextMenu;
1128
+ this.portRenderer = portRenderer;
1129
+ this.stack = new CommandStack();
1130
+ this.selection = /* @__PURE__ */ new Set();
1131
+ this.dragging = null;
1132
+ this.connecting = null;
1133
+ this.panning = null;
1134
+ this.resizing = null;
1135
+ this.gDragging = null;
1136
+ this.gResizing = null;
1137
+ this.boxSelecting = null;
1138
+ this.snapToGrid = true;
1139
+ this.gridSize = 20;
1140
+ this._cursor = "default";
1141
+ this._onKeyPressEvt = this._onKeyPress.bind(this);
1142
+ this._onDownEvt = this._onDown.bind(this);
1143
+ this._onWheelEvt = this._onWheel.bind(this);
1144
+ this._onMoveEvt = this._onMove.bind(this);
1145
+ this._onUpEvt = this._onUp.bind(this);
1146
+ this._onContextMenuEvt = this._onContextMenu.bind(this);
1147
+ this._onDblClickEvt = this._onDblClick.bind(this);
1148
+ this._bindEvents();
1149
+ }
1150
+ destroy() {
1151
+ const c = this.renderer.canvas;
1152
+ c.removeEventListener("mousedown", this._onDownEvt);
1153
+ c.removeEventListener("dblclick", this._onDblClickEvt);
1154
+ c.removeEventListener("wheel", this._onWheelEvt, { passive: false });
1155
+ c.removeEventListener("contextmenu", this._onContextMenuEvt);
1156
+ window.removeEventListener("mousemove", this._onMoveEvt);
1157
+ window.removeEventListener("mouseup", this._onUpEvt);
1158
+ window.removeEventListener("keydown", this._onKeyPressEvt);
1159
+ }
1160
+ _bindEvents() {
1161
+ const c = this.renderer.canvas;
1162
+ c.addEventListener("mousedown", this._onDownEvt);
1163
+ c.addEventListener("dblclick", this._onDblClickEvt);
1164
+ c.addEventListener("wheel", this._onWheelEvt, { passive: false });
1165
+ c.addEventListener("contextmenu", this._onContextMenuEvt);
1166
+ window.addEventListener("mousemove", this._onMoveEvt);
1167
+ window.addEventListener("mouseup", this._onUpEvt);
1168
+ window.addEventListener("keydown", this._onKeyPressEvt);
1169
+ }
1170
+ _onKeyPress(e) {
1171
+ this.isAlt = e.altKey;
1172
+ this.isShift = e.shiftKey;
1173
+ this.isCtrl = e.ctrlKey;
1174
+ if (e.key.toLowerCase() === "g" && !e.ctrlKey && !e.metaKey) {
1175
+ this.snapToGrid = !this.snapToGrid;
1176
+ this.render();
1177
+ return;
1178
+ }
1179
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "g") {
1180
+ e.preventDefault();
1181
+ this._createGroupFromSelection();
1182
+ return;
1183
+ }
1184
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
1185
+ e.preventDefault();
1186
+ if (e.shiftKey) this.stack.redo();
1187
+ else this.stack.undo();
1188
+ this.render();
1189
+ return;
1190
+ }
1191
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y") {
1192
+ e.preventDefault();
1193
+ this.stack.redo();
1194
+ this.render();
1195
+ return;
1196
+ }
1197
+ if (e.key.toLowerCase() === "a" && this.selection.size > 1) {
1198
+ e.preventDefault();
1199
+ if (e.shiftKey) {
1200
+ this._alignNodesVertical();
1201
+ } else {
1202
+ this._alignNodesHorizontal();
1203
+ }
1204
+ return;
1205
+ }
1206
+ if (e.key === "Delete") {
1207
+ [...this.selection].forEach((node) => {
1208
+ const nodeObj = this.graph.getNodeById(node);
1209
+ this.stack.exec(RemoveNodeCmd(this.graph, nodeObj));
1210
+ this.graph.removeNode(node);
1211
+ });
1212
+ this.render();
1213
+ }
1214
+ }
1215
+ _setCursor(c) {
1216
+ if (this._cursor !== c) {
1217
+ this._cursor = c;
1218
+ this.renderer.canvas.style.cursor = c;
1219
+ }
1220
+ }
1221
+ _posScreen(e) {
1222
+ const r = this.renderer.canvas.getBoundingClientRect();
1223
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
1224
+ }
1225
+ _posWorld(e) {
1226
+ const s = this._posScreen(e);
1227
+ return this.renderer.screenToWorld(s.x, s.y);
1228
+ }
1229
+ _findNodeAtWorld(x, y) {
1230
+ const list = [...this.graph.nodes.values()].reverse();
1231
+ for (const n of list) {
1232
+ const { x: nx, y: ny, w, h } = n.computed;
1233
+ if (x >= nx && x <= nx + w && y >= ny && y <= ny + h) {
1234
+ if (n.type === "core/Group") {
1235
+ const child = this._findChildNodeAtWorld(n, x, y);
1236
+ if (child) {
1237
+ return child;
1238
+ }
1239
+ }
1240
+ return n;
1241
+ }
1242
+ }
1243
+ return null;
1244
+ }
1245
+ /**
1246
+ * Find child node at world coordinates (recursive helper for _findNodeAtWorld)
1247
+ * @param {Node} parentNode - Parent node (group)
1248
+ * @param {number} x - World x coordinate
1249
+ * @param {number} y - World y coordinate
1250
+ * @returns {Node|null} - Child node at position, or null
1251
+ */
1252
+ _findChildNodeAtWorld(parentNode, x, y) {
1253
+ const children = [];
1254
+ for (const node of this.graph.nodes.values()) {
1255
+ if (node.parent === parentNode) {
1256
+ children.push(node);
1257
+ }
1258
+ }
1259
+ for (let i = children.length - 1; i >= 0; i--) {
1260
+ const child = children[i];
1261
+ const { x: nx, y: ny, w, h } = child.computed;
1262
+ if (x >= nx && x <= nx + w && y >= ny && y <= ny + h) {
1263
+ if (child.type === "core/Group") {
1264
+ const grandchild = this._findChildNodeAtWorld(child, x, y);
1265
+ if (grandchild) {
1266
+ return grandchild;
1267
+ }
1268
+ }
1269
+ return child;
1270
+ }
1271
+ }
1272
+ return null;
1273
+ }
1274
+ _findPortAtWorld(x, y) {
1275
+ for (const n of this.graph.nodes.values()) {
1276
+ for (let i = 0; i < n.inputs.length; i++) {
1277
+ const r = portRect(n, n.inputs[i], i, "in");
1278
+ if (rectHas(r, x, y))
1279
+ return { node: n, port: n.inputs[i], dir: "in", idx: i };
1280
+ }
1281
+ for (let i = 0; i < n.outputs.length; i++) {
1282
+ const r = portRect(n, n.outputs[i], i, "out");
1283
+ if (rectHas(r, x, y))
1284
+ return { node: n, port: n.outputs[i], dir: "out", idx: i };
1285
+ }
1286
+ }
1287
+ return null;
1288
+ }
1289
+ _findIncomingEdge(nodeId, portId) {
1290
+ for (const [eid, e] of this.graph.edges) {
1291
+ if (e.toNode === nodeId && e.toPort === portId) {
1292
+ return { id: eid, edge: e };
1293
+ }
1294
+ }
1295
+ return null;
1296
+ }
1297
+ _onWheel(e) {
1298
+ e.preventDefault();
1299
+ const { x, y } = this._posScreen(e);
1300
+ const factor = Math.pow(1.0015, -e.deltaY);
1301
+ this.renderer.zoomAt(factor, x, y);
1302
+ this.render();
1303
+ }
1304
+ _onContextMenu(e) {
1305
+ e.preventDefault();
1306
+ if (!this.contextMenu) return;
1307
+ const w = this._posWorld(e);
1308
+ const node = this._findNodeAtWorld(w.x, w.y);
1309
+ this.contextMenu.show(node, e.clientX, e.clientY, w);
1310
+ }
1311
+ _onDblClick(e) {
1312
+ var _a;
1313
+ const w = this._posWorld(e);
1314
+ const node = this._findNodeAtWorld(w.x, w.y);
1315
+ if (node) {
1316
+ (_a = this.hooks) == null ? void 0 : _a.emit("node:dblclick", node);
1317
+ }
1318
+ }
1319
+ _resizeHandleRect(node) {
1320
+ const s = 10;
1321
+ const { x, y, w, h } = node.computed;
1322
+ return {
1323
+ x: x + w - s,
1324
+ y: y + h - s,
1325
+ w: s,
1326
+ h: s
1327
+ };
1328
+ }
1329
+ _hitResizeHandle(node, wx, wy) {
1330
+ const r = this._resizeHandleRect(node);
1331
+ return wx >= r.x && wx <= r.x + r.w && wy >= r.y && wy <= r.y + r.h;
1332
+ }
1333
+ _onDown(e) {
1334
+ const s = this._posScreen(e);
1335
+ const w = this._posWorld(e);
1336
+ if (e.button === 1) {
1337
+ this.panning = { x: s.x, y: s.y };
1338
+ return;
1339
+ }
1340
+ const node = this._findNodeAtWorld(w.x, w.y);
1341
+ if (e.button === 0 && node && this._hitResizeHandle(node, w.x, w.y)) {
1342
+ this.resizing = {
1343
+ nodeId: node.id,
1344
+ startW: node.size.width,
1345
+ startH: node.size.height,
1346
+ startX: w.x,
1347
+ startY: w.y
1348
+ };
1349
+ if (!e.shiftKey) this.selection.clear();
1350
+ this.selection.add(node.id);
1351
+ this._setCursor("se-resize");
1352
+ this.render();
1353
+ return;
1354
+ }
1355
+ const port = this._findPortAtWorld(w.x, w.y);
1356
+ if (e.button === 0 && port && port.dir === "in") {
1357
+ const incoming = this._findIncomingEdge(port.node.id, port.port.id);
1358
+ if (incoming) {
1359
+ this.stack.exec(RemoveEdgeCmd(this.graph, incoming.id));
1360
+ this.render();
1361
+ return;
1362
+ }
1363
+ }
1364
+ if (e.button === 0 && port && port.dir === "out") {
1365
+ const outR = portRect(port.node, port.port, port.idx, "out");
1366
+ const screenFrom = this.renderer.worldToScreen(outR.x, outR.y + 7);
1367
+ this.connecting = {
1368
+ fromNode: port.node.id,
1369
+ fromPort: port.port.id,
1370
+ x: screenFrom.x,
1371
+ y: screenFrom.y
1372
+ };
1373
+ return;
1374
+ }
1375
+ if (e.button === 0 && node) {
1376
+ if (!e.shiftKey) this.selection.clear();
1377
+ this.selection.add(node.id);
1378
+ this.dragging = {
1379
+ nodeId: node.id,
1380
+ offsetX: w.x - node.computed.x,
1381
+ offsetY: w.y - node.computed.y,
1382
+ startPos: { ...node.pos },
1383
+ // for undo
1384
+ selectedNodes: []
1385
+ // Store all selected nodes and their initial positions
1386
+ };
1387
+ for (const selectedId of this.selection) {
1388
+ const selectedNode = this.graph.nodes.get(selectedId);
1389
+ if (selectedNode) {
1390
+ this.dragging.selectedNodes.push({
1391
+ node: selectedNode,
1392
+ startWorldX: selectedNode.computed.x,
1393
+ startWorldY: selectedNode.computed.y,
1394
+ startLocalX: selectedNode.pos.x,
1395
+ startLocalY: selectedNode.pos.y
1396
+ });
1397
+ }
1398
+ }
1399
+ if (node.type === "core/Group") {
1400
+ this.dragging.childrenWorldPos = [];
1401
+ for (const child of this.graph.nodes.values()) {
1402
+ if (child.parent === node) {
1403
+ this.dragging.childrenWorldPos.push({
1404
+ node: child,
1405
+ worldX: child.computed.x,
1406
+ worldY: child.computed.y
1407
+ });
1408
+ }
1409
+ }
1410
+ }
1411
+ this.render();
1412
+ return;
1413
+ }
1414
+ if (e.button === 0) {
1415
+ if (this.selection.size) this.selection.clear();
1416
+ if (e.ctrlKey || e.metaKey) {
1417
+ this.boxSelecting = {
1418
+ startX: w.x,
1419
+ startY: w.y,
1420
+ currentX: w.x,
1421
+ currentY: w.y
1422
+ };
1423
+ } else {
1424
+ this.panning = { x: s.x, y: s.y };
1425
+ }
1426
+ this.render();
1427
+ return;
1428
+ }
1429
+ }
1430
+ _onMove(e) {
1431
+ var _a, _b;
1432
+ this.isAlt = e.altKey;
1433
+ this.isShift = e.shiftKey;
1434
+ this.isCtrl = e.ctrlKey;
1435
+ const s = this._posScreen(e);
1436
+ const w = this.renderer.screenToWorld(s.x, s.y);
1437
+ if (this.resizing) {
1438
+ const n = this.graph.nodes.get(this.resizing.nodeId);
1439
+ const dx = w.x - this.resizing.startX;
1440
+ const dy = w.y - this.resizing.startY;
1441
+ const minW = _Controller.MIN_NODE_WIDTH;
1442
+ const minH = _Controller.MIN_NODE_HEIGHT;
1443
+ n.size.width = Math.max(minW, this.resizing.startW + dx);
1444
+ n.size.height = Math.max(minH, this.resizing.startH + dy);
1445
+ (_a = this.hooks) == null ? void 0 : _a.emit("node:resize", n);
1446
+ this._setCursor("se-resize");
1447
+ this.render();
1448
+ return;
1449
+ }
1450
+ if (this.panning) {
1451
+ const dx = s.x - this.panning.x;
1452
+ const dy = s.y - this.panning.y;
1453
+ this.panning = { x: s.x, y: s.y };
1454
+ this.renderer.panBy(dx, dy);
1455
+ this.render();
1456
+ return;
1457
+ }
1458
+ if (this.dragging) {
1459
+ const n = this.graph.nodes.get(this.dragging.nodeId);
1460
+ let targetWx = w.x - this.dragging.offsetX;
1461
+ let targetWy = this.isShift ? w.y - 0 : w.y - this.dragging.offsetY;
1462
+ if (this.snapToGrid) {
1463
+ targetWx = this._snapToGrid(targetWx);
1464
+ targetWy = this._snapToGrid(targetWy);
1465
+ }
1466
+ const deltaX = targetWx - this.dragging.selectedNodes.find((sn) => sn.node.id === n.id).startWorldX;
1467
+ const deltaY = targetWy - this.dragging.selectedNodes.find((sn) => sn.node.id === n.id).startWorldY;
1468
+ this.graph.updateWorldTransforms();
1469
+ for (const { node: selectedNode, startWorldX, startWorldY } of this.dragging.selectedNodes) {
1470
+ if (this.isShift && selectedNode.type === "core/Group") {
1471
+ continue;
1472
+ }
1473
+ let newWorldX = startWorldX + deltaX;
1474
+ let newWorldY = startWorldY + deltaY;
1475
+ let parentWx = 0;
1476
+ let parentWy = 0;
1477
+ if (selectedNode.parent) {
1478
+ parentWx = selectedNode.parent.computed.x;
1479
+ parentWy = selectedNode.parent.computed.y;
1480
+ }
1481
+ selectedNode.pos.x = newWorldX - parentWx;
1482
+ selectedNode.pos.y = newWorldY - parentWy;
1483
+ }
1484
+ if (this.isAlt && n.type === "core/Group" && this.dragging.childrenWorldPos) {
1485
+ this.graph.updateWorldTransforms();
1486
+ for (const childInfo of this.dragging.childrenWorldPos) {
1487
+ const child = childInfo.node;
1488
+ const newGroupX = n.computed.x;
1489
+ const newGroupY = n.computed.y;
1490
+ child.pos.x = childInfo.worldX - newGroupX;
1491
+ child.pos.y = childInfo.worldY - newGroupY;
1492
+ }
1493
+ }
1494
+ (_b = this.hooks) == null ? void 0 : _b.emit("node:move", n);
1495
+ this.render();
1496
+ return;
1497
+ }
1498
+ if (this.boxSelecting) {
1499
+ this.boxSelecting.currentX = w.x;
1500
+ this.boxSelecting.currentY = w.y;
1501
+ this.render();
1502
+ return;
1503
+ }
1504
+ if (this.connecting) {
1505
+ this.connecting.x = s.x;
1506
+ this.connecting.y = s.y;
1507
+ this.render();
1508
+ }
1509
+ const port = this._findPortAtWorld(w.x, w.y);
1510
+ const node = this._findNodeAtWorld(w.x, w.y);
1511
+ if (node && this._hitResizeHandle(node, w.x, w.y)) {
1512
+ this._setCursor("se-resize");
1513
+ } else if (port) {
1514
+ this._setCursor("pointer");
1515
+ } else {
1516
+ this._setCursor("default");
1517
+ }
1518
+ }
1519
+ _onUp(e) {
1520
+ this.isAlt = e.altKey;
1521
+ this.isShift = e.shiftKey;
1522
+ this.isCtrl = e.ctrlKey;
1523
+ const w = this._posWorld(e);
1524
+ if (this.panning) {
1525
+ this.panning = null;
1526
+ return;
1527
+ }
1528
+ if (this.connecting) {
1529
+ const from = this.connecting;
1530
+ const portIn = this._findPortAtWorld(w.x, w.y);
1531
+ if (portIn && portIn.dir === "in") {
1532
+ this.stack.exec(
1533
+ AddEdgeCmd(
1534
+ this.graph,
1535
+ from.fromNode,
1536
+ from.fromPort,
1537
+ portIn.node.id,
1538
+ portIn.port.id
1539
+ )
1540
+ );
1541
+ }
1542
+ this.connecting = null;
1543
+ this.render();
1544
+ }
1545
+ if (this.resizing) {
1546
+ const n = this.graph.nodes.get(this.resizing.nodeId);
1547
+ const from = { w: this.resizing.startW, h: this.resizing.startH };
1548
+ const to = { w: n.size.width, h: n.size.height };
1549
+ if (from.w !== to.w || from.h !== to.h) {
1550
+ this.stack.exec(ResizeNodeCmd(n, from, to));
1551
+ }
1552
+ this.resizing = null;
1553
+ this._setCursor("default");
1554
+ }
1555
+ if (this.dragging) {
1556
+ const n = this.graph.nodes.get(this.dragging.nodeId);
1557
+ if (n.type === "core/Group" && this.isAlt && this.dragging.childrenWorldPos) {
1558
+ for (const childInfo of this.dragging.childrenWorldPos) {
1559
+ const child = childInfo.node;
1560
+ this.graph.updateWorldTransforms();
1561
+ const newGroupX = n.computed.x;
1562
+ const newGroupY = n.computed.y;
1563
+ child.pos.x = childInfo.worldX - newGroupX;
1564
+ child.pos.y = childInfo.worldY - newGroupY;
1565
+ }
1566
+ } else if (n.type === "core/Group" && !this.isAlt) {
1567
+ this._autoParentNodesInGroup(n);
1568
+ } else if (n.type !== "core/Group") {
1569
+ const potentialParent = this._findPotentialParent(w.x, w.y, n);
1570
+ if (potentialParent && potentialParent !== n.parent) {
1571
+ this.graph.reparent(n, potentialParent);
1572
+ } else if (!potentialParent && n.parent) {
1573
+ this.graph.reparent(n, null);
1574
+ }
1575
+ }
1576
+ this.dragging = null;
1577
+ this.render();
1578
+ }
1579
+ if (this.boxSelecting) {
1580
+ const { startX, startY, currentX, currentY } = this.boxSelecting;
1581
+ const minX = Math.min(startX, currentX);
1582
+ const maxX = Math.max(startX, currentX);
1583
+ const minY = Math.min(startY, currentY);
1584
+ const maxY = Math.max(startY, currentY);
1585
+ for (const node of this.graph.nodes.values()) {
1586
+ const { x, y, w: w2, h } = node.computed;
1587
+ if (x + w2 >= minX && x <= maxX && y + h >= minY && y <= maxY) {
1588
+ this.selection.add(node.id);
1589
+ }
1590
+ }
1591
+ this.boxSelecting = null;
1592
+ this.render();
1593
+ }
1594
+ }
1595
+ /**
1596
+ * Automatically parent nodes that are within the group's bounds
1597
+ * @param {Node} groupNode - The group node
1598
+ */
1599
+ _autoParentNodesInGroup(groupNode) {
1600
+ const { x: gx, y: gy, w: gw, h: gh } = groupNode.computed;
1601
+ for (const node of this.graph.nodes.values()) {
1602
+ if (node === groupNode) continue;
1603
+ if (node.parent === groupNode) continue;
1604
+ if (node.type === "core/Group") continue;
1605
+ const { x: nx, y: ny, w: nw, h: nh } = node.computed;
1606
+ const nodeCenterX = nx + nw / 2;
1607
+ const nodeCenterY = ny + nh / 2;
1608
+ if (nodeCenterX >= gx && nodeCenterX <= gx + gw && nodeCenterY >= gy && nodeCenterY <= gy + gh) {
1609
+ this.graph.reparent(node, groupNode);
1610
+ }
1611
+ }
1612
+ }
1613
+ _findPotentialParent(x, y, excludeNode) {
1614
+ const list = [...this.graph.nodes.values()].reverse();
1615
+ for (const n of list) {
1616
+ if (n.type !== "core/Group") continue;
1617
+ if (n === excludeNode) continue;
1618
+ let p = n.parent;
1619
+ let isDescendant = false;
1620
+ while (p) {
1621
+ if (p === excludeNode) {
1622
+ isDescendant = true;
1623
+ break;
1624
+ }
1625
+ p = p.parent;
1626
+ }
1627
+ if (isDescendant) continue;
1628
+ const { x: nx, y: ny, w, h } = n.computed;
1629
+ if (x >= nx && x <= nx + w && y >= ny && y <= ny + h) {
1630
+ return n;
1631
+ }
1632
+ }
1633
+ return null;
1634
+ }
1635
+ /**
1636
+ * Snap a coordinate to the grid
1637
+ * @param {number} value - The value to snap
1638
+ * @returns {number} - Snapped value
1639
+ */
1640
+ _snapToGrid(value) {
1641
+ return Math.round(value / this.gridSize) * this.gridSize;
1642
+ }
1643
+ /**
1644
+ * Create a group from currently selected nodes
1645
+ */
1646
+ _createGroupFromSelection() {
1647
+ if (this.selection.size === 0) {
1648
+ console.warn("No nodes selected to group");
1649
+ return;
1650
+ }
1651
+ const selectedNodes = Array.from(this.selection).map((id) => this.graph.getNodeById(id));
1652
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1653
+ for (const node of selectedNodes) {
1654
+ const { x, y, w, h } = node.computed;
1655
+ minX = Math.min(minX, x);
1656
+ minY = Math.min(minY, y);
1657
+ maxX = Math.max(maxX, x + w);
1658
+ maxY = Math.max(maxY, y + h);
1659
+ }
1660
+ const margin = 20;
1661
+ const groupX = minX - margin;
1662
+ const groupY = minY - margin;
1663
+ const groupWidth = maxX - minX + margin * 2;
1664
+ const groupHeight = maxY - minY + margin * 2;
1665
+ if (this.graph.groupManager) {
1666
+ this.graph.groupManager.addGroup({
1667
+ title: "Group",
1668
+ x: groupX,
1669
+ y: groupY,
1670
+ width: groupWidth,
1671
+ height: groupHeight,
1672
+ members: Array.from(this.selection)
1673
+ });
1674
+ this.selection.clear();
1675
+ this.render();
1676
+ }
1677
+ }
1678
+ /**
1679
+ * Align selected nodes horizontally (same Y position)
1680
+ */
1681
+ _alignNodesHorizontal() {
1682
+ if (this.selection.size < 2) return;
1683
+ const nodes = Array.from(this.selection).map((id) => this.graph.getNodeById(id));
1684
+ const avgY = nodes.reduce((sum, n) => sum + n.computed.y, 0) / nodes.length;
1685
+ for (const node of nodes) {
1686
+ const parentY = node.parent ? node.parent.computed.y : 0;
1687
+ node.pos.y = avgY - parentY;
1688
+ }
1689
+ this.graph.updateWorldTransforms();
1690
+ this.render();
1691
+ }
1692
+ /**
1693
+ * Align selected nodes vertically (same X position)
1694
+ */
1695
+ _alignNodesVertical() {
1696
+ if (this.selection.size < 2) return;
1697
+ const nodes = Array.from(this.selection).map((id) => this.graph.getNodeById(id));
1698
+ const avgX = nodes.reduce((sum, n) => sum + n.computed.x, 0) / nodes.length;
1699
+ for (const node of nodes) {
1700
+ const parentX = node.parent ? node.parent.computed.x : 0;
1701
+ node.pos.x = avgX - parentX;
1702
+ }
1703
+ this.graph.updateWorldTransforms();
1704
+ this.render();
1705
+ }
1706
+ render() {
1707
+ var _a, _b, _c;
1708
+ const tEdge = this.renderTempEdge();
1709
+ this.renderer.draw(this.graph, {
1710
+ selection: this.selection,
1711
+ tempEdge: tEdge,
1712
+ boxSelecting: this.boxSelecting,
1713
+ activeEdges: this.activeEdges || /* @__PURE__ */ new Set()
1714
+ // For animation
1715
+ });
1716
+ (_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
1717
+ if (this.boxSelecting) {
1718
+ const { startX, startY, currentX, currentY } = this.boxSelecting;
1719
+ const minX = Math.min(startX, currentX);
1720
+ const minY = Math.min(startY, currentY);
1721
+ const width = Math.abs(currentX - startX);
1722
+ const height = Math.abs(currentY - startY);
1723
+ const screenStart = this.renderer.worldToScreen(minX, minY);
1724
+ const screenEnd = this.renderer.worldToScreen(minX + width, minY + height);
1725
+ const ctx = this.renderer.ctx;
1726
+ ctx.save();
1727
+ this.renderer._resetTransform();
1728
+ ctx.strokeStyle = "#6cf";
1729
+ ctx.fillStyle = "rgba(102, 204, 255, 0.1)";
1730
+ ctx.lineWidth = 2;
1731
+ ctx.strokeRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
1732
+ ctx.fillRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
1733
+ ctx.restore();
1734
+ }
1735
+ if (this.portRenderer) {
1736
+ const portCtx = this.portRenderer.ctx;
1737
+ portCtx.clearRect(0, 0, this.portRenderer.canvas.width, this.portRenderer.canvas.height);
1738
+ this.portRenderer.scale = this.renderer.scale;
1739
+ this.portRenderer.offsetX = this.renderer.offsetX;
1740
+ this.portRenderer.offsetY = this.renderer.offsetY;
1741
+ this.portRenderer._applyTransform();
1742
+ for (const n of this.graph.nodes.values()) {
1743
+ if (n.type !== "core/Group") {
1744
+ const def = (_c = (_b = this.portRenderer.registry) == null ? void 0 : _b.types) == null ? void 0 : _c.get(n.type);
1745
+ const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
1746
+ if (hasHtmlOverlay) {
1747
+ this.portRenderer._drawPorts(n);
1748
+ }
1749
+ }
1750
+ }
1751
+ this.portRenderer._resetTransform();
1752
+ }
1753
+ }
1754
+ renderTempEdge() {
1755
+ if (!this.connecting) return null;
1756
+ const a = this._portAnchorScreen(
1757
+ this.connecting.fromNode,
1758
+ this.connecting.fromPort
1759
+ );
1760
+ return {
1761
+ x1: a.x,
1762
+ y1: a.y,
1763
+ x2: this.connecting.x,
1764
+ y2: this.connecting.y
1765
+ };
1766
+ }
1767
+ _portAnchorScreen(nodeId, portId) {
1768
+ const n = this.graph.nodes.get(nodeId);
1769
+ const iOut = n.outputs.findIndex((p) => p.id === portId);
1770
+ const r = portRect(n, null, iOut, "out");
1771
+ return this.renderer.worldToScreen(r.x, r.y + 7);
1772
+ }
1773
+ };
1774
+ __publicField(_Controller, "MIN_NODE_WIDTH", 80);
1775
+ __publicField(_Controller, "MIN_NODE_HEIGHT", 60);
1776
+ let Controller = _Controller;
1777
+ function rectHas(r, x, y) {
1778
+ return x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h;
1779
+ }
1780
+ class ContextMenu {
1781
+ constructor({ graph, hooks, renderer, commandStack }) {
1782
+ this.graph = graph;
1783
+ this.hooks = hooks;
1784
+ this.renderer = renderer;
1785
+ this.commandStack = commandStack;
1786
+ this.items = [];
1787
+ this.visible = false;
1788
+ this.target = null;
1789
+ this.position = { x: 0, y: 0 };
1790
+ this.menuElement = this._createMenuElement();
1791
+ this._onDocumentClick = (e) => {
1792
+ if (!this.menuElement.contains(e.target)) {
1793
+ this.hide();
1794
+ }
1795
+ };
1796
+ }
1797
+ /**
1798
+ * Add a menu item
1799
+ * @param {string} id - Unique identifier for the menu item
1800
+ * @param {string} label - Display label
1801
+ * @param {Object} options - Options
1802
+ * @param {Function} options.action - Action to execute (receives target)
1803
+ * @param {Array} options.submenu - Submenu items
1804
+ * @param {Function} options.condition - Optional condition to show item (receives target)
1805
+ * @param {number} options.order - Optional sort order (default: 100)
1806
+ */
1807
+ addItem(id, label, options = {}) {
1808
+ const { action, submenu, condition, order = 100 } = options;
1809
+ if (!action && !submenu) {
1810
+ console.error("ContextMenu.addItem: either action or submenu is required");
1811
+ return;
1812
+ }
1813
+ this.removeItem(id);
1814
+ this.items.push({
1815
+ id,
1816
+ label,
1817
+ action,
1818
+ submenu,
1819
+ condition,
1820
+ order
1821
+ });
1822
+ this.items.sort((a, b) => a.order - b.order);
1823
+ }
1824
+ /**
1825
+ * Remove a menu item by id
1826
+ * @param {string} id - Item id to remove
1827
+ */
1828
+ removeItem(id) {
1829
+ this.items = this.items.filter((item) => item.id !== id);
1830
+ }
1831
+ /**
1832
+ * Show the context menu
1833
+ * @param {Object} target - Target node/group
1834
+ * @param {number} x - Screen x position
1835
+ * @param {number} y - Screen y position
1836
+ * @param {Object} worldPos - Optional world position {x, y}
1837
+ */
1838
+ show(target, x, y, worldPos = null) {
1839
+ this.target = target;
1840
+ this.position = { x, y };
1841
+ this.worldPosition = worldPos;
1842
+ this.visible = true;
1843
+ this._renderItems();
1844
+ this.menuElement.style.left = `${x}px`;
1845
+ this.menuElement.style.top = `${y}px`;
1846
+ this.menuElement.style.display = "block";
1847
+ requestAnimationFrame(() => {
1848
+ const rect = this.menuElement.getBoundingClientRect();
1849
+ const vw = window.innerWidth;
1850
+ const vh = window.innerHeight;
1851
+ let adjustedX = x;
1852
+ let adjustedY = y;
1853
+ if (rect.right > vw) {
1854
+ adjustedX = vw - rect.width - 5;
1855
+ }
1856
+ if (rect.bottom > vh) {
1857
+ adjustedY = vh - rect.height - 5;
1858
+ }
1859
+ this.menuElement.style.left = `${adjustedX}px`;
1860
+ this.menuElement.style.top = `${adjustedY}px`;
1861
+ });
1862
+ document.addEventListener("click", this._onDocumentClick);
1863
+ }
1864
+ /**
1865
+ * Hide the context menu
1866
+ */
1867
+ hide() {
1868
+ this.visible = false;
1869
+ this.target = null;
1870
+ const allSubmenus = document.querySelectorAll(".context-submenu");
1871
+ allSubmenus.forEach((submenu) => submenu.remove());
1872
+ this.menuElement.style.display = "none";
1873
+ document.removeEventListener("click", this._onDocumentClick);
1874
+ }
1875
+ /**
1876
+ * Cleanup
1877
+ */
1878
+ destroy() {
1879
+ this.hide();
1880
+ if (this.menuElement && this.menuElement.parentNode) {
1881
+ this.menuElement.parentNode.removeChild(this.menuElement);
1882
+ }
1883
+ }
1884
+ /**
1885
+ * Create the menu DOM element
1886
+ * @private
1887
+ */
1888
+ _createMenuElement() {
1889
+ const menu = document.createElement("div");
1890
+ menu.className = "html-overlay-node-context-menu";
1891
+ Object.assign(menu.style, {
1892
+ position: "fixed",
1893
+ display: "none",
1894
+ minWidth: "180px",
1895
+ backgroundColor: "#2a2a2e",
1896
+ border: "1px solid #444",
1897
+ borderRadius: "6px",
1898
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)",
1899
+ zIndex: "10000",
1900
+ padding: "4px 0",
1901
+ fontFamily: "system-ui, -apple-system, sans-serif",
1902
+ fontSize: "13px",
1903
+ color: "#e9e9ef"
1904
+ });
1905
+ document.body.appendChild(menu);
1906
+ return menu;
1907
+ }
1908
+ /**
1909
+ * Render menu items based on current target
1910
+ * @private
1911
+ */
1912
+ _renderItems() {
1913
+ this.menuElement.innerHTML = "";
1914
+ const visibleItems = this.items.filter((item) => {
1915
+ if (item.condition) {
1916
+ return item.condition(this.target);
1917
+ }
1918
+ return true;
1919
+ });
1920
+ if (visibleItems.length === 0) {
1921
+ this.hide();
1922
+ return;
1923
+ }
1924
+ visibleItems.forEach((item) => {
1925
+ const itemEl = document.createElement("div");
1926
+ itemEl.className = "context-menu-item";
1927
+ const contentWrapper = document.createElement("div");
1928
+ Object.assign(contentWrapper.style, {
1929
+ display: "flex",
1930
+ alignItems: "center",
1931
+ justifyContent: "space-between",
1932
+ width: "100%"
1933
+ });
1934
+ const labelEl = document.createElement("span");
1935
+ labelEl.textContent = item.label;
1936
+ contentWrapper.appendChild(labelEl);
1937
+ if (item.submenu) {
1938
+ const arrow = document.createElement("span");
1939
+ arrow.textContent = "▶";
1940
+ arrow.style.marginLeft = "12px";
1941
+ arrow.style.fontSize = "10px";
1942
+ arrow.style.opacity = "0.7";
1943
+ contentWrapper.appendChild(arrow);
1944
+ }
1945
+ itemEl.appendChild(contentWrapper);
1946
+ Object.assign(itemEl.style, {
1947
+ padding: "4px 8px",
1948
+ cursor: "pointer",
1949
+ transition: "background-color 0.15s ease",
1950
+ userSelect: "none",
1951
+ position: "relative"
1952
+ });
1953
+ itemEl.addEventListener("mouseenter", () => {
1954
+ itemEl.style.backgroundColor = "#3a3a3e";
1955
+ if (itemEl._hideTimeout) {
1956
+ clearTimeout(itemEl._hideTimeout);
1957
+ itemEl._hideTimeout = null;
1958
+ }
1959
+ if (item.submenu) {
1960
+ this._showSubmenu(item.submenu, itemEl);
1961
+ }
1962
+ });
1963
+ itemEl.addEventListener("mouseleave", (e) => {
1964
+ itemEl.style.backgroundColor = "transparent";
1965
+ if (item.submenu) {
1966
+ const submenuEl = itemEl._submenuElement;
1967
+ if (submenuEl) {
1968
+ itemEl._hideTimeout = setTimeout(() => {
1969
+ if (!submenuEl.contains(document.elementFromPoint(e.clientX, e.clientY))) {
1970
+ this._hideSubmenu(itemEl);
1971
+ }
1972
+ }, 150);
1973
+ }
1974
+ }
1975
+ });
1976
+ if (!item.submenu) {
1977
+ itemEl.addEventListener("click", (e) => {
1978
+ e.stopPropagation();
1979
+ item.action(this.target);
1980
+ this.hide();
1981
+ });
1982
+ }
1983
+ this.menuElement.appendChild(itemEl);
1984
+ });
1985
+ }
1986
+ /**
1987
+ * Show submenu for an item
1988
+ * @private
1989
+ */
1990
+ _showSubmenu(submenuItems, parentItemEl) {
1991
+ this._hideSubmenu(parentItemEl);
1992
+ const submenuEl = document.createElement("div");
1993
+ submenuEl.className = "context-submenu";
1994
+ Object.assign(submenuEl.style, {
1995
+ position: "fixed",
1996
+ minWidth: "140px",
1997
+ backgroundColor: "#2a2a2e",
1998
+ border: "1px solid #444",
1999
+ borderRadius: "6px",
2000
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)",
2001
+ zIndex: "10001",
2002
+ padding: "4px 0",
2003
+ fontFamily: "system-ui, -apple-system, sans-serif",
2004
+ fontSize: "13px",
2005
+ color: "#e9e9ef"
2006
+ });
2007
+ submenuItems.forEach((subItem) => {
2008
+ const subItemEl = document.createElement("div");
2009
+ subItemEl.className = "context-submenu-item";
2010
+ const contentWrapper = document.createElement("div");
2011
+ Object.assign(contentWrapper.style, {
2012
+ display: "flex",
2013
+ alignItems: "center",
2014
+ gap: "8px"
2015
+ });
2016
+ if (subItem.color) {
2017
+ const swatch = document.createElement("div");
2018
+ Object.assign(swatch.style, {
2019
+ width: "16px",
2020
+ height: "16px",
2021
+ borderRadius: "3px",
2022
+ backgroundColor: subItem.color,
2023
+ border: "1px solid #555",
2024
+ flexShrink: "0"
2025
+ });
2026
+ contentWrapper.appendChild(swatch);
2027
+ }
2028
+ const labelEl = document.createElement("span");
2029
+ labelEl.textContent = subItem.label;
2030
+ contentWrapper.appendChild(labelEl);
2031
+ subItemEl.appendChild(contentWrapper);
2032
+ Object.assign(subItemEl.style, {
2033
+ padding: "4px 8px",
2034
+ cursor: "pointer",
2035
+ transition: "background-color 0.15s ease",
2036
+ userSelect: "none"
2037
+ });
2038
+ subItemEl.addEventListener("mouseenter", () => {
2039
+ subItemEl.style.backgroundColor = "#3a3a3e";
2040
+ });
2041
+ subItemEl.addEventListener("mouseleave", () => {
2042
+ subItemEl.style.backgroundColor = "transparent";
2043
+ });
2044
+ subItemEl.addEventListener("click", (e) => {
2045
+ e.stopPropagation();
2046
+ subItem.action(this.target);
2047
+ this.hide();
2048
+ });
2049
+ submenuEl.appendChild(subItemEl);
2050
+ });
2051
+ submenuEl.addEventListener("mouseenter", () => {
2052
+ if (parentItemEl._hideTimeout) {
2053
+ clearTimeout(parentItemEl._hideTimeout);
2054
+ parentItemEl._hideTimeout = null;
2055
+ }
2056
+ });
2057
+ submenuEl.addEventListener("mouseleave", (e) => {
2058
+ if (!parentItemEl.contains(e.relatedTarget)) {
2059
+ this._hideSubmenu(parentItemEl);
2060
+ }
2061
+ });
2062
+ document.body.appendChild(submenuEl);
2063
+ parentItemEl._submenuElement = submenuEl;
2064
+ requestAnimationFrame(() => {
2065
+ const parentRect = parentItemEl.getBoundingClientRect();
2066
+ const submenuRect = submenuEl.getBoundingClientRect();
2067
+ let left = parentRect.right + 2;
2068
+ let top = parentRect.top;
2069
+ if (left + submenuRect.width > window.innerWidth) {
2070
+ left = parentRect.left - submenuRect.width - 2;
2071
+ }
2072
+ if (top + submenuRect.height > window.innerHeight) {
2073
+ top = window.innerHeight - submenuRect.height - 5;
2074
+ }
2075
+ submenuEl.style.left = `${left}px`;
2076
+ submenuEl.style.top = `${top}px`;
2077
+ });
2078
+ }
2079
+ /**
2080
+ * Hide submenu for an item
2081
+ * @private
2082
+ */
2083
+ _hideSubmenu(parentItemEl) {
2084
+ if (parentItemEl._submenuElement) {
2085
+ parentItemEl._submenuElement.remove();
2086
+ parentItemEl._submenuElement = null;
2087
+ }
2088
+ }
2089
+ }
2090
+ class Runner {
2091
+ constructor({ graph, registry, hooks, cyclesPerFrame = 1 }) {
2092
+ this.graph = graph;
2093
+ this.registry = registry;
2094
+ this.hooks = hooks;
2095
+ this.running = false;
2096
+ this._raf = null;
2097
+ this._last = 0;
2098
+ this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
2099
+ }
2100
+ // 외부에서 실행 중인지 확인
2101
+ isRunning() {
2102
+ return this.running;
2103
+ }
2104
+ // 실행 도중에도 CPS 변경 가능
2105
+ setCyclesPerFrame(n) {
2106
+ this.cyclesPerFrame = Math.max(1, n | 0);
2107
+ }
2108
+ step(cycles = 1, dt = 0) {
2109
+ var _a, _b;
2110
+ const nCycles = Math.max(1, cycles | 0);
2111
+ for (let c = 0; c < nCycles; c++) {
2112
+ for (const node of this.graph.nodes.values()) {
2113
+ const def = this.registry.types.get(node.type);
2114
+ if (def == null ? void 0 : def.onExecute) {
2115
+ try {
2116
+ def.onExecute(node, {
2117
+ dt,
2118
+ graph: this.graph,
2119
+ getInput: (portName) => {
2120
+ const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
2121
+ return p ? this.graph.getInput(node.id, p.id) : void 0;
2122
+ },
2123
+ setOutput: (portName, value) => {
2124
+ const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
2125
+ if (p) this.graph.setOutput(node.id, p.id, value);
2126
+ }
2127
+ });
2128
+ } catch (err) {
2129
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "error", err);
2130
+ }
2131
+ }
2132
+ }
2133
+ this.graph.swapBuffers();
2134
+ }
2135
+ }
2136
+ /**
2137
+ * Execute connected nodes once from a starting node
2138
+ * @param {string} startNodeId - ID of the node to start from
2139
+ * @param {number} dt - Delta time
2140
+ */
2141
+ runOnce(startNodeId, dt = 0) {
2142
+ console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
2143
+ const executedNodes = [];
2144
+ const allConnectedNodes = /* @__PURE__ */ new Set();
2145
+ let currentNodeId = startNodeId;
2146
+ while (currentNodeId) {
2147
+ const node = this.graph.nodes.get(currentNodeId);
2148
+ if (!node) {
2149
+ console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
2150
+ break;
2151
+ }
2152
+ executedNodes.push(currentNodeId);
2153
+ allConnectedNodes.add(currentNodeId);
2154
+ console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
2155
+ for (const input of node.inputs) {
2156
+ if (input.portType === "data") {
2157
+ for (const edge of this.graph.edges.values()) {
2158
+ if (edge.toNode === currentNodeId && edge.toPort === input.id) {
2159
+ const sourceNode = this.graph.nodes.get(edge.fromNode);
2160
+ if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
2161
+ allConnectedNodes.add(edge.fromNode);
2162
+ this.executeNode(edge.fromNode, dt);
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+ }
2168
+ this.executeNode(currentNodeId, dt);
2169
+ currentNodeId = this.findNextExecNode(currentNodeId);
2170
+ }
2171
+ console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
2172
+ const connectedEdges = /* @__PURE__ */ new Set();
2173
+ for (const edge of this.graph.edges.values()) {
2174
+ if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
2175
+ connectedEdges.add(edge.id);
2176
+ }
2177
+ }
2178
+ console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
2179
+ return { connectedNodes: allConnectedNodes, connectedEdges };
2180
+ }
2181
+ /**
2182
+ * Find the next node to execute by following exec output
2183
+ * @param {string} nodeId - Current node ID
2184
+ * @returns {string|null} Next node ID or null
2185
+ */
2186
+ findNextExecNode(nodeId) {
2187
+ const node = this.graph.nodes.get(nodeId);
2188
+ if (!node) return null;
2189
+ const execOutput = node.outputs.find((p) => p.portType === "exec");
2190
+ if (!execOutput) return null;
2191
+ for (const edge of this.graph.edges.values()) {
2192
+ if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
2193
+ return edge.toNode;
2194
+ }
2195
+ }
2196
+ return null;
2197
+ }
2198
+ /**
2199
+ * Execute a single node
2200
+ * @param {string} nodeId - Node ID to execute
2201
+ * @param {number} dt - Delta time
2202
+ */
2203
+ executeNode(nodeId, dt) {
2204
+ var _a, _b;
2205
+ const node = this.graph.nodes.get(nodeId);
2206
+ if (!node) return;
2207
+ const def = this.registry.types.get(node.type);
2208
+ if (!(def == null ? void 0 : def.onExecute)) return;
2209
+ try {
2210
+ def.onExecute(node, {
2211
+ dt,
2212
+ graph: this.graph,
2213
+ getInput: (portName) => {
2214
+ const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
2215
+ return p ? this.graph.getInput(node.id, p.id) : void 0;
2216
+ },
2217
+ setOutput: (portName, value) => {
2218
+ const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
2219
+ if (p) {
2220
+ const key = `${node.id}:${p.id}`;
2221
+ this.graph._curBuf().set(key, value);
2222
+ }
2223
+ }
2224
+ });
2225
+ } catch (err) {
2226
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "error", err);
2227
+ }
2228
+ }
2229
+ start() {
2230
+ var _a, _b;
2231
+ if (this.running) return;
2232
+ this.running = true;
2233
+ this._last = 0;
2234
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:start");
2235
+ const loop = (t) => {
2236
+ var _a2, _b2;
2237
+ if (!this.running) return;
2238
+ const dtMs = this._last ? t - this._last : 0;
2239
+ this._last = t;
2240
+ const dt = dtMs / 1e3;
2241
+ this.step(this.cyclesPerFrame, dt);
2242
+ (_b2 = (_a2 = this.hooks) == null ? void 0 : _a2.emit) == null ? void 0 : _b2.call(_a2, "runner:tick", {
2243
+ time: t,
2244
+ dt,
2245
+ running: true,
2246
+ cps: this.cyclesPerFrame
2247
+ });
2248
+ this._raf = requestAnimationFrame(loop);
2249
+ };
2250
+ this._raf = requestAnimationFrame(loop);
2251
+ }
2252
+ stop() {
2253
+ var _a, _b;
2254
+ if (!this.running) return;
2255
+ this.running = false;
2256
+ if (this._raf) cancelAnimationFrame(this._raf);
2257
+ this._raf = null;
2258
+ this._last = 0;
2259
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:stop");
2260
+ }
2261
+ }
2262
+ class HtmlOverlay {
2263
+ /**
2264
+ * @param {HTMLElement} host 캔버스를 감싸는 래퍼( position: relative )
2265
+ * @param {CanvasRenderer} renderer
2266
+ * @param {Registry} registry
2267
+ */
2268
+ constructor(host, renderer, registry) {
2269
+ this.host = host;
2270
+ this.renderer = renderer;
2271
+ this.registry = registry;
2272
+ this.container = document.createElement("div");
2273
+ Object.assign(this.container.style, {
2274
+ position: "absolute",
2275
+ inset: "0",
2276
+ pointerEvents: "none",
2277
+ // 기본은 통과
2278
+ zIndex: "10"
2279
+ });
2280
+ host.appendChild(this.container);
2281
+ this.nodes = /* @__PURE__ */ new Map();
2282
+ }
2283
+ /** 기본 노드 레이아웃 생성 (헤더 + 바디) */
2284
+ _createDefaultNodeLayout(node) {
2285
+ const container = document.createElement("div");
2286
+ container.className = "node-overlay";
2287
+ Object.assign(container.style, {
2288
+ position: "absolute",
2289
+ display: "flex",
2290
+ flexDirection: "column",
2291
+ boxSizing: "border-box",
2292
+ pointerEvents: "none",
2293
+ // 기본은 통과 (캔버스 인터랙션 위해)
2294
+ overflow: "hidden"
2295
+ // 둥근 모서리 등
2296
+ });
2297
+ const header = document.createElement("div");
2298
+ header.className = "node-header";
2299
+ Object.assign(header.style, {
2300
+ height: "24px",
2301
+ flexShrink: "0",
2302
+ display: "flex",
2303
+ alignItems: "center",
2304
+ padding: "0 8px",
2305
+ cursor: "grab",
2306
+ userSelect: "none",
2307
+ pointerEvents: "none"
2308
+ // 헤더 클릭시 드래그는 캔버스가 처리
2309
+ });
2310
+ const body = document.createElement("div");
2311
+ body.className = "node-body";
2312
+ Object.assign(body.style, {
2313
+ flex: "1",
2314
+ position: "relative",
2315
+ overflow: "hidden",
2316
+ pointerEvents: "auto",
2317
+ // 바디 내부는 인터랙션 가능하게? 아니면 이것도 none하고 자식만 auto?
2318
+ // 일단 바디는 auto로 두면 바디 영역 클릭시 드래그가 안됨.
2319
+ // 그래서 바디도 none으로 하고, 내부 컨텐츠(input 등)만 auto로 하는게 맞음.
2320
+ pointerEvents: "none"
2321
+ });
2322
+ container.appendChild(header);
2323
+ container.appendChild(body);
2324
+ container._domParts = { header, body };
2325
+ return container;
2326
+ }
2327
+ /** 노드용 엘리먼트 생성(한 번만) */
2328
+ _ensureNodeElement(node, def) {
2329
+ var _a;
2330
+ let el = this.nodes.get(node.id);
2331
+ if (!el) {
2332
+ if ((_a = def.html) == null ? void 0 : _a.render) {
2333
+ el = def.html.render(node);
2334
+ } else if (def.html) {
2335
+ el = this._createDefaultNodeLayout(node);
2336
+ if (def.html.init) {
2337
+ def.html.init(node, el, el._domParts);
2338
+ }
2339
+ } else {
2340
+ return null;
2341
+ }
2342
+ if (!el) return null;
2343
+ el.style.position = "absolute";
2344
+ el.style.pointerEvents = "none";
2345
+ this.container.appendChild(el);
2346
+ this.nodes.set(node.id, el);
2347
+ }
2348
+ return el;
2349
+ }
2350
+ /** 그래프와 변환 동기화하여 렌더링 */
2351
+ draw(graph, selection = /* @__PURE__ */ new Set()) {
2352
+ const { scale, offsetX, offsetY } = this.renderer;
2353
+ this.container.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
2354
+ this.container.style.transformOrigin = "0 0";
2355
+ const seen = /* @__PURE__ */ new Set();
2356
+ for (const node of graph.nodes.values()) {
2357
+ const def = this.registry.types.get(node.type);
2358
+ const hasHtml = !!(def == null ? void 0 : def.html);
2359
+ if (!hasHtml) continue;
2360
+ const el = this._ensureNodeElement(node, def);
2361
+ if (!el) continue;
2362
+ el.style.left = `${node.computed.x}px`;
2363
+ el.style.top = `${node.computed.y}px`;
2364
+ el.style.width = `${node.computed.w}px`;
2365
+ el.style.height = `${node.computed.h}px`;
2366
+ if (def.html.update) {
2367
+ const parts = el._domParts || {};
2368
+ def.html.update(node, el, {
2369
+ selected: selection.has(node.id),
2370
+ header: parts.header,
2371
+ body: parts.body
2372
+ });
2373
+ }
2374
+ seen.add(node.id);
2375
+ }
2376
+ for (const [id, el] of this.nodes) {
2377
+ if (!seen.has(id)) {
2378
+ el.remove();
2379
+ this.nodes.delete(id);
2380
+ }
2381
+ }
2382
+ }
2383
+ clear() {
2384
+ for (const [, el] of this.nodes) {
2385
+ el.remove();
2386
+ }
2387
+ this.nodes.clear();
2388
+ }
2389
+ destroy() {
2390
+ this.clear();
2391
+ this.container.remove();
2392
+ }
2393
+ }
2394
+ class Minimap {
2395
+ constructor(container, { graph, renderer, width = 200, height = 150 } = {}) {
2396
+ this.graph = graph;
2397
+ this.renderer = renderer;
2398
+ this.width = width;
2399
+ this.height = height;
2400
+ this.canvas = document.createElement("canvas");
2401
+ this.canvas.id = "minimap";
2402
+ this.canvas.width = width;
2403
+ this.canvas.height = height;
2404
+ this.canvas.style.position = "fixed";
2405
+ this.canvas.style.bottom = "20px";
2406
+ this.canvas.style.right = "20px";
2407
+ this.canvas.style.border = "2px solid #444";
2408
+ this.canvas.style.borderRadius = "8px";
2409
+ this.canvas.style.background = "rgba(20, 20, 23, 0.9)";
2410
+ this.canvas.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.5)";
2411
+ this.canvas.style.pointerEvents = "none";
2412
+ this.ctx = this.canvas.getContext("2d");
2413
+ container.appendChild(this.canvas);
2414
+ }
2415
+ /**
2416
+ * Render the minimap
2417
+ */
2418
+ render() {
2419
+ const { graph, renderer, ctx, width: w, height: h } = this;
2420
+ ctx.fillStyle = "#141417";
2421
+ ctx.fillRect(0, 0, w, h);
2422
+ if (graph.nodes.size === 0) return;
2423
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2424
+ for (const node of graph.nodes.values()) {
2425
+ const { x, y, w: nw, h: nh } = node.computed;
2426
+ minX = Math.min(minX, x);
2427
+ minY = Math.min(minY, y);
2428
+ maxX = Math.max(maxX, x + nw);
2429
+ maxY = Math.max(maxY, y + nh);
2430
+ }
2431
+ const margin = 100;
2432
+ const graphWidth = Math.max(300, maxX - minX + margin * 2);
2433
+ const graphHeight = Math.max(200, maxY - minY + margin * 2);
2434
+ minX -= margin;
2435
+ minY -= margin;
2436
+ const padding = 10;
2437
+ const scale = Math.min(
2438
+ (w - padding * 2) / graphWidth,
2439
+ (h - padding * 2) / graphHeight
2440
+ );
2441
+ const offsetX = (w - graphWidth * scale) / 2;
2442
+ const offsetY = (h - graphHeight * scale) / 2;
2443
+ ctx.strokeStyle = "rgba(127, 140, 255, 0.5)";
2444
+ ctx.lineWidth = 1;
2445
+ for (const edge of graph.edges.values()) {
2446
+ const fromNode = graph.nodes.get(edge.fromNode);
2447
+ const toNode = graph.nodes.get(edge.toNode);
2448
+ if (!fromNode || !toNode) continue;
2449
+ const x1 = fromNode.computed.x + fromNode.computed.w / 2;
2450
+ const y1 = fromNode.computed.y + fromNode.computed.h / 2;
2451
+ const x2 = toNode.computed.x + toNode.computed.w / 2;
2452
+ const y2 = toNode.computed.y + toNode.computed.h / 2;
2453
+ const mx1 = (x1 - minX) * scale + offsetX;
2454
+ const my1 = (y1 - minY) * scale + offsetY;
2455
+ const mx2 = (x2 - minX) * scale + offsetX;
2456
+ const my2 = (y2 - minY) * scale + offsetY;
2457
+ ctx.beginPath();
2458
+ ctx.moveTo(mx1, my1);
2459
+ ctx.lineTo(mx2, my2);
2460
+ ctx.stroke();
2461
+ }
2462
+ ctx.fillStyle = "#6cf";
2463
+ for (const node of graph.nodes.values()) {
2464
+ const { x, y, w: nw, h: nh } = node.computed;
2465
+ const mx = (x - minX) * scale + offsetX;
2466
+ const my = (y - minY) * scale + offsetY;
2467
+ const mw = nw * scale;
2468
+ const mh = nh * scale;
2469
+ if (node.type === "core/Group") {
2470
+ ctx.fillStyle = "rgba(102, 204, 255, 0.2)";
2471
+ ctx.strokeStyle = "#6cf";
2472
+ ctx.lineWidth = 1;
2473
+ ctx.fillRect(mx, my, mw, mh);
2474
+ ctx.strokeRect(mx, my, mw, mh);
2475
+ } else {
2476
+ ctx.fillStyle = "#6cf";
2477
+ ctx.fillRect(mx, my, Math.max(2, mw), Math.max(2, mh));
2478
+ }
2479
+ }
2480
+ const vx0 = -renderer.offsetX / renderer.scale;
2481
+ const vy0 = -renderer.offsetY / renderer.scale;
2482
+ const vw = renderer.canvas.width / renderer.scale;
2483
+ const vh = renderer.canvas.height / renderer.scale;
2484
+ const vmx = (vx0 - minX) * scale + offsetX;
2485
+ const vmy = (vy0 - minY) * scale + offsetY;
2486
+ const vmw = vw * scale;
2487
+ const vmh = vh * scale;
2488
+ ctx.strokeStyle = "#ff6b6b";
2489
+ ctx.lineWidth = 2;
2490
+ ctx.strokeRect(vmx, vmy, vmw, vmh);
2491
+ }
2492
+ /**
2493
+ * Cleanup
2494
+ */
2495
+ destroy() {
2496
+ if (this.canvas.parentElement) {
2497
+ this.canvas.parentElement.removeChild(this.canvas);
2498
+ }
2499
+ }
2500
+ }
2501
+ class PropertyPanel {
2502
+ constructor(container, { graph, hooks, registry, render }) {
2503
+ this.container = container;
2504
+ this.graph = graph;
2505
+ this.hooks = hooks;
2506
+ this.registry = registry;
2507
+ this.render = render;
2508
+ this.panel = null;
2509
+ this.currentNode = null;
2510
+ this.isVisible = false;
2511
+ this._createPanel();
2512
+ }
2513
+ _createPanel() {
2514
+ this.panel = document.createElement("div");
2515
+ this.panel.className = "property-panel";
2516
+ this.panel.style.display = "none";
2517
+ this.panel.innerHTML = `
2518
+ <div class="panel-inner">
2519
+ <div class="panel-header">
2520
+ <div class="panel-title">
2521
+ <span class="title-text">Node Properties</span>
2522
+ </div>
2523
+ <button class="panel-close" type="button">×</button>
2524
+ </div>
2525
+ <div class="panel-content">
2526
+ <!-- Content will be dynamically generated -->
2527
+ </div>
2528
+ </div>
2529
+ `;
2530
+ this.container.appendChild(this.panel);
2531
+ this.panel.querySelector(".panel-close").addEventListener("click", () => {
2532
+ this.close();
2533
+ });
2534
+ document.addEventListener("keydown", (e) => {
2535
+ if (e.key === "Escape" && this.isVisible) {
2536
+ this.close();
2537
+ }
2538
+ });
2539
+ }
2540
+ open(node) {
2541
+ if (!node) return;
2542
+ this.currentNode = node;
2543
+ this.isVisible = true;
2544
+ this._renderContent();
2545
+ this.panel.style.display = "block";
2546
+ this.panel.classList.add("panel-visible");
2547
+ }
2548
+ close() {
2549
+ this.isVisible = false;
2550
+ this.panel.classList.remove("panel-visible");
2551
+ setTimeout(() => {
2552
+ this.panel.style.display = "none";
2553
+ this.currentNode = null;
2554
+ }, 200);
2555
+ }
2556
+ _renderContent() {
2557
+ var _a, _b;
2558
+ const node = this.currentNode;
2559
+ if (!node) return;
2560
+ const content = this.panel.querySelector(".panel-content");
2561
+ (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
2562
+ content.innerHTML = `
2563
+ <div class="section">
2564
+ <div class="section-title">Basic Info</div>
2565
+ <div class="section-body">
2566
+ <div class="field">
2567
+ <label>Type</label>
2568
+ <input type="text" value="${node.type}" readonly />
2569
+ </div>
2570
+ <div class="field">
2571
+ <label>Title</label>
2572
+ <input type="text" data-field="title" value="${node.title || ""}" />
2573
+ </div>
2574
+ <div class="field">
2575
+ <label>ID</label>
2576
+ <input type="text" value="${node.id}" readonly />
2577
+ </div>
2578
+ </div>
2579
+ </div>
2580
+
2581
+ <div class="section">
2582
+ <div class="section-title">Position & Size</div>
2583
+ <div class="section-body">
2584
+ <div class="field-row">
2585
+ <div class="field">
2586
+ <label>X</label>
2587
+ <input type="number" data-field="x" value="${Math.round(node.computed.x)}" />
2588
+ </div>
2589
+ <div class="field">
2590
+ <label>Y</label>
2591
+ <input type="number" data-field="y" value="${Math.round(node.computed.y)}" />
2592
+ </div>
2593
+ </div>
2594
+ <div class="field-row">
2595
+ <div class="field">
2596
+ <label>Width</label>
2597
+ <input type="number" data-field="width" value="${node.computed.w}" />
2598
+ </div>
2599
+ <div class="field">
2600
+ <label>Height</label>
2601
+ <input type="number" data-field="height" value="${node.computed.h}" />
2602
+ </div>
2603
+ </div>
2604
+ </div>
2605
+ </div>
2606
+
2607
+ ${this._renderPorts(node)}
2608
+ ${this._renderState(node)}
2609
+
2610
+ <div class="panel-actions">
2611
+ <button class="btn-secondary panel-close-btn">Close</button>
2612
+ </div>
2613
+ `;
2614
+ this._attachInputListeners();
2615
+ }
2616
+ _renderPorts(node) {
2617
+ if (!node.inputs.length && !node.outputs.length) return "";
2618
+ return `
2619
+ <div class="section">
2620
+ <div class="section-title">Ports</div>
2621
+ <div class="section-body">
2622
+ ${node.inputs.length ? `
2623
+ <div class="port-group">
2624
+ <div class="port-group-title">Inputs (${node.inputs.length})</div>
2625
+ ${node.inputs.map((p) => `
2626
+ <div class="port-item">
2627
+ <span class="port-icon ${p.portType || "data"}"></span>
2628
+ <span class="port-name">${p.name}</span>
2629
+ ${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ""}
2630
+ </div>
2631
+ `).join("")}
2632
+ </div>
2633
+ ` : ""}
2634
+
2635
+ ${node.outputs.length ? `
2636
+ <div class="port-group">
2637
+ <div class="port-group-title">Outputs (${node.outputs.length})</div>
2638
+ ${node.outputs.map((p) => `
2639
+ <div class="port-item">
2640
+ <span class="port-icon ${p.portType || "data"}"></span>
2641
+ <span class="port-name">${p.name}</span>
2642
+ ${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ""}
2643
+ </div>
2644
+ `).join("")}
2645
+ </div>
2646
+ ` : ""}
2647
+ </div>
2648
+ </div>
2649
+ `;
2650
+ }
2651
+ _renderState(node) {
2652
+ if (!node.state || Object.keys(node.state).length === 0) return "";
2653
+ return `
2654
+ <div class="section">
2655
+ <div class="section-title">State</div>
2656
+ <div class="section-body">
2657
+ ${Object.entries(node.state).map(([key, value]) => `
2658
+ <div class="field">
2659
+ <label>${key}</label>
2660
+ <input
2661
+ type="${typeof value === "number" ? "number" : "text"}"
2662
+ data-field="state.${key}"
2663
+ value="${value}"
2664
+ />
2665
+ </div>
2666
+ `).join("")}
2667
+ </div>
2668
+ </div>
2669
+ `;
2670
+ }
2671
+ _attachInputListeners() {
2672
+ const inputs = this.panel.querySelectorAll("[data-field]");
2673
+ inputs.forEach((input) => {
2674
+ input.addEventListener("change", () => {
2675
+ this._handleFieldChange(input.dataset.field, input.value);
2676
+ });
2677
+ });
2678
+ this.panel.querySelector(".panel-close-btn").addEventListener("click", () => {
2679
+ this.close();
2680
+ });
2681
+ }
2682
+ _handleFieldChange(field, value) {
2683
+ var _a;
2684
+ const node = this.currentNode;
2685
+ if (!node) return;
2686
+ switch (field) {
2687
+ case "title":
2688
+ node.title = value;
2689
+ break;
2690
+ case "x":
2691
+ node.pos.x = parseFloat(value);
2692
+ this.graph.updateWorldTransforms();
2693
+ break;
2694
+ case "y":
2695
+ node.pos.y = parseFloat(value);
2696
+ this.graph.updateWorldTransforms();
2697
+ break;
2698
+ case "width":
2699
+ node.size.width = parseFloat(value);
2700
+ break;
2701
+ case "height":
2702
+ node.size.height = parseFloat(value);
2703
+ break;
2704
+ default:
2705
+ if (field.startsWith("state.")) {
2706
+ const key = field.substring(6);
2707
+ if (node.state) {
2708
+ const originalValue = node.state[key];
2709
+ node.state[key] = typeof originalValue === "number" ? parseFloat(value) : value;
2710
+ }
2711
+ }
2712
+ }
2713
+ (_a = this.hooks) == null ? void 0 : _a.emit("node:updated", node);
2714
+ if (this.render) {
2715
+ this.render();
2716
+ }
2717
+ }
2718
+ destroy() {
2719
+ if (this.panel) {
2720
+ this.panel.remove();
2721
+ }
2722
+ }
2723
+ }
2724
+ function createGraphEditor(target, {
2725
+ theme,
2726
+ hooks: customHooks,
2727
+ autorun = true,
2728
+ showMinimap = true,
2729
+ enablePropertyPanel = true,
2730
+ propertyPanelContainer = null
2731
+ } = {}) {
2732
+ let canvas;
2733
+ let container;
2734
+ if (typeof target === "string") {
2735
+ target = document.querySelector(target);
2736
+ }
2737
+ if (!target) {
2738
+ throw new Error("createGraphEditor: target element not found");
2739
+ }
2740
+ if (target instanceof HTMLCanvasElement) {
2741
+ canvas = target;
2742
+ container = canvas.parentElement;
2743
+ } else {
2744
+ container = target;
2745
+ canvas = container.querySelector("canvas");
2746
+ if (!canvas) {
2747
+ canvas = document.createElement("canvas");
2748
+ canvas.style.display = "block";
2749
+ canvas.style.width = "100%";
2750
+ canvas.style.height = "100%";
2751
+ container.appendChild(canvas);
2752
+ }
2753
+ }
2754
+ if (getComputedStyle(container).position === "static") {
2755
+ container.style.position = "relative";
2756
+ }
2757
+ const hooks = customHooks ?? createHooks([
2758
+ // essential hooks
2759
+ "node:create",
2760
+ "node:move",
2761
+ "node:click",
2762
+ "node:dblclick",
2763
+ "edge:create",
2764
+ "edge:delete",
2765
+ "graph:serialize",
2766
+ "graph:deserialize",
2767
+ "error",
2768
+ "runner:tick",
2769
+ "runner:start",
2770
+ "runner:stop",
2771
+ "node:resize",
2772
+ "group:change",
2773
+ "node:updated"
2774
+ ]);
2775
+ const registry = new Registry();
2776
+ const graph = new Graph({ hooks, registry });
2777
+ const renderer = new CanvasRenderer(canvas, { theme, registry });
2778
+ const htmlOverlay = new HtmlOverlay(canvas.parentElement, renderer, registry);
2779
+ const portCanvas = document.createElement("canvas");
2780
+ portCanvas.id = "port-canvas";
2781
+ Object.assign(portCanvas.style, {
2782
+ position: "absolute",
2783
+ top: "0",
2784
+ left: "0",
2785
+ pointerEvents: "none",
2786
+ // Pass through clicks
2787
+ zIndex: "20"
2788
+ // Above HTML overlay (z-index 10)
2789
+ });
2790
+ canvas.parentElement.appendChild(portCanvas);
2791
+ const portRenderer = new CanvasRenderer(portCanvas, { theme, registry });
2792
+ portRenderer.setTransform = renderer.setTransform.bind(renderer);
2793
+ portRenderer.scale = renderer.scale;
2794
+ portRenderer.offsetX = renderer.offsetX;
2795
+ portRenderer.offsetY = renderer.offsetY;
2796
+ const controller = new Controller({ graph, renderer, hooks, htmlOverlay, portRenderer });
2797
+ const contextMenu = new ContextMenu({
2798
+ graph,
2799
+ hooks,
2800
+ renderer,
2801
+ commandStack: controller.stack
2802
+ });
2803
+ controller.contextMenu = contextMenu;
2804
+ let minimap = null;
2805
+ if (showMinimap) {
2806
+ minimap = new Minimap(container, { graph, renderer });
2807
+ }
2808
+ let propertyPanel = null;
2809
+ if (enablePropertyPanel) {
2810
+ propertyPanel = new PropertyPanel(propertyPanelContainer || container, {
2811
+ graph,
2812
+ hooks,
2813
+ registry,
2814
+ render: () => controller.render()
2815
+ });
2816
+ hooks.on("node:dblclick", (node) => {
2817
+ propertyPanel.open(node);
2818
+ });
2819
+ }
2820
+ const runner = new Runner({ graph, registry, hooks });
2821
+ hooks.on("runner:tick", ({ time, dt }) => {
2822
+ renderer.draw(graph, {
2823
+ selection: controller.selection,
2824
+ tempEdge: controller.connecting ? controller.renderTempEdge() : null,
2825
+ // 필요시 helper
2826
+ running: true,
2827
+ time,
2828
+ dt
2829
+ });
2830
+ htmlOverlay.draw(graph, controller.selection);
2831
+ });
2832
+ hooks.on("runner:start", () => {
2833
+ renderer.draw(graph, {
2834
+ selection: controller.selection,
2835
+ tempEdge: controller.connecting ? controller.renderTempEdge() : null,
2836
+ running: true,
2837
+ time: performance.now(),
2838
+ dt: 0
2839
+ });
2840
+ htmlOverlay.draw(graph, controller.selection);
2841
+ });
2842
+ hooks.on("runner:stop", () => {
2843
+ renderer.draw(graph, {
2844
+ selection: controller.selection,
2845
+ tempEdge: controller.connecting ? controller.renderTempEdge() : null,
2846
+ running: false,
2847
+ time: performance.now(),
2848
+ dt: 0
2849
+ });
2850
+ htmlOverlay.draw(graph, controller.selection);
2851
+ });
2852
+ hooks.on("node:updated", () => {
2853
+ controller.render();
2854
+ });
2855
+ registry.register("core/Note", {
2856
+ title: "Note",
2857
+ size: { w: 180, h: 80 },
2858
+ inputs: [{ name: "in", datatype: "any" }],
2859
+ outputs: [{ name: "out", datatype: "any" }],
2860
+ onCreate(node) {
2861
+ node.state.text = "hello";
2862
+ },
2863
+ onExecute(node, { dt, getInput, setOutput }) {
2864
+ const incoming = getInput("in");
2865
+ const out = (incoming ?? node.state.text ?? "").toString().toUpperCase();
2866
+ setOutput(
2867
+ "out",
2868
+ out + ` · ${Math.floor(performance.now() / 1e3 % 100)}`
2869
+ );
2870
+ },
2871
+ onDraw(node, { ctx, theme: theme2 }) {
2872
+ const { x, y } = node.pos;
2873
+ const { width: w } = node.size;
2874
+ }
2875
+ });
2876
+ registry.register("core/HtmlNote", {
2877
+ title: "HTML Note",
2878
+ size: { w: 200, h: 150 },
2879
+ inputs: [{ name: "in", datatype: "any" }],
2880
+ outputs: [{ name: "out", datatype: "any" }],
2881
+ // HTML Overlay Configuration
2882
+ html: {
2883
+ // 초기화: 헤더/바디 구성
2884
+ init(node, el, { header, body }) {
2885
+ el.style.backgroundColor = "#222";
2886
+ el.style.borderRadius = "8px";
2887
+ el.style.border = "1px solid #444";
2888
+ el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
2889
+ header.style.backgroundColor = "#333";
2890
+ header.style.borderBottom = "1px solid #444";
2891
+ header.style.color = "#eee";
2892
+ header.style.fontSize = "12px";
2893
+ header.style.fontWeight = "bold";
2894
+ header.textContent = "My HTML Node";
2895
+ body.style.padding = "8px";
2896
+ body.style.color = "#ccc";
2897
+ body.style.fontSize = "12px";
2898
+ const contentDiv = document.createElement("div");
2899
+ contentDiv.textContent = "Event Name";
2900
+ body.appendChild(contentDiv);
2901
+ const input = document.createElement("input");
2902
+ Object.assign(input.style, {
2903
+ marginTop: "4px",
2904
+ padding: "4px",
2905
+ background: "#111",
2906
+ border: "1px solid #555",
2907
+ color: "#fff",
2908
+ borderRadius: "4px",
2909
+ pointerEvents: "auto"
2910
+ });
2911
+ input.placeholder = "Type here...";
2912
+ input.addEventListener("input", (e) => {
2913
+ node.state.text = e.target.value;
2914
+ });
2915
+ input.addEventListener("mousedown", (e) => e.stopPropagation());
2916
+ body.appendChild(input);
2917
+ el._input = input;
2918
+ },
2919
+ // 매 프레임(또는 필요시) 업데이트
2920
+ update(node, el, { header, body, selected }) {
2921
+ el.style.borderColor = selected ? "#6cf" : "#444";
2922
+ header.style.backgroundColor = selected ? "#3a4a5a" : "#333";
2923
+ if (el._input.value !== (node.state.text || "")) {
2924
+ el._input.value = node.state.text || "";
2925
+ }
2926
+ }
2927
+ },
2928
+ onCreate(node) {
2929
+ node.state.text = "";
2930
+ },
2931
+ onExecute(node, { getInput, setOutput }) {
2932
+ const incoming = getInput("in");
2933
+ setOutput("out", incoming);
2934
+ }
2935
+ // onDraw는 생략 가능 (HTML이 덮으니까)
2936
+ // 하지만 포트 등은 그려야 할 수도 있음.
2937
+ // 현재 구조상 CanvasRenderer가 기본 노드를 그리므로,
2938
+ // 투명하게 하거나 겹쳐서 그릴 수 있음.
2939
+ });
2940
+ registry.register("core/TodoNode", {
2941
+ title: "Todo List",
2942
+ size: { w: 240, h: 300 },
2943
+ inputs: [{ name: "in", datatype: "any" }],
2944
+ outputs: [{ name: "out", datatype: "any" }],
2945
+ html: {
2946
+ init(node, el, { header, body }) {
2947
+ el.style.backgroundColor = "#1e1e24";
2948
+ el.style.borderRadius = "8px";
2949
+ el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)";
2950
+ el.style.border = "1px solid #333";
2951
+ header.style.backgroundColor = "#2a2a31";
2952
+ header.style.padding = "8px";
2953
+ header.style.fontWeight = "bold";
2954
+ header.style.color = "#e9e9ef";
2955
+ header.textContent = node.title;
2956
+ body.style.display = "flex";
2957
+ body.style.flexDirection = "column";
2958
+ body.style.padding = "8px";
2959
+ body.style.color = "#e9e9ef";
2960
+ const inputRow = document.createElement("div");
2961
+ Object.assign(inputRow.style, { display: "flex", gap: "4px", marginBottom: "8px" });
2962
+ const input = document.createElement("input");
2963
+ Object.assign(input.style, {
2964
+ flex: "1",
2965
+ padding: "6px",
2966
+ borderRadius: "4px",
2967
+ border: "1px solid #444",
2968
+ background: "#141417",
2969
+ color: "#fff",
2970
+ pointerEvents: "auto"
2971
+ });
2972
+ input.placeholder = "Add task...";
2973
+ const addBtn = document.createElement("button");
2974
+ addBtn.textContent = "+";
2975
+ Object.assign(addBtn.style, {
2976
+ padding: "0 12px",
2977
+ cursor: "pointer",
2978
+ background: "#4f5b66",
2979
+ color: "#fff",
2980
+ border: "none",
2981
+ borderRadius: "4px",
2982
+ pointerEvents: "auto"
2983
+ });
2984
+ inputRow.append(input, addBtn);
2985
+ const list = document.createElement("ul");
2986
+ Object.assign(list.style, {
2987
+ listStyle: "none",
2988
+ padding: "0",
2989
+ margin: "0",
2990
+ overflow: "hidden",
2991
+ flex: "1"
2992
+ });
2993
+ body.append(inputRow, list);
2994
+ const addTodo = () => {
2995
+ const text = input.value.trim();
2996
+ if (!text) return;
2997
+ const todos = node.state.todos || [];
2998
+ node.state.todos = [...todos, { id: Date.now(), text, done: false }];
2999
+ input.value = "";
3000
+ hooks.emit("node:updated", node);
3001
+ };
3002
+ addBtn.onclick = addTodo;
3003
+ input.onkeydown = (e) => {
3004
+ if (e.key === "Enter") addTodo();
3005
+ e.stopPropagation();
3006
+ };
3007
+ input.onmousedown = (e) => e.stopPropagation();
3008
+ el._refs = { list };
3009
+ },
3010
+ update(node, el, { selected }) {
3011
+ el.style.borderColor = selected ? "#6cf" : "#333";
3012
+ const { list } = el._refs;
3013
+ const todos = node.state.todos || [];
3014
+ list.innerHTML = "";
3015
+ todos.forEach((todo) => {
3016
+ const li = document.createElement("li");
3017
+ Object.assign(li.style, {
3018
+ display: "flex",
3019
+ alignItems: "center",
3020
+ padding: "6px 0",
3021
+ borderBottom: "1px solid #2a2a31"
3022
+ });
3023
+ const chk = document.createElement("input");
3024
+ chk.type = "checkbox";
3025
+ chk.checked = todo.done;
3026
+ chk.style.marginRight = "8px";
3027
+ chk.style.pointerEvents = "auto";
3028
+ chk.onchange = () => {
3029
+ todo.done = chk.checked;
3030
+ hooks.emit("node:updated", node);
3031
+ };
3032
+ chk.onmousedown = (e) => e.stopPropagation();
3033
+ const span = document.createElement("span");
3034
+ span.textContent = todo.text;
3035
+ span.style.flex = "1";
3036
+ span.style.textDecoration = todo.done ? "line-through" : "none";
3037
+ span.style.color = todo.done ? "#777" : "#eee";
3038
+ const del = document.createElement("button");
3039
+ del.textContent = "×";
3040
+ Object.assign(del.style, {
3041
+ background: "none",
3042
+ border: "none",
3043
+ color: "#f44",
3044
+ cursor: "pointer",
3045
+ fontSize: "16px",
3046
+ pointerEvents: "auto"
3047
+ });
3048
+ del.onclick = () => {
3049
+ node.state.todos = node.state.todos.filter((t) => t.id !== todo.id);
3050
+ hooks.emit("node:updated", node);
3051
+ };
3052
+ del.onmousedown = (e) => e.stopPropagation();
3053
+ li.append(chk, span, del);
3054
+ list.appendChild(li);
3055
+ });
3056
+ }
3057
+ },
3058
+ onCreate(node) {
3059
+ node.state.todos = [
3060
+ { id: 1, text: "Welcome to Free Node", done: false },
3061
+ { id: 2, text: "Try adding a task", done: true }
3062
+ ];
3063
+ }
3064
+ });
3065
+ registry.register("math/Add", {
3066
+ title: "Add",
3067
+ size: { w: 140, h: 100 },
3068
+ inputs: [
3069
+ { name: "exec", portType: "exec" },
3070
+ { name: "a", portType: "data", datatype: "number" },
3071
+ { name: "b", portType: "data", datatype: "number" }
3072
+ ],
3073
+ outputs: [
3074
+ { name: "exec", portType: "exec" },
3075
+ { name: "result", portType: "data", datatype: "number" }
3076
+ ],
3077
+ onCreate(node) {
3078
+ node.state.a = 0;
3079
+ node.state.b = 0;
3080
+ },
3081
+ onExecute(node, { getInput, setOutput }) {
3082
+ const a = getInput("a") ?? 0;
3083
+ const b = getInput("b") ?? 0;
3084
+ const result = a + b;
3085
+ console.log("[Add] a:", a, "b:", b, "result:", result);
3086
+ setOutput("result", result);
3087
+ }
3088
+ });
3089
+ registry.register("math/Subtract", {
3090
+ title: "Subtract",
3091
+ size: { w: 140, h: 80 },
3092
+ inputs: [
3093
+ { name: "a", datatype: "number" },
3094
+ { name: "b", datatype: "number" }
3095
+ ],
3096
+ outputs: [{ name: "result", datatype: "number" }],
3097
+ onExecute(node, { getInput, setOutput }) {
3098
+ const a = getInput("a") ?? 0;
3099
+ const b = getInput("b") ?? 0;
3100
+ setOutput("result", a - b);
3101
+ }
3102
+ });
3103
+ registry.register("math/Multiply", {
3104
+ title: "Multiply",
3105
+ size: { w: 140, h: 100 },
3106
+ inputs: [
3107
+ { name: "exec", portType: "exec" },
3108
+ { name: "a", portType: "data", datatype: "number" },
3109
+ { name: "b", portType: "data", datatype: "number" }
3110
+ ],
3111
+ outputs: [
3112
+ { name: "exec", portType: "exec" },
3113
+ { name: "result", portType: "data", datatype: "number" }
3114
+ ],
3115
+ onExecute(node, { getInput, setOutput }) {
3116
+ const a = getInput("a") ?? 0;
3117
+ const b = getInput("b") ?? 0;
3118
+ const result = a * b;
3119
+ console.log("[Multiply] a:", a, "b:", b, "result:", result);
3120
+ setOutput("result", result);
3121
+ }
3122
+ });
3123
+ registry.register("math/Divide", {
3124
+ title: "Divide",
3125
+ size: { w: 140, h: 80 },
3126
+ inputs: [
3127
+ { name: "a", datatype: "number" },
3128
+ { name: "b", datatype: "number" }
3129
+ ],
3130
+ outputs: [{ name: "result", datatype: "number" }],
3131
+ onExecute(node, { getInput, setOutput }) {
3132
+ const a = getInput("a") ?? 0;
3133
+ const b = getInput("b") ?? 1;
3134
+ setOutput("result", b !== 0 ? a / b : 0);
3135
+ }
3136
+ });
3137
+ registry.register("logic/AND", {
3138
+ title: "AND",
3139
+ size: { w: 120, h: 100 },
3140
+ inputs: [
3141
+ { name: "exec", portType: "exec" },
3142
+ { name: "a", portType: "data", datatype: "boolean" },
3143
+ { name: "b", portType: "data", datatype: "boolean" }
3144
+ ],
3145
+ outputs: [
3146
+ { name: "exec", portType: "exec" },
3147
+ { name: "result", portType: "data", datatype: "boolean" }
3148
+ ],
3149
+ onExecute(node, { getInput, setOutput }) {
3150
+ const a = getInput("a") ?? false;
3151
+ const b = getInput("b") ?? false;
3152
+ console.log("[AND] Inputs - a:", a, "b:", b);
3153
+ const result = a && b;
3154
+ console.log("[AND] Result:", result);
3155
+ setOutput("result", result);
3156
+ }
3157
+ });
3158
+ registry.register("logic/OR", {
3159
+ title: "OR",
3160
+ size: { w: 120, h: 80 },
3161
+ inputs: [
3162
+ { name: "a", datatype: "boolean" },
3163
+ { name: "b", datatype: "boolean" }
3164
+ ],
3165
+ outputs: [{ name: "result", datatype: "boolean" }],
3166
+ onExecute(node, { getInput, setOutput }) {
3167
+ const a = getInput("a") ?? false;
3168
+ const b = getInput("b") ?? false;
3169
+ setOutput("result", a || b);
3170
+ }
3171
+ });
3172
+ registry.register("logic/NOT", {
3173
+ title: "NOT",
3174
+ size: { w: 120, h: 70 },
3175
+ inputs: [{ name: "in", datatype: "boolean" }],
3176
+ outputs: [{ name: "out", datatype: "boolean" }],
3177
+ onExecute(node, { getInput, setOutput }) {
3178
+ const val = getInput("in") ?? false;
3179
+ setOutput("out", !val);
3180
+ }
3181
+ });
3182
+ registry.register("value/Number", {
3183
+ title: "Number",
3184
+ size: { w: 140, h: 60 },
3185
+ outputs: [{ name: "value", portType: "data", datatype: "number" }],
3186
+ onCreate(node) {
3187
+ node.state.value = 0;
3188
+ },
3189
+ onExecute(node, { setOutput }) {
3190
+ console.log("[Number] Outputting value:", node.state.value ?? 0);
3191
+ setOutput("value", node.state.value ?? 0);
3192
+ },
3193
+ html: {
3194
+ init(node, el, { header, body }) {
3195
+ el.style.backgroundColor = "#1e1e24";
3196
+ el.style.border = "1px solid #444";
3197
+ el.style.borderRadius = "8px";
3198
+ header.style.backgroundColor = "#2a2a31";
3199
+ header.style.borderBottom = "1px solid #444";
3200
+ header.style.color = "#eee";
3201
+ header.style.fontSize = "12px";
3202
+ header.textContent = "Number";
3203
+ body.style.padding = "12px";
3204
+ body.style.display = "flex";
3205
+ body.style.alignItems = "center";
3206
+ body.style.justifyContent = "center";
3207
+ const input = document.createElement("input");
3208
+ input.type = "number";
3209
+ input.value = node.state.value ?? 0;
3210
+ Object.assign(input.style, {
3211
+ width: "100%",
3212
+ padding: "6px",
3213
+ background: "#141417",
3214
+ border: "1px solid #444",
3215
+ borderRadius: "4px",
3216
+ color: "#fff",
3217
+ fontSize: "14px",
3218
+ textAlign: "center",
3219
+ pointerEvents: "auto"
3220
+ });
3221
+ input.addEventListener("change", (e) => {
3222
+ node.state.value = parseFloat(e.target.value) || 0;
3223
+ });
3224
+ input.addEventListener("mousedown", (e) => e.stopPropagation());
3225
+ input.addEventListener("keydown", (e) => e.stopPropagation());
3226
+ body.appendChild(input);
3227
+ },
3228
+ update(node, el, { header, body, selected }) {
3229
+ el.style.borderColor = selected ? "#6cf" : "#444";
3230
+ header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
3231
+ }
3232
+ },
3233
+ onDraw(node, { ctx, theme: theme2 }) {
3234
+ const { x, y } = node.computed;
3235
+ ctx.fillStyle = "#8f8";
3236
+ ctx.font = "14px sans-serif";
3237
+ ctx.textAlign = "center";
3238
+ ctx.fillText(String(node.state.value ?? 0), x + 70, y + 42);
3239
+ }
3240
+ });
3241
+ registry.register("value/String", {
3242
+ title: "String",
3243
+ size: { w: 160, h: 60 },
3244
+ outputs: [{ name: "value", datatype: "string" }],
3245
+ onCreate(node) {
3246
+ node.state.value = "Hello";
3247
+ },
3248
+ onExecute(node, { setOutput }) {
3249
+ setOutput("value", node.state.value ?? "");
3250
+ },
3251
+ onDraw(node, { ctx, theme: theme2 }) {
3252
+ const { x, y } = node.computed;
3253
+ ctx.fillStyle = "#8f8";
3254
+ ctx.font = "12px sans-serif";
3255
+ ctx.textAlign = "center";
3256
+ const text = String(node.state.value ?? "");
3257
+ const displayText = text.length > 15 ? text.substring(0, 15) + "..." : text;
3258
+ ctx.fillText(displayText, x + 80, y + 42);
3259
+ }
3260
+ });
3261
+ registry.register("value/Boolean", {
3262
+ title: "Boolean",
3263
+ size: { w: 140, h: 60 },
3264
+ outputs: [{ name: "value", portType: "data", datatype: "boolean" }],
3265
+ onCreate(node) {
3266
+ node.state.value = true;
3267
+ },
3268
+ onExecute(node, { setOutput }) {
3269
+ console.log("[Boolean] Outputting value:", node.state.value ?? false);
3270
+ setOutput("value", node.state.value ?? false);
3271
+ },
3272
+ onDraw(node, { ctx, theme: theme2 }) {
3273
+ const { x, y } = node.computed;
3274
+ ctx.fillStyle = node.state.value ? "#8f8" : "#f88";
3275
+ ctx.font = "14px sans-serif";
3276
+ ctx.textAlign = "center";
3277
+ ctx.fillText(String(node.state.value), x + 70, y + 42);
3278
+ }
3279
+ });
3280
+ registry.register("util/Print", {
3281
+ title: "Print",
3282
+ size: { w: 140, h: 80 },
3283
+ inputs: [
3284
+ { name: "exec", portType: "exec" },
3285
+ { name: "value", portType: "data", datatype: "any" }
3286
+ ],
3287
+ onCreate(node) {
3288
+ node.state.lastValue = null;
3289
+ },
3290
+ onExecute(node, { getInput }) {
3291
+ const val = getInput("value");
3292
+ if (val !== node.state.lastValue) {
3293
+ console.log("[Print]", val);
3294
+ node.state.lastValue = val;
3295
+ }
3296
+ }
3297
+ });
3298
+ registry.register("util/Watch", {
3299
+ title: "Watch",
3300
+ size: { w: 180, h: 110 },
3301
+ inputs: [
3302
+ { name: "exec", portType: "exec" },
3303
+ { name: "value", portType: "data", datatype: "any" }
3304
+ ],
3305
+ outputs: [
3306
+ { name: "exec", portType: "exec" },
3307
+ { name: "value", portType: "data", datatype: "any" }
3308
+ ],
3309
+ onCreate(node) {
3310
+ node.state.displayValue = "---";
3311
+ },
3312
+ onExecute(node, { getInput, setOutput }) {
3313
+ const val = getInput("value");
3314
+ console.log("[Watch] onExecute called, value:", val);
3315
+ node.state.displayValue = String(val ?? "---");
3316
+ setOutput("value", val);
3317
+ },
3318
+ onDraw(node, { ctx, theme: theme2 }) {
3319
+ const { x, y } = node.computed;
3320
+ ctx.fillStyle = "#fa3";
3321
+ ctx.font = "11px monospace";
3322
+ ctx.textAlign = "left";
3323
+ const text = String(node.state.displayValue ?? "---");
3324
+ const displayText = text.length > 20 ? text.substring(0, 20) + "..." : text;
3325
+ ctx.fillText(displayText, x + 8, y + 50);
3326
+ }
3327
+ });
3328
+ registry.register("util/Timer", {
3329
+ title: "Timer",
3330
+ size: { w: 140, h: 60 },
3331
+ outputs: [{ name: "time", datatype: "number" }],
3332
+ onCreate(node) {
3333
+ node.state.startTime = performance.now();
3334
+ },
3335
+ onExecute(node, { setOutput }) {
3336
+ const elapsed = (performance.now() - (node.state.startTime ?? 0)) / 1e3;
3337
+ setOutput("time", elapsed.toFixed(2));
3338
+ }
3339
+ });
3340
+ registry.register("util/Trigger", {
3341
+ title: "Trigger",
3342
+ size: { w: 140, h: 80 },
3343
+ outputs: [{ name: "exec", portType: "exec" }],
3344
+ // Changed to exec port
3345
+ html: {
3346
+ init(node, el, { header, body }) {
3347
+ el.style.backgroundColor = "#1e1e24";
3348
+ el.style.border = "1px solid #444";
3349
+ el.style.borderRadius = "8px";
3350
+ header.style.backgroundColor = "#2a2a31";
3351
+ header.style.borderBottom = "1px solid #444";
3352
+ header.style.color = "#eee";
3353
+ header.style.fontSize = "12px";
3354
+ header.textContent = "Trigger";
3355
+ body.style.padding = "12px";
3356
+ body.style.display = "flex";
3357
+ body.style.alignItems = "center";
3358
+ body.style.justifyContent = "center";
3359
+ const button = document.createElement("button");
3360
+ button.textContent = "Fire!";
3361
+ Object.assign(button.style, {
3362
+ padding: "8px 16px",
3363
+ background: "#4a9eff",
3364
+ border: "none",
3365
+ borderRadius: "4px",
3366
+ color: "#fff",
3367
+ fontWeight: "bold",
3368
+ cursor: "pointer",
3369
+ pointerEvents: "auto",
3370
+ transition: "background 0.2s"
3371
+ });
3372
+ button.addEventListener("mousedown", (e) => {
3373
+ e.stopPropagation();
3374
+ button.style.background = "#2a7ede";
3375
+ });
3376
+ button.addEventListener("mouseup", () => {
3377
+ button.style.background = "#4a9eff";
3378
+ });
3379
+ button.addEventListener("click", (e) => {
3380
+ e.stopPropagation();
3381
+ node.state.triggered = true;
3382
+ console.log("[Trigger] Button clicked!");
3383
+ if (node.__runnerRef && node.__controllerRef) {
3384
+ console.log("[Trigger] Runner and controller found");
3385
+ const runner2 = node.__runnerRef;
3386
+ const controller2 = node.__controllerRef;
3387
+ const graph2 = controller2.graph;
3388
+ console.log("[Trigger] Calling runner.runOnce with node.id:", node.id);
3389
+ const result = runner2.runOnce(node.id, 0);
3390
+ const connectedEdges = result.connectedEdges;
3391
+ const startTime = performance.now();
3392
+ const animationDuration = 500;
3393
+ const animate = () => {
3394
+ var _a;
3395
+ const elapsed = performance.now() - startTime;
3396
+ if (elapsed < animationDuration) {
3397
+ controller2.renderer.draw(graph2, {
3398
+ selection: controller2.selection,
3399
+ tempEdge: null,
3400
+ running: true,
3401
+ time: performance.now(),
3402
+ dt: 0,
3403
+ activeEdges: connectedEdges
3404
+ // Only animate connected edges
3405
+ });
3406
+ (_a = controller2.htmlOverlay) == null ? void 0 : _a.draw(graph2, controller2.selection);
3407
+ requestAnimationFrame(animate);
3408
+ } else {
3409
+ controller2.render();
3410
+ node.state.triggered = false;
3411
+ }
3412
+ };
3413
+ animate();
3414
+ }
3415
+ });
3416
+ body.appendChild(button);
3417
+ },
3418
+ update(node, el, { header, body, selected }) {
3419
+ el.style.borderColor = selected ? "#6cf" : "#444";
3420
+ header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
3421
+ }
3422
+ },
3423
+ onCreate(node) {
3424
+ node.state.triggered = false;
3425
+ },
3426
+ onExecute(node, { setOutput }) {
3427
+ console.log("[Trigger] Outputting triggered:", node.state.triggered);
3428
+ setOutput("triggered", node.state.triggered);
3429
+ }
3430
+ });
3431
+ registry.register("core/Group", {
3432
+ title: "Group",
3433
+ size: { w: 240, h: 160 },
3434
+ onDraw(node, { ctx, theme: theme2 }) {
3435
+ const { x, y, w, h } = node.computed;
3436
+ const headerH = 24;
3437
+ const color = node.state.color || "#39424e";
3438
+ const bgAlpha = 0.5;
3439
+ const textColor = theme2.text || "#e9e9ef";
3440
+ const rgba = (hex, a) => {
3441
+ const c = hex.replace("#", "");
3442
+ const n = parseInt(
3443
+ c.length === 3 ? c.split("").map((x2) => x2 + x2).join("") : c,
3444
+ 16
3445
+ );
3446
+ const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
3447
+ return `rgba(${r},${g},${b},${a})`;
3448
+ };
3449
+ const roundRect2 = (ctx2, x2, y2, w2, h2, r) => {
3450
+ if (w2 < 2 * r) r = w2 / 2;
3451
+ if (h2 < 2 * r) r = h2 / 2;
3452
+ ctx2.beginPath();
3453
+ ctx2.moveTo(x2 + r, y2);
3454
+ ctx2.arcTo(x2 + w2, y2, x2 + w2, y2 + h2, r);
3455
+ ctx2.arcTo(x2 + w2, y2 + h2, x2, y2 + h2, r);
3456
+ ctx2.arcTo(x2, y2 + h2, x2, y2, r);
3457
+ ctx2.arcTo(x2, y2, x2 + w2, y2, r);
3458
+ ctx2.closePath();
3459
+ };
3460
+ ctx.fillStyle = rgba(color, bgAlpha);
3461
+ roundRect2(ctx, x, y, w, h, 10);
3462
+ ctx.fill();
3463
+ ctx.fillStyle = rgba(color, 0.3);
3464
+ ctx.beginPath();
3465
+ ctx.roundRect(x, y, w, headerH, [10, 10, 0, 0]);
3466
+ ctx.fill();
3467
+ ctx.fillStyle = textColor;
3468
+ ctx.font = "600 13px system-ui";
3469
+ ctx.textBaseline = "top";
3470
+ ctx.fillText(node.title, x + 12, y + 6);
3471
+ }
3472
+ });
3473
+ function setupDefaultContextMenu(contextMenu2, { controller: controller2, graph: graph2, hooks: hooks2 }) {
3474
+ const nodeTypes = [];
3475
+ for (const [key, typeDef] of graph2.registry.types.entries()) {
3476
+ nodeTypes.push({
3477
+ id: `add-${key}`,
3478
+ label: typeDef.title || key,
3479
+ action: () => {
3480
+ const worldPos = contextMenu2.worldPosition || { x: 100, y: 100 };
3481
+ const node = graph2.addNode(key, {
3482
+ x: worldPos.x,
3483
+ y: worldPos.y
3484
+ });
3485
+ hooks2 == null ? void 0 : hooks2.emit("node:updated", node);
3486
+ controller2.render();
3487
+ }
3488
+ });
3489
+ }
3490
+ contextMenu2.addItem("add-node", "Add Node", {
3491
+ condition: (target2) => !target2,
3492
+ submenu: nodeTypes,
3493
+ order: 5
3494
+ });
3495
+ contextMenu2.addItem("delete-node", "Delete Node", {
3496
+ condition: (target2) => target2 && target2.type !== "core/Group",
3497
+ action: (target2) => {
3498
+ const cmd = RemoveNodeCmd(graph2, target2);
3499
+ controller2.stack.exec(cmd);
3500
+ hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3501
+ },
3502
+ order: 10
3503
+ });
3504
+ const colors = [
3505
+ { name: "Default", color: "#39424e" },
3506
+ { name: "Slate", color: "#4a5568" },
3507
+ { name: "Gray", color: "#2d3748" },
3508
+ { name: "Blue", color: "#1a365d" },
3509
+ { name: "Green", color: "#22543d" },
3510
+ { name: "Red", color: "#742a2a" },
3511
+ { name: "Purple", color: "#44337a" }
3512
+ ];
3513
+ contextMenu2.addItem("change-group-color", "Change Color", {
3514
+ condition: (target2) => target2 && target2.type === "core/Group",
3515
+ submenu: colors.map((colorInfo) => ({
3516
+ id: `color-${colorInfo.color}`,
3517
+ label: colorInfo.name,
3518
+ color: colorInfo.color,
3519
+ action: (target2) => {
3520
+ const currentColor = target2.state.color || "#39424e";
3521
+ const cmd = ChangeGroupColorCmd(target2, currentColor, colorInfo.color);
3522
+ controller2.stack.exec(cmd);
3523
+ hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3524
+ }
3525
+ })),
3526
+ order: 20
3527
+ });
3528
+ contextMenu2.addItem("delete-group", "Delete Group", {
3529
+ condition: (target2) => target2 && target2.type === "core/Group",
3530
+ action: (target2) => {
3531
+ const cmd = RemoveNodeCmd(graph2, target2);
3532
+ controller2.stack.exec(cmd);
3533
+ hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3534
+ },
3535
+ order: 20
3536
+ });
3537
+ }
3538
+ setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3539
+ renderer.resize(canvas.clientWidth, canvas.clientHeight);
3540
+ portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3541
+ controller.render();
3542
+ const ro = new ResizeObserver(() => {
3543
+ renderer.resize(canvas.clientWidth, canvas.clientHeight);
3544
+ portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3545
+ controller.render();
3546
+ });
3547
+ ro.observe(canvas);
3548
+ const originalRender = controller.render.bind(controller);
3549
+ controller.render = function() {
3550
+ originalRender();
3551
+ if (minimap) {
3552
+ minimap.render();
3553
+ }
3554
+ };
3555
+ const api = {
3556
+ addGroup: (args = {}) => {
3557
+ controller.graph.groupManager.addGroup(args);
3558
+ controller.render();
3559
+ },
3560
+ graph,
3561
+ renderer,
3562
+ controller,
3563
+ // Expose controller for snap-to-grid access
3564
+ runner,
3565
+ // Expose runner for trigger
3566
+ minimap,
3567
+ // Expose minimap
3568
+ contextMenu,
3569
+ hooks,
3570
+ // Expose hooks for event handling
3571
+ registry,
3572
+ // Expose registry for node types
3573
+ htmlOverlay,
3574
+ // Expose htmlOverlay for clearing/resetting
3575
+ propertyPanel,
3576
+ // Expose propertyPanel
3577
+ render: () => controller.render(),
3578
+ start: () => runner.start(),
3579
+ stop: () => runner.stop(),
3580
+ destroy: () => {
3581
+ runner.stop();
3582
+ ro.disconnect();
3583
+ controller.destroy();
3584
+ htmlOverlay.destroy();
3585
+ contextMenu.destroy();
3586
+ if (propertyPanel) propertyPanel.destroy();
3587
+ if (minimap) minimap.destroy();
3588
+ }
3589
+ };
3590
+ if (autorun) runner.start();
3591
+ return api;
3592
+ }
3593
+ export {
3594
+ createGraphEditor
3595
+ };
3596
+ //# sourceMappingURL=html-overlay-node.es.js.map