html-overlay-node 0.1.6 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -10,6 +10,8 @@ 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 { HelpOverlay } from "./ui/HelpOverlay.js";
14
+ import { setupDefaultContextMenu as defaultContextMenuSetup } from "./defaults/contextMenu.js";
13
15
 
14
16
 
15
17
 
@@ -22,6 +24,11 @@ export function createGraphEditor(
22
24
  showMinimap = true,
23
25
  enablePropertyPanel = true,
24
26
  propertyPanelContainer = null,
27
+ enableHelp = true,
28
+ helpShortcuts = null,
29
+ setupDefaultContextMenu = true,
30
+ setupContextMenu = null,
31
+ plugins = [],
25
32
  } = {}
26
33
  ) {
27
34
  let canvas;
@@ -80,6 +87,39 @@ export function createGraphEditor(
80
87
  // HTML Overlay
81
88
  const htmlOverlay = new HtmlOverlay(canvas.parentElement, renderer, registry);
82
89
 
90
+ // Register callback to sync HTML overlay transform when renderer zoom/pan changes
91
+ renderer.setTransformChangeCallback(() => {
92
+ htmlOverlay.syncTransform();
93
+ });
94
+
95
+ // Edge Canvas (above HTML overlay, for edge animations)
96
+ const edgeCanvas = document.createElement("canvas");
97
+ edgeCanvas.id = "edge-canvas";
98
+ Object.assign(edgeCanvas.style, {
99
+ position: "absolute",
100
+ top: "0",
101
+ left: "0",
102
+ pointerEvents: "none", // Pass through clicks
103
+ zIndex: "15", // Above HTML overlay (10), below port canvas (20)
104
+ });
105
+ canvas.parentElement.appendChild(edgeCanvas);
106
+
107
+ // Create edge renderer (shares transform with main renderer)
108
+ const edgeRenderer = new CanvasRenderer(edgeCanvas, { theme, registry });
109
+ // Sync transform properties with main renderer
110
+ Object.defineProperty(edgeRenderer, 'scale', {
111
+ get() { return renderer.scale; },
112
+ set(v) { renderer.scale = v; }
113
+ });
114
+ Object.defineProperty(edgeRenderer, 'offsetX', {
115
+ get() { return renderer.offsetX; },
116
+ set(v) { renderer.offsetX = v; }
117
+ });
118
+ Object.defineProperty(edgeRenderer, 'offsetY', {
119
+ get() { return renderer.offsetY; },
120
+ set(v) { renderer.offsetY = v; }
121
+ });
122
+
83
123
  // Port Canvas (above HTML overlay)
84
124
  const portCanvas = document.createElement("canvas");
85
125
  portCanvas.id = "port-canvas";
@@ -88,7 +128,7 @@ export function createGraphEditor(
88
128
  top: "0",
89
129
  left: "0",
90
130
  pointerEvents: "none", // Pass through clicks
91
- zIndex: "20", // Above HTML overlay (z-index 10)
131
+ zIndex: "20", // Above edge canvas (15)
92
132
  });
93
133
  canvas.parentElement.appendChild(portCanvas);
94
134
 
@@ -99,7 +139,7 @@ export function createGraphEditor(
99
139
  portRenderer.offsetX = renderer.offsetX;
100
140
  portRenderer.offsetY = renderer.offsetY;
101
141
 
102
- const controller = new Controller({ graph, renderer, hooks, htmlOverlay, portRenderer });
142
+ const controller = new Controller({ graph, renderer, hooks, htmlOverlay, edgeRenderer, portRenderer });
103
143
 
104
144
  // Create context menu after controller (needs commandStack)
105
145
  const contextMenu = new ContextMenu({
@@ -134,8 +174,21 @@ export function createGraphEditor(
134
174
  });
135
175
  }
136
176
 
177
+ // Initialize Help Overlay if enabled
178
+ let helpOverlay = null;
179
+ if (enableHelp) {
180
+ helpOverlay = new HelpOverlay(container, {
181
+ shortcuts: helpShortcuts,
182
+ });
183
+ }
184
+
137
185
  const runner = new Runner({ graph, registry, hooks });
138
186
 
187
+ // Attach runner and controller to graph for node access
188
+ // This allows any node (like Trigger) to execute flows without tight coupling
189
+ graph.runner = runner;
190
+ graph.controller = controller;
191
+
139
192
  hooks.on("runner:tick", ({ time, dt }) => {
140
193
  renderer.draw(graph, {
141
194
  selection: controller.selection,
@@ -173,815 +226,53 @@ export function createGraphEditor(
173
226
  controller.render();
174
227
  });
175
228
 
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
- },
229
+ hooks.on("graph:deserialize", () => {
230
+ renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
231
+ controller.render();
207
232
  });
208
233
 
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)";
234
+ // Note: Example nodes have been moved to src/nodes/
235
+ // Users can import and register them selectively:
236
+ // import { registerAllNodes } from "html-overlay-node/nodes";
237
+ // registerAllNodes(registry, hooks);
224
238
 
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";
237
-
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
- },
239
+ // Setup context menu
240
+ if (setupDefaultContextMenu) {
241
+ // Use default context menu setup
242
+ defaultContextMenuSetup(contextMenu, { controller, graph, hooks });
243
+ }
264
244
 
265
- // 프레임(또는 필요시) 업데이트
266
- update(node, el, { header, _body, selected }) {
267
- el.style.borderColor = selected ? "#6cf" : "#444";
268
- header.style.backgroundColor = selected ? "#3a4a5a" : "#333";
245
+ // Allow custom context menu setup
246
+ if (setupContextMenu) {
247
+ setupContextMenu(contextMenu, { controller, graph, hooks });
248
+ }
269
249
 
270
- // 상태 동기화 (외부에서 변경되었을 경우)
271
- if (el._input.value !== (node.state.text || "")) {
272
- el._input.value = node.state.text || "";
250
+ // Install plugins
251
+ if (plugins && plugins.length > 0) {
252
+ for (const plugin of plugins) {
253
+ if (typeof plugin.install === "function") {
254
+ try {
255
+ plugin.install({ graph, registry, hooks, runner, controller, contextMenu }, plugin.options || {});
256
+ } catch (err) {
257
+ console.error(`[createGraphEditor] Failed to install plugin "${plugin.name || 'unknown'}":`, err);
258
+ hooks?.emit?.("error", err);
273
259
  }
260
+ } else {
261
+ console.warn(`[createGraphEditor] Plugin "${plugin.name || 'unknown'}" does not have an install() method`);
274
262
  }
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
263
  }
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
264
  }
972
265
 
973
266
 
974
- // Setup default context menu items
975
- // Users can easily override, remove, or add items here
976
- setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
977
-
978
267
  // initial render & resize
979
268
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
269
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
980
270
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
981
271
  controller.render();
982
272
 
983
273
  const ro = new ResizeObserver(() => {
984
274
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
275
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
985
276
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
986
277
  controller.render();
987
278
  });
@@ -1022,6 +313,7 @@ export function createGraphEditor(
1022
313
  contextMenu.destroy();
1023
314
  if (propertyPanel) propertyPanel.destroy();
1024
315
  if (minimap) minimap.destroy();
316
+ if (helpOverlay) helpOverlay.destroy();
1025
317
  },
1026
318
  };
1027
319