html-overlay-node 0.1.6 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import { HtmlOverlay } from "./render/HtmlOverlay.js";
10
10
  import { RemoveNodeCmd, ChangeGroupColorCmd } from "./core/commands.js";
11
11
  import { Minimap } from "./minimap/Minimap.js";
12
12
  import { PropertyPanel } from "./ui/PropertyPanel.js";
13
+ import { setupDefaultContextMenu as defaultContextMenuSetup } from "./defaults/contextMenu.js";
13
14
 
14
15
 
15
16
 
@@ -22,6 +23,9 @@ export function createGraphEditor(
22
23
  showMinimap = true,
23
24
  enablePropertyPanel = true,
24
25
  propertyPanelContainer = null,
26
+ setupDefaultContextMenu = true,
27
+ setupContextMenu = null,
28
+ plugins = [],
25
29
  } = {}
26
30
  ) {
27
31
  let canvas;
@@ -80,6 +84,39 @@ export function createGraphEditor(
80
84
  // HTML Overlay
81
85
  const htmlOverlay = new HtmlOverlay(canvas.parentElement, renderer, registry);
82
86
 
87
+ // Register callback to sync HTML overlay transform when renderer zoom/pan changes
88
+ renderer.setTransformChangeCallback(() => {
89
+ htmlOverlay.syncTransform();
90
+ });
91
+
92
+ // Edge Canvas (above HTML overlay, for edge animations)
93
+ const edgeCanvas = document.createElement("canvas");
94
+ edgeCanvas.id = "edge-canvas";
95
+ Object.assign(edgeCanvas.style, {
96
+ position: "absolute",
97
+ top: "0",
98
+ left: "0",
99
+ pointerEvents: "none", // Pass through clicks
100
+ zIndex: "15", // Above HTML overlay (10), below port canvas (20)
101
+ });
102
+ canvas.parentElement.appendChild(edgeCanvas);
103
+
104
+ // Create edge renderer (shares transform with main renderer)
105
+ const edgeRenderer = new CanvasRenderer(edgeCanvas, { theme, registry });
106
+ // Sync transform properties with main renderer
107
+ Object.defineProperty(edgeRenderer, 'scale', {
108
+ get() { return renderer.scale; },
109
+ set(v) { renderer.scale = v; }
110
+ });
111
+ Object.defineProperty(edgeRenderer, 'offsetX', {
112
+ get() { return renderer.offsetX; },
113
+ set(v) { renderer.offsetX = v; }
114
+ });
115
+ Object.defineProperty(edgeRenderer, 'offsetY', {
116
+ get() { return renderer.offsetY; },
117
+ set(v) { renderer.offsetY = v; }
118
+ });
119
+
83
120
  // Port Canvas (above HTML overlay)
84
121
  const portCanvas = document.createElement("canvas");
85
122
  portCanvas.id = "port-canvas";
@@ -88,7 +125,7 @@ export function createGraphEditor(
88
125
  top: "0",
89
126
  left: "0",
90
127
  pointerEvents: "none", // Pass through clicks
91
- zIndex: "20", // Above HTML overlay (z-index 10)
128
+ zIndex: "20", // Above edge canvas (15)
92
129
  });
93
130
  canvas.parentElement.appendChild(portCanvas);
94
131
 
@@ -99,7 +136,7 @@ export function createGraphEditor(
99
136
  portRenderer.offsetX = renderer.offsetX;
100
137
  portRenderer.offsetY = renderer.offsetY;
101
138
 
102
- const controller = new Controller({ graph, renderer, hooks, htmlOverlay, portRenderer });
139
+ const controller = new Controller({ graph, renderer, hooks, htmlOverlay, edgeRenderer, portRenderer });
103
140
 
104
141
  // Create context menu after controller (needs commandStack)
105
142
  const contextMenu = new ContextMenu({
@@ -136,6 +173,11 @@ export function createGraphEditor(
136
173
 
137
174
  const runner = new Runner({ graph, registry, hooks });
138
175
 
176
+ // Attach runner and controller to graph for node access
177
+ // This allows any node (like Trigger) to execute flows without tight coupling
178
+ graph.runner = runner;
179
+ graph.controller = controller;
180
+
139
181
  hooks.on("runner:tick", ({ time, dt }) => {
140
182
  renderer.draw(graph, {
141
183
  selection: controller.selection,
@@ -173,815 +215,48 @@ export function createGraphEditor(
173
215
  controller.render();
174
216
  });
175
217
 
176
- // default node
177
- registry.register("core/Note", {
178
- title: "Note",
179
- size: { w: 180, h: 80 },
180
- inputs: [{ name: "in", datatype: "any" }],
181
- outputs: [{ name: "out", datatype: "any" }],
182
- onCreate(node) {
183
- node.state.text = "hello";
184
- },
185
- onExecute(node, { dt, getInput, setOutput }) {
186
- // Simple passthrough with uppercase and a heartbeat value
187
- const incoming = getInput("in");
188
- const out = (incoming ?? node.state.text ?? "").toString().toUpperCase();
189
- setOutput(
190
- "out",
191
- out + ` · ${Math.floor((performance.now() / 1000) % 100)}`
192
- );
193
- },
194
- onDraw(node, { ctx, theme }) {
195
- const pr = 8;
196
- const { x, y } = node.pos;
197
- const { width: w } = node.size;
198
- const lx = x + pr; // 월드 x
199
- const ly = y + 24 + 6; // 타이틀 바(24) 아래 여백 6
200
- // renderer._drawScreenText(node.state.text ?? "hello", lx, ly, {
201
- // fontPx: 11,
202
- // color: theme.text,
203
- // baseline: "top",
204
- // align: "left",
205
- // });
206
- },
207
- });
208
-
209
- // HTML Custom Node Example
210
- registry.register("core/HtmlNote", {
211
- title: "HTML Note",
212
- size: { w: 200, h: 150 },
213
- inputs: [{ name: "in", datatype: "any" }],
214
- outputs: [{ name: "out", datatype: "any" }],
215
-
216
- // HTML Overlay Configuration
217
- html: {
218
- // 초기화: 헤더/바디 구성
219
- init(node, el, { header, body }) {
220
- el.style.backgroundColor = "#222";
221
- el.style.borderRadius = "8px";
222
- el.style.border = "1px solid #444";
223
- el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
224
-
225
- // Header
226
- header.style.backgroundColor = "#333";
227
- header.style.borderBottom = "1px solid #444";
228
- header.style.color = "#eee";
229
- header.style.fontSize = "12px";
230
- header.style.fontWeight = "bold";
231
- header.textContent = "My HTML Node";
232
-
233
- // Body
234
- body.style.padding = "8px";
235
- body.style.color = "#ccc";
236
- body.style.fontSize = "12px";
218
+ // Note: Example nodes have been moved to src/nodes/
219
+ // Users can import and register them selectively:
220
+ // import { registerAllNodes } from "html-overlay-node/nodes";
221
+ // registerAllNodes(registry, hooks);
237
222
 
238
- const contentDiv = document.createElement("div");
239
- contentDiv.textContent = "Event Name";
240
- body.appendChild(contentDiv);
241
-
242
- // Add some interactive content
243
- const input = document.createElement("input");
244
- Object.assign(input.style, {
245
- marginTop: "4px",
246
- padding: "4px",
247
- background: "#111",
248
- border: "1px solid #555",
249
- color: "#fff",
250
- borderRadius: "4px",
251
- pointerEvents: "auto",
252
- });
253
- input.placeholder = "Type here...";
254
- input.addEventListener("input", (e) => {
255
- node.state.text = e.target.value;
256
- });
257
- input.addEventListener("mousedown", (e) => e.stopPropagation()); // 캔버스 드래그 방지
258
-
259
- body.appendChild(input);
260
-
261
- // Store input ref for updates
262
- el._input = input;
263
- },
223
+ // Setup context menu
224
+ if (setupDefaultContextMenu) {
225
+ // Use default context menu setup
226
+ defaultContextMenuSetup(contextMenu, { controller, graph, hooks });
227
+ }
264
228
 
265
- // 프레임(또는 필요시) 업데이트
266
- update(node, el, { header, _body, selected }) {
267
- el.style.borderColor = selected ? "#6cf" : "#444";
268
- header.style.backgroundColor = selected ? "#3a4a5a" : "#333";
229
+ // Allow custom context menu setup
230
+ if (setupContextMenu) {
231
+ setupContextMenu(contextMenu, { controller, graph, hooks });
232
+ }
269
233
 
270
- // 상태 동기화 (외부에서 변경되었을 경우)
271
- if (el._input.value !== (node.state.text || "")) {
272
- el._input.value = node.state.text || "";
234
+ // Install plugins
235
+ if (plugins && plugins.length > 0) {
236
+ for (const plugin of plugins) {
237
+ if (typeof plugin.install === "function") {
238
+ try {
239
+ plugin.install({ graph, registry, hooks, runner, controller, contextMenu }, plugin.options || {});
240
+ } catch (err) {
241
+ console.error(`[createGraphEditor] Failed to install plugin "${plugin.name || 'unknown'}":`, err);
242
+ hooks?.emit?.("error", err);
273
243
  }
244
+ } else {
245
+ console.warn(`[createGraphEditor] Plugin "${plugin.name || 'unknown'}" does not have an install() method`);
274
246
  }
275
- },
276
-
277
- onCreate(node) {
278
- node.state.text = "";
279
- },
280
- onExecute(node, { getInput, setOutput }) {
281
- const incoming = getInput("in");
282
- setOutput("out", incoming);
283
- },
284
- // onDraw는 생략 가능 (HTML이 덮으니까)
285
- // 하지만 포트 등은 그려야 할 수도 있음.
286
- // 현재 구조상 CanvasRenderer가 기본 노드를 그리므로,
287
- // 투명하게 하거나 겹쳐서 그릴 수 있음.
288
- });
289
-
290
- // Todo List Node Example (HTML Overlay)
291
- registry.register("core/TodoNode", {
292
- title: "Todo List",
293
- size: { w: 240, h: 300 },
294
- inputs: [{ name: "in", datatype: "any" }],
295
- outputs: [{ name: "out", datatype: "any" }],
296
- html: {
297
- init(node, el, { header, body }) {
298
- el.style.backgroundColor = "#1e1e24";
299
- el.style.borderRadius = "8px";
300
- el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)";
301
- el.style.border = "1px solid #333";
302
-
303
- header.style.backgroundColor = "#2a2a31";
304
- header.style.padding = "8px";
305
- header.style.fontWeight = "bold";
306
- header.style.color = "#e9e9ef";
307
- header.textContent = node.title;
308
-
309
- body.style.display = "flex";
310
- body.style.flexDirection = "column";
311
- body.style.padding = "8px";
312
- body.style.color = "#e9e9ef";
313
-
314
- // Input Area
315
- const inputRow = document.createElement("div");
316
- Object.assign(inputRow.style, { display: "flex", gap: "4px", marginBottom: "8px" });
317
-
318
- const input = document.createElement("input");
319
- Object.assign(input.style, {
320
- flex: "1", padding: "6px", borderRadius: "4px",
321
- border: "1px solid #444", background: "#141417", color: "#fff",
322
- pointerEvents: "auto",
323
- });
324
- input.placeholder = "Add task...";
325
-
326
- const addBtn = document.createElement("button");
327
- addBtn.textContent = "+";
328
- Object.assign(addBtn.style, {
329
- padding: "0 12px", cursor: "pointer", background: "#4f5b66",
330
- color: "#fff", border: "none", borderRadius: "4px",
331
- pointerEvents: "auto",
332
- });
333
-
334
- inputRow.append(input, addBtn);
335
-
336
- // List Area
337
- const list = document.createElement("ul");
338
- Object.assign(list.style, {
339
- listStyle: "none", padding: "0", margin: "0",
340
- overflow: "hidden", flex: "1"
341
- });
342
-
343
- body.append(inputRow, list);
344
-
345
- // Logic
346
- const addTodo = () => {
347
- const text = input.value.trim();
348
- if (!text) return;
349
- const todos = node.state.todos || [];
350
- node.state.todos = [...todos, { id: Date.now(), text, done: false }];
351
- input.value = "";
352
- hooks.emit("node:updated", node);
353
- };
354
-
355
- addBtn.onclick = addTodo;
356
- input.onkeydown = (e) => {
357
- if (e.key === "Enter") addTodo();
358
- e.stopPropagation();
359
- };
360
- input.onmousedown = (e) => e.stopPropagation(); // prevent drag
361
-
362
- el._refs = { list };
363
- },
364
- update(node, el, { selected }) {
365
- el.style.borderColor = selected ? "#6cf" : "#333";
366
-
367
- const { list } = el._refs;
368
- const todos = node.state.todos || [];
369
-
370
- // Re-render list (simple approach)
371
- list.innerHTML = "";
372
- todos.forEach((todo) => {
373
- const li = document.createElement("li");
374
- Object.assign(li.style, {
375
- display: "flex", alignItems: "center", padding: "6px 0",
376
- borderBottom: "1px solid #2a2a31"
377
- });
378
-
379
- const chk = document.createElement("input");
380
- chk.type = "checkbox";
381
- chk.checked = todo.done;
382
- chk.style.marginRight = "8px";
383
- chk.style.pointerEvents = "auto";
384
- chk.onchange = () => {
385
- todo.done = chk.checked;
386
- hooks.emit("node:updated", node);
387
- };
388
- chk.onmousedown = (e) => e.stopPropagation();
389
-
390
- const span = document.createElement("span");
391
- span.textContent = todo.text;
392
- span.style.flex = "1";
393
- span.style.textDecoration = todo.done ? "line-through" : "none";
394
- span.style.color = todo.done ? "#777" : "#eee";
395
-
396
- const del = document.createElement("button");
397
- del.textContent = "×";
398
- Object.assign(del.style, {
399
- background: "none", border: "none", color: "#f44",
400
- cursor: "pointer", fontSize: "16px",
401
- pointerEvents: "auto",
402
- });
403
- del.onclick = () => {
404
- node.state.todos = node.state.todos.filter((t) => t.id !== todo.id);
405
- hooks.emit("node:updated", node);
406
- };
407
- del.onmousedown = (e) => e.stopPropagation();
408
-
409
- li.append(chk, span, del);
410
- list.appendChild(li);
411
- });
412
- }
413
- },
414
- onCreate(node) {
415
- node.state.todos = [
416
- { id: 1, text: "Welcome to Free Node", done: false },
417
- { id: 2, text: "Try adding a task", done: true },
418
- ];
419
- },
420
- });
421
-
422
- // ===== MATH NODES =====
423
- registry.register("math/Add", {
424
- title: "Add",
425
- size: { w: 140, h: 100 },
426
- inputs: [
427
- { name: "exec", portType: "exec" },
428
- { name: "a", portType: "data", datatype: "number" },
429
- { name: "b", portType: "data", datatype: "number" },
430
- ],
431
- outputs: [
432
- { name: "exec", portType: "exec" },
433
- { name: "result", portType: "data", datatype: "number" },
434
- ],
435
- onCreate(node) {
436
- node.state.a = 0;
437
- node.state.b = 0;
438
- },
439
- onExecute(node, { getInput, setOutput }) {
440
- const a = getInput("a") ?? 0;
441
- const b = getInput("b") ?? 0;
442
- const result = a + b;
443
- console.log("[Add] a:", a, "b:", b, "result:", result);
444
- setOutput("result", result);
445
- },
446
- });
447
-
448
- registry.register("math/Subtract", {
449
- title: "Subtract",
450
- size: { w: 140, h: 80 },
451
- inputs: [
452
- { name: "a", datatype: "number" },
453
- { name: "b", datatype: "number" },
454
- ],
455
- outputs: [{ name: "result", datatype: "number" }],
456
- onExecute(node, { getInput, setOutput }) {
457
- const a = getInput("a") ?? 0;
458
- const b = getInput("b") ?? 0;
459
- setOutput("result", a - b);
460
- },
461
- });
462
-
463
- registry.register("math/Multiply", {
464
- title: "Multiply",
465
- size: { w: 140, h: 100 },
466
- inputs: [
467
- { name: "exec", portType: "exec" },
468
- { name: "a", portType: "data", datatype: "number" },
469
- { name: "b", portType: "data", datatype: "number" },
470
- ],
471
- outputs: [
472
- { name: "exec", portType: "exec" },
473
- { name: "result", portType: "data", datatype: "number" },
474
- ],
475
- onExecute(node, { getInput, setOutput }) {
476
- const a = getInput("a") ?? 0;
477
- const b = getInput("b") ?? 0;
478
- const result = a * b;
479
- console.log("[Multiply] a:", a, "b:", b, "result:", result);
480
- setOutput("result", result);
481
- },
482
- });
483
-
484
- registry.register("math/Divide", {
485
- title: "Divide",
486
- size: { w: 140, h: 80 },
487
- inputs: [
488
- { name: "a", datatype: "number" },
489
- { name: "b", datatype: "number" },
490
- ],
491
- outputs: [{ name: "result", datatype: "number" }],
492
- onExecute(node, { getInput, setOutput }) {
493
- const a = getInput("a") ?? 0;
494
- const b = getInput("b") ?? 1;
495
- setOutput("result", b !== 0 ? a / b : 0);
496
- },
497
- });
498
-
499
- // ===== LOGIC NODES =====
500
- registry.register("logic/AND", {
501
- title: "AND",
502
- size: { w: 120, h: 100 },
503
- inputs: [
504
- { name: "exec", portType: "exec" },
505
- { name: "a", portType: "data", datatype: "boolean" },
506
- { name: "b", portType: "data", datatype: "boolean" },
507
- ],
508
- outputs: [
509
- { name: "exec", portType: "exec" },
510
- { name: "result", portType: "data", datatype: "boolean" },
511
- ],
512
- onExecute(node, { getInput, setOutput }) {
513
- const a = getInput("a") ?? false;
514
- const b = getInput("b") ?? false;
515
- console.log("[AND] Inputs - a:", a, "b:", b);
516
- const result = a && b;
517
- console.log("[AND] Result:", result);
518
- setOutput("result", result);
519
- },
520
- });
521
-
522
- registry.register("logic/OR", {
523
- title: "OR",
524
- size: { w: 120, h: 80 },
525
- inputs: [
526
- { name: "a", datatype: "boolean" },
527
- { name: "b", datatype: "boolean" },
528
- ],
529
- outputs: [{ name: "result", datatype: "boolean" }],
530
- onExecute(node, { getInput, setOutput }) {
531
- const a = getInput("a") ?? false;
532
- const b = getInput("b") ?? false;
533
- setOutput("result", a || b);
534
- },
535
- });
536
-
537
- registry.register("logic/NOT", {
538
- title: "NOT",
539
- size: { w: 120, h: 70 },
540
- inputs: [{ name: "in", datatype: "boolean" }],
541
- outputs: [{ name: "out", datatype: "boolean" }],
542
- onExecute(node, { getInput, setOutput }) {
543
- const val = getInput("in") ?? false;
544
- setOutput("out", !val);
545
- },
546
- });
547
-
548
- // ===== VALUE NODES =====
549
- registry.register("value/Number", {
550
- title: "Number",
551
- size: { w: 140, h: 60 },
552
- outputs: [{ name: "value", portType: "data", datatype: "number" }],
553
- onCreate(node) {
554
- node.state.value = 0;
555
- },
556
- onExecute(node, { setOutput }) {
557
- console.log("[Number] Outputting value:", node.state.value ?? 0);
558
- setOutput("value", node.state.value ?? 0);
559
- },
560
- html: {
561
- init(node, el, { header, body }) {
562
- el.style.backgroundColor = "#1e1e24";
563
- el.style.border = "1px solid #444";
564
- el.style.borderRadius = "8px";
565
-
566
- header.style.backgroundColor = "#2a2a31";
567
- header.style.borderBottom = "1px solid #444";
568
- header.style.color = "#eee";
569
- header.style.fontSize = "12px";
570
- header.textContent = "Number";
571
-
572
- body.style.padding = "12px";
573
- body.style.display = "flex";
574
- body.style.alignItems = "center";
575
- body.style.justifyContent = "center";
576
-
577
- const input = document.createElement("input");
578
- input.type = "number";
579
- input.value = node.state.value ?? 0;
580
- Object.assign(input.style, {
581
- width: "100%",
582
- padding: "6px",
583
- background: "#141417",
584
- border: "1px solid #444",
585
- borderRadius: "4px",
586
- color: "#fff",
587
- fontSize: "14px",
588
- textAlign: "center",
589
- pointerEvents: "auto",
590
- });
591
-
592
- input.addEventListener("change", (e) => {
593
- node.state.value = parseFloat(e.target.value) || 0;
594
- });
595
-
596
- input.addEventListener("mousedown", (e) => e.stopPropagation());
597
- input.addEventListener("keydown", (e) => e.stopPropagation());
598
-
599
- body.appendChild(input);
600
- },
601
- update(node, el, { header, body, selected }) {
602
- el.style.borderColor = selected ? "#6cf" : "#444";
603
- header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
604
- },
605
- },
606
- onDraw(node, { ctx, theme }) {
607
- const { x, y } = node.computed;
608
- ctx.fillStyle = "#8f8";
609
- ctx.font = "14px sans-serif";
610
- ctx.textAlign = "center";
611
- ctx.fillText(String(node.state.value ?? 0), x + 70, y + 42);
612
- },
613
- });
614
-
615
- registry.register("value/String", {
616
- title: "String",
617
- size: { w: 160, h: 60 },
618
- outputs: [{ name: "value", datatype: "string" }],
619
- onCreate(node) {
620
- node.state.value = "Hello";
621
- },
622
- onExecute(node, { setOutput }) {
623
- setOutput("value", node.state.value ?? "");
624
- },
625
- onDraw(node, { ctx, theme }) {
626
- const { x, y } = node.computed;
627
- ctx.fillStyle = "#8f8";
628
- ctx.font = "12px sans-serif";
629
- ctx.textAlign = "center";
630
- const text = String(node.state.value ?? "");
631
- const displayText = text.length > 15 ? text.substring(0, 15) + "..." : text;
632
- ctx.fillText(displayText, x + 80, y + 42);
633
- },
634
- });
635
-
636
- registry.register("value/Boolean", {
637
- title: "Boolean",
638
- size: { w: 140, h: 60 },
639
- outputs: [{ name: "value", portType: "data", datatype: "boolean" }],
640
- onCreate(node) {
641
- node.state.value = true;
642
- },
643
- onExecute(node, { setOutput }) {
644
- console.log("[Boolean] Outputting value:", node.state.value ?? false);
645
- setOutput("value", node.state.value ?? false);
646
- },
647
- onDraw(node, { ctx, theme }) {
648
- const { x, y } = node.computed;
649
- ctx.fillStyle = node.state.value ? "#8f8" : "#f88";
650
- ctx.font = "14px sans-serif";
651
- ctx.textAlign = "center";
652
- ctx.fillText(String(node.state.value), x + 70, y + 42);
653
- },
654
- });
655
-
656
- // ===== UTILITY NODES =====
657
- registry.register("util/Print", {
658
- title: "Print",
659
- size: { w: 140, h: 80 },
660
- inputs: [
661
- { name: "exec", portType: "exec" },
662
- { name: "value", portType: "data", datatype: "any" },
663
- ],
664
- onCreate(node) {
665
- node.state.lastValue = null;
666
- },
667
- onExecute(node, { getInput }) {
668
- const val = getInput("value");
669
- if (val !== node.state.lastValue) {
670
- console.log("[Print]", val);
671
- node.state.lastValue = val;
672
- }
673
- },
674
- });
675
-
676
- registry.register("util/Watch", {
677
- title: "Watch",
678
- size: { w: 180, h: 110 },
679
- inputs: [
680
- { name: "exec", portType: "exec" },
681
- { name: "value", portType: "data", datatype: "any" },
682
- ],
683
- outputs: [
684
- { name: "exec", portType: "exec" },
685
- { name: "value", portType: "data", datatype: "any" },
686
- ],
687
- onCreate(node) {
688
- node.state.displayValue = "---";
689
- },
690
- onExecute(node, { getInput, setOutput }) {
691
- const val = getInput("value");
692
- console.log("[Watch] onExecute called, value:", val);
693
- node.state.displayValue = String(val ?? "---");
694
- setOutput("value", val);
695
- },
696
- onDraw(node, { ctx, theme }) {
697
- const { x, y } = node.computed;
698
- ctx.fillStyle = "#fa3";
699
- ctx.font = "11px monospace";
700
- ctx.textAlign = "left";
701
- const text = String(node.state.displayValue ?? "---");
702
- const displayText = text.length > 20 ? text.substring(0, 20) + "..." : text;
703
- ctx.fillText(displayText, x + 8, y + 50);
704
- },
705
- });
706
-
707
- registry.register("util/Timer", {
708
- title: "Timer",
709
- size: { w: 140, h: 60 },
710
- outputs: [{ name: "time", datatype: "number" }],
711
- onCreate(node) {
712
- node.state.startTime = performance.now();
713
- },
714
- onExecute(node, { setOutput }) {
715
- const elapsed = (performance.now() - (node.state.startTime ?? 0)) / 1000;
716
- setOutput("time", elapsed.toFixed(2));
717
- },
718
- });
719
-
720
- // Trigger Node with Button (HTML Overlay)
721
- registry.register("util/Trigger", {
722
- title: "Trigger",
723
- size: { w: 140, h: 80 },
724
- outputs: [{ name: "exec", portType: "exec" }], // Changed to exec port
725
-
726
- html: {
727
- init(node, el, { header, body }) {
728
- el.style.backgroundColor = "#1e1e24";
729
- el.style.border = "1px solid #444";
730
- el.style.borderRadius = "8px";
731
-
732
- header.style.backgroundColor = "#2a2a31";
733
- header.style.borderBottom = "1px solid #444";
734
- header.style.color = "#eee";
735
- header.style.fontSize = "12px";
736
- header.textContent = "Trigger";
737
-
738
- body.style.padding = "12px";
739
- body.style.display = "flex";
740
- body.style.alignItems = "center";
741
- body.style.justifyContent = "center";
742
-
743
- const button = document.createElement("button");
744
- button.textContent = "Fire!";
745
- Object.assign(button.style, {
746
- padding: "8px 16px",
747
- background: "#4a9eff",
748
- border: "none",
749
- borderRadius: "4px",
750
- color: "#fff",
751
- fontWeight: "bold",
752
- cursor: "pointer",
753
- pointerEvents: "auto",
754
- transition: "background 0.2s",
755
- });
756
-
757
- button.addEventListener("mousedown", (e) => {
758
- e.stopPropagation();
759
- button.style.background = "#2a7ede";
760
- });
761
-
762
- button.addEventListener("mouseup", () => {
763
- button.style.background = "#4a9eff";
764
- });
765
-
766
- button.addEventListener("click", (e) => {
767
- e.stopPropagation();
768
- node.state.triggered = true;
769
- console.log("[Trigger] Button clicked!");
770
-
771
- // Use runner.runOnce for connected node execution
772
- if (node.__runnerRef && node.__controllerRef) {
773
- console.log("[Trigger] Runner and controller found");
774
- const runner = node.__runnerRef;
775
- const controller = node.__controllerRef;
776
- const graph = controller.graph;
777
- console.log("[Trigger] Calling runner.runOnce with node.id:", node.id);
778
-
779
- // Execute connected nodes using runner
780
- const result = runner.runOnce(node.id, 0);
781
- const connectedEdges = result.connectedEdges;
782
-
783
-
784
-
785
- // Show animation with manual rendering
786
- const startTime = performance.now();
787
- const animationDuration = 500;
788
-
789
- const animate = () => {
790
- const elapsed = performance.now() - startTime;
791
- if (elapsed < animationDuration) {
792
- controller.renderer.draw(graph, {
793
- selection: controller.selection,
794
- tempEdge: null,
795
- running: true,
796
- time: performance.now(),
797
- dt: 0,
798
- activeEdges: connectedEdges, // Only animate connected edges
799
- });
800
- controller.htmlOverlay?.draw(graph, controller.selection);
801
- requestAnimationFrame(animate);
802
- } else {
803
- controller.render();
804
- node.state.triggered = false;
805
- }
806
- };
807
-
808
- animate();
809
- }
810
- });
811
-
812
- body.appendChild(button);
813
- },
814
-
815
- update(node, el, { header, body, selected }) {
816
- el.style.borderColor = selected ? "#6cf" : "#444";
817
- header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
818
- },
819
- },
820
-
821
- onCreate(node) {
822
- node.state.triggered = false;
823
- },
824
-
825
- onExecute(node, { setOutput }) {
826
- console.log("[Trigger] Outputting triggered:", node.state.triggered);
827
- setOutput("triggered", node.state.triggered);
828
- },
829
- });
830
-
831
- // Group Node
832
- registry.register("core/Group", {
833
- title: "Group",
834
- size: { w: 240, h: 160 },
835
- onDraw(node, { ctx, theme }) {
836
- const { x, y, w, h } = node.computed;
837
- const headerH = 24;
838
- const color = node.state.color || "#39424e";
839
- const bgAlpha = 0.5;
840
- const textColor = theme.text || "#e9e9ef";
841
-
842
- // Helper for rgba
843
- const rgba = (hex, a) => {
844
- const c = hex.replace("#", "");
845
- const n = parseInt(
846
- c.length === 3
847
- ? c
848
- .split("")
849
- .map((x) => x + x)
850
- .join("")
851
- : c,
852
- 16
853
- );
854
- const r = (n >> 16) & 255,
855
- g = (n >> 8) & 255,
856
- b = n & 255;
857
- return `rgba(${r},${g},${b},${a})`;
858
- };
859
-
860
- // Helper for roundRect
861
- const roundRect = (ctx, x, y, w, h, r) => {
862
- if (w < 2 * r) r = w / 2;
863
- if (h < 2 * r) r = h / 2;
864
- ctx.beginPath();
865
- ctx.moveTo(x + r, y);
866
- ctx.arcTo(x + w, y, x + w, y + h, r);
867
- ctx.arcTo(x + w, y + h, x, y + h, r);
868
- ctx.arcTo(x, y + h, x, y, r);
869
- ctx.arcTo(x, y, x + w, y, r);
870
- ctx.closePath();
871
- };
872
-
873
- // Body
874
- ctx.fillStyle = rgba(color, bgAlpha);
875
- roundRect(ctx, x, y, w, h, 10);
876
- ctx.fill();
877
-
878
- // Header bar (subtle)
879
- ctx.fillStyle = rgba(color, 0.3);
880
- ctx.beginPath();
881
- ctx.roundRect(x, y, w, headerH, [10, 10, 0, 0]);
882
- ctx.fill();
883
-
884
- // Title - top left with better styling
885
- ctx.fillStyle = textColor;
886
- ctx.font = "600 13px system-ui";
887
- ctx.textBaseline = "top";
888
- ctx.fillText(node.title, x + 12, y + 6);
889
- },
890
- });
891
-
892
- /**
893
- * Setup default context menu items
894
- * This function can be customized or replaced by users
895
- */
896
- function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
897
- // Add Node submenu (canvas background only)
898
- const nodeTypes = [];
899
- for (const [key, typeDef] of graph.registry.types.entries()) {
900
- nodeTypes.push({
901
- id: `add-${key}`,
902
- label: typeDef.title || key,
903
- action: () => {
904
- // Get world position from context menu
905
- const worldPos = contextMenu.worldPosition || { x: 100, y: 100 };
906
-
907
- // Add node at click position
908
- const node = graph.addNode(key, {
909
- x: worldPos.x,
910
- y: worldPos.y,
911
- });
912
-
913
- hooks?.emit("node:updated", node);
914
- controller.render(); // Update minimap and canvas
915
- },
916
- });
917
247
  }
918
-
919
- contextMenu.addItem("add-node", "Add Node", {
920
- condition: (target) => !target,
921
- submenu: nodeTypes,
922
- order: 5,
923
- });
924
-
925
- // Delete Node (for all nodes except groups)
926
- contextMenu.addItem("delete-node", "Delete Node", {
927
- condition: (target) => target && target.type !== "core/Group",
928
- action: (target) => {
929
- const cmd = RemoveNodeCmd(graph, target);
930
- controller.stack.exec(cmd);
931
- hooks?.emit("node:updated", target);
932
- },
933
- order: 10,
934
- });
935
-
936
- // Change Group Color (for groups only) - with submenu
937
- const colors = [
938
- { name: "Default", color: "#39424e" },
939
- { name: "Slate", color: "#4a5568" },
940
- { name: "Gray", color: "#2d3748" },
941
- { name: "Blue", color: "#1a365d" },
942
- { name: "Green", color: "#22543d" },
943
- { name: "Red", color: "#742a2a" },
944
- { name: "Purple", color: "#44337a" },
945
- ];
946
-
947
- contextMenu.addItem("change-group-color", "Change Color", {
948
- condition: (target) => target && target.type === "core/Group",
949
- submenu: colors.map((colorInfo) => ({
950
- id: `color-${colorInfo.color}`,
951
- label: colorInfo.name,
952
- color: colorInfo.color,
953
- action: (target) => {
954
- const currentColor = target.state.color || "#39424e";
955
- const cmd = ChangeGroupColorCmd(target, currentColor, colorInfo.color);
956
- controller.stack.exec(cmd);
957
- hooks?.emit("node:updated", target);
958
- },
959
- })),
960
- order: 20,
961
- });
962
- contextMenu.addItem("delete-group", "Delete Group", {
963
- condition: (target) => target && target.type === "core/Group",
964
- action: (target) => {
965
- const cmd = RemoveNodeCmd(graph, target);
966
- controller.stack.exec(cmd);
967
- hooks?.emit("node:updated", target);
968
- },
969
- order: 20,
970
- });
971
248
  }
972
249
 
973
250
 
974
- // Setup default context menu items
975
- // Users can easily override, remove, or add items here
976
- setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
977
-
978
251
  // initial render & resize
979
252
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
253
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
980
254
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
981
255
  controller.render();
982
256
 
983
257
  const ro = new ResizeObserver(() => {
984
258
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
259
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
985
260
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
986
261
  controller.render();
987
262
  });