html-overlay-node 0.1.9 → 0.1.11

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.
@@ -342,11 +342,12 @@ class Graph {
342
342
  const available = Array.from(this.registry.types.keys()).join(", ") || "none";
343
343
  throw new Error(`Unknown node type: "${type}". Available types: ${available}`);
344
344
  }
345
+ const height = opts.height || ((_a = def.size) == null ? void 0 : _a.h) || this._calculateDefaultNodeHeight(def);
345
346
  const node = new Node({
346
347
  type,
347
348
  title: def.title,
348
- width: (_a = def.size) == null ? void 0 : _a.w,
349
- height: (_b = def.size) == null ? void 0 : _b.h,
349
+ width: opts.width || ((_b = def.size) == null ? void 0 : _b.w) || 140,
350
+ height,
350
351
  ...opts
351
352
  });
352
353
  for (const i of def.inputs || []) node.addInput(i.name, i.datatype, i.portType || "data");
@@ -488,8 +489,12 @@ class Graph {
488
489
  }
489
490
  fromJSON(json) {
490
491
  var _a, _b, _c;
491
- this.clear();
492
+ this.nodes.clear();
493
+ this.edges.clear();
492
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;
493
498
  const node = new Node({
494
499
  id: nd.id,
495
500
  type: nd.type,
@@ -497,9 +502,8 @@ class Graph {
497
502
  x: nd.x,
498
503
  y: nd.y,
499
504
  width: nd.w,
500
- height: nd.h
505
+ height
501
506
  });
502
- const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(nd.type);
503
507
  if (def == null ? void 0 : def.onCreate) {
504
508
  def.onCreate(node);
505
509
  }
@@ -525,6 +529,21 @@ class Graph {
525
529
  (_c = this.hooks) == null ? void 0 : _c.emit("graph:deserialize", json);
526
530
  return this;
527
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 portSpacing = 20;
539
+ if (def.html) {
540
+ const lastPortBottom = maxPorts > 0 ? 50 + (maxPorts - 1) * portSpacing : 26;
541
+ return Math.max(lastPortBottom + 50, 90);
542
+ }
543
+ const padding = 8;
544
+ let h = headerHeight + padding + maxPorts * portSpacing + padding;
545
+ return Math.max(h, 40);
546
+ }
528
547
  }
529
548
  function portRect(node, port, idx, dir) {
530
549
  const {
@@ -538,8 +557,10 @@ function portRect(node, port, idx, dir) {
538
557
  w: node.size.width,
539
558
  h: node.size.height
540
559
  };
541
- const headerHeight = 28;
542
- const y = ny + headerHeight + 10 + idx * 24;
560
+ const headerHeight = 26;
561
+ const padding = 8;
562
+ const portSpacing = 20;
563
+ const y = ny + headerHeight + padding + idx * portSpacing + portSpacing / 2;
543
564
  const portWidth = 12;
544
565
  const portHeight = 12;
545
566
  if (dir === "in") {
@@ -562,32 +583,21 @@ const _CanvasRenderer = class _CanvasRenderer {
562
583
  this.edgeStyle = edgeStyle;
563
584
  this.theme = Object.assign(
564
585
  {
565
- bg: "#0d0d0f",
566
- // Darker background
567
- grid: "#1a1a1d",
568
- // Subtle grid
569
- node: "#16161a",
570
- // Darker nodes
571
- nodeBorder: "#2a2a2f",
572
- // Subtle border
573
- title: "#1f1f24",
574
- // Darker header
575
- text: "#e4e4e7",
576
- // Softer white
577
- textMuted: "#a1a1aa",
578
- // Muted text
579
- port: "#6366f1",
580
- // Indigo for data ports
586
+ bg: "#0e0e16",
587
+ grid: "#1c1c2c",
588
+ node: "rgba(22, 22, 34, 0.9)",
589
+ nodeBorder: "rgba(255, 255, 255, 0.08)",
590
+ title: "rgba(28, 28, 42, 0.95)",
591
+ text: "#f5f5f7",
592
+ textMuted: "#8e8eaf",
593
+ port: "#4f46e5",
581
594
  portExec: "#10b981",
582
- // Emerald for exec ports
583
- edge: "#52525b",
584
- // Neutral edge color
585
- edgeActive: "#8b5cf6",
586
- // Purple for active
595
+ edge: "rgba(255, 255, 255, 0.12)",
596
+ edgeActive: "#34c38f",
597
+ // green for active edge animation
587
598
  accent: "#6366f1",
588
- // Indigo accent
589
- accentBright: "#818cf8"
590
- // Brighter accent
599
+ accentBright: "#818cf8",
600
+ accentGlow: "rgba(99, 102, 241, 0.25)"
591
601
  },
592
602
  theme
593
603
  );
@@ -609,10 +619,6 @@ const _CanvasRenderer = class _CanvasRenderer {
609
619
  this.offsetY = offsetY;
610
620
  (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
611
621
  }
612
- /**
613
- * Set callback to be called when transform changes (zoom/pan)
614
- * @param {Function} callback - Function to call on transform change
615
- */
616
622
  setTransformChangeCallback(callback) {
617
623
  this._onTransformChange = callback;
618
624
  }
@@ -656,7 +662,7 @@ const _CanvasRenderer = class _CanvasRenderer {
656
662
  this.ctx.setTransform(1, 0, 0, 1, 0, 0);
657
663
  }
658
664
  // ── Drawing ────────────────────────────────────────────────────────────────
659
- _drawArrowhead(x1, y1, x2, y2, size = 10) {
665
+ _drawArrowhead(x1, y1, x2, y2, size = 8) {
660
666
  const { ctx } = this;
661
667
  const s = size / this.scale;
662
668
  const ang = Math.atan2(y2 - y1, x2 - x1);
@@ -667,21 +673,14 @@ const _CanvasRenderer = class _CanvasRenderer {
667
673
  ctx.closePath();
668
674
  ctx.fill();
669
675
  }
670
- _drawScreenText(text, lx, ly, {
671
- fontPx = 12,
672
- color = this.theme.text,
673
- align = "left",
674
- baseline = "alphabetic",
675
- dpr = 1
676
- // 추후 devicePixelRatio 도입
677
- } = {}) {
676
+ _drawScreenText(text, lx, ly, { fontPx = 11, color = this.theme.text, align = "left", baseline = "alphabetic" } = {}) {
678
677
  const { ctx } = this;
679
678
  const { x: sx, y: sy } = this.worldToScreen(lx, ly);
680
679
  ctx.save();
681
680
  this._resetTransform();
682
681
  const px = Math.round(sx) + 0.5;
683
682
  const py = Math.round(sy) + 0.5;
684
- ctx.font = `${fontPx * this.scale}px system-ui`;
683
+ ctx.font = `${fontPx * this.scale}px "Inter", system-ui, sans-serif`;
685
684
  ctx.fillStyle = color;
686
685
  ctx.textAlign = align;
687
686
  ctx.textBaseline = baseline;
@@ -694,39 +693,51 @@ const _CanvasRenderer = class _CanvasRenderer {
694
693
  ctx.fillStyle = theme.bg;
695
694
  ctx.fillRect(0, 0, canvas.width, canvas.height);
696
695
  this._applyTransform();
697
- ctx.strokeStyle = this._rgba(theme.grid, 0.35);
698
- ctx.lineWidth = 1 / scale;
699
- const base = 20;
700
- const step = base;
701
696
  const x0 = -offsetX / scale;
702
697
  const y0 = -offsetY / scale;
703
698
  const x1 = (canvas.width - offsetX) / scale;
704
699
  const y1 = (canvas.height - offsetY) / scale;
705
- const startX = Math.floor(x0 / step) * step;
706
- const startY = Math.floor(y0 / step) * step;
707
- ctx.beginPath();
708
- for (let x = startX; x <= x1; x += step) {
709
- ctx.moveTo(x, y0);
710
- ctx.lineTo(x, y1);
700
+ const minorStep = 24;
701
+ const majorStep = 120;
702
+ const minorR = 1 / scale;
703
+ const majorR = 1.5 / scale;
704
+ const startX = Math.floor(x0 / minorStep) * minorStep;
705
+ const startY = Math.floor(y0 / minorStep) * minorStep;
706
+ ctx.fillStyle = this._rgba(theme.grid, 0.7);
707
+ for (let gx = startX; gx <= x1; gx += minorStep) {
708
+ for (let gy = startY; gy <= y1; gy += minorStep) {
709
+ const isMajorX = Math.round(gx / majorStep) * majorStep === Math.round(gx);
710
+ const isMajorY = Math.round(gy / majorStep) * majorStep === Math.round(gy);
711
+ if (isMajorX && isMajorY) continue;
712
+ ctx.beginPath();
713
+ ctx.arc(gx, gy, minorR, 0, Math.PI * 2);
714
+ ctx.fill();
715
+ }
711
716
  }
712
- for (let y = startY; y <= y1; y += step) {
713
- ctx.moveTo(x0, y);
714
- ctx.lineTo(x1, y);
717
+ const majorStartX = Math.floor(x0 / majorStep) * majorStep;
718
+ const majorStartY = Math.floor(y0 / majorStep) * majorStep;
719
+ ctx.fillStyle = this._rgba(theme.grid, 1);
720
+ for (let gx = majorStartX; gx <= x1; gx += majorStep) {
721
+ for (let gy = majorStartY; gy <= y1; gy += majorStep) {
722
+ ctx.beginPath();
723
+ ctx.arc(gx, gy, majorR, 0, Math.PI * 2);
724
+ ctx.fill();
725
+ }
715
726
  }
716
- ctx.stroke();
717
727
  this._resetTransform();
718
728
  }
719
729
  draw(graph, {
720
730
  selection = /* @__PURE__ */ new Set(),
721
731
  tempEdge = null,
722
- running = false,
723
732
  time = performance.now(),
724
- dt = 0,
725
- groups = null,
733
+ activeNodes = /* @__PURE__ */ new Set(),
734
+ // Now explicitly passing active nodes
726
735
  activeEdges = /* @__PURE__ */ new Set(),
727
- drawEdges = true
736
+ activeEdgeTimes = /* @__PURE__ */ new Map(),
737
+ drawEdges = true,
738
+ loopActiveEdges = false
728
739
  } = {}) {
729
- var _a, _b, _c, _d, _e, _f;
740
+ var _a, _b, _c, _d;
730
741
  graph.updateWorldTransforms();
731
742
  this.drawGrid();
732
743
  const { ctx, theme } = this;
@@ -741,40 +752,48 @@ const _CanvasRenderer = class _CanvasRenderer {
741
752
  }
742
753
  }
743
754
  if (drawEdges) {
744
- ctx.lineWidth = 1.5 / this.scale;
745
- let dashArray = null;
746
- let dashOffset = 0;
747
- if (running) {
748
- const speed = 120;
749
- const phase = time / 1e3 * speed / this.scale % _CanvasRenderer.FONT_SIZE;
750
- dashArray = [6 / this.scale, 6 / this.scale];
751
- dashOffset = -phase;
752
- }
755
+ ctx.lineWidth = 2.5 / this.scale;
753
756
  for (const e of graph.edges.values()) {
754
- const shouldAnimate = activeEdges && activeEdges.size > 0 && activeEdges.has(e.id);
755
- if (running && shouldAnimate && dashArray) {
756
- ctx.setLineDash(dashArray);
757
- ctx.lineDashOffset = dashOffset;
758
- } else {
759
- ctx.setLineDash([]);
760
- ctx.lineDashOffset = 0;
761
- }
762
757
  const isActive = activeEdges && activeEdges.has(e.id);
763
758
  if (isActive) {
764
- ctx.strokeStyle = "#00ffff";
759
+ ctx.save();
760
+ ctx.shadowColor = this.theme.edgeActive;
761
+ ctx.shadowBlur = 8 / this.scale;
762
+ ctx.strokeStyle = this.theme.edgeActive;
765
763
  ctx.lineWidth = 3 / this.scale;
764
+ ctx.setLineDash([]);
765
+ this._drawEdge(graph, e);
766
+ ctx.restore();
767
+ (activeEdgeTimes == null ? void 0 : activeEdgeTimes.get(e.id)) ?? time;
768
+ const flowSpeed = this.theme.flowSpeed || 150;
769
+ edgeLen / flowSpeed * 1e3;
770
+ const dotT = loopActiveEdges ? time / 1e3 * (flowSpeed / edgeLen) % 1 : time / 1e3 * 1.2 % 1;
771
+ const dotPos = this._getEdgeDotPosition(graph, e, dotT);
772
+ if (dotPos) {
773
+ ctx.save();
774
+ ctx.fillStyle = "#ffffff";
775
+ ctx.shadowColor = this.theme.edgeActive;
776
+ ctx.shadowBlur = 10 / this.scale;
777
+ ctx.beginPath();
778
+ ctx.arc(dotPos.x, dotPos.y, 3 / this.scale, 0, Math.PI * 2);
779
+ ctx.fill();
780
+ ctx.restore();
781
+ }
766
782
  } else {
783
+ ctx.setLineDash([]);
767
784
  ctx.strokeStyle = theme.edge;
768
- ctx.lineWidth = 1.5 / this.scale;
785
+ ctx.lineWidth = 2.5 / this.scale;
786
+ this._drawEdge(graph, e);
769
787
  }
770
- this._drawEdge(graph, e);
771
788
  }
772
789
  }
773
790
  if (tempEdge) {
774
791
  const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
775
792
  const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
776
793
  const prevDash = this.ctx.getLineDash();
777
- this.ctx.setLineDash([6 / this.scale, 6 / this.scale]);
794
+ this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
795
+ this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
796
+ this.ctx.lineWidth = 2.5 / this.scale;
778
797
  let ptsForArrow = null;
779
798
  if (this.edgeStyle === "line") {
780
799
  this._drawLine(a.x, a.y, b.x, b.y);
@@ -795,9 +814,8 @@ const _CanvasRenderer = class _CanvasRenderer {
795
814
  if (ptsForArrow && ptsForArrow.length >= 2) {
796
815
  const p1 = ptsForArrow[ptsForArrow.length - 2];
797
816
  const p2 = ptsForArrow[ptsForArrow.length - 1];
798
- this.ctx.fillStyle = this.theme.edge;
799
- this.ctx.strokeStyle = this.theme.edge;
800
- this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
817
+ this.ctx.fillStyle = this.theme.accentBright;
818
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
801
819
  }
802
820
  }
803
821
  for (const n of graph.nodes.values()) {
@@ -805,21 +823,21 @@ const _CanvasRenderer = class _CanvasRenderer {
805
823
  const sel = selection.has(n.id);
806
824
  const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
807
825
  const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
808
- if (!hasHtmlOverlay) {
809
- this._drawNode(n, sel, true);
810
- if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme, renderer: this });
826
+ this._drawNode(n, sel, !hasHtmlOverlay ? true : false);
827
+ if (def == null ? void 0 : def.onDraw) {
828
+ def.onDraw(n, { ctx, theme, renderer: this });
811
829
  }
812
- }
813
- }
814
- for (const n of graph.nodes.values()) {
815
- if (n.type !== "core/Group") {
816
- const def = (_f = (_e = this.registry) == null ? void 0 : _e.types) == null ? void 0 : _f.get(n.type);
817
- const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
818
830
  if (hasHtmlOverlay) {
819
831
  this._drawPorts(n);
820
832
  }
821
833
  }
822
834
  }
835
+ if (activeNodes.size > 0) {
836
+ for (const nodeId of activeNodes) {
837
+ const node = graph.nodes.get(nodeId);
838
+ if (node) this._drawActiveNodeBorder(node, time);
839
+ }
840
+ }
823
841
  this._resetTransform();
824
842
  }
825
843
  _rgba(hex, a) {
@@ -832,40 +850,58 @@ const _CanvasRenderer = class _CanvasRenderer {
832
850
  return `rgba(${r},${g},${b},${a})`;
833
851
  }
834
852
  _drawNode(node, selected, skipPorts = false) {
853
+ var _a, _b;
835
854
  const { ctx, theme } = this;
836
- const r = 8;
855
+ const r = 2;
837
856
  const { x, y, w, h } = node.computed;
838
- if (!selected) {
857
+ const headerH = 26;
858
+ const typeDef = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
859
+ const categoryColor = node.color || (typeDef == null ? void 0 : typeDef.color) || theme.accent;
860
+ if (selected) {
839
861
  ctx.save();
840
- ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
862
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
841
863
  ctx.shadowBlur = 8 / this.scale;
842
- ctx.shadowOffsetY = 2 / this.scale;
843
- ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
844
- roundRect(ctx, x, y, w, h, r);
845
- ctx.fill();
864
+ ctx.strokeStyle = "#ffffff";
865
+ ctx.lineWidth = 1.5 / this.scale;
866
+ const pad = 8 / this.scale;
867
+ roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
868
+ ctx.stroke();
846
869
  ctx.restore();
847
870
  }
871
+ ctx.save();
872
+ ctx.shadowColor = "rgba(0,0,0,0.7)";
873
+ ctx.shadowBlur = 20 / this.scale;
874
+ ctx.shadowOffsetY = 6 / this.scale;
848
875
  ctx.fillStyle = theme.node;
849
- ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
850
- ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
876
+ roundRect(ctx, x, y, w, h, r);
877
+ ctx.fill();
878
+ ctx.restore();
879
+ ctx.fillStyle = theme.node;
880
+ ctx.strokeStyle = selected ? "rgba(255,255,255,0.4)" : theme.nodeBorder;
881
+ ctx.lineWidth = 1 / this.scale;
851
882
  roundRect(ctx, x, y, w, h, r);
852
883
  ctx.fill();
853
884
  ctx.stroke();
854
885
  ctx.fillStyle = theme.title;
855
- roundRect(ctx, x, y, w, 24, { tl: r, tr: r, br: 0, bl: 0 });
886
+ roundRect(ctx, x, y, w, headerH, { tl: r, tr: r, br: 0, bl: 0 });
856
887
  ctx.fill();
857
- ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
858
- ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
888
+ ctx.save();
889
+ ctx.globalCompositeOperation = "source-atop";
890
+ ctx.fillStyle = categoryColor;
891
+ ctx.globalAlpha = 0.25;
892
+ ctx.fillRect(x, y, w, headerH);
893
+ ctx.restore();
894
+ ctx.strokeStyle = selected ? "rgba(255,255,255,0.2)" : this._rgba(theme.nodeBorder, 0.6);
895
+ ctx.lineWidth = 1 / this.scale;
859
896
  ctx.beginPath();
860
- ctx.moveTo(x + r, y);
861
- ctx.lineTo(x + w - r, y);
862
- ctx.quadraticCurveTo(x + w, y, x + w, y + r);
863
- ctx.lineTo(x + w, y + 24);
864
- ctx.moveTo(x, y + 24);
865
- ctx.lineTo(x, y + r);
866
- ctx.quadraticCurveTo(x, y, x + r, y);
897
+ ctx.moveTo(x, y + headerH);
898
+ ctx.lineTo(x + w, y + headerH);
867
899
  ctx.stroke();
868
- this._drawScreenText(node.title, x + 8, y + _CanvasRenderer.FONT_SIZE, {
900
+ ctx.fillStyle = categoryColor;
901
+ ctx.beginPath();
902
+ ctx.roundRect(x, y, w, 2.5 / this.scale, { tl: r, tr: r, br: 0, bl: 0 });
903
+ ctx.fill();
904
+ this._drawScreenText(node.title, x + 10, y + headerH / 2, {
869
905
  fontPx: _CanvasRenderer.FONT_SIZE,
870
906
  color: theme.text,
871
907
  baseline: "middle",
@@ -876,97 +912,165 @@ const _CanvasRenderer = class _CanvasRenderer {
876
912
  const rct = portRect(node, p, i, "in");
877
913
  const cx = rct.x + rct.w / 2;
878
914
  const cy = rct.y + rct.h / 2;
879
- if (p.portType === "exec") {
880
- const portSize = 8;
881
- ctx.fillStyle = theme.portExec;
882
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
883
- ctx.lineWidth = 2 / this.scale;
884
- ctx.beginPath();
885
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
886
- ctx.fill();
887
- ctx.stroke();
888
- } else {
889
- ctx.fillStyle = theme.port;
890
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
891
- ctx.lineWidth = 2 / this.scale;
892
- ctx.beginPath();
893
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
894
- ctx.fill();
895
- ctx.stroke();
915
+ this._drawPortShape(cx, cy, p.portType);
916
+ if (p.name) {
917
+ this._drawScreenText(p.name, cx + 10, cy, {
918
+ fontPx: 10,
919
+ color: theme.textMuted,
920
+ baseline: "middle",
921
+ align: "left"
922
+ });
896
923
  }
897
924
  });
898
925
  node.outputs.forEach((p, i) => {
899
926
  const rct = portRect(node, p, i, "out");
900
927
  const cx = rct.x + rct.w / 2;
901
928
  const cy = rct.y + rct.h / 2;
902
- if (p.portType === "exec") {
903
- const portSize = 8;
904
- ctx.fillStyle = theme.portExec;
905
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
906
- ctx.lineWidth = 2 / this.scale;
907
- ctx.beginPath();
908
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
909
- ctx.fill();
910
- ctx.stroke();
911
- } else {
912
- ctx.fillStyle = theme.port;
913
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
914
- ctx.lineWidth = 2 / this.scale;
915
- ctx.beginPath();
916
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
917
- ctx.fill();
918
- ctx.stroke();
929
+ this._drawPortShape(cx, cy, p.portType);
930
+ if (p.name) {
931
+ this._drawScreenText(p.name, cx - 10, cy, {
932
+ fontPx: 10,
933
+ color: theme.textMuted,
934
+ baseline: "middle",
935
+ align: "right"
936
+ });
919
937
  }
920
938
  });
921
939
  }
922
- _drawPorts(node) {
940
+ _drawActiveNodeBorder(node, time) {
941
+ const { ctx, theme } = this;
942
+ const { x, y, w, h } = node.computed;
943
+ const r = node.radius || 12;
944
+ const speed = 30;
945
+ const dashLen = 6;
946
+ const gapLen = 6;
947
+ ctx.save();
948
+ ctx.shadowColor = theme.accentBright || "#7080d8";
949
+ ctx.shadowBlur = (12 + Math.sin(time / 200) * 4) / this.scale;
950
+ ctx.strokeStyle = theme.accentBright || "#7080d8";
951
+ ctx.lineWidth = 3 / this.scale;
952
+ ctx.setLineDash([dashLen / this.scale, gapLen / this.scale]);
953
+ ctx.lineDashOffset = -(time / 1e3) * speed / this.scale;
954
+ ctx.beginPath();
955
+ const pad = 2 / this.scale;
956
+ this._roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
957
+ ctx.stroke();
958
+ ctx.restore();
959
+ }
960
+ // Internal helper for rounded rectangles if not using the browser's native one
961
+ _roundRect(ctx, x, y, w, h, r) {
962
+ if (typeof r === "number") {
963
+ r = { tl: r, tr: r, br: r, bl: r };
964
+ }
965
+ ctx.beginPath();
966
+ ctx.moveTo(x + r.tl, y);
967
+ ctx.lineTo(x + w - r.tr, y);
968
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr);
969
+ ctx.lineTo(x + w, y + h - r.br);
970
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
971
+ ctx.lineTo(x + r.bl, y + h);
972
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl);
973
+ ctx.lineTo(x, y + r.tl);
974
+ ctx.quadraticCurveTo(x, y, x + r.tl, y);
975
+ ctx.closePath();
976
+ }
977
+ _drawPortShape(cx, cy, portType) {
923
978
  const { ctx, theme } = this;
979
+ if (portType === "exec") {
980
+ const s = 5 / this.scale;
981
+ ctx.save();
982
+ ctx.fillStyle = theme.portExec;
983
+ ctx.strokeStyle = this._rgba(theme.portExec, 0.4);
984
+ ctx.lineWidth = 2 / this.scale;
985
+ ctx.beginPath();
986
+ ctx.moveTo(cx, cy - s);
987
+ ctx.lineTo(cx + s, cy);
988
+ ctx.lineTo(cx, cy + s);
989
+ ctx.lineTo(cx - s, cy);
990
+ ctx.closePath();
991
+ ctx.fill();
992
+ ctx.stroke();
993
+ ctx.restore();
994
+ } else {
995
+ ctx.save();
996
+ ctx.strokeStyle = this._rgba(theme.port, 0.35);
997
+ ctx.lineWidth = 3 / this.scale;
998
+ ctx.beginPath();
999
+ ctx.arc(cx, cy, 5 / this.scale, 0, Math.PI * 2);
1000
+ ctx.stroke();
1001
+ ctx.fillStyle = theme.port;
1002
+ ctx.beginPath();
1003
+ ctx.arc(cx, cy, 3.5 / this.scale, 0, Math.PI * 2);
1004
+ ctx.fill();
1005
+ ctx.restore();
1006
+ }
1007
+ }
1008
+ _drawPorts(node) {
924
1009
  node.inputs.forEach((p, i) => {
925
1010
  const rct = portRect(node, p, i, "in");
926
1011
  const cx = rct.x + rct.w / 2;
927
1012
  const cy = rct.y + rct.h / 2;
928
- if (p.portType === "exec") {
929
- const portSize = 8;
930
- ctx.fillStyle = theme.portExec;
931
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
932
- ctx.lineWidth = 2 / this.scale;
933
- ctx.beginPath();
934
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
935
- ctx.fill();
936
- ctx.stroke();
937
- } else {
938
- ctx.fillStyle = theme.port;
939
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
940
- ctx.lineWidth = 2 / this.scale;
941
- ctx.beginPath();
942
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
943
- ctx.fill();
944
- }
1013
+ this._drawPortShape(cx, cy, p.portType);
945
1014
  });
946
1015
  node.outputs.forEach((p, i) => {
947
1016
  const rct = portRect(node, p, i, "out");
948
1017
  const cx = rct.x + rct.w / 2;
949
1018
  const cy = rct.y + rct.h / 2;
950
- if (p.portType === "exec") {
951
- const portSize = 8;
952
- ctx.fillStyle = theme.portExec;
953
- ctx.strokeStyle = "rgba(16, 185, 129, 0.3)";
954
- ctx.lineWidth = 2 / this.scale;
955
- ctx.beginPath();
956
- ctx.roundRect(cx - portSize / 2, cy - portSize / 2, portSize, portSize, 2);
957
- ctx.fill();
958
- ctx.stroke();
959
- } else {
960
- ctx.fillStyle = theme.port;
961
- ctx.strokeStyle = "rgba(99, 102, 241, 0.3)";
962
- ctx.lineWidth = 2 / this.scale;
963
- ctx.beginPath();
964
- ctx.arc(cx, cy, 5, 0, Math.PI * 2);
965
- ctx.fill();
966
- ctx.stroke();
967
- }
1019
+ this._drawPortShape(cx, cy, p.portType);
968
1020
  });
969
1021
  }
1022
+ /** Selection border for HTML overlay nodes, drawn on the edge canvas */
1023
+ _drawHtmlSelectionBorder(node) {
1024
+ const { ctx } = this;
1025
+ const { x, y, w, h } = node.computed;
1026
+ const r = 2;
1027
+ const pad = 2.5 / this.scale;
1028
+ ctx.save();
1029
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
1030
+ ctx.shadowBlur = 8 / this.scale;
1031
+ ctx.strokeStyle = "#ffffff";
1032
+ ctx.lineWidth = 1.5 / this.scale;
1033
+ roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r);
1034
+ ctx.stroke();
1035
+ ctx.restore();
1036
+ }
1037
+ /** Rotating dashed border drawn on the edge canvas for executing nodes */
1038
+ _drawActiveNodeBorder(node, time) {
1039
+ const { ctx } = this;
1040
+ const { x, y, w, h } = node.computed;
1041
+ const r = 2;
1042
+ const pad = 8 / this.scale;
1043
+ const dashLen = 8 / this.scale;
1044
+ const gapLen = 6 / this.scale;
1045
+ const offset = -(time / 1e3) * (50 / this.scale);
1046
+ ctx.save();
1047
+ ctx.setLineDash([dashLen, gapLen]);
1048
+ ctx.lineDashOffset = offset;
1049
+ ctx.strokeStyle = "rgba(74,176,217,0.9)";
1050
+ ctx.lineWidth = 2 / this.scale;
1051
+ ctx.shadowColor = "#4ab0d9";
1052
+ ctx.shadowBlur = 6 / this.scale;
1053
+ roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
1054
+ ctx.stroke();
1055
+ ctx.restore();
1056
+ }
1057
+ /** Approximate arc length of an edge in world coordinates */
1058
+ _getEdgeLength(graph, e) {
1059
+ const from = graph.nodes.get(e.fromNode);
1060
+ const to = graph.nodes.get(e.toNode);
1061
+ if (!from || !to) return 200;
1062
+ const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
1063
+ const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
1064
+ const pr1 = portRect(from, null, iOut, "out");
1065
+ const pr2 = portRect(to, null, iIn, "in");
1066
+ const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
1067
+ const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
1068
+ if (this.edgeStyle === "orthogonal") {
1069
+ return Math.abs(x2 - x1) + Math.abs(y2 - y1);
1070
+ }
1071
+ const chord = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
1072
+ return this.edgeStyle === "bezier" ? chord * 1.4 : chord;
1073
+ }
970
1074
  _drawEdge(graph, e) {
971
1075
  const from = graph.nodes.get(e.fromNode);
972
1076
  const to = graph.nodes.get(e.toNode);
@@ -975,7 +1079,8 @@ const _CanvasRenderer = class _CanvasRenderer {
975
1079
  const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
976
1080
  const pr1 = portRect(from, null, iOut, "out");
977
1081
  const pr2 = portRect(to, null, iIn, "in");
978
- const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2, x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
1082
+ const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
1083
+ const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
979
1084
  if (this.edgeStyle === "line") {
980
1085
  this._drawLine(x1, y1, x2, y2);
981
1086
  } else if (this.edgeStyle === "orthogonal") {
@@ -984,6 +1089,32 @@ const _CanvasRenderer = class _CanvasRenderer {
984
1089
  this._drawCurve(x1, y1, x2, y2);
985
1090
  }
986
1091
  }
1092
+ _getEdgeDotPosition(graph, e, t) {
1093
+ const from = graph.nodes.get(e.fromNode);
1094
+ const to = graph.nodes.get(e.toNode);
1095
+ if (!from || !to) return null;
1096
+ const iOut = from.outputs.findIndex((p) => p.id === e.fromPort);
1097
+ const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
1098
+ const pr1 = portRect(from, null, iOut, "out");
1099
+ const pr2 = portRect(to, null, iIn, "in");
1100
+ const x1 = pr1.x + pr1.w / 2, y1 = pr1.y + pr1.h / 2;
1101
+ const x2 = pr2.x + pr2.w / 2, y2 = pr2.y + pr2.h / 2;
1102
+ if (this.edgeStyle === "bezier") {
1103
+ const dx = Math.max(40, Math.abs(x2 - x1) * 0.4);
1104
+ return cubicBezierPoint(x1, y1, x1 + dx, y1, x2 - dx, y2, x2, y2, t);
1105
+ } else if (this.edgeStyle === "orthogonal") {
1106
+ const midX = (x1 + x2) / 2;
1107
+ const pts = [
1108
+ { x: x1, y: y1 },
1109
+ { x: midX, y: y1 },
1110
+ { x: midX, y: y2 },
1111
+ { x: x2, y: y2 }
1112
+ ];
1113
+ return polylinePoint(pts, t);
1114
+ } else {
1115
+ return { x: x1 + (x2 - x1) * t, y: y1 + (y2 - y1) * t };
1116
+ }
1117
+ }
987
1118
  _drawLine(x1, y1, x2, y2) {
988
1119
  const { ctx } = this;
989
1120
  ctx.beginPath();
@@ -1000,15 +1131,12 @@ const _CanvasRenderer = class _CanvasRenderer {
1000
1131
  }
1001
1132
  _drawOrthogonal(x1, y1, x2, y2) {
1002
1133
  const midX = (x1 + x2) / 2;
1003
- let pts;
1004
- {
1005
- pts = [
1006
- { x: x1, y: y1 },
1007
- { x: midX, y: y1 },
1008
- { x: midX, y: y2 },
1009
- { x: x2, y: y2 }
1010
- ];
1011
- }
1134
+ const pts = [
1135
+ { x: x1, y: y1 },
1136
+ { x: midX, y: y1 },
1137
+ { x: midX, y: y2 },
1138
+ { x: x2, y: y2 }
1139
+ ];
1012
1140
  const { ctx } = this;
1013
1141
  const prevJoin = ctx.lineJoin, prevCap = ctx.lineCap;
1014
1142
  ctx.lineJoin = "round";
@@ -1026,45 +1154,74 @@ const _CanvasRenderer = class _CanvasRenderer {
1026
1154
  ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
1027
1155
  ctx.stroke();
1028
1156
  }
1029
- /**
1030
- * Draw only edges on a separate canvas (for layering above HTML overlay)
1031
- * @param {Graph} graph - The graph
1032
- * @param {Object} options - Rendering options
1033
- */
1034
- drawEdgesOnly(graph, { activeEdges = /* @__PURE__ */ new Set(), running = false, time = performance.now(), tempEdge = null } = {}) {
1157
+ drawEdgesOnly(graph, {
1158
+ activeEdges = /* @__PURE__ */ new Set(),
1159
+ activeEdgeTimes = /* @__PURE__ */ new Map(),
1160
+ activeNodes = /* @__PURE__ */ new Set(),
1161
+ selection = /* @__PURE__ */ new Set(),
1162
+ time = performance.now(),
1163
+ tempEdge = null,
1164
+ loopActiveEdges = false
1165
+ } = {}) {
1166
+ var _a, _b;
1035
1167
  this._resetTransform();
1036
1168
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1037
1169
  this._applyTransform();
1038
1170
  const { ctx, theme } = this;
1039
- let dashArray = null;
1040
- let dashOffset = 0;
1041
- if (running || activeEdges.size > 0) {
1042
- const speed = 120;
1043
- const phase = time / 1e3 * speed / this.scale % 12;
1044
- dashArray = [6 / this.scale, 6 / this.scale];
1045
- dashOffset = -phase;
1046
- }
1047
- ctx.lineWidth = 1.5 / this.scale;
1048
- ctx.strokeStyle = theme.edge;
1049
1171
  for (const e of graph.edges.values()) {
1050
- const isActive = activeEdges && activeEdges.has(e.id);
1051
- if (isActive && dashArray) {
1052
- ctx.setLineDash(dashArray);
1053
- ctx.lineDashOffset = dashOffset;
1054
- ctx.strokeStyle = "#00ffff";
1172
+ const isActive = activeEdges.has(e.id);
1173
+ if (isActive) {
1174
+ ctx.save();
1175
+ ctx.shadowColor = theme.edgeActive;
1176
+ ctx.shadowBlur = 6 / this.scale;
1177
+ ctx.strokeStyle = theme.edgeActive;
1055
1178
  ctx.lineWidth = 3 / this.scale;
1179
+ ctx.setLineDash([]);
1180
+ this._drawEdge(graph, e);
1181
+ ctx.restore();
1182
+ const flowSpeed = this.theme.flowSpeed || 150;
1183
+ const activationTime = activeEdgeTimes.get(e.id) ?? time;
1184
+ const edgeLen2 = Math.max(50, this._getEdgeLength(graph, e));
1185
+ const duration = edgeLen2 / flowSpeed * 1e3;
1186
+ const rawT = (time - activationTime) / duration;
1187
+ const dotT = loopActiveEdges ? time / 1e3 * (flowSpeed / edgeLen2) % 1 : Math.min(1, rawT);
1188
+ const dotPos = this._getEdgeDotPosition(graph, e, dotT);
1189
+ if (dotPos) {
1190
+ ctx.save();
1191
+ ctx.fillStyle = this._rgba(theme.edgeActive, 0.9);
1192
+ ctx.shadowColor = theme.edgeActive;
1193
+ ctx.shadowBlur = 8 / this.scale;
1194
+ ctx.beginPath();
1195
+ ctx.arc(dotPos.x, dotPos.y, 2.5 / this.scale, 0, Math.PI * 2);
1196
+ ctx.fill();
1197
+ ctx.restore();
1198
+ }
1056
1199
  } else {
1057
1200
  ctx.setLineDash([]);
1058
1201
  ctx.strokeStyle = theme.edge;
1059
- ctx.lineWidth = 1.5 / this.scale;
1202
+ ctx.lineWidth = 2.5 / this.scale;
1203
+ this._drawEdge(graph, e);
1204
+ }
1205
+ }
1206
+ for (const nodeId of selection) {
1207
+ const node = graph.nodes.get(nodeId);
1208
+ if (!node) continue;
1209
+ const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type);
1210
+ if (def == null ? void 0 : def.html) this._drawHtmlSelectionBorder(node);
1211
+ }
1212
+ if (activeNodes.size > 0) {
1213
+ for (const nodeId of activeNodes) {
1214
+ const node = graph.nodes.get(nodeId);
1215
+ if (node) this._drawActiveNodeBorder(node, time);
1060
1216
  }
1061
- this._drawEdge(graph, e);
1062
1217
  }
1063
1218
  if (tempEdge) {
1064
1219
  const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
1065
1220
  const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
1066
1221
  const prevDash = this.ctx.getLineDash();
1067
- this.ctx.setLineDash([6 / this.scale, 6 / this.scale]);
1222
+ this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
1223
+ this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
1224
+ this.ctx.lineWidth = 2.5 / this.scale;
1068
1225
  let ptsForArrow = null;
1069
1226
  if (this.edgeStyle === "line") {
1070
1227
  this._drawLine(a.x, a.y, b.x, b.y);
@@ -1085,15 +1242,14 @@ const _CanvasRenderer = class _CanvasRenderer {
1085
1242
  if (ptsForArrow && ptsForArrow.length >= 2) {
1086
1243
  const p1 = ptsForArrow[ptsForArrow.length - 2];
1087
1244
  const p2 = ptsForArrow[ptsForArrow.length - 1];
1088
- this.ctx.fillStyle = this.theme.edge;
1089
- this.ctx.strokeStyle = this.theme.edge;
1090
- this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 12);
1245
+ this.ctx.fillStyle = this.theme.accentBright;
1246
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
1091
1247
  }
1092
1248
  }
1093
1249
  this._resetTransform();
1094
1250
  }
1095
1251
  };
1096
- __publicField(_CanvasRenderer, "FONT_SIZE", 12);
1252
+ __publicField(_CanvasRenderer, "FONT_SIZE", 11);
1097
1253
  __publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
1098
1254
  let CanvasRenderer = _CanvasRenderer;
1099
1255
  function roundRect(ctx, x, y, w, h, r = 6) {
@@ -1110,6 +1266,38 @@ function roundRect(ctx, x, y, w, h, r = 6) {
1110
1266
  ctx.quadraticCurveTo(x, y, x + r.tl, y);
1111
1267
  ctx.closePath();
1112
1268
  }
1269
+ function cubicBezierPoint(x0, y0, x1, y1, x2, y2, x3, y3, t) {
1270
+ const mt = 1 - t;
1271
+ return {
1272
+ x: mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
1273
+ y: mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3
1274
+ };
1275
+ }
1276
+ function polylinePoint(pts, t) {
1277
+ let totalLen = 0;
1278
+ const lens = [];
1279
+ for (let i = 0; i < pts.length - 1; i++) {
1280
+ const dx = pts[i + 1].x - pts[i].x;
1281
+ const dy = pts[i + 1].y - pts[i].y;
1282
+ const len = Math.sqrt(dx * dx + dy * dy);
1283
+ lens.push(len);
1284
+ totalLen += len;
1285
+ }
1286
+ if (totalLen === 0) return pts[0];
1287
+ let target = t * totalLen;
1288
+ let accum = 0;
1289
+ for (let i = 0; i < lens.length; i++) {
1290
+ if (accum + lens[i] >= target) {
1291
+ const segT = lens[i] > 0 ? (target - accum) / lens[i] : 0;
1292
+ return {
1293
+ x: pts[i].x + (pts[i + 1].x - pts[i].x) * segT,
1294
+ y: pts[i].y + (pts[i + 1].y - pts[i].y) * segT
1295
+ };
1296
+ }
1297
+ accum += lens[i];
1298
+ }
1299
+ return pts[pts.length - 1];
1300
+ }
1113
1301
  function findEdgeId(graph, a, b, c, d) {
1114
1302
  for (const [id, e] of graph.edges) {
1115
1303
  if (e.fromNode === a && e.fromPort === b && e.toNode === c && e.toPort === d)
@@ -1232,6 +1420,9 @@ const _Controller = class _Controller {
1232
1420
  this.gDragging = null;
1233
1421
  this.gResizing = null;
1234
1422
  this.boxSelecting = null;
1423
+ this.activeEdges = /* @__PURE__ */ new Set();
1424
+ this.activeEdgeTimes = /* @__PURE__ */ new Map();
1425
+ this.activeNodes = /* @__PURE__ */ new Set();
1235
1426
  this.snapToGrid = true;
1236
1427
  this.gridSize = 20;
1237
1428
  this._cursor = "default";
@@ -1243,6 +1434,16 @@ const _Controller = class _Controller {
1243
1434
  this._onContextMenuEvt = this._onContextMenu.bind(this);
1244
1435
  this._onDblClickEvt = this._onDblClick.bind(this);
1245
1436
  this._bindEvents();
1437
+ this.hooks.on("runner:step-updated", ({ activeNodeId, activeEdgeIds = [] }) => {
1438
+ this.activeNodes = activeNodeId ? /* @__PURE__ */ new Set([activeNodeId]) : /* @__PURE__ */ new Set();
1439
+ this.activeEdges = new Set(activeEdgeIds);
1440
+ this.activeEdgeTimes.clear();
1441
+ const now = performance.now();
1442
+ for (const edgeId of activeEdgeIds) {
1443
+ this.activeEdgeTimes.set(edgeId, now);
1444
+ }
1445
+ this.render();
1446
+ });
1246
1447
  }
1247
1448
  destroy() {
1248
1449
  const c = this.renderer.canvas;
@@ -1534,7 +1735,8 @@ const _Controller = class _Controller {
1534
1735
  const dx = w.x - this.resizing.startX;
1535
1736
  const dy = w.y - this.resizing.startY;
1536
1737
  const minW = _Controller.MIN_NODE_WIDTH;
1537
- const minH = _Controller.MIN_NODE_HEIGHT;
1738
+ const maxPorts = Math.max(n.inputs.length, n.outputs.length);
1739
+ const minH = maxPorts > 0 ? Math.max(_Controller.MIN_NODE_HEIGHT, 42 + maxPorts * 20) : _Controller.MIN_NODE_HEIGHT;
1538
1740
  n.size.width = Math.max(minW, this.resizing.startW + dx);
1539
1741
  n.size.height = Math.max(minH, this.resizing.startH + dy);
1540
1742
  (_a = this.hooks) == null ? void 0 : _a.emit("node:resize", n);
@@ -1792,17 +1994,22 @@ const _Controller = class _Controller {
1792
1994
  this.graph.updateWorldTransforms();
1793
1995
  this.render();
1794
1996
  }
1795
- render() {
1997
+ render(time = performance.now()) {
1796
1998
  var _a;
1797
1999
  const tEdge = this.renderTempEdge();
2000
+ const runner = this.graph.runner;
2001
+ const isStepMode = !!runner && runner.executionMode === "step";
1798
2002
  this.renderer.draw(this.graph, {
1799
2003
  selection: this.selection,
1800
2004
  tempEdge: null,
1801
2005
  // Don't draw temp edge on background
1802
2006
  boxSelecting: this.boxSelecting,
1803
2007
  activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
1804
- drawEdges: !this.edgeRenderer
2008
+ activeEdgeTimes: this.activeEdgeTimes,
2009
+ drawEdges: !this.edgeRenderer,
1805
2010
  // Only draw edges here if no separate edge renderer
2011
+ time,
2012
+ loopActiveEdges: isStepMode
1806
2013
  });
1807
2014
  (_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
1808
2015
  if (this.edgeRenderer) {
@@ -1810,11 +2017,13 @@ const _Controller = class _Controller {
1810
2017
  edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
1811
2018
  this.edgeRenderer._applyTransform();
1812
2019
  this.edgeRenderer.drawEdgesOnly(this.graph, {
1813
- activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
1814
- running: false,
1815
- time: performance.now(),
1816
- tempEdge: tEdge
1817
- // Draw temp edge on edge layer
2020
+ activeEdges: this.activeEdges,
2021
+ activeEdgeTimes: this.activeEdgeTimes,
2022
+ activeNodes: this.activeNodes,
2023
+ selection: this.selection,
2024
+ time,
2025
+ tempEdge: tEdge,
2026
+ loopActiveEdges: isStepMode
1818
2027
  });
1819
2028
  this.edgeRenderer._resetTransform();
1820
2029
  }
@@ -2208,12 +2417,14 @@ class Runner {
2208
2417
  this._raf = null;
2209
2418
  this._last = 0;
2210
2419
  this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
2420
+ this.executionMode = "run";
2421
+ this.activePlan = null;
2422
+ this.activeStepIndex = -1;
2423
+ this.stepCache = /* @__PURE__ */ new Map();
2211
2424
  }
2212
- // 외부에서 실행 중인지 확인
2213
2425
  isRunning() {
2214
2426
  return this.running;
2215
2427
  }
2216
- // 실행 도중에도 CPS 변경 가능
2217
2428
  setCyclesPerFrame(n) {
2218
2429
  this.cyclesPerFrame = Math.max(1, n | 0);
2219
2430
  }
@@ -2246,29 +2457,23 @@ class Runner {
2246
2457
  }
2247
2458
  }
2248
2459
  /**
2249
- * Execute connected nodes once from a starting node
2250
- * Uses queue-based traversal to support branching exec flows
2251
- * @param {string} startNodeId - ID of the node to start from
2252
- * @param {number} dt - Delta time
2460
+ * Execute connected nodes once from a starting node.
2461
+ * Returns execEdgeOrder: exec edges in the order they were traversed.
2253
2462
  */
2254
2463
  runOnce(startNodeId, dt = 0) {
2255
- console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
2256
- const executedNodes = [];
2257
2464
  const allConnectedNodes = /* @__PURE__ */ new Set();
2258
- const queue = [startNodeId];
2465
+ const execEdgeOrder = [];
2466
+ const runCache = /* @__PURE__ */ new Map();
2467
+ const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
2259
2468
  const visited = /* @__PURE__ */ new Set();
2260
2469
  while (queue.length > 0) {
2261
- const currentNodeId = queue.shift();
2470
+ const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
2262
2471
  if (visited.has(currentNodeId)) continue;
2263
2472
  visited.add(currentNodeId);
2473
+ if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
2264
2474
  const node = this.graph.nodes.get(currentNodeId);
2265
- if (!node) {
2266
- console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
2267
- continue;
2268
- }
2269
- executedNodes.push(currentNodeId);
2475
+ if (!node) continue;
2270
2476
  allConnectedNodes.add(currentNodeId);
2271
- console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
2272
2477
  for (const input of node.inputs) {
2273
2478
  if (input.portType === "data") {
2274
2479
  for (const edge of this.graph.edges.values()) {
@@ -2276,32 +2481,154 @@ class Runner {
2276
2481
  const sourceNode = this.graph.nodes.get(edge.fromNode);
2277
2482
  if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
2278
2483
  allConnectedNodes.add(edge.fromNode);
2279
- this.executeNode(edge.fromNode, dt);
2484
+ this._executeNodeWithCache(edge.fromNode, dt, runCache);
2280
2485
  }
2281
2486
  }
2282
2487
  }
2283
2488
  }
2284
2489
  }
2285
- this.executeNode(currentNodeId, dt);
2286
- const nextNodes = this.findAllNextExecNodes(currentNodeId);
2287
- queue.push(...nextNodes);
2490
+ this._executeNodeWithCache(currentNodeId, dt, runCache);
2491
+ const execOutputPorts = node.outputs.filter((p) => p.portType === "exec");
2492
+ for (const execOutput of execOutputPorts) {
2493
+ for (const edge of this.graph.edges.values()) {
2494
+ if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
2495
+ queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
2496
+ }
2497
+ }
2498
+ }
2288
2499
  }
2289
- console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
2290
2500
  const connectedEdges = /* @__PURE__ */ new Set();
2291
2501
  for (const edge of this.graph.edges.values()) {
2292
2502
  if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
2293
2503
  connectedEdges.add(edge.id);
2294
2504
  }
2295
2505
  }
2296
- console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
2297
- return { connectedNodes: allConnectedNodes, connectedEdges };
2506
+ return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
2507
+ }
2508
+ setExecutionMode(mode) {
2509
+ this.executionMode = mode;
2510
+ if (mode === "run") this.resetStepping();
2511
+ }
2512
+ resetStepping() {
2513
+ var _a, _b;
2514
+ this.activePlan = null;
2515
+ this.activeStepIndex = -1;
2516
+ this.stepCache.clear();
2517
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:step-updated", { activeNodeId: null });
2518
+ }
2519
+ buildPlan(startNodeId) {
2520
+ const plan = [];
2521
+ const allConnectedNodes = /* @__PURE__ */ new Set();
2522
+ const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
2523
+ const visited = /* @__PURE__ */ new Set();
2524
+ while (queue.length > 0) {
2525
+ const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
2526
+ if (visited.has(currentNodeId)) continue;
2527
+ visited.add(currentNodeId);
2528
+ allConnectedNodes.add(currentNodeId);
2529
+ const node = this.graph.nodes.get(currentNodeId);
2530
+ if (!node) continue;
2531
+ const dataDeps = [];
2532
+ for (const input of node.inputs) {
2533
+ if (input.portType === "data") {
2534
+ for (const edge of this.graph.edges.values()) {
2535
+ if (edge.toNode === currentNodeId && edge.toPort === input.id) {
2536
+ const srcId = edge.fromNode;
2537
+ if (!allConnectedNodes.has(srcId)) {
2538
+ allConnectedNodes.add(srcId);
2539
+ dataDeps.push(srcId);
2540
+ }
2541
+ }
2542
+ }
2543
+ }
2544
+ }
2545
+ const incomingEdges = [];
2546
+ for (const edge of this.graph.edges.values()) {
2547
+ if (edge.toNode === currentNodeId) {
2548
+ incomingEdges.push(edge.id);
2549
+ }
2550
+ }
2551
+ plan.push({ nodeId: currentNodeId, fromEdgeId, incomingEdges, dataDeps });
2552
+ const execOutputs = node.outputs.filter((p) => p.portType === "exec");
2553
+ for (const execOutput of execOutputs) {
2554
+ for (const edge of this.graph.edges.values()) {
2555
+ if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
2556
+ queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
2557
+ }
2558
+ }
2559
+ }
2560
+ }
2561
+ return plan;
2562
+ }
2563
+ startStepping(startNodeId) {
2564
+ var _a, _b;
2565
+ this.stepCache.clear();
2566
+ this.activePlan = this.buildPlan(startNodeId);
2567
+ this.activeStepIndex = 0;
2568
+ const step = this.activePlan[0];
2569
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:step-updated", {
2570
+ activeNodeId: step == null ? void 0 : step.nodeId,
2571
+ activeEdgeIds: (step == null ? void 0 : step.incomingEdges) || []
2572
+ });
2573
+ this.start();
2574
+ }
2575
+ executeNextStep() {
2576
+ var _a, _b, _c, _d;
2577
+ if (!this.activePlan || this.activeStepIndex < 0 || this.activeStepIndex >= this.activePlan.length) {
2578
+ this.resetStepping();
2579
+ return null;
2580
+ }
2581
+ const step = this.activePlan[this.activeStepIndex];
2582
+ for (const depId of step.dataDeps) {
2583
+ this._executeNodeWithCache(depId, 0, this.stepCache);
2584
+ }
2585
+ this._executeNodeWithCache(step.nodeId, 0, this.stepCache);
2586
+ this.activeStepIndex++;
2587
+ if (this.activeStepIndex < this.activePlan.length) {
2588
+ const nextStep = this.activePlan[this.activeStepIndex];
2589
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "runner:step-updated", {
2590
+ activeNodeId: nextStep.nodeId,
2591
+ activeEdgeIds: nextStep.incomingEdges || []
2592
+ });
2593
+ } else {
2594
+ (_d = (_c = this.hooks) == null ? void 0 : _c.emit) == null ? void 0 : _d.call(_c, "runner:step-updated", { activeNodeId: null });
2595
+ this.resetStepping();
2596
+ }
2597
+ return step.nodeId;
2598
+ }
2599
+ /** Execute a node using a shared run-local output cache for reliable data passing. */
2600
+ _executeNodeWithCache(nodeId, dt, runCache) {
2601
+ var _a, _b;
2602
+ const node = this.graph.nodes.get(nodeId);
2603
+ if (!node) return;
2604
+ const def = this.registry.types.get(node.type);
2605
+ if (!(def == null ? void 0 : def.onExecute)) return;
2606
+ try {
2607
+ def.onExecute(node, {
2608
+ dt,
2609
+ graph: this.graph,
2610
+ getInput: (portName) => {
2611
+ const p = node.inputs.find((i) => i.name === portName) || node.inputs[0];
2612
+ if (!p) return void 0;
2613
+ for (const edge of this.graph.edges.values()) {
2614
+ if (edge.toNode === nodeId && edge.toPort === p.id) {
2615
+ const key = `${edge.fromNode}:${edge.fromPort}`;
2616
+ return runCache.has(key) ? runCache.get(key) : this.graph._curBuf().get(key);
2617
+ }
2618
+ }
2619
+ return void 0;
2620
+ },
2621
+ setOutput: (portName, value) => {
2622
+ const p = node.outputs.find((o) => o.name === portName) || node.outputs[0];
2623
+ if (p) {
2624
+ runCache.set(`${node.id}:${p.id}`, value);
2625
+ }
2626
+ }
2627
+ });
2628
+ } catch (err) {
2629
+ (_b = (_a = this.hooks) == null ? void 0 : _a.emit) == null ? void 0 : _b.call(_a, "error", err);
2630
+ }
2298
2631
  }
2299
- /**
2300
- * Find all nodes connected via exec outputs
2301
- * Supports multiple connections from a single exec output
2302
- * @param {string} nodeId - Current node ID
2303
- * @returns {string[]} Array of next node IDs
2304
- */
2305
2632
  findAllNextExecNodes(nodeId) {
2306
2633
  const node = this.graph.nodes.get(nodeId);
2307
2634
  if (!node) return [];
@@ -2317,11 +2644,6 @@ class Runner {
2317
2644
  }
2318
2645
  return nextNodes;
2319
2646
  }
2320
- /**
2321
- * Execute a single node
2322
- * @param {string} nodeId - Node ID to execute
2323
- * @param {number} dt - Delta time
2324
- */
2325
2647
  executeNode(nodeId, dt) {
2326
2648
  var _a, _b;
2327
2649
  const node = this.graph.nodes.get(nodeId);
@@ -2360,7 +2682,9 @@ class Runner {
2360
2682
  const dtMs = this._last ? t - this._last : 0;
2361
2683
  this._last = t;
2362
2684
  const dt = dtMs / 1e3;
2363
- this.step(this.cyclesPerFrame, dt);
2685
+ if (this.executionMode === "run") {
2686
+ this.step(this.cyclesPerFrame, dt);
2687
+ }
2364
2688
  (_b2 = (_a2 = this.hooks) == null ? void 0 : _a2.emit) == null ? void 0 : _b2.call(_a2, "runner:tick", {
2365
2689
  time: t,
2366
2690
  dt,
@@ -2419,7 +2743,7 @@ class HtmlOverlay {
2419
2743
  const header = document.createElement("div");
2420
2744
  header.className = "node-header";
2421
2745
  Object.assign(header.style, {
2422
- height: "24px",
2746
+ height: "26px",
2423
2747
  flexShrink: "0",
2424
2748
  display: "flex",
2425
2749
  alignItems: "center",
@@ -2494,6 +2818,7 @@ class HtmlOverlay {
2494
2818
  }
2495
2819
  seen.add(node.id);
2496
2820
  }
2821
+ this._drawStepOverlay(graph);
2497
2822
  for (const [id, el] of this.nodes) {
2498
2823
  if (!seen.has(id)) {
2499
2824
  el.remove();
@@ -2501,6 +2826,59 @@ class HtmlOverlay {
2501
2826
  }
2502
2827
  }
2503
2828
  }
2829
+ _drawStepOverlay(graph) {
2830
+ const runner = graph.runner;
2831
+ if (!runner || runner.executionMode !== "step" || !runner.activePlan) {
2832
+ if (this._stepBtn) {
2833
+ this._stepBtn.style.display = "none";
2834
+ }
2835
+ return;
2836
+ }
2837
+ const nextStep = runner.activePlan[runner.activeStepIndex];
2838
+ if (!nextStep) {
2839
+ if (this._stepBtn) this._stepBtn.style.display = "none";
2840
+ return;
2841
+ }
2842
+ const node = graph.nodes.get(nextStep.nodeId);
2843
+ if (!node) return;
2844
+ if (!this._stepBtn) {
2845
+ this._stepBtn = document.createElement("button");
2846
+ this._stepBtn.className = "step-play-button";
2847
+ this._stepBtn.innerHTML = "▶";
2848
+ Object.assign(this._stepBtn.style, {
2849
+ position: "absolute",
2850
+ zIndex: "100",
2851
+ width: "20px",
2852
+ height: "20px",
2853
+ borderRadius: "4px",
2854
+ border: "none",
2855
+ background: "transparent",
2856
+ color: "white",
2857
+ fontSize: "12px",
2858
+ cursor: "pointer",
2859
+ display: "flex",
2860
+ alignItems: "center",
2861
+ justifyContent: "center",
2862
+ // boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
2863
+ pointerEvents: "auto",
2864
+ transition: "transform 0.1s, background 0.2s"
2865
+ });
2866
+ this._stepBtn.addEventListener("mouseover", () => {
2867
+ this._stepBtn.style.transform = "scale(1)";
2868
+ });
2869
+ this._stepBtn.addEventListener("mouseout", () => {
2870
+ this._stepBtn.style.transform = "scale(1)";
2871
+ });
2872
+ this._stepBtn.addEventListener("click", (e) => {
2873
+ e.stopPropagation();
2874
+ runner.executeNextStep();
2875
+ });
2876
+ this.container.appendChild(this._stepBtn);
2877
+ }
2878
+ this._stepBtn.style.display = "flex";
2879
+ this._stepBtn.style.left = `${node.computed.x + node.computed.w - 26}px`;
2880
+ this._stepBtn.style.top = `${node.computed.y + 2}px`;
2881
+ }
2504
2882
  /**
2505
2883
  * Sync container transform with renderer state (lightweight update)
2506
2884
  * Called when zoom/pan occurs without needing full redraw
@@ -2635,10 +3013,44 @@ class PropertyPanel {
2635
3013
  this.hooks = hooks;
2636
3014
  this.registry = registry;
2637
3015
  this.render = render;
3016
+ this._def = null;
2638
3017
  this.panel = null;
2639
3018
  this.currentNode = null;
2640
3019
  this.isVisible = false;
3020
+ this._selfUpdating = false;
2641
3021
  this._createPanel();
3022
+ this._bindHooks();
3023
+ }
3024
+ _bindHooks() {
3025
+ var _a, _b, _c, _d, _e, _f;
3026
+ (_a = this.hooks) == null ? void 0 : _a.on("edge:create", () => {
3027
+ if (this._canRefresh()) this._renderContent();
3028
+ });
3029
+ (_b = this.hooks) == null ? void 0 : _b.on("edge:delete", () => {
3030
+ if (this._canRefresh()) this._renderContent();
3031
+ });
3032
+ (_c = this.hooks) == null ? void 0 : _c.on("node:updated", (node) => {
3033
+ var _a2;
3034
+ if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id) && !this._selfUpdating) {
3035
+ this._renderContent();
3036
+ }
3037
+ });
3038
+ (_d = this.hooks) == null ? void 0 : _d.on("node:move", (node) => {
3039
+ var _a2;
3040
+ if (this._canRefresh() && ((_a2 = this.currentNode) == null ? void 0 : _a2.id) === (node == null ? void 0 : node.id)) {
3041
+ this._updatePositionFields();
3042
+ }
3043
+ });
3044
+ (_e = this.hooks) == null ? void 0 : _e.on("runner:tick", () => {
3045
+ if (this._canRefresh()) this._updateLiveValues();
3046
+ });
3047
+ (_f = this.hooks) == null ? void 0 : _f.on("runner:stop", () => {
3048
+ if (this._canRefresh()) this._updateLiveValues();
3049
+ });
3050
+ }
3051
+ _canRefresh() {
3052
+ if (!this.isVisible || !this.currentNode) return false;
3053
+ return !this.panel.querySelector("[data-field]:focus");
2642
3054
  }
2643
3055
  _createPanel() {
2644
3056
  this.panel = document.createElement("div");
@@ -2668,8 +3080,10 @@ class PropertyPanel {
2668
3080
  });
2669
3081
  }
2670
3082
  open(node) {
3083
+ var _a, _b;
2671
3084
  if (!node) return;
2672
3085
  this.currentNode = node;
3086
+ this._def = ((_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type)) || null;
2673
3087
  this.isVisible = true;
2674
3088
  this._renderContent();
2675
3089
  this.panel.style.display = "block";
@@ -2705,9 +3119,9 @@ class PropertyPanel {
2705
3119
  </div>
2706
3120
  </div>
2707
3121
  </div>
2708
-
3122
+
2709
3123
  <div class="section">
2710
- <div class="section-title">Position & Size</div>
3124
+ <div class="section-title">Position &amp; Size</div>
2711
3125
  <div class="section-body">
2712
3126
  <div class="field-row">
2713
3127
  <div class="field">
@@ -2731,16 +3145,92 @@ class PropertyPanel {
2731
3145
  </div>
2732
3146
  </div>
2733
3147
  </div>
2734
-
3148
+
3149
+ ${this._renderConnections(node)}
2735
3150
  ${this._renderPorts(node)}
3151
+ ${this._renderLiveValues(node)}
2736
3152
  ${this._renderState(node)}
2737
-
3153
+
2738
3154
  <div class="panel-actions">
2739
3155
  <button class="btn-secondary panel-close-btn">Close</button>
2740
3156
  </div>
2741
3157
  `;
2742
3158
  this._attachInputListeners();
2743
3159
  }
3160
+ _renderConnections(node) {
3161
+ const edges = [...this.graph.edges.values()];
3162
+ const incoming = edges.filter((e) => e.toNode === node.id);
3163
+ const outgoing = edges.filter((e) => e.fromNode === node.id);
3164
+ if (!incoming.length && !outgoing.length) return "";
3165
+ const edgeLabel = (e, dir) => {
3166
+ const otherId = dir === "in" ? e.fromNode : e.toNode;
3167
+ const other = this.graph.nodes.get(otherId);
3168
+ return `<div class="port-item">
3169
+ <span class="port-icon data"></span>
3170
+ <span class="port-name" style="font-size:10px;color:#5a5a78;">${(other == null ? void 0 : other.title) ?? otherId}</span>
3171
+ </div>`;
3172
+ };
3173
+ return `
3174
+ <div class="section">
3175
+ <div class="section-title">Connections</div>
3176
+ <div class="section-body">
3177
+ ${incoming.length ? `
3178
+ <div class="port-group">
3179
+ <div class="port-group-title">Incoming (${incoming.length})</div>
3180
+ ${incoming.map((e) => edgeLabel(e, "in")).join("")}
3181
+ </div>` : ""}
3182
+ ${outgoing.length ? `
3183
+ <div class="port-group">
3184
+ <div class="port-group-title">Outgoing (${outgoing.length})</div>
3185
+ ${outgoing.map((e) => edgeLabel(e, "out")).join("")}
3186
+ </div>` : ""}
3187
+ </div>
3188
+ </div>
3189
+ `;
3190
+ }
3191
+ _renderLiveValues(node) {
3192
+ var _a, _b;
3193
+ const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
3194
+ if (!cur) return "";
3195
+ const lines = [];
3196
+ for (const input of node.inputs) {
3197
+ `${node.id}:${input.id}`;
3198
+ for (const edge of this.graph.edges.values()) {
3199
+ if (edge.toNode === node.id && edge.toPort === input.id) {
3200
+ const upKey = `${edge.fromNode}:${edge.fromPort}`;
3201
+ const val = cur.get(upKey);
3202
+ if (val !== void 0) {
3203
+ lines.push(`<div class="port-item">
3204
+ <span class="port-icon data"></span>
3205
+ <span class="port-name">↳ ${input.name}</span>
3206
+ <span class="port-type" style="color:var(--color-primary);background:rgba(99,102,241,0.1);">${JSON.stringify(val)}</span>
3207
+ </div>`);
3208
+ }
3209
+ break;
3210
+ }
3211
+ }
3212
+ }
3213
+ for (const output of node.outputs) {
3214
+ const key = `${node.id}:${output.id}`;
3215
+ const val = cur.get(key);
3216
+ if (val !== void 0) {
3217
+ lines.push(`<div class="port-item">
3218
+ <span class="port-icon exec" style="background:#10b981;"></span>
3219
+ <span class="port-name">↳ ${output.name}</span>
3220
+ <span class="port-type" style="color:#10b981;background:rgba(16,185,129,0.1);">${JSON.stringify(val)}</span>
3221
+ </div>`);
3222
+ }
3223
+ }
3224
+ if (!lines.length) return "";
3225
+ return `
3226
+ <div class="section">
3227
+ <div class="section-title">Live Values</div>
3228
+ <div class="section-body">
3229
+ ${lines.join("")}
3230
+ </div>
3231
+ </div>
3232
+ `;
3233
+ }
2744
3234
  _renderPorts(node) {
2745
3235
  if (!node.inputs.length && !node.outputs.length) return "";
2746
3236
  return `
@@ -2759,7 +3249,6 @@ class PropertyPanel {
2759
3249
  `).join("")}
2760
3250
  </div>
2761
3251
  ` : ""}
2762
-
2763
3252
  ${node.outputs.length ? `
2764
3253
  <div class="port-group">
2765
3254
  <div class="port-group-title">Outputs (${node.outputs.length})</div>
@@ -2777,38 +3266,56 @@ class PropertyPanel {
2777
3266
  `;
2778
3267
  }
2779
3268
  _renderState(node) {
2780
- if (!node.state || Object.keys(node.state).length === 0) return "";
3269
+ if (!node.state) return "";
3270
+ const entries = Object.entries(node.state).filter(([key, value]) => {
3271
+ if (key.startsWith("_")) return false;
3272
+ const t = typeof value;
3273
+ return t === "string" || t === "number" || t === "boolean";
3274
+ });
3275
+ if (!entries.length) return "";
3276
+ const fieldHtml = ([key, value]) => {
3277
+ if (typeof value === "boolean") {
3278
+ return `
3279
+ <div class="field">
3280
+ <label>${key}</label>
3281
+ <select data-field="state.${key}">
3282
+ <option value="true"${value ? " selected" : ""}>true</option>
3283
+ <option value="false"${!value ? " selected" : ""}>false</option>
3284
+ </select>
3285
+ </div>`;
3286
+ }
3287
+ return `
3288
+ <div class="field">
3289
+ <label>${key}</label>
3290
+ <input type="${typeof value === "number" ? "number" : "text"}"
3291
+ data-field="state.${key}"
3292
+ value="${value}" />
3293
+ </div>`;
3294
+ };
2781
3295
  return `
2782
3296
  <div class="section">
2783
3297
  <div class="section-title">State</div>
2784
3298
  <div class="section-body">
2785
- ${Object.entries(node.state).map(([key, value]) => `
2786
- <div class="field">
2787
- <label>${key}</label>
2788
- <input
2789
- type="${typeof value === "number" ? "number" : "text"}"
2790
- data-field="state.${key}"
2791
- value="${value}"
2792
- />
2793
- </div>
2794
- `).join("")}
3299
+ ${entries.map(fieldHtml).join("")}
2795
3300
  </div>
2796
3301
  </div>
2797
3302
  `;
2798
3303
  }
2799
3304
  _attachInputListeners() {
2800
- const inputs = this.panel.querySelectorAll("[data-field]");
2801
- inputs.forEach((input) => {
3305
+ var _a;
3306
+ this.panel.querySelectorAll("[data-field]").forEach((input) => {
2802
3307
  input.addEventListener("change", () => {
3308
+ this._selfUpdating = true;
2803
3309
  this._handleFieldChange(input.dataset.field, input.value);
3310
+ this._selfUpdating = false;
2804
3311
  });
2805
3312
  });
2806
- this.panel.querySelector(".panel-close-btn").addEventListener("click", () => {
3313
+ (_a = this.panel.querySelector(".panel-close-btn")) == null ? void 0 : _a.addEventListener("click", () => {
2807
3314
  this.close();
2808
3315
  });
2809
3316
  }
2810
3317
  _handleFieldChange(field, value) {
2811
- var _a;
3318
+ var _a, _b;
2812
3319
  const node = this.currentNode;
2813
3320
  if (!node) return;
2814
3321
  switch (field) {
@@ -2832,21 +3339,192 @@ class PropertyPanel {
2832
3339
  default:
2833
3340
  if (field.startsWith("state.")) {
2834
3341
  const key = field.substring(6);
2835
- if (node.state) {
2836
- const originalValue = node.state[key];
2837
- node.state[key] = typeof originalValue === "number" ? parseFloat(value) : value;
3342
+ if (node.state && key in node.state) {
3343
+ const orig = node.state[key];
3344
+ if (typeof orig === "boolean") {
3345
+ node.state[key] = value === "true";
3346
+ } else if (typeof orig === "number") {
3347
+ node.state[key] = parseFloat(value);
3348
+ } else {
3349
+ node.state[key] = value;
3350
+ }
2838
3351
  }
2839
3352
  }
2840
3353
  }
2841
3354
  (_a = this.hooks) == null ? void 0 : _a.emit("node:updated", node);
2842
- if (this.render) {
2843
- this.render();
3355
+ (_b = this.render) == null ? void 0 : _b.call(this);
3356
+ }
3357
+ /** Lightweight update of position fields only (no full re-render) */
3358
+ _updatePositionFields() {
3359
+ const node = this.currentNode;
3360
+ if (!node) return;
3361
+ const xEl = this.panel.querySelector('[data-field="x"]');
3362
+ const yEl = this.panel.querySelector('[data-field="y"]');
3363
+ if (xEl) xEl.value = Math.round(node.computed.x);
3364
+ if (yEl) yEl.value = Math.round(node.computed.y);
3365
+ }
3366
+ /** Lightweight in-place update of the Live Values section */
3367
+ _updateLiveValues() {
3368
+ var _a, _b;
3369
+ const node = this.currentNode;
3370
+ if (!node) return;
3371
+ const cur = (_b = (_a = this.graph) == null ? void 0 : _a._curBuf) == null ? void 0 : _b.call(_a);
3372
+ if (!cur) return;
3373
+ let section = this.panel.querySelector(".live-values-section");
3374
+ const newHtml = this._renderLiveValues(node);
3375
+ if (!newHtml) {
3376
+ if (section) section.remove();
3377
+ return;
3378
+ }
3379
+ const wrapper = document.createElement("div");
3380
+ wrapper.innerHTML = newHtml;
3381
+ const newSection = wrapper.firstElementChild;
3382
+ newSection.classList.add("live-values-section");
3383
+ if (section) {
3384
+ section.replaceWith(newSection);
3385
+ } else {
3386
+ this.panel.querySelectorAll(".section");
3387
+ const actions = this.panel.querySelector(".panel-actions");
3388
+ if (actions) {
3389
+ actions.before(newSection);
3390
+ } else {
3391
+ this.panel.querySelector(".panel-content").appendChild(newSection);
3392
+ }
2844
3393
  }
2845
3394
  }
2846
3395
  destroy() {
2847
- if (this.panel) {
2848
- this.panel.remove();
3396
+ var _a;
3397
+ (_a = this.panel) == null ? void 0 : _a.remove();
3398
+ }
3399
+ }
3400
+ class HelpOverlay {
3401
+ constructor(container, options = {}) {
3402
+ this.container = container;
3403
+ this.options = {
3404
+ shortcuts: options.shortcuts || this._getDefaultShortcuts(),
3405
+ onToggle: options.onToggle || null
3406
+ };
3407
+ this.isVisible = false;
3408
+ this.overlay = null;
3409
+ this.toggleBtn = null;
3410
+ this._createElements();
3411
+ this._bindEvents();
3412
+ }
3413
+ _getDefaultShortcuts() {
3414
+ return [
3415
+ {
3416
+ group: "Selection",
3417
+ items: [
3418
+ { label: "Select node", key: "Click" },
3419
+ { label: "Multi-select", key: "Shift+Click" },
3420
+ { label: "Box select", key: "Ctrl+Drag" }
3421
+ ]
3422
+ },
3423
+ {
3424
+ group: "Edit",
3425
+ items: [
3426
+ { label: "Delete", key: "Del" },
3427
+ { label: "Undo", key: "Ctrl+Z" },
3428
+ { label: "Redo", key: "Ctrl+Y" }
3429
+ ]
3430
+ },
3431
+ {
3432
+ group: "Group & Align",
3433
+ items: [
3434
+ { label: "Create group", key: "Ctrl+G" },
3435
+ { label: "Align horizontal", key: "A" },
3436
+ { label: "Align vertical", key: "Shift+A" }
3437
+ ]
3438
+ },
3439
+ {
3440
+ group: "View",
3441
+ items: [
3442
+ { label: "Toggle snap", key: "G" },
3443
+ { label: "Pan", key: "Mid+Drag" },
3444
+ { label: "Zoom", key: "Scroll" },
3445
+ { label: "Context menu", key: "RClick" }
3446
+ ]
3447
+ }
3448
+ ];
3449
+ }
3450
+ _createElements() {
3451
+ this.toggleBtn = document.createElement("div");
3452
+ this.toggleBtn.id = "helpToggle";
3453
+ this.toggleBtn.title = "단축키 (?)";
3454
+ this.toggleBtn.textContent = "?";
3455
+ this.container.appendChild(this.toggleBtn);
3456
+ this.overlay = document.createElement("div");
3457
+ this.overlay.id = "helpOverlay";
3458
+ const sectionsHtml = this.options.shortcuts.map(
3459
+ (group) => `
3460
+ <h4>${group.group}</h4>
3461
+ ${group.items.map(
3462
+ (item) => `
3463
+ <div class="shortcut-item">
3464
+ <span>${item.label}</span>
3465
+ <span class="shortcut-key">${item.key}</span>
3466
+ </div>
3467
+ `
3468
+ ).join("")}
3469
+ `
3470
+ ).join("");
3471
+ this.overlay.innerHTML = `
3472
+ <h3>
3473
+ <span>Keyboard Shortcuts</span>
3474
+ <button class="close-btn" id="helpClose" title="Close">×</button>
3475
+ </h3>
3476
+ ${sectionsHtml}
3477
+ `;
3478
+ this.container.appendChild(this.overlay);
3479
+ }
3480
+ _bindEvents() {
3481
+ this.toggleBtn.addEventListener("click", () => this.toggle());
3482
+ const closeBtn = this.overlay.querySelector("#helpClose");
3483
+ if (closeBtn) {
3484
+ closeBtn.addEventListener("click", (e) => {
3485
+ e.stopPropagation();
3486
+ this.close();
3487
+ });
2849
3488
  }
3489
+ document.addEventListener("mousedown", (e) => {
3490
+ if (this.isVisible) {
3491
+ if (!this.overlay.contains(e.target) && !this.toggleBtn.contains(e.target)) {
3492
+ this.close();
3493
+ }
3494
+ }
3495
+ });
3496
+ window.addEventListener("keydown", (e) => {
3497
+ if (e.key === "?" || e.shiftKey && e.key === "/") {
3498
+ if (!["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
3499
+ e.preventDefault();
3500
+ this.toggle();
3501
+ }
3502
+ }
3503
+ if (e.key === "Escape" && this.isVisible) {
3504
+ this.close();
3505
+ }
3506
+ });
3507
+ }
3508
+ toggle() {
3509
+ if (this.isVisible) this.close();
3510
+ else this.open();
3511
+ }
3512
+ open() {
3513
+ this.isVisible = true;
3514
+ this.overlay.classList.add("visible");
3515
+ this.toggleBtn.classList.add("active");
3516
+ if (this.options.onToggle) this.options.onToggle(true);
3517
+ }
3518
+ close() {
3519
+ this.isVisible = false;
3520
+ this.overlay.classList.remove("visible");
3521
+ this.toggleBtn.classList.remove("active");
3522
+ if (this.options.onToggle) this.options.onToggle(false);
3523
+ }
3524
+ destroy() {
3525
+ var _a, _b;
3526
+ (_a = this.toggleBtn) == null ? void 0 : _a.remove();
3527
+ (_b = this.overlay) == null ? void 0 : _b.remove();
2850
3528
  }
2851
3529
  }
2852
3530
  function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
@@ -2925,6 +3603,8 @@ function createGraphEditor(target, {
2925
3603
  showMinimap = true,
2926
3604
  enablePropertyPanel = true,
2927
3605
  propertyPanelContainer = null,
3606
+ enableHelp = true,
3607
+ helpShortcuts = null,
2928
3608
  setupDefaultContextMenu: setupDefaultContextMenu$1 = true,
2929
3609
  setupContextMenu = null,
2930
3610
  plugins = []
@@ -3058,43 +3738,31 @@ function createGraphEditor(target, {
3058
3738
  propertyPanel.open(node);
3059
3739
  });
3060
3740
  }
3741
+ let helpOverlay = null;
3742
+ if (enableHelp) {
3743
+ helpOverlay = new HelpOverlay(container, {
3744
+ shortcuts: helpShortcuts
3745
+ });
3746
+ }
3061
3747
  const runner = new Runner({ graph, registry, hooks });
3062
3748
  graph.runner = runner;
3063
3749
  graph.controller = controller;
3064
3750
  hooks.on("runner:tick", ({ time, dt }) => {
3065
- renderer.draw(graph, {
3066
- selection: controller.selection,
3067
- tempEdge: controller.connecting ? controller.renderTempEdge() : null,
3068
- // 필요시 helper
3069
- running: true,
3070
- time,
3071
- dt
3072
- });
3073
- htmlOverlay.draw(graph, controller.selection);
3751
+ controller.render(time);
3074
3752
  });
3075
3753
  hooks.on("runner:start", () => {
3076
- renderer.draw(graph, {
3077
- selection: controller.selection,
3078
- tempEdge: controller.connecting ? controller.renderTempEdge() : null,
3079
- running: true,
3080
- time: performance.now(),
3081
- dt: 0
3082
- });
3083
- htmlOverlay.draw(graph, controller.selection);
3754
+ controller.render(performance.now());
3084
3755
  });
3085
3756
  hooks.on("runner:stop", () => {
3086
- renderer.draw(graph, {
3087
- selection: controller.selection,
3088
- tempEdge: controller.connecting ? controller.renderTempEdge() : null,
3089
- running: false,
3090
- time: performance.now(),
3091
- dt: 0
3092
- });
3093
- htmlOverlay.draw(graph, controller.selection);
3757
+ controller.render(performance.now());
3094
3758
  });
3095
3759
  hooks.on("node:updated", () => {
3096
3760
  controller.render();
3097
3761
  });
3762
+ hooks.on("graph:deserialize", () => {
3763
+ renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
3764
+ controller.render();
3765
+ });
3098
3766
  if (setupDefaultContextMenu$1) {
3099
3767
  setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3100
3768
  }
@@ -3140,6 +3808,8 @@ function createGraphEditor(target, {
3140
3808
  },
3141
3809
  graph,
3142
3810
  renderer,
3811
+ edgeRenderer,
3812
+ // Expose edge renderer for style changes
3143
3813
  controller,
3144
3814
  // Expose controller for snap-to-grid access
3145
3815
  runner,
@@ -3158,6 +3828,14 @@ function createGraphEditor(target, {
3158
3828
  render: () => controller.render(),
3159
3829
  start: () => runner.start(),
3160
3830
  stop: () => runner.stop(),
3831
+ setEdgeStyle: (style) => {
3832
+ renderer.setEdgeStyle(style);
3833
+ edgeRenderer.setEdgeStyle(style);
3834
+ },
3835
+ setExecutionMode: (mode) => {
3836
+ runner.setExecutionMode(mode);
3837
+ controller.render();
3838
+ },
3161
3839
  destroy: () => {
3162
3840
  runner.stop();
3163
3841
  ro.disconnect();
@@ -3166,6 +3844,7 @@ function createGraphEditor(target, {
3166
3844
  contextMenu.destroy();
3167
3845
  if (propertyPanel) propertyPanel.destroy();
3168
3846
  if (minimap) minimap.destroy();
3847
+ if (helpOverlay) helpOverlay.destroy();
3169
3848
  }
3170
3849
  };
3171
3850
  if (autorun) runner.start();