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.
@@ -153,27 +153,36 @@ class Node {
153
153
  * @param {string} [portType="data"] - Port type: "exec" or "data"
154
154
  * @returns {Object} The created port
155
155
  */
156
+ /**
157
+ * Recalculate minimum size based on ports
158
+ */
159
+ _updateMinSize() {
160
+ const HEADER_HEIGHT = 28;
161
+ const PORT_SPACING = 24;
162
+ const BOTTOM_PADDING = 10;
163
+ const inHeight = HEADER_HEIGHT + 10 + this.inputs.length * PORT_SPACING + BOTTOM_PADDING;
164
+ const outHeight = HEADER_HEIGHT + 10 + this.outputs.length * PORT_SPACING + BOTTOM_PADDING;
165
+ const minHeight = Math.max(inHeight, outHeight, 60);
166
+ if (this.size.height < minHeight) {
167
+ this.size.height = minHeight;
168
+ }
169
+ }
156
170
  addInput(name, datatype = "any", portType = "data") {
157
- if (!name || typeof name !== "string") {
158
- throw new Error("Input port name must be a non-empty string");
171
+ if (typeof name !== "string" || portType === "data" && !name) {
172
+ throw new Error("Input port name must be a string (non-empty for data ports)");
159
173
  }
160
174
  const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
161
175
  this.inputs.push(port);
176
+ this._updateMinSize();
162
177
  return port;
163
178
  }
164
- /**
165
- * Add an output port to this node
166
- * @param {string} name - Port name
167
- * @param {string} [datatype="any"] - Data type for the port
168
- * @param {string} [portType="data"] - Port type: "exec" or "data"
169
- * @returns {Object} The created port
170
- */
171
179
  addOutput(name, datatype = "any", portType = "data") {
172
- if (!name || typeof name !== "string") {
173
- throw new Error("Output port name must be a non-empty string");
180
+ if (typeof name !== "string" || portType === "data" && !name) {
181
+ throw new Error("Output port name must be a string (non-empty for data ports)");
174
182
  }
175
183
  const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
176
184
  this.outputs.push(port);
185
+ this._updateMinSize();
177
186
  return port;
178
187
  }
179
188
  }
@@ -188,8 +197,8 @@ class Edge {
188
197
  * @param {string} options.toPort - Target port ID
189
198
  */
190
199
  constructor({ id, fromNode, fromPort, toNode, toPort }) {
191
- if (!fromNode || !fromPort || !toNode || !toPort) {
192
- throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
200
+ if (fromNode == null || fromPort == null || toNode == null || toPort == null) {
201
+ throw new Error("Edge requires fromNode, fromPort, toNode, and toPort (null/undefined not allowed)");
193
202
  }
194
203
  this.id = id ?? randomUUID();
195
204
  this.fromNode = fromNode;
@@ -333,11 +342,12 @@ class Graph {
333
342
  const available = Array.from(this.registry.types.keys()).join(", ") || "none";
334
343
  throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
335
344
  }
345
+ const height = opts.height || ((_a = def.size) == null ? void 0 : _a.h) || this._calculateDefaultNodeHeight(def);
336
346
  const node = new Node({
337
347
  type,
338
348
  title: def.title,
339
- width: (_a = def.size) == null ? void 0 : _a.w,
340
- height: (_b = def.size) == null ? void 0 : _b.h,
349
+ width: opts.width || ((_b = def.size) == null ? void 0 : _b.w) || 140,
350
+ height,
341
351
  ...opts
342
352
  });
343
353
  for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
@@ -479,8 +489,12 @@ class Graph {
479
489
  }
480
490
  fromJSON(json) {
481
491
  var _a, _b, _c;
482
- this.clear();
492
+ this.nodes.clear();
493
+ this.edges.clear();
483
494
  for (const nd of json.nodes) {
495
+ const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
496
+ const minH = def ? this._calculateDefaultNodeHeight(def) : 60;
497
+ const height = nd.h !== void 0 ? nd.h : minH;
484
498
  const node = new Node({
485
499
  id: nd.id,
486
500
  type: nd.type,
@@ -488,9 +502,8 @@ class Graph {
488
502
  x: nd.x,
489
503
  y: nd.y,
490
504
  width: nd.w,
491
- height: nd.h
505
+ height
492
506
  });
493
- const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
494
507
  if (def == null ? void 0 : def.onCreate) {
495
508
  def.onCreate(node);
496
509
  }
@@ -516,19 +529,35 @@ class Graph {
516
529
  (_c = this.hooks) == null ? void 0 : _c.emit("graph:deserialize", json);
517
530
  return this;
518
531
  }
532
+ _calculateDefaultNodeHeight(def) {
533
+ var _a, _b;
534
+ const inCount = ((_a = def.inputs) == null ? void 0 : _a.length) || 0;
535
+ const outCount = ((_b = def.outputs) == null ? void 0 : _b.length) || 0;
536
+ const maxPorts = Math.max(inCount, outCount);
537
+ const headerHeight = 26;
538
+ const padding = 8;
539
+ const portSpacing = 20;
540
+ let h = headerHeight + padding + maxPorts * portSpacing + padding;
541
+ if (def.html) h += 16;
542
+ return Math.max(h, 40);
543
+ }
519
544
  }
520
545
  function portRect(node, port, idx, dir) {
521
- const { x: nx, y: ny, w: width, h: height } = node.computed || {
546
+ const {
547
+ x: nx,
548
+ y: ny,
549
+ w: width,
550
+ h: height
551
+ } = node.computed || {
522
552
  x: node.pos.x,
523
553
  y: node.pos.y,
524
554
  w: node.size.width,
525
555
  h: node.size.height
526
556
  };
527
- const portCount = dir === "in" ? node.inputs.length : node.outputs.length;
528
- const headerHeight = 28;
529
- const availableHeight = (height || node.size.height) - headerHeight - 16;
530
- const spacing = availableHeight / (portCount + 1);
531
- const y = ny + headerHeight + spacing * (idx + 1);
557
+ const headerHeight = 26;
558
+ const padding = 8;
559
+ const portSpacing = 20;
560
+ const y = ny + headerHeight + padding + idx * portSpacing + portSpacing / 2;
532
561
  const portWidth = 12;
533
562
  const portHeight = 12;
534
563
  if (dir === "in") {
@@ -551,32 +580,20 @@ const _CanvasRenderer = class _CanvasRenderer {
551
580
  this.edgeStyle = edgeStyle;
552
581
  this.theme = Object.assign(
553
582
  {
554
- bg: "#0d0d0f",
555
- // Darker background
556
- grid: "#1a1a1d",
557
- // Subtle grid
558
- node: "#16161a",
559
- // Darker nodes
560
- nodeBorder: "#2a2a2f",
561
- // Subtle border
562
- title: "#1f1f24",
563
- // Darker header
564
- text: "#e4e4e7",
565
- // Softer white
566
- textMuted: "#a1a1aa",
567
- // Muted text
568
- port: "#6366f1",
569
- // Indigo for data ports
583
+ bg: "#0e0e16",
584
+ grid: "#1c1c2c",
585
+ node: "rgba(22, 22, 34, 0.9)",
586
+ nodeBorder: "rgba(255, 255, 255, 0.08)",
587
+ title: "rgba(28, 28, 42, 0.95)",
588
+ text: "#f5f5f7",
589
+ textMuted: "#8e8eaf",
590
+ port: "#4f46e5",
570
591
  portExec: "#10b981",
571
- // Emerald for exec ports
572
- edge: "#52525b",
573
- // Neutral edge color
574
- edgeActive: "#8b5cf6",
575
- // Purple for active
592
+ edge: "rgba(255, 255, 255, 0.12)",
593
+ edgeActive: "#6366f1",
576
594
  accent: "#6366f1",
577
- // Indigo accent
578
- accentBright: "#818cf8"
579
- // Brighter accent
595
+ accentBright: "#818cf8",
596
+ accentGlow: "rgba(99, 102, 241, 0.25)"
580
597
  },
581
598
  theme
582
599
  );
@@ -591,31 +608,33 @@ const _CanvasRenderer = class _CanvasRenderer {
591
608
  this.canvas.width = w;
592
609
  this.canvas.height = h;
593
610
  }
594
- setTransform({
595
- scale = this.scale,
596
- offsetX = this.offsetX,
597
- offsetY = this.offsetY
598
- } = {}) {
611
+ setTransform({ scale = this.scale, offsetX = this.offsetX, offsetY = this.offsetY } = {}) {
612
+ var _a;
599
613
  this.scale = Math.min(this.maxScale, Math.max(this.minScale, scale));
600
614
  this.offsetX = offsetX;
601
615
  this.offsetY = offsetY;
616
+ (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
617
+ }
618
+ setTransformChangeCallback(callback) {
619
+ this._onTransformChange = callback;
602
620
  }
603
621
  panBy(dx, dy) {
622
+ var _a;
604
623
  this.offsetX += dx;
605
624
  this.offsetY += dy;
625
+ (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
606
626
  }
607
627
  zoomAt(factor, cx, cy) {
628
+ var _a;
608
629
  const prev = this.scale;
609
- const next = Math.min(
610
- this.maxScale,
611
- Math.max(this.minScale, prev * factor)
612
- );
630
+ const next = Math.min(this.maxScale, Math.max(this.minScale, prev * factor));
613
631
  if (next === prev) return;
614
632
  const wx = (cx - this.offsetX) / prev;
615
633
  const wy = (cy - this.offsetY) / prev;
616
634
  this.offsetX = cx - wx * next;
617
635
  this.offsetY = cy - wy * next;
618
636
  this.scale = next;
637
+ (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
619
638
  }
620
639
  screenToWorld(x, y) {
621
640
  return {
@@ -631,36 +650,30 @@ const _CanvasRenderer = class _CanvasRenderer {
631
650
  }
632
651
  _applyTransform() {
633
652
  const { ctx } = this;
634
- ctx.setTransform(this.scale, 0, 0, this.scale, this.offsetX, this.offsetY);
653
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
654
+ ctx.translate(this.offsetX, this.offsetY);
655
+ ctx.scale(this.scale, this.scale);
635
656
  }
636
657
  _resetTransform() {
637
658
  this.ctx.setTransform(1, 0, 0, 1, 0, 0);
638
659
  }
639
660
  // ── Drawing ────────────────────────────────────────────────────────────────
640
- _drawArrowhead(x1, y1, x2, y2, size = 10) {
661
+ _drawArrowhead(x1, y1, x2, y2, size = 8) {
641
662
  const { ctx } = this;
642
663
  const s = size / this.scale;
643
664
  const ang = Math.atan2(y2 - y1, x2 - x1);
644
665
  ctx.beginPath();
645
666
  ctx.moveTo(x2, y2);
646
- ctx.lineTo(
647
- x2 - s * Math.cos(ang - Math.PI / 6),
648
- y2 - s * Math.sin(ang - Math.PI / 6)
649
- );
650
- ctx.lineTo(
651
- x2 - s * Math.cos(ang + Math.PI / 6),
652
- y2 - s * Math.sin(ang + Math.PI / 6)
653
- );
667
+ ctx.lineTo(x2 - s * Math.cos(ang - Math.PI / 6), y2 - s * Math.sin(ang - Math.PI / 6));
668
+ ctx.lineTo(x2 - s * Math.cos(ang + Math.PI / 6), y2 - s * Math.sin(ang + Math.PI / 6));
654
669
  ctx.closePath();
655
670
  ctx.fill();
656
671
  }
657
672
  _drawScreenText(text, lx, ly, {
658
- fontPx = 12,
673
+ fontPx = 11,
659
674
  color = this.theme.text,
660
675
  align = "left",
661
- baseline = "alphabetic",
662
- dpr = 1
663
- // 추후 devicePixelRatio 도입
676
+ baseline = "alphabetic"
664
677
  } = {}) {
665
678
  const { ctx } = this;
666
679
  const { x: sx, y: sy } = this.worldToScreen(lx, ly);
@@ -668,7 +681,7 @@ const _CanvasRenderer = class _CanvasRenderer {
668
681
  this._resetTransform();
669
682
  const px = Math.round(sx) + 0.5;
670
683
  const py = Math.round(sy) + 0.5;
671
- ctx.font = `${fontPx * this.scale}px system-ui`;
684
+ ctx.font = `${fontPx * this.scale}px "Inter", system-ui, sans-serif`;
672
685
  ctx.fillStyle = color;
673
686
  ctx.textAlign = align;
674
687
  ctx.textBaseline = baseline;
@@ -681,38 +694,47 @@ const _CanvasRenderer = class _CanvasRenderer {
681
694
  ctx.fillStyle = theme.bg;
682
695
  ctx.fillRect(0, 0, canvas.width, canvas.height);
683
696
  this._applyTransform();
684
- ctx.strokeStyle = this._rgba(theme.grid, 0.35);
685
- ctx.lineWidth = 1 / scale;
686
- const base = 20;
687
- const step = base;
688
697
  const x0 = -offsetX / scale;
689
698
  const y0 = -offsetY / scale;
690
699
  const x1 = (canvas.width - offsetX) / scale;
691
700
  const y1 = (canvas.height - offsetY) / scale;
692
- const startX = Math.floor(x0 / step) * step;
693
- const startY = Math.floor(y0 / step) * step;
694
- ctx.beginPath();
695
- for (let x = startX; x <= x1; x += step) {
696
- ctx.moveTo(x, y0);
697
- ctx.lineTo(x, y1);
701
+ const minorStep = 24;
702
+ const majorStep = 120;
703
+ const minorR = 1 / scale;
704
+ const majorR = 1.5 / scale;
705
+ const startX = Math.floor(x0 / minorStep) * minorStep;
706
+ const startY = Math.floor(y0 / minorStep) * minorStep;
707
+ ctx.fillStyle = this._rgba(theme.grid, 0.7);
708
+ for (let gx = startX; gx <= x1; gx += minorStep) {
709
+ for (let gy = startY; gy <= y1; gy += minorStep) {
710
+ const isMajorX = Math.round(gx / majorStep) * majorStep === Math.round(gx);
711
+ const isMajorY = Math.round(gy / majorStep) * majorStep === Math.round(gy);
712
+ if (isMajorX && isMajorY) continue;
713
+ ctx.beginPath();
714
+ ctx.arc(gx, gy, minorR, 0, Math.PI * 2);
715
+ ctx.fill();
716
+ }
698
717
  }
699
- for (let y = startY; y <= y1; y += step) {
700
- ctx.moveTo(x0, y);
701
- ctx.lineTo(x1, y);
718
+ const majorStartX = Math.floor(x0 / majorStep) * majorStep;
719
+ const majorStartY = Math.floor(y0 / majorStep) * majorStep;
720
+ ctx.fillStyle = this._rgba(theme.grid, 1);
721
+ for (let gx = majorStartX; gx <= x1; gx += majorStep) {
722
+ for (let gy = majorStartY; gy <= y1; gy += majorStep) {
723
+ ctx.beginPath();
724
+ ctx.arc(gx, gy, majorR, 0, Math.PI * 2);
725
+ ctx.fill();
726
+ }
702
727
  }
703
- ctx.stroke();
704
728
  this._resetTransform();
705
729
  }
706
730
  draw(graph, {
707
731
  selection = /* @__PURE__ */ new Set(),
708
732
  tempEdge = null,
709
- running = false,
710
733
  time = performance.now(),
711
- dt = 0,
712
- groups = null,
713
- activeEdges = /* @__PURE__ */ new Set()
734
+ activeEdges = /* @__PURE__ */ new Set(),
735
+ drawEdges = true
714
736
  } = {}) {
715
- var _a, _b, _c, _d, _e, _f;
737
+ var _a, _b, _c, _d;
716
738
  graph.updateWorldTransforms();
717
739
  this.drawGrid();
718
740
  const { ctx, theme } = this;
@@ -722,43 +744,50 @@ const _CanvasRenderer = class _CanvasRenderer {
722
744
  if (n.type === "core/Group") {
723
745
  const sel = selection.has(n.id);
724
746
  const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(n.type);
725
- if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
747
+ if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme, renderer: this });
726
748
  else this._drawNode(n, sel);
727
749
  }
728
750
  }
729
- ctx.lineWidth = 1.5 / this.scale;
730
- let dashArray = null;
731
- let dashOffset = 0;
732
- if (running) {
733
- const speed = 120;
734
- const phase = time / 1e3 * speed / this.scale % _CanvasRenderer.FONT_SIZE;
735
- dashArray = [6 / this.scale, 6 / this.scale];
736
- dashOffset = -phase;
737
- }
738
- for (const e of graph.edges.values()) {
739
- const shouldAnimate = activeEdges && activeEdges.size > 0 && activeEdges.has(e.id);
740
- if (running && shouldAnimate && dashArray) {
741
- ctx.setLineDash(dashArray);
742
- ctx.lineDashOffset = dashOffset;
743
- } else {
744
- ctx.setLineDash([]);
745
- ctx.lineDashOffset = 0;
746
- }
747
- const isActive = activeEdges && activeEdges.has(e.id);
748
- if (isActive) {
749
- ctx.strokeStyle = "#00ffff";
750
- ctx.lineWidth = 3 * this.scale;
751
- } else {
752
- ctx.strokeStyle = theme.edge;
753
- ctx.lineWidth = 1.5 / this.scale;
751
+ if (drawEdges) {
752
+ ctx.lineWidth = 1.5 / this.scale;
753
+ for (const e of graph.edges.values()) {
754
+ const isActive = activeEdges && activeEdges.has(e.id);
755
+ if (isActive) {
756
+ ctx.save();
757
+ ctx.shadowColor = this.theme.edgeActive;
758
+ ctx.shadowBlur = 8 / this.scale;
759
+ ctx.strokeStyle = this.theme.edgeActive;
760
+ ctx.lineWidth = 2 / this.scale;
761
+ ctx.setLineDash([]);
762
+ this._drawEdge(graph, e);
763
+ ctx.restore();
764
+ const dotT = time / 1e3 * 1.2 % 1;
765
+ const dotPos = this._getEdgeDotPosition(graph, e, dotT);
766
+ if (dotPos) {
767
+ ctx.save();
768
+ ctx.fillStyle = "#ffffff";
769
+ ctx.shadowColor = this.theme.edgeActive;
770
+ ctx.shadowBlur = 10 / this.scale;
771
+ ctx.beginPath();
772
+ ctx.arc(dotPos.x, dotPos.y, 3 / this.scale, 0, Math.PI * 2);
773
+ ctx.fill();
774
+ ctx.restore();
775
+ }
776
+ } else {
777
+ ctx.setLineDash([]);
778
+ ctx.strokeStyle = theme.edge;
779
+ ctx.lineWidth = 1.5 / this.scale;
780
+ this._drawEdge(graph, e);
781
+ }
754
782
  }
755
- this._drawEdge(graph, e);
756
783
  }
757
784
  if (tempEdge) {
758
785
  const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
759
786
  const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
760
787
  const prevDash = this.ctx.getLineDash();
761
- this.ctx.setLineDash([6 / this.scale, 6 / this.scale]);
788
+ this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
789
+ this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
790
+ this.ctx.lineWidth = 1.5 / this.scale;
762
791
  let ptsForArrow = null;
763
792
  if (this.edgeStyle === "line") {
764
793
  this._drawLine(a.x, a.y, b.x, b.y);
@@ -773,9 +802,8 @@ const _CanvasRenderer = class _CanvasRenderer {
773
802
  if (ptsForArrow && ptsForArrow.length >= 2) {
774
803
  const p1 = ptsForArrow[ptsForArrow.length - 2];
775
804
  const p2 = ptsForArrow[ptsForArrow.length - 1];
776
- this.ctx.fillStyle = this.theme.edge;
777
- this.ctx.strokeStyle = this.theme.edge;
778
- this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
805
+ this.ctx.fillStyle = this.theme.accentBright;
806
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
779
807
  }
780
808
  }
781
809
  for (const n of graph.nodes.values()) {
@@ -783,14 +811,10 @@ const _CanvasRenderer = class _CanvasRenderer {
783
811
  const sel = selection.has(n.id);
784
812
  const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
785
813
  const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
786
- this._drawNode(n, sel, hasHtmlOverlay);
787
- if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
788
- }
789
- }
790
- for (const n of graph.nodes.values()) {
791
- if (n.type !== "core/Group") {
792
- const def = (_f = (_e = this.registry) == null ? void 0 : _e.types) == null ? void 0 : _f.get(n.type);
793
- const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
814
+ this._drawNode(n, sel, !hasHtmlOverlay ? true : false);
815
+ if (def == null ? void 0 : def.onDraw) {
816
+ def.onDraw(n, { ctx, theme, renderer: this });
817
+ }
794
818
  if (hasHtmlOverlay) {
795
819
  this._drawPorts(n);
796
820
  }
@@ -808,40 +832,58 @@ const _CanvasRenderer = class _CanvasRenderer {
808
832
  return `rgba(${r},${g},${b},${a})`;
809
833
  }
810
834
  _drawNode(node, selected, skipPorts = false) {
835
+ var _a, _b;
811
836
  const { ctx, theme } = this;
812
- const r = 8;
837
+ const r = 2;
813
838
  const { x, y, w, h } = node.computed;
814
- if (!selected) {
839
+ const headerH = 26;
840
+ const typeDef = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
841
+ const categoryColor = node.color || (typeDef == null ? void 0 : typeDef.color) || theme.accent;
842
+ if (selected) {
815
843
  ctx.save();
816
- ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
817
- ctx.shadowBlur = 8 / this.scale;
818
- ctx.shadowOffsetY = 2 / this.scale;
819
- ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
820
- roundRect(ctx, x, y, w, h, r);
821
- ctx.fill();
844
+ ctx.shadowColor = theme.accentGlow;
845
+ ctx.shadowBlur = 10 / this.scale;
846
+ ctx.strokeStyle = theme.accentBright;
847
+ ctx.lineWidth = 2 / this.scale;
848
+ const pad = 1.5 / this.scale;
849
+ roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
850
+ ctx.stroke();
822
851
  ctx.restore();
823
852
  }
853
+ ctx.save();
854
+ ctx.shadowColor = "rgba(0,0,0,0.7)";
855
+ ctx.shadowBlur = 20 / this.scale;
856
+ ctx.shadowOffsetY = 6 / this.scale;
857
+ ctx.fillStyle = theme.node;
858
+ roundRect(ctx, x, y, w, h, r);
859
+ ctx.fill();
860
+ ctx.restore();
824
861
  ctx.fillStyle = theme.node;
825
862
  ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
826
- ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
863
+ ctx.lineWidth = 1 / this.scale;
827
864
  roundRect(ctx, x, y, w, h, r);
828
865
  ctx.fill();
829
866
  ctx.stroke();
830
867
  ctx.fillStyle = theme.title;
831
- roundRect(ctx, x, y, w, 24, { tl: r, tr: r, br: 0, bl: 0 });
868
+ roundRect(ctx, x, y, w, headerH, { tl: r, tr: r, br: 0, bl: 0 });
832
869
  ctx.fill();
833
- ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
834
- ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
870
+ ctx.save();
871
+ ctx.globalCompositeOperation = "source-atop";
872
+ ctx.fillStyle = categoryColor;
873
+ ctx.globalAlpha = 0.25;
874
+ ctx.fillRect(x, y, w, headerH);
875
+ ctx.restore();
876
+ ctx.strokeStyle = selected ? this._rgba(theme.accentBright, 0.3) : this._rgba(theme.nodeBorder, 0.6);
877
+ ctx.lineWidth = 1 / this.scale;
835
878
  ctx.beginPath();
836
- ctx.moveTo(x + r, y);
837
- ctx.lineTo(x + w - r, y);
838
- ctx.quadraticCurveTo(x + w, y, x + w, y + r);
839
- ctx.lineTo(x + w, y + 24);
840
- ctx.moveTo(x, y + 24);
841
- ctx.lineTo(x, y + r);
842
- ctx.quadraticCurveTo(x, y, x + r, y);
879
+ ctx.moveTo(x, y + headerH);
880
+ ctx.lineTo(x + w, y + headerH);
843
881
  ctx.stroke();
844
- this._drawScreenText(node.title, x + 8, y + _CanvasRenderer.FONT_SIZE, {
882
+ ctx.fillStyle = categoryColor;
883
+ ctx.beginPath();
884
+ ctx.roundRect(x, y, w, 2.5 / this.scale, { tl: r, tr: r, br: 0, bl: 0 });
885
+ ctx.fill();
886
+ this._drawScreenText(node.title, x + 10, y + headerH / 2, {
845
887
  fontPx: _CanvasRenderer.FONT_SIZE,
846
888
  color: theme.text,
847
889
  baseline: "middle",
@@ -852,97 +894,111 @@ const _CanvasRenderer = class _CanvasRenderer {
852
894
  const rct = portRect(node, p, i, "in");
853
895
  const cx = rct.x + rct.w / 2;
854
896
  const cy = rct.y + rct.h / 2;
855
- if (p.portType === "exec") {
856
- const portSize = 8;
857
- ctx.fillStyle = theme.portExec;
858
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
859
- ctx.lineWidth = 2 / this.scale;
860
- ctx.beginPath();
861
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
862
- ctx.fill();
863
- ctx.stroke();
864
- } else {
865
- ctx.fillStyle = theme.port;
866
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
867
- ctx.lineWidth = 2 / this.scale;
868
- ctx.beginPath();
869
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
870
- ctx.fill();
871
- ctx.stroke();
897
+ this._drawPortShape(cx, cy, p.portType);
898
+ if (p.name) {
899
+ this._drawScreenText(p.name, cx + 10, cy, {
900
+ fontPx: 10,
901
+ color: theme.textMuted,
902
+ baseline: "middle",
903
+ align: "left"
904
+ });
872
905
  }
873
906
  });
874
907
  node.outputs.forEach((p, i) => {
875
908
  const rct = portRect(node, p, i, "out");
876
909
  const cx = rct.x + rct.w / 2;
877
910
  const cy = rct.y + rct.h / 2;
878
- if (p.portType === "exec") {
879
- const portSize = 8;
880
- ctx.fillStyle = theme.portExec;
881
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
882
- ctx.lineWidth = 2 / this.scale;
883
- ctx.beginPath();
884
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
885
- ctx.fill();
886
- ctx.stroke();
887
- } else {
888
- ctx.fillStyle = theme.port;
889
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
890
- ctx.lineWidth = 2 / this.scale;
891
- ctx.beginPath();
892
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
893
- ctx.fill();
894
- ctx.stroke();
911
+ this._drawPortShape(cx, cy, p.portType);
912
+ if (p.name) {
913
+ this._drawScreenText(p.name, cx - 10, cy, {
914
+ fontPx: 10,
915
+ color: theme.textMuted,
916
+ baseline: "middle",
917
+ align: "right"
918
+ });
895
919
  }
896
920
  });
897
921
  }
898
- _drawPorts(node) {
922
+ _drawPortShape(cx, cy, portType) {
899
923
  const { ctx, theme } = this;
924
+ if (portType === "exec") {
925
+ const s = 5 / this.scale;
926
+ ctx.save();
927
+ ctx.fillStyle = theme.portExec;
928
+ ctx.strokeStyle = this._rgba(theme.portExec, 0.4);
929
+ ctx.lineWidth = 2 / this.scale;
930
+ ctx.beginPath();
931
+ ctx.moveTo(cx, cy - s);
932
+ ctx.lineTo(cx + s, cy);
933
+ ctx.lineTo(cx, cy + s);
934
+ ctx.lineTo(cx - s, cy);
935
+ ctx.closePath();
936
+ ctx.fill();
937
+ ctx.stroke();
938
+ ctx.restore();
939
+ } else {
940
+ ctx.save();
941
+ ctx.strokeStyle = this._rgba(theme.port, 0.35);
942
+ ctx.lineWidth = 3 / this.scale;
943
+ ctx.beginPath();
944
+ ctx.arc(cx, cy, 5 / this.scale, 0, Math.PI * 2);
945
+ ctx.stroke();
946
+ ctx.fillStyle = theme.port;
947
+ ctx.beginPath();
948
+ ctx.arc(cx, cy, 3.5 / this.scale, 0, Math.PI * 2);
949
+ ctx.fill();
950
+ ctx.restore();
951
+ }
952
+ }
953
+ _drawPorts(node) {
900
954
  node.inputs.forEach((p, i) => {
901
955
  const rct = portRect(node, p, i, "in");
902
956
  const cx = rct.x + rct.w / 2;
903
957
  const cy = rct.y + rct.h / 2;
904
- if (p.portType === "exec") {
905
- const portSize = 8;
906
- ctx.fillStyle = theme.portExec;
907
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
908
- ctx.lineWidth = 2 / this.scale;
909
- ctx.beginPath();
910
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
911
- ctx.fill();
912
- ctx.stroke();
913
- } else {
914
- ctx.fillStyle = theme.port;
915
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
916
- ctx.lineWidth = 2 / this.scale;
917
- ctx.beginPath();
918
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
919
- ctx.fill();
920
- }
958
+ this._drawPortShape(cx, cy, p.portType);
921
959
  });
922
960
  node.outputs.forEach((p, i) => {
923
961
  const rct = portRect(node, p, i, "out");
924
962
  const cx = rct.x + rct.w / 2;
925
963
  const cy = rct.y + rct.h / 2;
926
- if (p.portType === "exec") {
927
- const portSize = 8;
928
- ctx.fillStyle = theme.portExec;
929
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
930
- ctx.lineWidth = 2 / this.scale;
931
- ctx.beginPath();
932
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
933
- ctx.fill();
934
- ctx.stroke();
935
- } else {
936
- ctx.fillStyle = theme.port;
937
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
938
- ctx.lineWidth = 2 / this.scale;
939
- ctx.beginPath();
940
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
941
- ctx.fill();
942
- ctx.stroke();
943
- }
964
+ this._drawPortShape(cx, cy, p.portType);
944
965
  });
945
966
  }
967
+ /** Selection border for HTML overlay nodes, drawn on the edge canvas */
968
+ _drawHtmlSelectionBorder(node) {
969
+ const { ctx, theme } = this;
970
+ const { x, y, w, h } = node.computed;
971
+ const r = 2;
972
+ const pad = 1.5 / this.scale;
973
+ ctx.save();
974
+ ctx.shadowColor = theme.accentGlow;
975
+ ctx.shadowBlur = 14 / this.scale;
976
+ ctx.strokeStyle = theme.accentBright;
977
+ ctx.lineWidth = 1.5 / this.scale;
978
+ roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r);
979
+ ctx.stroke();
980
+ ctx.restore();
981
+ }
982
+ /** Rotating dashed border drawn on the edge canvas for executing nodes */
983
+ _drawActiveNodeBorder(node, time) {
984
+ const { ctx, theme } = this;
985
+ const { x, y, w, h } = node.computed;
986
+ const r = 2;
987
+ const pad = 2.5 / this.scale;
988
+ const dashLen = 8 / this.scale;
989
+ const gapLen = 6 / this.scale;
990
+ const offset = -(time / 1e3) * (50 / this.scale);
991
+ ctx.save();
992
+ ctx.setLineDash([dashLen, gapLen]);
993
+ ctx.lineDashOffset = offset;
994
+ ctx.strokeStyle = this._rgba(theme.portExec, 0.9);
995
+ ctx.lineWidth = 1.5 / this.scale;
996
+ ctx.shadowColor = theme.portExec;
997
+ ctx.shadowBlur = 4 / this.scale;
998
+ roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
999
+ ctx.stroke();
1000
+ ctx.restore();
1001
+ }
946
1002
  _drawEdge(graph, e) {
947
1003
  const from = graph.nodes.get(e.fromNode);
948
1004
  const to = graph.nodes.get(e.toNode);
@@ -951,7 +1007,8 @@ const _CanvasRenderer = class _CanvasRenderer {
951
1007
  const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
952
1008
  const pr1 = portRect(from, null, iOut, "out");
953
1009
  const pr2 = portRect(to, null, iIn, "in");
954
- const x1 = pr1.x, y1 = pr1.y + 7, x2 = pr2.x, y2 = pr2.y + 7;
1010
+ const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
1011
+ const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
955
1012
  if (this.edgeStyle === "line") {
956
1013
  this._drawLine(x1, y1, x2, y2);
957
1014
  } else if (this.edgeStyle === "orthogonal") {
@@ -960,6 +1017,32 @@ const _CanvasRenderer = class _CanvasRenderer {
960
1017
  this._drawCurve(x1, y1, x2, y2);
961
1018
  }
962
1019
  }
1020
+ _getEdgeDotPosition(graph, e, t) {
1021
+ const from = graph.nodes.get(e.fromNode);
1022
+ const to = graph.nodes.get(e.toNode);
1023
+ if (!from || !to) return null;
1024
+ const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
1025
+ const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
1026
+ const pr1 = portRect(from, null, iOut, "out");
1027
+ const pr2 = portRect(to, null, iIn, "in");
1028
+ const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
1029
+ const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
1030
+ if (this.edgeStyle === "bezier") {
1031
+ const dx = Math.max(40, Math.abs(x2 - x1) * 0.4);
1032
+ return cubicBezierPoint(x1, y1, x1 + dx, y1, x2 - dx, y2, x2, y2, t);
1033
+ } else if (this.edgeStyle === "orthogonal") {
1034
+ const midX = (x1 + x2) / 2;
1035
+ const pts = [
1036
+ { x: x1, y: y1 },
1037
+ { x: midX, y: y1 },
1038
+ { x: midX, y: y2 },
1039
+ { x: x2, y: y2 }
1040
+ ];
1041
+ return polylinePoint(pts, t);
1042
+ } else {
1043
+ return { x: x1 + (x2 - x1) * t, y: y1 + (y2 - y1) * t };
1044
+ }
1045
+ }
963
1046
  _drawLine(x1, y1, x2, y2) {
964
1047
  const { ctx } = this;
965
1048
  ctx.beginPath();
@@ -971,21 +1054,17 @@ const _CanvasRenderer = class _CanvasRenderer {
971
1054
  const { ctx } = this;
972
1055
  ctx.beginPath();
973
1056
  ctx.moveTo(points[0].x, points[0].y);
974
- for (let i = 1; i < points.length; i++)
975
- ctx.lineTo(points[i].x, points[i].y);
1057
+ for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
976
1058
  ctx.stroke();
977
1059
  }
978
1060
  _drawOrthogonal(x1, y1, x2, y2) {
979
1061
  const midX = (x1 + x2) / 2;
980
- let pts;
981
- {
982
- pts = [
983
- { x: x1, y: y1 },
984
- { x: midX, y: y1 },
985
- { x: midX, y: y2 },
986
- { x: x2, y: y2 }
987
- ];
988
- }
1062
+ const pts = [
1063
+ { x: x1, y: y1 },
1064
+ { x: midX, y: y1 },
1065
+ { x: midX, y: y2 },
1066
+ { x: x2, y: y2 }
1067
+ ];
989
1068
  const { ctx } = this;
990
1069
  const prevJoin = ctx.lineJoin, prevCap = ctx.lineCap;
991
1070
  ctx.lineJoin = "round";
@@ -1003,8 +1082,91 @@ const _CanvasRenderer = class _CanvasRenderer {
1003
1082
  ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
1004
1083
  ctx.stroke();
1005
1084
  }
1085
+ drawEdgesOnly(graph, {
1086
+ activeEdges = /* @__PURE__ */ new Set(),
1087
+ activeEdgeTimes = /* @__PURE__ */ new Map(),
1088
+ activeNodes = /* @__PURE__ */ new Set(),
1089
+ selection = /* @__PURE__ */ new Set(),
1090
+ time = performance.now(),
1091
+ tempEdge = null
1092
+ } = {}) {
1093
+ var _a, _b;
1094
+ this._resetTransform();
1095
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1096
+ this._applyTransform();
1097
+ const { ctx, theme } = this;
1098
+ for (const e of graph.edges.values()) {
1099
+ const isActive = activeEdges.has(e.id);
1100
+ if (isActive) {
1101
+ ctx.save();
1102
+ ctx.shadowColor = theme.edgeActive;
1103
+ ctx.shadowBlur = 6 / this.scale;
1104
+ ctx.strokeStyle = theme.edgeActive;
1105
+ ctx.lineWidth = 2 / this.scale;
1106
+ ctx.setLineDash([]);
1107
+ this._drawEdge(graph, e);
1108
+ ctx.restore();
1109
+ const activationTime = activeEdgeTimes.get(e.id) ?? time;
1110
+ const dotT = Math.min(1, (time - activationTime) / 620);
1111
+ const dotPos = this._getEdgeDotPosition(graph, e, dotT);
1112
+ if (dotPos) {
1113
+ ctx.save();
1114
+ ctx.fillStyle = this._rgba(theme.edgeActive, 0.9);
1115
+ ctx.shadowColor = theme.edgeActive;
1116
+ ctx.shadowBlur = 8 / this.scale;
1117
+ ctx.beginPath();
1118
+ ctx.arc(dotPos.x, dotPos.y, 2.5 / this.scale, 0, Math.PI * 2);
1119
+ ctx.fill();
1120
+ ctx.restore();
1121
+ }
1122
+ } else {
1123
+ ctx.setLineDash([]);
1124
+ ctx.strokeStyle = theme.edge;
1125
+ ctx.lineWidth = 1.5 / this.scale;
1126
+ this._drawEdge(graph, e);
1127
+ }
1128
+ }
1129
+ for (const nodeId of selection) {
1130
+ const node = graph.nodes.get(nodeId);
1131
+ if (!node) continue;
1132
+ const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
1133
+ if (def == null ? void 0 : def.html) this._drawHtmlSelectionBorder(node);
1134
+ }
1135
+ if (activeNodes.size > 0) {
1136
+ for (const nodeId of activeNodes) {
1137
+ const node = graph.nodes.get(nodeId);
1138
+ if (node) this._drawActiveNodeBorder(node, time);
1139
+ }
1140
+ }
1141
+ if (tempEdge) {
1142
+ const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
1143
+ const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
1144
+ const prevDash = this.ctx.getLineDash();
1145
+ this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
1146
+ this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
1147
+ this.ctx.lineWidth = 1.5 / this.scale;
1148
+ let ptsForArrow = null;
1149
+ if (this.edgeStyle === "line") {
1150
+ this._drawLine(a.x, a.y, b.x, b.y);
1151
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
1152
+ } else if (this.edgeStyle === "orthogonal") {
1153
+ ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
1154
+ } else {
1155
+ this._drawCurve(a.x, a.y, b.x, b.y);
1156
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
1157
+ }
1158
+ this.ctx.setLineDash(prevDash);
1159
+ if (ptsForArrow && ptsForArrow.length >= 2) {
1160
+ const p1 = ptsForArrow[ptsForArrow.length - 2];
1161
+ const p2 = ptsForArrow[ptsForArrow.length - 1];
1162
+ this.ctx.fillStyle = this.theme.accentBright;
1163
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
1164
+ }
1165
+ }
1166
+ this._resetTransform();
1167
+ }
1006
1168
  };
1007
- __publicField(_CanvasRenderer, "FONT_SIZE", 12);
1169
+ __publicField(_CanvasRenderer, "FONT_SIZE", 11);
1008
1170
  __publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
1009
1171
  let CanvasRenderer = _CanvasRenderer;
1010
1172
  function roundRect(ctx, x, y, w, h, r = 6) {
@@ -1021,6 +1183,38 @@ function roundRect(ctx, x, y, w, h, r = 6) {
1021
1183
  ctx.quadraticCurveTo(x, y, x + r.tl, y);
1022
1184
  ctx.closePath();
1023
1185
  }
1186
+ function cubicBezierPoint(x0, y0, x1, y1, x2, y2, x3, y3, t) {
1187
+ const mt = 1 - t;
1188
+ return {
1189
+ x: mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
1190
+ y: mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3
1191
+ };
1192
+ }
1193
+ function polylinePoint(pts, t) {
1194
+ let totalLen = 0;
1195
+ const lens = [];
1196
+ for (let i = 0; i < pts.length - 1; i++) {
1197
+ const dx = pts[i + 1].x - pts[i].x;
1198
+ const dy = pts[i + 1].y - pts[i].y;
1199
+ const len = Math.sqrt(dx * dx + dy * dy);
1200
+ lens.push(len);
1201
+ totalLen += len;
1202
+ }
1203
+ if (totalLen === 0) return pts[0];
1204
+ let target = t * totalLen;
1205
+ let accum = 0;
1206
+ for (let i = 0; i < lens.length; i++) {
1207
+ if (accum + lens[i] >= target) {
1208
+ const segT = lens[i] > 0 ? (target - accum) / lens[i] : 0;
1209
+ return {
1210
+ x: pts[i].x + (pts[i + 1].x - pts[i].x) * segT,
1211
+ y: pts[i].y + (pts[i + 1].y - pts[i].y) * segT
1212
+ };
1213
+ }
1214
+ accum += lens[i];
1215
+ }
1216
+ return pts[pts.length - 1];
1217
+ }
1024
1218
  function findEdgeId(graph, a, b, c, d) {
1025
1219
  for (const [id, e] of graph.edges) {
1026
1220
  if (e.fromNode === a && e.fromPort === b && e.toNode === c && e.toPort === d)
@@ -1126,12 +1320,13 @@ class CommandStack {
1126
1320
  }
1127
1321
  }
1128
1322
  const _Controller = class _Controller {
1129
- constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, portRenderer }) {
1323
+ constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, edgeRenderer, portRenderer }) {
1130
1324
  this.graph = graph;
1131
1325
  this.renderer = renderer;
1132
1326
  this.hooks = hooks;
1133
1327
  this.htmlOverlay = htmlOverlay;
1134
1328
  this.contextMenu = contextMenu;
1329
+ this.edgeRenderer = edgeRenderer;
1135
1330
  this.portRenderer = portRenderer;
1136
1331
  this.stack = new CommandStack();
1137
1332
  this.selection = /* @__PURE__ */ new Set();
@@ -1142,6 +1337,9 @@ const _Controller = class _Controller {
1142
1337
  this.gDragging = null;
1143
1338
  this.gResizing = null;
1144
1339
  this.boxSelecting = null;
1340
+ this.activeEdges = /* @__PURE__ */ new Set();
1341
+ this.activeEdgeTimes = /* @__PURE__ */ new Map();
1342
+ this.activeNodes = /* @__PURE__ */ new Set();
1145
1343
  this.snapToGrid = true;
1146
1344
  this.gridSize = 20;
1147
1345
  this._cursor = "default";
@@ -1282,13 +1480,11 @@ const _Controller = class _Controller {
1282
1480
  for (const n of this.graph.nodes.values()) {
1283
1481
  for (let i = 0; i < n.inputs.length; i++) {
1284
1482
  const r = portRect(n, n.inputs[i], i, "in");
1285
- if (rectHas(r, x, y))
1286
- return { node: n, port: n.inputs[i], dir: "in", idx: i };
1483
+ if (rectHas(r, x, y)) return { node: n, port: n.inputs[i], dir: "in", idx: i };
1287
1484
  }
1288
1485
  for (let i = 0; i < n.outputs.length; i++) {
1289
1486
  const r = portRect(n, n.outputs[i], i, "out");
1290
- if (rectHas(r, x, y))
1291
- return { node: n, port: n.outputs[i], dir: "out", idx: i };
1487
+ if (rectHas(r, x, y)) return { node: n, port: n.outputs[i], dir: "out", idx: i };
1292
1488
  }
1293
1489
  }
1294
1490
  return null;
@@ -1537,13 +1733,7 @@ const _Controller = class _Controller {
1537
1733
  const portIn = this._findPortAtWorld(w.x, w.y);
1538
1734
  if (portIn && portIn.dir === "in") {
1539
1735
  this.stack.exec(
1540
- AddEdgeCmd(
1541
- this.graph,
1542
- from.fromNode,
1543
- from.fromPort,
1544
- portIn.node.id,
1545
- portIn.port.id
1546
- )
1736
+ AddEdgeCmd(this.graph, from.fromNode, from.fromPort, portIn.node.id, portIn.port.id)
1547
1737
  );
1548
1738
  }
1549
1739
  this.connecting = null;
@@ -1711,16 +1901,32 @@ const _Controller = class _Controller {
1711
1901
  this.render();
1712
1902
  }
1713
1903
  render() {
1714
- var _a, _b, _c;
1904
+ var _a;
1715
1905
  const tEdge = this.renderTempEdge();
1716
1906
  this.renderer.draw(this.graph, {
1717
1907
  selection: this.selection,
1718
- tempEdge: tEdge,
1908
+ tempEdge: null,
1909
+ // Don't draw temp edge on background
1719
1910
  boxSelecting: this.boxSelecting,
1720
- activeEdges: this.activeEdges || /* @__PURE__ */ new Set()
1721
- // For animation
1911
+ activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
1912
+ drawEdges: !this.edgeRenderer
1913
+ // Only draw edges here if no separate edge renderer
1722
1914
  });
1723
1915
  (_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
1916
+ if (this.edgeRenderer) {
1917
+ const edgeCtx = this.edgeRenderer.ctx;
1918
+ edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
1919
+ this.edgeRenderer._applyTransform();
1920
+ this.edgeRenderer.drawEdgesOnly(this.graph, {
1921
+ activeEdges: this.activeEdges,
1922
+ activeEdgeTimes: this.activeEdgeTimes,
1923
+ activeNodes: this.activeNodes,
1924
+ selection: this.selection,
1925
+ time: performance.now(),
1926
+ tempEdge: tEdge
1927
+ });
1928
+ this.edgeRenderer._resetTransform();
1929
+ }
1724
1930
  if (this.boxSelecting) {
1725
1931
  const { startX, startY, currentX, currentY } = this.boxSelecting;
1726
1932
  const minX = Math.min(startX, currentX);
@@ -1729,14 +1935,28 @@ const _Controller = class _Controller {
1729
1935
  const height = Math.abs(currentY - startY);
1730
1936
  const screenStart = this.renderer.worldToScreen(minX, minY);
1731
1937
  const screenEnd = this.renderer.worldToScreen(minX + width, minY + height);
1732
- const ctx = this.renderer.ctx;
1938
+ const ctx = this.edgeRenderer ? this.edgeRenderer.ctx : this.renderer.ctx;
1733
1939
  ctx.save();
1734
- this.renderer._resetTransform();
1940
+ if (this.edgeRenderer) {
1941
+ this.edgeRenderer._resetTransform();
1942
+ } else {
1943
+ this.renderer._resetTransform();
1944
+ }
1735
1945
  ctx.strokeStyle = "#6cf";
1736
1946
  ctx.fillStyle = "rgba(102, 204, 255, 0.1)";
1737
1947
  ctx.lineWidth = 2;
1738
- ctx.strokeRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
1739
- ctx.fillRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
1948
+ ctx.strokeRect(
1949
+ screenStart.x,
1950
+ screenStart.y,
1951
+ screenEnd.x - screenStart.x,
1952
+ screenEnd.y - screenStart.y
1953
+ );
1954
+ ctx.fillRect(
1955
+ screenStart.x,
1956
+ screenStart.y,
1957
+ screenEnd.x - screenStart.x,
1958
+ screenEnd.y - screenStart.y
1959
+ );
1740
1960
  ctx.restore();
1741
1961
  }
1742
1962
  if (this.portRenderer) {
@@ -1748,11 +1968,7 @@ const _Controller = class _Controller {
1748
1968
  this.portRenderer._applyTransform();
1749
1969
  for (const n of this.graph.nodes.values()) {
1750
1970
  if (n.type !== "core/Group") {
1751
- const def = (_c = (_b = this.portRenderer.registry) == null ? void 0 : _b.types) == null ? void 0 : _c.get(n.type);
1752
- const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
1753
- if (hasHtmlOverlay) {
1754
- this.portRenderer._drawPorts(n);
1755
- }
1971
+ this.portRenderer._drawPorts(n);
1756
1972
  }
1757
1973
  }
1758
1974
  this.portRenderer._resetTransform();
@@ -1760,10 +1976,7 @@ const _Controller = class _Controller {
1760
1976
  }
1761
1977
  renderTempEdge() {
1762
1978
  if (!this.connecting) return null;
1763
- const a = this._portAnchorScreen(
1764
- this.connecting.fromNode,
1765
- this.connecting.fromPort
1766
- );
1979
+ const a = this._portAnchorScreen(this.connecting.fromNode, this.connecting.fromPort);
1767
1980
  return {
1768
1981
  x1: a.x,
1769
1982
  y1: a.y,
@@ -1775,7 +1988,7 @@ const _Controller = class _Controller {
1775
1988
  const n = this.graph.nodes.get(nodeId);
1776
1989
  const iOut = n.outputs.findIndex((p) => p.id === portId);
1777
1990
  const r = portRect(n, null, iOut, "out");
1778
- return this.renderer.worldToScreen(r.x, r.y + 7);
1991
+ return this.renderer.worldToScreen(r.x + r.w / 2, r.y + r.h / 2);
1779
1992
  }
1780
1993
  };
1781
1994
  __publicField(_Controller, "MIN_NODE_WIDTH", 80);
@@ -1964,7 +2177,8 @@ class ContextMenu {
1964
2177
  itemEl._hideTimeout = null;
1965
2178
  }
1966
2179
  if (item.submenu) {
1967
- this._showSubmenu(item.submenu, itemEl);
2180
+ const submenuItems = typeof item.submenu === "function" ? item.submenu() : item.submenu;
2181
+ this._showSubmenu(submenuItems, itemEl);
1968
2182
  }
1969
2183
  });
1970
2184
  itemEl.addEventListener("mouseleave", (e) => {
@@ -2104,11 +2318,9 @@ class Runner {
2104
2318
  this._last = 0;
2105
2319
  this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
2106
2320
  }
2107
- // 외부에서 실행 중인지 확인
2108
2321
  isRunning() {
2109
2322
  return this.running;
2110
2323
  }
2111
- // 실행 도중에도 CPS 변경 가능
2112
2324
  setCyclesPerFrame(n) {
2113
2325
  this.cyclesPerFrame = Math.max(1, n | 0);
2114
2326
  }
@@ -2141,24 +2353,22 @@ class Runner {
2141
2353
  }
2142
2354
  }
2143
2355
  /**
2144
- * Execute connected nodes once from a starting node
2145
- * @param {string} startNodeId - ID of the node to start from
2146
- * @param {number} dt - Delta time
2356
+ * Execute connected nodes once from a starting node.
2357
+ * Returns execEdgeOrder: exec edges in the order they were traversed.
2147
2358
  */
2148
2359
  runOnce(startNodeId, dt = 0) {
2149
- console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
2150
- const executedNodes = [];
2151
2360
  const allConnectedNodes = /* @__PURE__ */ new Set();
2152
- let currentNodeId = startNodeId;
2153
- while (currentNodeId) {
2361
+ const execEdgeOrder = [];
2362
+ const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
2363
+ const visited = /* @__PURE__ */ new Set();
2364
+ while (queue.length > 0) {
2365
+ const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
2366
+ if (visited.has(currentNodeId)) continue;
2367
+ visited.add(currentNodeId);
2368
+ if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
2154
2369
  const node = this.graph.nodes.get(currentNodeId);
2155
- if (!node) {
2156
- console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
2157
- break;
2158
- }
2159
- executedNodes.push(currentNodeId);
2370
+ if (!node) continue;
2160
2371
  allConnectedNodes.add(currentNodeId);
2161
- console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
2162
2372
  for (const input of node.inputs) {
2163
2373
  if (input.portType === "data") {
2164
2374
  for (const edge of this.graph.edges.values()) {
@@ -2173,40 +2383,38 @@ class Runner {
2173
2383
  }
2174
2384
  }
2175
2385
  this.executeNode(currentNodeId, dt);
2176
- currentNodeId = this.findNextExecNode(currentNodeId);
2386
+ const execOutputs = node.outputs.filter((p) => p.portType === "exec");
2387
+ for (const execOutput of execOutputs) {
2388
+ for (const edge of this.graph.edges.values()) {
2389
+ if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
2390
+ queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
2391
+ }
2392
+ }
2393
+ }
2177
2394
  }
2178
- console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
2179
2395
  const connectedEdges = /* @__PURE__ */ new Set();
2180
2396
  for (const edge of this.graph.edges.values()) {
2181
2397
  if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
2182
2398
  connectedEdges.add(edge.id);
2183
2399
  }
2184
2400
  }
2185
- console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
2186
- return { connectedNodes: allConnectedNodes, connectedEdges };
2401
+ return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
2187
2402
  }
2188
- /**
2189
- * Find the next node to execute by following exec output
2190
- * @param {string} nodeId - Current node ID
2191
- * @returns {string|null} Next node ID or null
2192
- */
2193
- findNextExecNode(nodeId) {
2403
+ findAllNextExecNodes(nodeId) {
2194
2404
  const node = this.graph.nodes.get(nodeId);
2195
- if (!node) return null;
2196
- const execOutput = node.outputs.find((p) => p.portType === "exec");
2197
- if (!execOutput) return null;
2198
- for (const edge of this.graph.edges.values()) {
2199
- if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
2200
- return edge.toNode;
2405
+ if (!node) return [];
2406
+ const execOutputs = node.outputs.filter((p) => p.portType === "exec");
2407
+ if (execOutputs.length === 0) return [];
2408
+ const nextNodes = [];
2409
+ for (const execOutput of execOutputs) {
2410
+ for (const edge of this.graph.edges.values()) {
2411
+ if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
2412
+ nextNodes.push(edge.toNode);
2413
+ }
2201
2414
  }
2202
2415
  }
2203
- return null;
2416
+ return nextNodes;
2204
2417
  }
2205
- /**
2206
- * Execute a single node
2207
- * @param {string} nodeId - Node ID to execute
2208
- * @param {number} dt - Delta time
2209
- */
2210
2418
  executeNode(nodeId, dt) {
2211
2419
  var _a, _b;
2212
2420
  const node = this.graph.nodes.get(nodeId);
@@ -2304,7 +2512,7 @@ class HtmlOverlay {
2304
2512
  const header = document.createElement("div");
2305
2513
  header.className = "node-header";
2306
2514
  Object.assign(header.style, {
2307
- height: "24px",
2515
+ height: "26px",
2308
2516
  flexShrink: "0",
2309
2517
  display: "flex",
2310
2518
  alignItems: "center",
@@ -2331,7 +2539,7 @@ class HtmlOverlay {
2331
2539
  return container;
2332
2540
  }
2333
2541
  /** 노드용 엘리먼트 생성(한 번만) */
2334
- _ensureNodeElement(node, def) {
2542
+ _ensureNodeElement(node, def, graph) {
2335
2543
  var _a;
2336
2544
  let el = this.nodes.get(node.id);
2337
2545
  if (!el) {
@@ -2340,7 +2548,7 @@ class HtmlOverlay {
2340
2548
  } else if (def.html) {
2341
2549
  el = this._createDefaultNodeLayout(node);
2342
2550
  if (def.html.init) {
2343
- def.html.init(node, el, el._domParts);
2551
+ def.html.init(node, el, { ...el._domParts, graph });
2344
2552
  }
2345
2553
  } else {
2346
2554
  return null;
@@ -2363,7 +2571,7 @@ class HtmlOverlay {
2363
2571
  const def = this.registry.types.get(node.type);
2364
2572
  const hasHtml = !!(def == null ? void 0 : def.html);
2365
2573
  if (!hasHtml) continue;
2366
- const el = this._ensureNodeElement(node, def);
2574
+ const el = this._ensureNodeElement(node, def, graph);
2367
2575
  if (!el) continue;
2368
2576
  el.style.left = `${node.computed.x}px`;
2369
2577
  el.style.top = `${node.computed.y}px`;
@@ -2386,6 +2594,15 @@ class HtmlOverlay {
2386
2594
  }
2387
2595
  }
2388
2596
  }
2597
+ /**
2598
+ * Sync container transform with renderer state (lightweight update)
2599
+ * Called when zoom/pan occurs without needing full redraw
2600
+ */
2601
+ syncTransform() {
2602
+ const { scale, offsetX, offsetY } = this.renderer;
2603
+ this.container.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
2604
+ this.container.style.transformOrigin = "0 0";
2605
+ }
2389
2606
  clear() {
2390
2607
  for (const [, el] of this.nodes) {
2391
2608
  el.remove();
@@ -2511,10 +2728,44 @@ class PropertyPanel {
2511
2728
  this.hooks = hooks;
2512
2729
  this.registry = registry;
2513
2730
  this.render = render;
2731
+ this._def = null;
2514
2732
  this.panel = null;
2515
2733
  this.currentNode = null;
2516
2734
  this.isVisible = false;
2735
+ this._selfUpdating = false;
2517
2736
  this._createPanel();
2737
+ this._bindHooks();
2738
+ }
2739
+ _bindHooks() {
2740
+ var _a, _b, _c, _d, _e, _f;
2741
+ (_a = this.hooks) == null ? void 0 : _a.on("edge:create", () => {
2742
+ if (this._canRefresh()) this._renderContent();
2743
+ });
2744
+ (_b = this.hooks) == null ? void 0 : _b.on("edge:delete", () => {
2745
+ if (this._canRefresh()) this._renderContent();
2746
+ });
2747
+ (_c = this.hooks) == null ? void 0 : _c.on("node:updated", (node) => {
2748
+ var _a2;
2749
+ if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id) && !this._selfUpdating) {
2750
+ this._renderContent();
2751
+ }
2752
+ });
2753
+ (_d = this.hooks) == null ? void 0 : _d.on("node:move", (node) => {
2754
+ var _a2;
2755
+ if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id)) {
2756
+ this._updatePositionFields();
2757
+ }
2758
+ });
2759
+ (_e = this.hooks) == null ? void 0 : _e.on("runner:tick", () => {
2760
+ if (this._canRefresh()) this._updateLiveValues();
2761
+ });
2762
+ (_f = this.hooks) == null ? void 0 : _f.on("runner:stop", () => {
2763
+ if (this._canRefresh()) this._updateLiveValues();
2764
+ });
2765
+ }
2766
+ _canRefresh() {
2767
+ if (!this.isVisible || !this.currentNode) return false;
2768
+ return !this.panel.querySelector("[data-field]:focus");
2518
2769
  }
2519
2770
  _createPanel() {
2520
2771
  this.panel = document.createElement("div");
@@ -2544,8 +2795,10 @@ class PropertyPanel {
2544
2795
  });
2545
2796
  }
2546
2797
  open(node) {
2798
+ var _a, _b;
2547
2799
  if (!node) return;
2548
2800
  this.currentNode = node;
2801
+ this._def = ((_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type)) || null;
2549
2802
  this.isVisible = true;
2550
2803
  this._renderContent();
2551
2804
  this.panel.style.display = "block";
@@ -2581,9 +2834,9 @@ class PropertyPanel {
2581
2834
  </div>
2582
2835
  </div>
2583
2836
  </div>
2584
-
2837
+
2585
2838
  <div class="section">
2586
- <div class="section-title">Position & Size</div>
2839
+ <div class="section-title">Position &amp; Size</div>
2587
2840
  <div class="section-body">
2588
2841
  <div class="field-row">
2589
2842
  <div class="field">
@@ -2607,16 +2860,92 @@ class PropertyPanel {
2607
2860
  </div>
2608
2861
  </div>
2609
2862
  </div>
2610
-
2863
+
2864
+ ${this._renderConnections(node)}
2611
2865
  ${this._renderPorts(node)}
2866
+ ${this._renderLiveValues(node)}
2612
2867
  ${this._renderState(node)}
2613
-
2868
+
2614
2869
  <div class="panel-actions">
2615
2870
  <button class="btn-secondary panel-close-btn">Close</button>
2616
2871
  </div>
2617
2872
  `;
2618
2873
  this._attachInputListeners();
2619
2874
  }
2875
+ _renderConnections(node) {
2876
+ const edges = [...this.graph.edges.values()];
2877
+ const incoming = edges.filter((e) => e.toNode === node.id);
2878
+ const outgoing = edges.filter((e) => e.fromNode === node.id);
2879
+ if (!incoming.length && !outgoing.length) return "";
2880
+ const edgeLabel = (e, dir) => {
2881
+ const otherId = dir === "in" ? e.fromNode : e.toNode;
2882
+ const other = this.graph.nodes.get(otherId);
2883
+ return `<div class="port-item">
2884
+ <span class="port-icon data"></span>
2885
+ <span class="port-name" style="font-size:10px;color:#5a5a78;">${(other == null ? void 0 : other.title) ?? otherId}</span>
2886
+ </div>`;
2887
+ };
2888
+ return `
2889
+ <div class="section">
2890
+ <div class="section-title">Connections</div>
2891
+ <div class="section-body">
2892
+ ${incoming.length ? `
2893
+ <div class="port-group">
2894
+ <div class="port-group-title">Incoming (${incoming.length})</div>
2895
+ ${incoming.map((e) => edgeLabel(e, "in")).join("")}
2896
+ </div>` : ""}
2897
+ ${outgoing.length ? `
2898
+ <div class="port-group">
2899
+ <div class="port-group-title">Outgoing (${outgoing.length})</div>
2900
+ ${outgoing.map((e) => edgeLabel(e, "out")).join("")}
2901
+ </div>` : ""}
2902
+ </div>
2903
+ </div>
2904
+ `;
2905
+ }
2906
+ _renderLiveValues(node) {
2907
+ var _a, _b;
2908
+ const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
2909
+ if (!cur) return "";
2910
+ const lines = [];
2911
+ for (const input of node.inputs) {
2912
+ `${node.id}:${input.id}`;
2913
+ for (const edge of this.graph.edges.values()) {
2914
+ if (edge.toNode === node.id && edge.toPort === input.id) {
2915
+ const upKey = `${edge.fromNode}:${edge.fromPort}`;
2916
+ const val = cur.get(upKey);
2917
+ if (val !== void 0) {
2918
+ lines.push(`<div class="port-item">
2919
+ <span class="port-icon data"></span>
2920
+ <span class="port-name">↳ ${input.name}</span>
2921
+ <span class="port-type" style="color:var(--color-primary);background:rgba(99,102,241,0.1);">${JSON.stringify(val)}</span>
2922
+ </div>`);
2923
+ }
2924
+ break;
2925
+ }
2926
+ }
2927
+ }
2928
+ for (const output of node.outputs) {
2929
+ const key = `${node.id}:${output.id}`;
2930
+ const val = cur.get(key);
2931
+ if (val !== void 0) {
2932
+ lines.push(`<div class="port-item">
2933
+ <span class="port-icon exec" style="background:#10b981;"></span>
2934
+ <span class="port-name">↳ ${output.name}</span>
2935
+ <span class="port-type" style="color:#10b981;background:rgba(16,185,129,0.1);">${JSON.stringify(val)}</span>
2936
+ </div>`);
2937
+ }
2938
+ }
2939
+ if (!lines.length) return "";
2940
+ return `
2941
+ <div class="section">
2942
+ <div class="section-title">Live Values</div>
2943
+ <div class="section-body">
2944
+ ${lines.join("")}
2945
+ </div>
2946
+ </div>
2947
+ `;
2948
+ }
2620
2949
  _renderPorts(node) {
2621
2950
  if (!node.inputs.length && !node.outputs.length) return "";
2622
2951
  return `
@@ -2635,7 +2964,6 @@ class PropertyPanel {
2635
2964
  `).join("")}
2636
2965
  </div>
2637
2966
  ` : ""}
2638
-
2639
2967
  ${node.outputs.length ? `
2640
2968
  <div class="port-group">
2641
2969
  <div class="port-group-title">Outputs (${node.outputs.length})</div>
@@ -2653,38 +2981,56 @@ class PropertyPanel {
2653
2981
  `;
2654
2982
  }
2655
2983
  _renderState(node) {
2656
- if (!node.state || Object.keys(node.state).length === 0) return "";
2984
+ if (!node.state) return "";
2985
+ const entries = Object.entries(node.state).filter(([key, value]) => {
2986
+ if (key.startsWith("_")) return false;
2987
+ const t = typeof value;
2988
+ return t === "string" || t === "number" || t === "boolean";
2989
+ });
2990
+ if (!entries.length) return "";
2991
+ const fieldHtml = ([key, value]) => {
2992
+ if (typeof value === "boolean") {
2993
+ return `
2994
+ <div class="field">
2995
+ <label>${key}</label>
2996
+ <select data-field="state.${key}">
2997
+ <option value="true"${value ? " selected" : ""}>true</option>
2998
+ <option value="false"${!value ? " selected" : ""}>false</option>
2999
+ </select>
3000
+ </div>`;
3001
+ }
3002
+ return `
3003
+ <div class="field">
3004
+ <label>${key}</label>
3005
+ <input type="${typeof value === "number" ? "number" : "text"}"
3006
+ data-field="state.${key}"
3007
+ value="${value}" />
3008
+ </div>`;
3009
+ };
2657
3010
  return `
2658
3011
  <div class="section">
2659
3012
  <div class="section-title">State</div>
2660
3013
  <div class="section-body">
2661
- ${Object.entries(node.state).map(([key, value]) => `
2662
- <div class="field">
2663
- <label>${key}</label>
2664
- <input
2665
- type="${typeof value === "number" ? "number" : "text"}"
2666
- data-field="state.${key}"
2667
- value="${value}"
2668
- />
2669
- </div>
2670
- `).join("")}
3014
+ ${entries.map(fieldHtml).join("")}
2671
3015
  </div>
2672
3016
  </div>
2673
3017
  `;
2674
3018
  }
2675
3019
  _attachInputListeners() {
2676
- const inputs = this.panel.querySelectorAll("[data-field]");
2677
- inputs.forEach((input) => {
3020
+ var _a;
3021
+ this.panel.querySelectorAll("[data-field]").forEach((input) => {
2678
3022
  input.addEventListener("change", () => {
3023
+ this._selfUpdating = true;
2679
3024
  this._handleFieldChange(input.dataset.field, input.value);
3025
+ this._selfUpdating = false;
2680
3026
  });
2681
3027
  });
2682
- this.panel.querySelector(".panel-close-btn").addEventListener("click", () => {
3028
+ (_a = this.panel.querySelector(".panel-close-btn")) == null ? void 0 : _a.addEventListener("click", () => {
2683
3029
  this.close();
2684
3030
  });
2685
3031
  }
2686
3032
  _handleFieldChange(field, value) {
2687
- var _a;
3033
+ var _a, _b;
2688
3034
  const node = this.currentNode;
2689
3035
  if (!node) return;
2690
3036
  switch (field) {
@@ -2708,31 +3054,277 @@ class PropertyPanel {
2708
3054
  default:
2709
3055
  if (field.startsWith("state.")) {
2710
3056
  const key = field.substring(6);
2711
- if (node.state) {
2712
- const originalValue = node.state[key];
2713
- node.state[key] = typeof originalValue === "number" ? parseFloat(value) : value;
3057
+ if (node.state && key in node.state) {
3058
+ const orig = node.state[key];
3059
+ if (typeof orig === "boolean") {
3060
+ node.state[key] = value === "true";
3061
+ } else if (typeof orig === "number") {
3062
+ node.state[key] = parseFloat(value);
3063
+ } else {
3064
+ node.state[key] = value;
3065
+ }
2714
3066
  }
2715
3067
  }
2716
3068
  }
2717
3069
  (_a = this.hooks) == null ? void 0 : _a.emit("node:updated", node);
2718
- if (this.render) {
2719
- this.render();
3070
+ (_b = this.render) == null ? void 0 : _b.call(this);
3071
+ }
3072
+ /** Lightweight update of position fields only (no full re-render) */
3073
+ _updatePositionFields() {
3074
+ const node = this.currentNode;
3075
+ if (!node) return;
3076
+ const xEl = this.panel.querySelector('[data-field="x"]');
3077
+ const yEl = this.panel.querySelector('[data-field="y"]');
3078
+ if (xEl) xEl.value = Math.round(node.computed.x);
3079
+ if (yEl) yEl.value = Math.round(node.computed.y);
3080
+ }
3081
+ /** Lightweight in-place update of the Live Values section */
3082
+ _updateLiveValues() {
3083
+ var _a, _b;
3084
+ const node = this.currentNode;
3085
+ if (!node) return;
3086
+ const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
3087
+ if (!cur) return;
3088
+ let section = this.panel.querySelector(".live-values-section");
3089
+ const newHtml = this._renderLiveValues(node);
3090
+ if (!newHtml) {
3091
+ if (section) section.remove();
3092
+ return;
3093
+ }
3094
+ const wrapper = document.createElement("div");
3095
+ wrapper.innerHTML = newHtml;
3096
+ const newSection = wrapper.firstElementChild;
3097
+ newSection.classList.add("live-values-section");
3098
+ if (section) {
3099
+ section.replaceWith(newSection);
3100
+ } else {
3101
+ this.panel.querySelectorAll(".section");
3102
+ const actions = this.panel.querySelector(".panel-actions");
3103
+ if (actions) {
3104
+ actions.before(newSection);
3105
+ } else {
3106
+ this.panel.querySelector(".panel-content").appendChild(newSection);
3107
+ }
2720
3108
  }
2721
3109
  }
2722
3110
  destroy() {
2723
- if (this.panel) {
2724
- this.panel.remove();
3111
+ var _a;
3112
+ (_a = this.panel) == null ? void 0 : _a.remove();
3113
+ }
3114
+ }
3115
+ class HelpOverlay {
3116
+ constructor(container, options = {}) {
3117
+ this.container = container;
3118
+ this.options = {
3119
+ shortcuts: options.shortcuts || this._getDefaultShortcuts(),
3120
+ onToggle: options.onToggle || null
3121
+ };
3122
+ this.isVisible = false;
3123
+ this.overlay = null;
3124
+ this.toggleBtn = null;
3125
+ this._createElements();
3126
+ this._bindEvents();
3127
+ }
3128
+ _getDefaultShortcuts() {
3129
+ return [
3130
+ {
3131
+ group: "Selection",
3132
+ items: [
3133
+ { label: "Select node", key: "Click" },
3134
+ { label: "Multi-select", key: "Shift+Click" },
3135
+ { label: "Box select", key: "Ctrl+Drag" }
3136
+ ]
3137
+ },
3138
+ {
3139
+ group: "Edit",
3140
+ items: [
3141
+ { label: "Delete", key: "Del" },
3142
+ { label: "Undo", key: "Ctrl+Z" },
3143
+ { label: "Redo", key: "Ctrl+Y" }
3144
+ ]
3145
+ },
3146
+ {
3147
+ group: "Group & Align",
3148
+ items: [
3149
+ { label: "Create group", key: "Ctrl+G" },
3150
+ { label: "Align horizontal", key: "A" },
3151
+ { label: "Align vertical", key: "Shift+A" }
3152
+ ]
3153
+ },
3154
+ {
3155
+ group: "View",
3156
+ items: [
3157
+ { label: "Toggle snap", key: "G" },
3158
+ { label: "Pan", key: "Mid+Drag" },
3159
+ { label: "Zoom", key: "Scroll" },
3160
+ { label: "Context menu", key: "RClick" }
3161
+ ]
3162
+ }
3163
+ ];
3164
+ }
3165
+ _createElements() {
3166
+ this.toggleBtn = document.createElement("div");
3167
+ this.toggleBtn.id = "helpToggle";
3168
+ this.toggleBtn.title = "Keyboard shortcuts (?)";
3169
+ this.toggleBtn.textContent = "?";
3170
+ this.container.appendChild(this.toggleBtn);
3171
+ this.overlay = document.createElement("div");
3172
+ this.overlay.id = "helpOverlay";
3173
+ const sectionsHtml = this.options.shortcuts.map(
3174
+ (group) => `
3175
+ <h4>${group.group}</h4>
3176
+ ${group.items.map(
3177
+ (item) => `
3178
+ <div class="shortcut-item">
3179
+ <span>${item.label}</span>
3180
+ <span class="shortcut-key">${item.key}</span>
3181
+ </div>
3182
+ `
3183
+ ).join("")}
3184
+ `
3185
+ ).join("");
3186
+ this.overlay.innerHTML = `
3187
+ <h3>
3188
+ <span>Keyboard Shortcuts</span>
3189
+ <button class="close-btn" id="helpClose" title="Close">×</button>
3190
+ </h3>
3191
+ ${sectionsHtml}
3192
+ `;
3193
+ this.container.appendChild(this.overlay);
3194
+ }
3195
+ _bindEvents() {
3196
+ this.toggleBtn.addEventListener("click", () => this.toggle());
3197
+ const closeBtn = this.overlay.querySelector("#helpClose");
3198
+ if (closeBtn) {
3199
+ closeBtn.addEventListener("click", (e) => {
3200
+ e.stopPropagation();
3201
+ this.close();
3202
+ });
2725
3203
  }
3204
+ document.addEventListener("mousedown", (e) => {
3205
+ if (this.isVisible) {
3206
+ if (!this.overlay.contains(e.target) && !this.toggleBtn.contains(e.target)) {
3207
+ this.close();
3208
+ }
3209
+ }
3210
+ });
3211
+ window.addEventListener("keydown", (e) => {
3212
+ if (e.key === "?" || e.shiftKey && e.key === "/") {
3213
+ if (!["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
3214
+ e.preventDefault();
3215
+ this.toggle();
3216
+ }
3217
+ }
3218
+ if (e.key === "Escape" && this.isVisible) {
3219
+ this.close();
3220
+ }
3221
+ });
3222
+ }
3223
+ toggle() {
3224
+ if (this.isVisible) this.close();
3225
+ else this.open();
3226
+ }
3227
+ open() {
3228
+ this.isVisible = true;
3229
+ this.overlay.classList.add("visible");
3230
+ this.toggleBtn.classList.add("active");
3231
+ if (this.options.onToggle) this.options.onToggle(true);
3232
+ }
3233
+ close() {
3234
+ this.isVisible = false;
3235
+ this.overlay.classList.remove("visible");
3236
+ this.toggleBtn.classList.remove("active");
3237
+ if (this.options.onToggle) this.options.onToggle(false);
3238
+ }
3239
+ destroy() {
3240
+ var _a, _b;
3241
+ (_a = this.toggleBtn) == null ? void 0 : _a.remove();
3242
+ (_b = this.overlay) == null ? void 0 : _b.remove();
2726
3243
  }
2727
3244
  }
3245
+ function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
3246
+ const getNodeTypes = () => {
3247
+ const nodeTypes = [];
3248
+ for (const [key, typeDef] of graph.registry.types.entries()) {
3249
+ nodeTypes.push({
3250
+ id: `add-${key}`,
3251
+ label: typeDef.title || key,
3252
+ action: () => {
3253
+ const worldPos = contextMenu.worldPosition || { x: 100, y: 100 };
3254
+ const node = graph.addNode(key, {
3255
+ x: worldPos.x,
3256
+ y: worldPos.y
3257
+ });
3258
+ hooks == null ? void 0 : hooks.emit("node:updated", node);
3259
+ controller.render();
3260
+ }
3261
+ });
3262
+ }
3263
+ return nodeTypes;
3264
+ };
3265
+ contextMenu.addItem("add-node", "Add Node", {
3266
+ condition: (target) => !target,
3267
+ submenu: getNodeTypes,
3268
+ // Pass function instead of array
3269
+ order: 5
3270
+ });
3271
+ contextMenu.addItem("delete-node", "Delete Node", {
3272
+ condition: (target) => target && target.type !== "core/Group",
3273
+ action: (target) => {
3274
+ const cmd = RemoveNodeCmd(graph, target);
3275
+ controller.stack.exec(cmd);
3276
+ hooks == null ? void 0 : hooks.emit("node:updated", target);
3277
+ },
3278
+ order: 10
3279
+ });
3280
+ const colors = [
3281
+ { name: "Default", color: "#39424e" },
3282
+ { name: "Slate", color: "#4a5568" },
3283
+ { name: "Gray", color: "#2d3748" },
3284
+ { name: "Blue", color: "#1a365d" },
3285
+ { name: "Green", color: "#22543d" },
3286
+ { name: "Red", color: "#742a2a" },
3287
+ { name: "Purple", color: "#44337a" }
3288
+ ];
3289
+ contextMenu.addItem("change-group-color", "Change Color", {
3290
+ condition: (target) => target && target.type === "core/Group",
3291
+ submenu: colors.map((colorInfo) => ({
3292
+ id: `color-${colorInfo.color}`,
3293
+ label: colorInfo.name,
3294
+ color: colorInfo.color,
3295
+ action: (target) => {
3296
+ const currentColor = target.state.color || "#39424e";
3297
+ const cmd = ChangeGroupColorCmd(target, currentColor, colorInfo.color);
3298
+ controller.stack.exec(cmd);
3299
+ hooks == null ? void 0 : hooks.emit("node:updated", target);
3300
+ }
3301
+ })),
3302
+ order: 20
3303
+ });
3304
+ contextMenu.addItem("delete-group", "Delete Group", {
3305
+ condition: (target) => target && target.type === "core/Group",
3306
+ action: (target) => {
3307
+ const cmd = RemoveNodeCmd(graph, target);
3308
+ controller.stack.exec(cmd);
3309
+ hooks == null ? void 0 : hooks.emit("node:updated", target);
3310
+ },
3311
+ order: 20
3312
+ });
3313
+ }
2728
3314
  function createGraphEditor(target, {
2729
3315
  theme,
2730
3316
  hooks: customHooks,
2731
3317
  autorun = true,
2732
3318
  showMinimap = true,
2733
3319
  enablePropertyPanel = true,
2734
- propertyPanelContainer = null
3320
+ propertyPanelContainer = null,
3321
+ enableHelp = true,
3322
+ helpShortcuts = null,
3323
+ setupDefaultContextMenu: setupDefaultContextMenu$1 = true,
3324
+ setupContextMenu = null,
3325
+ plugins = []
2735
3326
  } = {}) {
3327
+ var _a;
2736
3328
  let canvas;
2737
3329
  let container;
2738
3330
  if (typeof target === "string") {
@@ -2780,6 +3372,46 @@ function createGraphEditor(target, {
2780
3372
  const graph = new Graph({ hooks, registry });
2781
3373
  const renderer = new CanvasRenderer(canvas, { theme, registry });
2782
3374
  const htmlOverlay = new HtmlOverlay(canvas.parentElement, renderer, registry);
3375
+ renderer.setTransformChangeCallback(() => {
3376
+ htmlOverlay.syncTransform();
3377
+ });
3378
+ const edgeCanvas = document.createElement("canvas");
3379
+ edgeCanvas.id = "edge-canvas";
3380
+ Object.assign(edgeCanvas.style, {
3381
+ position: "absolute",
3382
+ top: "0",
3383
+ left: "0",
3384
+ pointerEvents: "none",
3385
+ // Pass through clicks
3386
+ zIndex: "15"
3387
+ // Above HTML overlay (10), below port canvas (20)
3388
+ });
3389
+ canvas.parentElement.appendChild(edgeCanvas);
3390
+ const edgeRenderer = new CanvasRenderer(edgeCanvas, { theme, registry });
3391
+ Object.defineProperty(edgeRenderer, "scale", {
3392
+ get() {
3393
+ return renderer.scale;
3394
+ },
3395
+ set(v) {
3396
+ renderer.scale = v;
3397
+ }
3398
+ });
3399
+ Object.defineProperty(edgeRenderer, "offsetX", {
3400
+ get() {
3401
+ return renderer.offsetX;
3402
+ },
3403
+ set(v) {
3404
+ renderer.offsetX = v;
3405
+ }
3406
+ });
3407
+ Object.defineProperty(edgeRenderer, "offsetY", {
3408
+ get() {
3409
+ return renderer.offsetY;
3410
+ },
3411
+ set(v) {
3412
+ renderer.offsetY = v;
3413
+ }
3414
+ });
2783
3415
  const portCanvas = document.createElement("canvas");
2784
3416
  portCanvas.id = "port-canvas";
2785
3417
  Object.assign(portCanvas.style, {
@@ -2789,7 +3421,7 @@ function createGraphEditor(target, {
2789
3421
  pointerEvents: "none",
2790
3422
  // Pass through clicks
2791
3423
  zIndex: "20"
2792
- // Above HTML overlay (z-index 10)
3424
+ // Above edge canvas (15)
2793
3425
  });
2794
3426
  canvas.parentElement.appendChild(portCanvas);
2795
3427
  const portRenderer = new CanvasRenderer(portCanvas, { theme, registry });
@@ -2797,7 +3429,7 @@ function createGraphEditor(target, {
2797
3429
  portRenderer.scale = renderer.scale;
2798
3430
  portRenderer.offsetX = renderer.offsetX;
2799
3431
  portRenderer.offsetY = renderer.offsetY;
2800
- const controller = new Controller({ graph, renderer, hooks, htmlOverlay, portRenderer });
3432
+ const controller = new Controller({ graph, renderer, hooks, htmlOverlay, edgeRenderer, portRenderer });
2801
3433
  const contextMenu = new ContextMenu({
2802
3434
  graph,
2803
3435
  hooks,
@@ -2821,7 +3453,15 @@ function createGraphEditor(target, {
2821
3453
  propertyPanel.open(node);
2822
3454
  });
2823
3455
  }
3456
+ let helpOverlay = null;
3457
+ if (enableHelp) {
3458
+ helpOverlay = new HelpOverlay(container, {
3459
+ shortcuts: helpShortcuts
3460
+ });
3461
+ }
2824
3462
  const runner = new Runner({ graph, registry, hooks });
3463
+ graph.runner = runner;
3464
+ graph.controller = controller;
2825
3465
  hooks.on("runner:tick", ({ time, dt }) => {
2826
3466
  renderer.draw(graph, {
2827
3467
  selection: controller.selection,
@@ -2856,695 +3496,37 @@ function createGraphEditor(target, {
2856
3496
  hooks.on("node:updated", () => {
2857
3497
  controller.render();
2858
3498
  });
2859
- registry.register("core/Note", {
2860
- title: "Note",
2861
- size: { w: 180, h: 80 },
2862
- inputs: [{ name: "in", datatype: "any" }],
2863
- outputs: [{ name: "out", datatype: "any" }],
2864
- onCreate(node) {
2865
- node.state.text = "hello";
2866
- },
2867
- onExecute(node, { dt, getInput, setOutput }) {
2868
- const incoming = getInput("in");
2869
- const out = (incoming ?? node.state.text ?? "").toString().toUpperCase();
2870
- setOutput(
2871
- "out",
2872
- out + ` · ${Math.floor(performance.now() / 1e3 % 100)}`
2873
- );
2874
- },
2875
- onDraw(node, { ctx, theme: theme2 }) {
2876
- const { x, y } = node.pos;
2877
- const { width: w } = node.size;
2878
- }
3499
+ hooks.on("graph:deserialize", () => {
3500
+ renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
3501
+ controller.render();
2879
3502
  });
2880
- registry.register("core/HtmlNote", {
2881
- title: "HTML Note",
2882
- size: { w: 200, h: 150 },
2883
- inputs: [{ name: "in", datatype: "any" }],
2884
- outputs: [{ name: "out", datatype: "any" }],
2885
- // HTML Overlay Configuration
2886
- html: {
2887
- // 초기화: 헤더/바디 구성
2888
- init(node, el, { header, body }) {
2889
- el.style.backgroundColor = "#222";
2890
- el.style.borderRadius = "8px";
2891
- el.style.border = "1px solid #444";
2892
- el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
2893
- header.style.backgroundColor = "#333";
2894
- header.style.borderBottom = "1px solid #444";
2895
- header.style.color = "#eee";
2896
- header.style.fontSize = "12px";
2897
- header.style.fontWeight = "bold";
2898
- header.textContent = "My HTML Node";
2899
- body.style.padding = "8px";
2900
- body.style.color = "#ccc";
2901
- body.style.fontSize = "12px";
2902
- const contentDiv = document.createElement("div");
2903
- contentDiv.textContent = "Event Name";
2904
- body.appendChild(contentDiv);
2905
- const input = document.createElement("input");
2906
- Object.assign(input.style, {
2907
- marginTop: "4px",
2908
- padding: "4px",
2909
- background: "#111",
2910
- border: "1px solid #555",
2911
- color: "#fff",
2912
- borderRadius: "4px",
2913
- pointerEvents: "auto"
2914
- });
2915
- input.placeholder = "Type here...";
2916
- input.addEventListener("input", (e) => {
2917
- node.state.text = e.target.value;
2918
- });
2919
- input.addEventListener("mousedown", (e) => e.stopPropagation());
2920
- body.appendChild(input);
2921
- el._input = input;
2922
- },
2923
- // 매 프레임(또는 필요시) 업데이트
2924
- update(node, el, { header, _body, selected }) {
2925
- el.style.borderColor = selected ? "#6cf" : "#444";
2926
- header.style.backgroundColor = selected ? "#3a4a5a" : "#333";
2927
- if (el._input.value !== (node.state.text || "")) {
2928
- el._input.value = node.state.text || "";
3503
+ if (setupDefaultContextMenu$1) {
3504
+ setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3505
+ }
3506
+ if (setupContextMenu) {
3507
+ setupContextMenu(contextMenu, { controller, graph, hooks });
3508
+ }
3509
+ if (plugins && plugins.length > 0) {
3510
+ for (const plugin of plugins) {
3511
+ if (typeof plugin.install === "function") {
3512
+ try {
3513
+ plugin.install({ graph, registry, hooks, runner, controller, contextMenu }, plugin.options || {});
3514
+ } catch (err) {
3515
+ console.error(`[createGraphEditor] Failed to install plugin "${plugin.name || "unknown"}":`, err);
3516
+ (_a = hooks == null ? void 0 : hooks.emit) == null ? void 0 : _a.call(hooks, "error", err);
2929
3517
  }
3518
+ } else {
3519
+ console.warn(`[createGraphEditor] Plugin "${plugin.name || "unknown"}" does not have an install() method`);
2930
3520
  }
2931
- },
2932
- onCreate(node) {
2933
- node.state.text = "";
2934
- },
2935
- onExecute(node, { getInput, setOutput }) {
2936
- const incoming = getInput("in");
2937
- setOutput("out", incoming);
2938
- }
2939
- // onDraw는 생략 가능 (HTML이 덮으니까)
2940
- // 하지만 포트 등은 그려야 할 수도 있음.
2941
- // 현재 구조상 CanvasRenderer가 기본 노드를 그리므로,
2942
- // 투명하게 하거나 겹쳐서 그릴 수 있음.
2943
- });
2944
- registry.register("core/TodoNode", {
2945
- title: "Todo List",
2946
- size: { w: 240, h: 300 },
2947
- inputs: [{ name: "in", datatype: "any" }],
2948
- outputs: [{ name: "out", datatype: "any" }],
2949
- html: {
2950
- init(node, el, { header, body }) {
2951
- el.style.backgroundColor = "#1e1e24";
2952
- el.style.borderRadius = "8px";
2953
- el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)";
2954
- el.style.border = "1px solid #333";
2955
- header.style.backgroundColor = "#2a2a31";
2956
- header.style.padding = "8px";
2957
- header.style.fontWeight = "bold";
2958
- header.style.color = "#e9e9ef";
2959
- header.textContent = node.title;
2960
- body.style.display = "flex";
2961
- body.style.flexDirection = "column";
2962
- body.style.padding = "8px";
2963
- body.style.color = "#e9e9ef";
2964
- const inputRow = document.createElement("div");
2965
- Object.assign(inputRow.style, { display: "flex", gap: "4px", marginBottom: "8px" });
2966
- const input = document.createElement("input");
2967
- Object.assign(input.style, {
2968
- flex: "1",
2969
- padding: "6px",
2970
- borderRadius: "4px",
2971
- border: "1px solid #444",
2972
- background: "#141417",
2973
- color: "#fff",
2974
- pointerEvents: "auto"
2975
- });
2976
- input.placeholder = "Add task...";
2977
- const addBtn = document.createElement("button");
2978
- addBtn.textContent = "+";
2979
- Object.assign(addBtn.style, {
2980
- padding: "0 12px",
2981
- cursor: "pointer",
2982
- background: "#4f5b66",
2983
- color: "#fff",
2984
- border: "none",
2985
- borderRadius: "4px",
2986
- pointerEvents: "auto"
2987
- });
2988
- inputRow.append(input, addBtn);
2989
- const list = document.createElement("ul");
2990
- Object.assign(list.style, {
2991
- listStyle: "none",
2992
- padding: "0",
2993
- margin: "0",
2994
- overflow: "hidden",
2995
- flex: "1"
2996
- });
2997
- body.append(inputRow, list);
2998
- const addTodo = () => {
2999
- const text = input.value.trim();
3000
- if (!text) return;
3001
- const todos = node.state.todos || [];
3002
- node.state.todos = [...todos, { id: Date.now(), text, done: false }];
3003
- input.value = "";
3004
- hooks.emit("node:updated", node);
3005
- };
3006
- addBtn.onclick = addTodo;
3007
- input.onkeydown = (e) => {
3008
- if (e.key === "Enter") addTodo();
3009
- e.stopPropagation();
3010
- };
3011
- input.onmousedown = (e) => e.stopPropagation();
3012
- el._refs = { list };
3013
- },
3014
- update(node, el, { selected }) {
3015
- el.style.borderColor = selected ? "#6cf" : "#333";
3016
- const { list } = el._refs;
3017
- const todos = node.state.todos || [];
3018
- list.innerHTML = "";
3019
- todos.forEach((todo) => {
3020
- const li = document.createElement("li");
3021
- Object.assign(li.style, {
3022
- display: "flex",
3023
- alignItems: "center",
3024
- padding: "6px 0",
3025
- borderBottom: "1px solid #2a2a31"
3026
- });
3027
- const chk = document.createElement("input");
3028
- chk.type = "checkbox";
3029
- chk.checked = todo.done;
3030
- chk.style.marginRight = "8px";
3031
- chk.style.pointerEvents = "auto";
3032
- chk.onchange = () => {
3033
- todo.done = chk.checked;
3034
- hooks.emit("node:updated", node);
3035
- };
3036
- chk.onmousedown = (e) => e.stopPropagation();
3037
- const span = document.createElement("span");
3038
- span.textContent = todo.text;
3039
- span.style.flex = "1";
3040
- span.style.textDecoration = todo.done ? "line-through" : "none";
3041
- span.style.color = todo.done ? "#777" : "#eee";
3042
- const del = document.createElement("button");
3043
- del.textContent = "×";
3044
- Object.assign(del.style, {
3045
- background: "none",
3046
- border: "none",
3047
- color: "#f44",
3048
- cursor: "pointer",
3049
- fontSize: "16px",
3050
- pointerEvents: "auto"
3051
- });
3052
- del.onclick = () => {
3053
- node.state.todos = node.state.todos.filter((t) => t.id !== todo.id);
3054
- hooks.emit("node:updated", node);
3055
- };
3056
- del.onmousedown = (e) => e.stopPropagation();
3057
- li.append(chk, span, del);
3058
- list.appendChild(li);
3059
- });
3060
- }
3061
- },
3062
- onCreate(node) {
3063
- node.state.todos = [
3064
- { id: 1, text: "Welcome to Free Node", done: false },
3065
- { id: 2, text: "Try adding a task", done: true }
3066
- ];
3067
- }
3068
- });
3069
- registry.register("math/Add", {
3070
- title: "Add",
3071
- size: { w: 140, h: 100 },
3072
- inputs: [
3073
- { name: "exec", portType: "exec" },
3074
- { name: "a", portType: "data", datatype: "number" },
3075
- { name: "b", portType: "data", datatype: "number" }
3076
- ],
3077
- outputs: [
3078
- { name: "exec", portType: "exec" },
3079
- { name: "result", portType: "data", datatype: "number" }
3080
- ],
3081
- onCreate(node) {
3082
- node.state.a = 0;
3083
- node.state.b = 0;
3084
- },
3085
- onExecute(node, { getInput, setOutput }) {
3086
- const a = getInput("a") ?? 0;
3087
- const b = getInput("b") ?? 0;
3088
- const result = a + b;
3089
- console.log("[Add] a:", a, "b:", b, "result:", result);
3090
- setOutput("result", result);
3091
- }
3092
- });
3093
- registry.register("math/Subtract", {
3094
- title: "Subtract",
3095
- size: { w: 140, h: 80 },
3096
- inputs: [
3097
- { name: "a", datatype: "number" },
3098
- { name: "b", datatype: "number" }
3099
- ],
3100
- outputs: [{ name: "result", datatype: "number" }],
3101
- onExecute(node, { getInput, setOutput }) {
3102
- const a = getInput("a") ?? 0;
3103
- const b = getInput("b") ?? 0;
3104
- setOutput("result", a - b);
3105
- }
3106
- });
3107
- registry.register("math/Multiply", {
3108
- title: "Multiply",
3109
- size: { w: 140, h: 100 },
3110
- inputs: [
3111
- { name: "exec", portType: "exec" },
3112
- { name: "a", portType: "data", datatype: "number" },
3113
- { name: "b", portType: "data", datatype: "number" }
3114
- ],
3115
- outputs: [
3116
- { name: "exec", portType: "exec" },
3117
- { name: "result", portType: "data", datatype: "number" }
3118
- ],
3119
- onExecute(node, { getInput, setOutput }) {
3120
- const a = getInput("a") ?? 0;
3121
- const b = getInput("b") ?? 0;
3122
- const result = a * b;
3123
- console.log("[Multiply] a:", a, "b:", b, "result:", result);
3124
- setOutput("result", result);
3125
- }
3126
- });
3127
- registry.register("math/Divide", {
3128
- title: "Divide",
3129
- size: { w: 140, h: 80 },
3130
- inputs: [
3131
- { name: "a", datatype: "number" },
3132
- { name: "b", datatype: "number" }
3133
- ],
3134
- outputs: [{ name: "result", datatype: "number" }],
3135
- onExecute(node, { getInput, setOutput }) {
3136
- const a = getInput("a") ?? 0;
3137
- const b = getInput("b") ?? 1;
3138
- setOutput("result", b !== 0 ? a / b : 0);
3139
- }
3140
- });
3141
- registry.register("logic/AND", {
3142
- title: "AND",
3143
- size: { w: 120, h: 100 },
3144
- inputs: [
3145
- { name: "exec", portType: "exec" },
3146
- { name: "a", portType: "data", datatype: "boolean" },
3147
- { name: "b", portType: "data", datatype: "boolean" }
3148
- ],
3149
- outputs: [
3150
- { name: "exec", portType: "exec" },
3151
- { name: "result", portType: "data", datatype: "boolean" }
3152
- ],
3153
- onExecute(node, { getInput, setOutput }) {
3154
- const a = getInput("a") ?? false;
3155
- const b = getInput("b") ?? false;
3156
- console.log("[AND] Inputs - a:", a, "b:", b);
3157
- const result = a && b;
3158
- console.log("[AND] Result:", result);
3159
- setOutput("result", result);
3160
- }
3161
- });
3162
- registry.register("logic/OR", {
3163
- title: "OR",
3164
- size: { w: 120, h: 80 },
3165
- inputs: [
3166
- { name: "a", datatype: "boolean" },
3167
- { name: "b", datatype: "boolean" }
3168
- ],
3169
- outputs: [{ name: "result", datatype: "boolean" }],
3170
- onExecute(node, { getInput, setOutput }) {
3171
- const a = getInput("a") ?? false;
3172
- const b = getInput("b") ?? false;
3173
- setOutput("result", a || b);
3174
- }
3175
- });
3176
- registry.register("logic/NOT", {
3177
- title: "NOT",
3178
- size: { w: 120, h: 70 },
3179
- inputs: [{ name: "in", datatype: "boolean" }],
3180
- outputs: [{ name: "out", datatype: "boolean" }],
3181
- onExecute(node, { getInput, setOutput }) {
3182
- const val = getInput("in") ?? false;
3183
- setOutput("out", !val);
3184
- }
3185
- });
3186
- registry.register("value/Number", {
3187
- title: "Number",
3188
- size: { w: 140, h: 60 },
3189
- outputs: [{ name: "value", portType: "data", datatype: "number" }],
3190
- onCreate(node) {
3191
- node.state.value = 0;
3192
- },
3193
- onExecute(node, { setOutput }) {
3194
- console.log("[Number] Outputting value:", node.state.value ?? 0);
3195
- setOutput("value", node.state.value ?? 0);
3196
- },
3197
- html: {
3198
- init(node, el, { header, body }) {
3199
- el.style.backgroundColor = "#1e1e24";
3200
- el.style.border = "1px solid #444";
3201
- el.style.borderRadius = "8px";
3202
- header.style.backgroundColor = "#2a2a31";
3203
- header.style.borderBottom = "1px solid #444";
3204
- header.style.color = "#eee";
3205
- header.style.fontSize = "12px";
3206
- header.textContent = "Number";
3207
- body.style.padding = "12px";
3208
- body.style.display = "flex";
3209
- body.style.alignItems = "center";
3210
- body.style.justifyContent = "center";
3211
- const input = document.createElement("input");
3212
- input.type = "number";
3213
- input.value = node.state.value ?? 0;
3214
- Object.assign(input.style, {
3215
- width: "100%",
3216
- padding: "6px",
3217
- background: "#141417",
3218
- border: "1px solid #444",
3219
- borderRadius: "4px",
3220
- color: "#fff",
3221
- fontSize: "14px",
3222
- textAlign: "center",
3223
- pointerEvents: "auto"
3224
- });
3225
- input.addEventListener("change", (e) => {
3226
- node.state.value = parseFloat(e.target.value) || 0;
3227
- });
3228
- input.addEventListener("mousedown", (e) => e.stopPropagation());
3229
- input.addEventListener("keydown", (e) => e.stopPropagation());
3230
- body.appendChild(input);
3231
- },
3232
- update(node, el, { header, body, selected }) {
3233
- el.style.borderColor = selected ? "#6cf" : "#444";
3234
- header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
3235
- }
3236
- },
3237
- onDraw(node, { ctx, theme: theme2 }) {
3238
- const { x, y } = node.computed;
3239
- ctx.fillStyle = "#8f8";
3240
- ctx.font = "14px sans-serif";
3241
- ctx.textAlign = "center";
3242
- ctx.fillText(String(node.state.value ?? 0), x + 70, y + 42);
3243
- }
3244
- });
3245
- registry.register("value/String", {
3246
- title: "String",
3247
- size: { w: 160, h: 60 },
3248
- outputs: [{ name: "value", datatype: "string" }],
3249
- onCreate(node) {
3250
- node.state.value = "Hello";
3251
- },
3252
- onExecute(node, { setOutput }) {
3253
- setOutput("value", node.state.value ?? "");
3254
- },
3255
- onDraw(node, { ctx, theme: theme2 }) {
3256
- const { x, y } = node.computed;
3257
- ctx.fillStyle = "#8f8";
3258
- ctx.font = "12px sans-serif";
3259
- ctx.textAlign = "center";
3260
- const text = String(node.state.value ?? "");
3261
- const displayText = text.length > 15 ? text.substring(0, 15) + "..." : text;
3262
- ctx.fillText(displayText, x + 80, y + 42);
3263
- }
3264
- });
3265
- registry.register("value/Boolean", {
3266
- title: "Boolean",
3267
- size: { w: 140, h: 60 },
3268
- outputs: [{ name: "value", portType: "data", datatype: "boolean" }],
3269
- onCreate(node) {
3270
- node.state.value = true;
3271
- },
3272
- onExecute(node, { setOutput }) {
3273
- console.log("[Boolean] Outputting value:", node.state.value ?? false);
3274
- setOutput("value", node.state.value ?? false);
3275
- },
3276
- onDraw(node, { ctx, theme: theme2 }) {
3277
- const { x, y } = node.computed;
3278
- ctx.fillStyle = node.state.value ? "#8f8" : "#f88";
3279
- ctx.font = "14px sans-serif";
3280
- ctx.textAlign = "center";
3281
- ctx.fillText(String(node.state.value), x + 70, y + 42);
3282
- }
3283
- });
3284
- registry.register("util/Print", {
3285
- title: "Print",
3286
- size: { w: 140, h: 80 },
3287
- inputs: [
3288
- { name: "exec", portType: "exec" },
3289
- { name: "value", portType: "data", datatype: "any" }
3290
- ],
3291
- onCreate(node) {
3292
- node.state.lastValue = null;
3293
- },
3294
- onExecute(node, { getInput }) {
3295
- const val = getInput("value");
3296
- if (val !== node.state.lastValue) {
3297
- console.log("[Print]", val);
3298
- node.state.lastValue = val;
3299
- }
3300
- }
3301
- });
3302
- registry.register("util/Watch", {
3303
- title: "Watch",
3304
- size: { w: 180, h: 110 },
3305
- inputs: [
3306
- { name: "exec", portType: "exec" },
3307
- { name: "value", portType: "data", datatype: "any" }
3308
- ],
3309
- outputs: [
3310
- { name: "exec", portType: "exec" },
3311
- { name: "value", portType: "data", datatype: "any" }
3312
- ],
3313
- onCreate(node) {
3314
- node.state.displayValue = "---";
3315
- },
3316
- onExecute(node, { getInput, setOutput }) {
3317
- const val = getInput("value");
3318
- console.log("[Watch] onExecute called, value:", val);
3319
- node.state.displayValue = String(val ?? "---");
3320
- setOutput("value", val);
3321
- },
3322
- onDraw(node, { ctx, theme: theme2 }) {
3323
- const { x, y } = node.computed;
3324
- ctx.fillStyle = "#fa3";
3325
- ctx.font = "11px monospace";
3326
- ctx.textAlign = "left";
3327
- const text = String(node.state.displayValue ?? "---");
3328
- const displayText = text.length > 20 ? text.substring(0, 20) + "..." : text;
3329
- ctx.fillText(displayText, x + 8, y + 50);
3330
- }
3331
- });
3332
- registry.register("util/Timer", {
3333
- title: "Timer",
3334
- size: { w: 140, h: 60 },
3335
- outputs: [{ name: "time", datatype: "number" }],
3336
- onCreate(node) {
3337
- node.state.startTime = performance.now();
3338
- },
3339
- onExecute(node, { setOutput }) {
3340
- const elapsed = (performance.now() - (node.state.startTime ?? 0)) / 1e3;
3341
- setOutput("time", elapsed.toFixed(2));
3342
- }
3343
- });
3344
- registry.register("util/Trigger", {
3345
- title: "Trigger",
3346
- size: { w: 140, h: 80 },
3347
- outputs: [{ name: "exec", portType: "exec" }],
3348
- // Changed to exec port
3349
- html: {
3350
- init(node, el, { header, body }) {
3351
- el.style.backgroundColor = "#1e1e24";
3352
- el.style.border = "1px solid #444";
3353
- el.style.borderRadius = "8px";
3354
- header.style.backgroundColor = "#2a2a31";
3355
- header.style.borderBottom = "1px solid #444";
3356
- header.style.color = "#eee";
3357
- header.style.fontSize = "12px";
3358
- header.textContent = "Trigger";
3359
- body.style.padding = "12px";
3360
- body.style.display = "flex";
3361
- body.style.alignItems = "center";
3362
- body.style.justifyContent = "center";
3363
- const button = document.createElement("button");
3364
- button.textContent = "Fire!";
3365
- Object.assign(button.style, {
3366
- padding: "8px 16px",
3367
- background: "#4a9eff",
3368
- border: "none",
3369
- borderRadius: "4px",
3370
- color: "#fff",
3371
- fontWeight: "bold",
3372
- cursor: "pointer",
3373
- pointerEvents: "auto",
3374
- transition: "background 0.2s"
3375
- });
3376
- button.addEventListener("mousedown", (e) => {
3377
- e.stopPropagation();
3378
- button.style.background = "#2a7ede";
3379
- });
3380
- button.addEventListener("mouseup", () => {
3381
- button.style.background = "#4a9eff";
3382
- });
3383
- button.addEventListener("click", (e) => {
3384
- e.stopPropagation();
3385
- node.state.triggered = true;
3386
- console.log("[Trigger] Button clicked!");
3387
- if (node.__runnerRef && node.__controllerRef) {
3388
- console.log("[Trigger] Runner and controller found");
3389
- const runner2 = node.__runnerRef;
3390
- const controller2 = node.__controllerRef;
3391
- const graph2 = controller2.graph;
3392
- console.log("[Trigger] Calling runner.runOnce with node.id:", node.id);
3393
- const result = runner2.runOnce(node.id, 0);
3394
- const connectedEdges = result.connectedEdges;
3395
- const startTime = performance.now();
3396
- const animationDuration = 500;
3397
- const animate = () => {
3398
- var _a;
3399
- const elapsed = performance.now() - startTime;
3400
- if (elapsed < animationDuration) {
3401
- controller2.renderer.draw(graph2, {
3402
- selection: controller2.selection,
3403
- tempEdge: null,
3404
- running: true,
3405
- time: performance.now(),
3406
- dt: 0,
3407
- activeEdges: connectedEdges
3408
- // Only animate connected edges
3409
- });
3410
- (_a = controller2.htmlOverlay) == null ? void 0 : _a.draw(graph2, controller2.selection);
3411
- requestAnimationFrame(animate);
3412
- } else {
3413
- controller2.render();
3414
- node.state.triggered = false;
3415
- }
3416
- };
3417
- animate();
3418
- }
3419
- });
3420
- body.appendChild(button);
3421
- },
3422
- update(node, el, { header, body, selected }) {
3423
- el.style.borderColor = selected ? "#6cf" : "#444";
3424
- header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
3425
- }
3426
- },
3427
- onCreate(node) {
3428
- node.state.triggered = false;
3429
- },
3430
- onExecute(node, { setOutput }) {
3431
- console.log("[Trigger] Outputting triggered:", node.state.triggered);
3432
- setOutput("triggered", node.state.triggered);
3433
3521
  }
3434
- });
3435
- registry.register("core/Group", {
3436
- title: "Group",
3437
- size: { w: 240, h: 160 },
3438
- onDraw(node, { ctx, theme: theme2 }) {
3439
- const { x, y, w, h } = node.computed;
3440
- const headerH = 24;
3441
- const color = node.state.color || "#39424e";
3442
- const bgAlpha = 0.5;
3443
- const textColor = theme2.text || "#e9e9ef";
3444
- const rgba = (hex, a) => {
3445
- const c = hex.replace("#", "");
3446
- const n = parseInt(
3447
- c.length === 3 ? c.split("").map((x2) => x2 + x2).join("") : c,
3448
- 16
3449
- );
3450
- const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
3451
- return `rgba(${r},${g},${b},${a})`;
3452
- };
3453
- const roundRect2 = (ctx2, x2, y2, w2, h2, r) => {
3454
- if (w2 < 2 * r) r = w2 / 2;
3455
- if (h2 < 2 * r) r = h2 / 2;
3456
- ctx2.beginPath();
3457
- ctx2.moveTo(x2 + r, y2);
3458
- ctx2.arcTo(x2 + w2, y2, x2 + w2, y2 + h2, r);
3459
- ctx2.arcTo(x2 + w2, y2 + h2, x2, y2 + h2, r);
3460
- ctx2.arcTo(x2, y2 + h2, x2, y2, r);
3461
- ctx2.arcTo(x2, y2, x2 + w2, y2, r);
3462
- ctx2.closePath();
3463
- };
3464
- ctx.fillStyle = rgba(color, bgAlpha);
3465
- roundRect2(ctx, x, y, w, h, 10);
3466
- ctx.fill();
3467
- ctx.fillStyle = rgba(color, 0.3);
3468
- ctx.beginPath();
3469
- ctx.roundRect(x, y, w, headerH, [10, 10, 0, 0]);
3470
- ctx.fill();
3471
- ctx.fillStyle = textColor;
3472
- ctx.font = "600 13px system-ui";
3473
- ctx.textBaseline = "top";
3474
- ctx.fillText(node.title, x + 12, y + 6);
3475
- }
3476
- });
3477
- function setupDefaultContextMenu(contextMenu2, { controller: controller2, graph: graph2, hooks: hooks2 }) {
3478
- const nodeTypes = [];
3479
- for (const [key, typeDef] of graph2.registry.types.entries()) {
3480
- nodeTypes.push({
3481
- id: `add-${key}`,
3482
- label: typeDef.title || key,
3483
- action: () => {
3484
- const worldPos = contextMenu2.worldPosition || { x: 100, y: 100 };
3485
- const node = graph2.addNode(key, {
3486
- x: worldPos.x,
3487
- y: worldPos.y
3488
- });
3489
- hooks2 == null ? void 0 : hooks2.emit("node:updated", node);
3490
- controller2.render();
3491
- }
3492
- });
3493
- }
3494
- contextMenu2.addItem("add-node", "Add Node", {
3495
- condition: (target2) => !target2,
3496
- submenu: nodeTypes,
3497
- order: 5
3498
- });
3499
- contextMenu2.addItem("delete-node", "Delete Node", {
3500
- condition: (target2) => target2 && target2.type !== "core/Group",
3501
- action: (target2) => {
3502
- const cmd = RemoveNodeCmd(graph2, target2);
3503
- controller2.stack.exec(cmd);
3504
- hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3505
- },
3506
- order: 10
3507
- });
3508
- const colors = [
3509
- { name: "Default", color: "#39424e" },
3510
- { name: "Slate", color: "#4a5568" },
3511
- { name: "Gray", color: "#2d3748" },
3512
- { name: "Blue", color: "#1a365d" },
3513
- { name: "Green", color: "#22543d" },
3514
- { name: "Red", color: "#742a2a" },
3515
- { name: "Purple", color: "#44337a" }
3516
- ];
3517
- contextMenu2.addItem("change-group-color", "Change Color", {
3518
- condition: (target2) => target2 && target2.type === "core/Group",
3519
- submenu: colors.map((colorInfo) => ({
3520
- id: `color-${colorInfo.color}`,
3521
- label: colorInfo.name,
3522
- color: colorInfo.color,
3523
- action: (target2) => {
3524
- const currentColor = target2.state.color || "#39424e";
3525
- const cmd = ChangeGroupColorCmd(target2, currentColor, colorInfo.color);
3526
- controller2.stack.exec(cmd);
3527
- hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3528
- }
3529
- })),
3530
- order: 20
3531
- });
3532
- contextMenu2.addItem("delete-group", "Delete Group", {
3533
- condition: (target2) => target2 && target2.type === "core/Group",
3534
- action: (target2) => {
3535
- const cmd = RemoveNodeCmd(graph2, target2);
3536
- controller2.stack.exec(cmd);
3537
- hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3538
- },
3539
- order: 20
3540
- });
3541
3522
  }
3542
- setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3543
3523
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
3524
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3544
3525
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3545
3526
  controller.render();
3546
3527
  const ro = new ResizeObserver(() => {
3547
3528
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
3529
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3548
3530
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3549
3531
  controller.render();
3550
3532
  });
@@ -3589,6 +3571,7 @@ function createGraphEditor(target, {
3589
3571
  contextMenu.destroy();
3590
3572
  if (propertyPanel) propertyPanel.destroy();
3591
3573
  if (minimap) minimap.destroy();
3574
+ if (helpOverlay) helpOverlay.destroy();
3592
3575
  }
3593
3576
  };
3594
3577
  if (autorun) runner.start();