html-overlay-node 0.1.9 → 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.
@@ -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,18 @@ 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 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
+ }
528
544
  }
529
545
  function portRect(node, port, idx, dir) {
530
546
  const {
@@ -538,8 +554,10 @@ function portRect(node, port, idx, dir) {
538
554
  w: node.size.width,
539
555
  h: node.size.height
540
556
  };
541
- const headerHeight = 28;
542
- const y = ny + headerHeight + 10 + idx * 24;
557
+ const headerHeight = 26;
558
+ const padding = 8;
559
+ const portSpacing = 20;
560
+ const y = ny + headerHeight + padding + idx * portSpacing + portSpacing / 2;
543
561
  const portWidth = 12;
544
562
  const portHeight = 12;
545
563
  if (dir === "in") {
@@ -562,32 +580,20 @@ const _CanvasRenderer = class _CanvasRenderer {
562
580
  this.edgeStyle = edgeStyle;
563
581
  this.theme = Object.assign(
564
582
  {
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
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",
581
591
  portExec: "#10b981",
582
- // Emerald for exec ports
583
- edge: "#52525b",
584
- // Neutral edge color
585
- edgeActive: "#8b5cf6",
586
- // Purple for active
592
+ edge: "rgba(255, 255, 255, 0.12)",
593
+ edgeActive: "#6366f1",
587
594
  accent: "#6366f1",
588
- // Indigo accent
589
- accentBright: "#818cf8"
590
- // Brighter accent
595
+ accentBright: "#818cf8",
596
+ accentGlow: "rgba(99, 102, 241, 0.25)"
591
597
  },
592
598
  theme
593
599
  );
@@ -609,10 +615,6 @@ const _CanvasRenderer = class _CanvasRenderer {
609
615
  this.offsetY = offsetY;
610
616
  (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
611
617
  }
612
- /**
613
- * Set callback to be called when transform changes (zoom/pan)
614
- * @param {Function} callback - Function to call on transform change
615
- */
616
618
  setTransformChangeCallback(callback) {
617
619
  this._onTransformChange = callback;
618
620
  }
@@ -656,7 +658,7 @@ const _CanvasRenderer = class _CanvasRenderer {
656
658
  this.ctx.setTransform(1, 0, 0, 1, 0, 0);
657
659
  }
658
660
  // ── Drawing ────────────────────────────────────────────────────────────────
659
- _drawArrowhead(x1, y1, x2, y2, size = 10) {
661
+ _drawArrowhead(x1, y1, x2, y2, size = 8) {
660
662
  const { ctx } = this;
661
663
  const s = size / this.scale;
662
664
  const ang = Math.atan2(y2 - y1, x2 - x1);
@@ -668,12 +670,10 @@ const _CanvasRenderer = class _CanvasRenderer {
668
670
  ctx.fill();
669
671
  }
670
672
  _drawScreenText(text, lx, ly, {
671
- fontPx = 12,
673
+ fontPx = 11,
672
674
  color = this.theme.text,
673
675
  align = "left",
674
- baseline = "alphabetic",
675
- dpr = 1
676
- // 추후 devicePixelRatio 도입
676
+ baseline = "alphabetic"
677
677
  } = {}) {
678
678
  const { ctx } = this;
679
679
  const { x: sx, y: sy } = this.worldToScreen(lx, ly);
@@ -681,7 +681,7 @@ const _CanvasRenderer = class _CanvasRenderer {
681
681
  this._resetTransform();
682
682
  const px = Math.round(sx) + 0.5;
683
683
  const py = Math.round(sy) + 0.5;
684
- ctx.font = `${fontPx * this.scale}px system-ui`;
684
+ ctx.font = `${fontPx * this.scale}px "Inter", system-ui, sans-serif`;
685
685
  ctx.fillStyle = color;
686
686
  ctx.textAlign = align;
687
687
  ctx.textBaseline = baseline;
@@ -694,39 +694,47 @@ const _CanvasRenderer = class _CanvasRenderer {
694
694
  ctx.fillStyle = theme.bg;
695
695
  ctx.fillRect(0, 0, canvas.width, canvas.height);
696
696
  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
697
  const x0 = -offsetX / scale;
702
698
  const y0 = -offsetY / scale;
703
699
  const x1 = (canvas.width - offsetX) / scale;
704
700
  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);
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
+ }
711
717
  }
712
- for (let y = startY; y <= y1; y += step) {
713
- ctx.moveTo(x0, y);
714
- 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
+ }
715
727
  }
716
- ctx.stroke();
717
728
  this._resetTransform();
718
729
  }
719
730
  draw(graph, {
720
731
  selection = /* @__PURE__ */ new Set(),
721
732
  tempEdge = null,
722
- running = false,
723
733
  time = performance.now(),
724
- dt = 0,
725
- groups = null,
726
734
  activeEdges = /* @__PURE__ */ new Set(),
727
735
  drawEdges = true
728
736
  } = {}) {
729
- var _a, _b, _c, _d, _e, _f;
737
+ var _a, _b, _c, _d;
730
738
  graph.updateWorldTransforms();
731
739
  this.drawGrid();
732
740
  const { ctx, theme } = this;
@@ -742,62 +750,60 @@ const _CanvasRenderer = class _CanvasRenderer {
742
750
  }
743
751
  if (drawEdges) {
744
752
  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
- }
753
753
  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
754
  const isActive = activeEdges && activeEdges.has(e.id);
763
755
  if (isActive) {
764
- ctx.strokeStyle = "#00ffff";
765
- ctx.lineWidth = 3 / this.scale;
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
+ }
766
776
  } else {
777
+ ctx.setLineDash([]);
767
778
  ctx.strokeStyle = theme.edge;
768
779
  ctx.lineWidth = 1.5 / this.scale;
780
+ this._drawEdge(graph, e);
769
781
  }
770
- this._drawEdge(graph, e);
771
782
  }
772
783
  }
773
784
  if (tempEdge) {
774
785
  const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
775
786
  const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
776
787
  const prevDash = this.ctx.getLineDash();
777
- 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;
778
791
  let ptsForArrow = null;
779
792
  if (this.edgeStyle === "line") {
780
793
  this._drawLine(a.x, a.y, b.x, b.y);
781
- ptsForArrow = [
782
- { x: a.x, y: a.y },
783
- { x: b.x, y: b.y }
784
- ];
794
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
785
795
  } else if (this.edgeStyle === "orthogonal") {
786
796
  ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
787
797
  } else {
788
798
  this._drawCurve(a.x, a.y, b.x, b.y);
789
- ptsForArrow = [
790
- { x: a.x, y: a.y },
791
- { x: b.x, y: b.y }
792
- ];
799
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
793
800
  }
794
801
  this.ctx.setLineDash(prevDash);
795
802
  if (ptsForArrow && ptsForArrow.length >= 2) {
796
803
  const p1 = ptsForArrow[ptsForArrow.length - 2];
797
804
  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);
805
+ this.ctx.fillStyle = this.theme.accentBright;
806
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
801
807
  }
802
808
  }
803
809
  for (const n of graph.nodes.values()) {
@@ -805,16 +811,10 @@ const _CanvasRenderer = class _CanvasRenderer {
805
811
  const sel = selection.has(n.id);
806
812
  const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
807
813
  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 });
814
+ this._drawNode(n, sel, !hasHtmlOverlay ? true : false);
815
+ if (def == null ? void 0 : def.onDraw) {
816
+ def.onDraw(n, { ctx, theme, renderer: this });
811
817
  }
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
818
  if (hasHtmlOverlay) {
819
819
  this._drawPorts(n);
820
820
  }
@@ -832,40 +832,58 @@ const _CanvasRenderer = class _CanvasRenderer {
832
832
  return `rgba(${r},${g},${b},${a})`;
833
833
  }
834
834
  _drawNode(node, selected, skipPorts = false) {
835
+ var _a, _b;
835
836
  const { ctx, theme } = this;
836
- const r = 8;
837
+ const r = 2;
837
838
  const { x, y, w, h } = node.computed;
838
- 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) {
839
843
  ctx.save();
840
- ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
841
- 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();
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();
846
851
  ctx.restore();
847
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();
848
861
  ctx.fillStyle = theme.node;
849
862
  ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
850
- ctx.lineWidth = (selected ? 1.5 : 1) / this.scale;
863
+ ctx.lineWidth = 1 / this.scale;
851
864
  roundRect(ctx, x, y, w, h, r);
852
865
  ctx.fill();
853
866
  ctx.stroke();
854
867
  ctx.fillStyle = theme.title;
855
- 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 });
856
869
  ctx.fill();
857
- ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
858
- 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;
859
878
  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);
879
+ ctx.moveTo(x, y + headerH);
880
+ ctx.lineTo(x + w, y + headerH);
867
881
  ctx.stroke();
868
- 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, {
869
887
  fontPx: _CanvasRenderer.FONT_SIZE,
870
888
  color: theme.text,
871
889
  baseline: "middle",
@@ -876,97 +894,111 @@ const _CanvasRenderer = class _CanvasRenderer {
876
894
  const rct = portRect(node, p, i, "in");
877
895
  const cx = rct.x + rct.w / 2;
878
896
  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();
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
+ });
896
905
  }
897
906
  });
898
907
  node.outputs.forEach((p, i) => {
899
908
  const rct = portRect(node, p, i, "out");
900
909
  const cx = rct.x + rct.w / 2;
901
910
  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();
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
+ });
919
919
  }
920
920
  });
921
921
  }
922
- _drawPorts(node) {
922
+ _drawPortShape(cx, cy, portType) {
923
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) {
924
954
  node.inputs.forEach((p, i) => {
925
955
  const rct = portRect(node, p, i, "in");
926
956
  const cx = rct.x + rct.w / 2;
927
957
  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
- }
958
+ this._drawPortShape(cx, cy, p.portType);
945
959
  });
946
960
  node.outputs.forEach((p, i) => {
947
961
  const rct = portRect(node, p, i, "out");
948
962
  const cx = rct.x + rct.w / 2;
949
963
  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
- }
964
+ this._drawPortShape(cx, cy, p.portType);
968
965
  });
969
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
+ }
970
1002
  _drawEdge(graph, e) {
971
1003
  const from = graph.nodes.get(e.fromNode);
972
1004
  const to = graph.nodes.get(e.toNode);
@@ -975,7 +1007,8 @@ const _CanvasRenderer = class _CanvasRenderer {
975
1007
  const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
976
1008
  const pr1 = portRect(from, null, iOut, "out");
977
1009
  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;
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;
979
1012
  if (this.edgeStyle === "line") {
980
1013
  this._drawLine(x1, y1, x2, y2);
981
1014
  } else if (this.edgeStyle === "orthogonal") {
@@ -984,6 +1017,32 @@ const _CanvasRenderer = class _CanvasRenderer {
984
1017
  this._drawCurve(x1, y1, x2, y2);
985
1018
  }
986
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
+ }
987
1046
  _drawLine(x1, y1, x2, y2) {
988
1047
  const { ctx } = this;
989
1048
  ctx.beginPath();
@@ -1000,15 +1059,12 @@ const _CanvasRenderer = class _CanvasRenderer {
1000
1059
  }
1001
1060
  _drawOrthogonal(x1, y1, x2, y2) {
1002
1061
  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
- }
1062
+ const pts = [
1063
+ { x: x1, y: y1 },
1064
+ { x: midX, y: y1 },
1065
+ { x: midX, y: y2 },
1066
+ { x: x2, y: y2 }
1067
+ ];
1012
1068
  const { ctx } = this;
1013
1069
  const prevJoin = ctx.lineJoin, prevCap = ctx.lineCap;
1014
1070
  ctx.lineJoin = "round";
@@ -1026,74 +1082,91 @@ const _CanvasRenderer = class _CanvasRenderer {
1026
1082
  ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
1027
1083
  ctx.stroke();
1028
1084
  }
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 } = {}) {
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;
1035
1094
  this._resetTransform();
1036
1095
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1037
1096
  this._applyTransform();
1038
1097
  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
1098
  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";
1055
- ctx.lineWidth = 3 / this.scale;
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
+ }
1056
1122
  } else {
1057
1123
  ctx.setLineDash([]);
1058
1124
  ctx.strokeStyle = theme.edge;
1059
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);
1060
1139
  }
1061
- this._drawEdge(graph, e);
1062
1140
  }
1063
1141
  if (tempEdge) {
1064
1142
  const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
1065
1143
  const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
1066
1144
  const prevDash = this.ctx.getLineDash();
1067
- this.ctx.setLineDash([6 / this.scale, 6 / this.scale]);
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;
1068
1148
  let ptsForArrow = null;
1069
1149
  if (this.edgeStyle === "line") {
1070
1150
  this._drawLine(a.x, a.y, b.x, b.y);
1071
- ptsForArrow = [
1072
- { x: a.x, y: a.y },
1073
- { x: b.x, y: b.y }
1074
- ];
1151
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
1075
1152
  } else if (this.edgeStyle === "orthogonal") {
1076
1153
  ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
1077
1154
  } else {
1078
1155
  this._drawCurve(a.x, a.y, b.x, b.y);
1079
- ptsForArrow = [
1080
- { x: a.x, y: a.y },
1081
- { x: b.x, y: b.y }
1082
- ];
1156
+ ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
1083
1157
  }
1084
1158
  this.ctx.setLineDash(prevDash);
1085
1159
  if (ptsForArrow && ptsForArrow.length >= 2) {
1086
1160
  const p1 = ptsForArrow[ptsForArrow.length - 2];
1087
1161
  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);
1162
+ this.ctx.fillStyle = this.theme.accentBright;
1163
+ this._drawArrowhead(p1.x, p1.y, p2.x, p2.y, 10);
1091
1164
  }
1092
1165
  }
1093
1166
  this._resetTransform();
1094
1167
  }
1095
1168
  };
1096
- __publicField(_CanvasRenderer, "FONT_SIZE", 12);
1169
+ __publicField(_CanvasRenderer, "FONT_SIZE", 11);
1097
1170
  __publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
1098
1171
  let CanvasRenderer = _CanvasRenderer;
1099
1172
  function roundRect(ctx, x, y, w, h, r = 6) {
@@ -1110,6 +1183,38 @@ function roundRect(ctx, x, y, w, h, r = 6) {
1110
1183
  ctx.quadraticCurveTo(x, y, x + r.tl, y);
1111
1184
  ctx.closePath();
1112
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
+ }
1113
1218
  function findEdgeId(graph, a, b, c, d) {
1114
1219
  for (const [id, e] of graph.edges) {
1115
1220
  if (e.fromNode === a && e.fromPort === b && e.toNode === c && e.toPort === d)
@@ -1232,6 +1337,9 @@ const _Controller = class _Controller {
1232
1337
  this.gDragging = null;
1233
1338
  this.gResizing = null;
1234
1339
  this.boxSelecting = null;
1340
+ this.activeEdges = /* @__PURE__ */ new Set();
1341
+ this.activeEdgeTimes = /* @__PURE__ */ new Map();
1342
+ this.activeNodes = /* @__PURE__ */ new Set();
1235
1343
  this.snapToGrid = true;
1236
1344
  this.gridSize = 20;
1237
1345
  this._cursor = "default";
@@ -1810,11 +1918,12 @@ const _Controller = class _Controller {
1810
1918
  edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
1811
1919
  this.edgeRenderer._applyTransform();
1812
1920
  this.edgeRenderer.drawEdgesOnly(this.graph, {
1813
- activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
1814
- running: false,
1921
+ activeEdges: this.activeEdges,
1922
+ activeEdgeTimes: this.activeEdgeTimes,
1923
+ activeNodes: this.activeNodes,
1924
+ selection: this.selection,
1815
1925
  time: performance.now(),
1816
1926
  tempEdge: tEdge
1817
- // Draw temp edge on edge layer
1818
1927
  });
1819
1928
  this.edgeRenderer._resetTransform();
1820
1929
  }
@@ -2209,11 +2318,9 @@ class Runner {
2209
2318
  this._last = 0;
2210
2319
  this.cyclesPerFrame = Math.max(1, cyclesPerFrame | 0);
2211
2320
  }
2212
- // 외부에서 실행 중인지 확인
2213
2321
  isRunning() {
2214
2322
  return this.running;
2215
2323
  }
2216
- // 실행 도중에도 CPS 변경 가능
2217
2324
  setCyclesPerFrame(n) {
2218
2325
  this.cyclesPerFrame = Math.max(1, n | 0);
2219
2326
  }
@@ -2246,29 +2353,22 @@ class Runner {
2246
2353
  }
2247
2354
  }
2248
2355
  /**
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
2356
+ * Execute connected nodes once from a starting node.
2357
+ * Returns execEdgeOrder: exec edges in the order they were traversed.
2253
2358
  */
2254
2359
  runOnce(startNodeId, dt = 0) {
2255
- console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
2256
- const executedNodes = [];
2257
2360
  const allConnectedNodes = /* @__PURE__ */ new Set();
2258
- const queue = [startNodeId];
2361
+ const execEdgeOrder = [];
2362
+ const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
2259
2363
  const visited = /* @__PURE__ */ new Set();
2260
2364
  while (queue.length > 0) {
2261
- const currentNodeId = queue.shift();
2365
+ const { nodeId: currentNodeId, fromEdgeId } = queue.shift();
2262
2366
  if (visited.has(currentNodeId)) continue;
2263
2367
  visited.add(currentNodeId);
2368
+ if (fromEdgeId) execEdgeOrder.push(fromEdgeId);
2264
2369
  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);
2370
+ if (!node) continue;
2270
2371
  allConnectedNodes.add(currentNodeId);
2271
- console.log(`[Runner.runOnce] Executing: ${node.title} (${node.type})`);
2272
2372
  for (const input of node.inputs) {
2273
2373
  if (input.portType === "data") {
2274
2374
  for (const edge of this.graph.edges.values()) {
@@ -2283,25 +2383,23 @@ class Runner {
2283
2383
  }
2284
2384
  }
2285
2385
  this.executeNode(currentNodeId, dt);
2286
- const nextNodes = this.findAllNextExecNodes(currentNodeId);
2287
- queue.push(...nextNodes);
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
+ }
2288
2394
  }
2289
- console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
2290
2395
  const connectedEdges = /* @__PURE__ */ new Set();
2291
2396
  for (const edge of this.graph.edges.values()) {
2292
2397
  if (allConnectedNodes.has(edge.fromNode) && allConnectedNodes.has(edge.toNode)) {
2293
2398
  connectedEdges.add(edge.id);
2294
2399
  }
2295
2400
  }
2296
- console.log("[Runner.runOnce] Connected edges count:", connectedEdges.size);
2297
- return { connectedNodes: allConnectedNodes, connectedEdges };
2401
+ return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
2298
2402
  }
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
2403
  findAllNextExecNodes(nodeId) {
2306
2404
  const node = this.graph.nodes.get(nodeId);
2307
2405
  if (!node) return [];
@@ -2317,11 +2415,6 @@ class Runner {
2317
2415
  }
2318
2416
  return nextNodes;
2319
2417
  }
2320
- /**
2321
- * Execute a single node
2322
- * @param {string} nodeId - Node ID to execute
2323
- * @param {number} dt - Delta time
2324
- */
2325
2418
  executeNode(nodeId, dt) {
2326
2419
  var _a, _b;
2327
2420
  const node = this.graph.nodes.get(nodeId);
@@ -2419,7 +2512,7 @@ class HtmlOverlay {
2419
2512
  const header = document.createElement("div");
2420
2513
  header.className = "node-header";
2421
2514
  Object.assign(header.style, {
2422
- height: "24px",
2515
+ height: "26px",
2423
2516
  flexShrink: "0",
2424
2517
  display: "flex",
2425
2518
  alignItems: "center",
@@ -2635,10 +2728,44 @@ class PropertyPanel {
2635
2728
  this.hooks = hooks;
2636
2729
  this.registry = registry;
2637
2730
  this.render = render;
2731
+ this._def = null;
2638
2732
  this.panel = null;
2639
2733
  this.currentNode = null;
2640
2734
  this.isVisible = false;
2735
+ this._selfUpdating = false;
2641
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");
2642
2769
  }
2643
2770
  _createPanel() {
2644
2771
  this.panel = document.createElement("div");
@@ -2668,8 +2795,10 @@ class PropertyPanel {
2668
2795
  });
2669
2796
  }
2670
2797
  open(node) {
2798
+ var _a, _b;
2671
2799
  if (!node) return;
2672
2800
  this.currentNode = node;
2801
+ this._def = ((_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(node.type)) || null;
2673
2802
  this.isVisible = true;
2674
2803
  this._renderContent();
2675
2804
  this.panel.style.display = "block";
@@ -2705,9 +2834,9 @@ class PropertyPanel {
2705
2834
  </div>
2706
2835
  </div>
2707
2836
  </div>
2708
-
2837
+
2709
2838
  <div class="section">
2710
- <div class="section-title">Position & Size</div>
2839
+ <div class="section-title">Position &amp; Size</div>
2711
2840
  <div class="section-body">
2712
2841
  <div class="field-row">
2713
2842
  <div class="field">
@@ -2731,16 +2860,92 @@ class PropertyPanel {
2731
2860
  </div>
2732
2861
  </div>
2733
2862
  </div>
2734
-
2863
+
2864
+ ${this._renderConnections(node)}
2735
2865
  ${this._renderPorts(node)}
2866
+ ${this._renderLiveValues(node)}
2736
2867
  ${this._renderState(node)}
2737
-
2868
+
2738
2869
  <div class="panel-actions">
2739
2870
  <button class="btn-secondary panel-close-btn">Close</button>
2740
2871
  </div>
2741
2872
  `;
2742
2873
  this._attachInputListeners();
2743
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
+ }
2744
2949
  _renderPorts(node) {
2745
2950
  if (!node.inputs.length && !node.outputs.length) return "";
2746
2951
  return `
@@ -2759,7 +2964,6 @@ class PropertyPanel {
2759
2964
  `).join("")}
2760
2965
  </div>
2761
2966
  ` : ""}
2762
-
2763
2967
  ${node.outputs.length ? `
2764
2968
  <div class="port-group">
2765
2969
  <div class="port-group-title">Outputs (${node.outputs.length})</div>
@@ -2777,38 +2981,56 @@ class PropertyPanel {
2777
2981
  `;
2778
2982
  }
2779
2983
  _renderState(node) {
2780
- 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
+ };
2781
3010
  return `
2782
3011
  <div class="section">
2783
3012
  <div class="section-title">State</div>
2784
3013
  <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("")}
3014
+ ${entries.map(fieldHtml).join("")}
2795
3015
  </div>
2796
3016
  </div>
2797
3017
  `;
2798
3018
  }
2799
3019
  _attachInputListeners() {
2800
- const inputs = this.panel.querySelectorAll("[data-field]");
2801
- inputs.forEach((input) => {
3020
+ var _a;
3021
+ this.panel.querySelectorAll("[data-field]").forEach((input) => {
2802
3022
  input.addEventListener("change", () => {
3023
+ this._selfUpdating = true;
2803
3024
  this._handleFieldChange(input.dataset.field, input.value);
3025
+ this._selfUpdating = false;
2804
3026
  });
2805
3027
  });
2806
- this.panel.querySelector(".panel-close-btn").addEventListener("click", () => {
3028
+ (_a = this.panel.querySelector(".panel-close-btn")) == null ? void 0 : _a.addEventListener("click", () => {
2807
3029
  this.close();
2808
3030
  });
2809
3031
  }
2810
3032
  _handleFieldChange(field, value) {
2811
- var _a;
3033
+ var _a, _b;
2812
3034
  const node = this.currentNode;
2813
3035
  if (!node) return;
2814
3036
  switch (field) {
@@ -2832,21 +3054,192 @@ class PropertyPanel {
2832
3054
  default:
2833
3055
  if (field.startsWith("state.")) {
2834
3056
  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;
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
+ }
2838
3066
  }
2839
3067
  }
2840
3068
  }
2841
3069
  (_a = this.hooks) == null ? void 0 : _a.emit("node:updated", node);
2842
- if (this.render) {
2843
- 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
+ }
2844
3108
  }
2845
3109
  }
2846
3110
  destroy() {
2847
- if (this.panel) {
2848
- 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
+ });
2849
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();
2850
3243
  }
2851
3244
  }
2852
3245
  function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
@@ -2925,6 +3318,8 @@ function createGraphEditor(target, {
2925
3318
  showMinimap = true,
2926
3319
  enablePropertyPanel = true,
2927
3320
  propertyPanelContainer = null,
3321
+ enableHelp = true,
3322
+ helpShortcuts = null,
2928
3323
  setupDefaultContextMenu: setupDefaultContextMenu$1 = true,
2929
3324
  setupContextMenu = null,
2930
3325
  plugins = []
@@ -3058,6 +3453,12 @@ function createGraphEditor(target, {
3058
3453
  propertyPanel.open(node);
3059
3454
  });
3060
3455
  }
3456
+ let helpOverlay = null;
3457
+ if (enableHelp) {
3458
+ helpOverlay = new HelpOverlay(container, {
3459
+ shortcuts: helpShortcuts
3460
+ });
3461
+ }
3061
3462
  const runner = new Runner({ graph, registry, hooks });
3062
3463
  graph.runner = runner;
3063
3464
  graph.controller = controller;
@@ -3095,6 +3496,10 @@ function createGraphEditor(target, {
3095
3496
  hooks.on("node:updated", () => {
3096
3497
  controller.render();
3097
3498
  });
3499
+ hooks.on("graph:deserialize", () => {
3500
+ renderer.setTransform({ scale: 1, offsetX: 0, offsetY: 0 });
3501
+ controller.render();
3502
+ });
3098
3503
  if (setupDefaultContextMenu$1) {
3099
3504
  setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3100
3505
  }
@@ -3166,6 +3571,7 @@ function createGraphEditor(target, {
3166
3571
  contextMenu.destroy();
3167
3572
  if (propertyPanel) propertyPanel.destroy();
3168
3573
  if (minimap) minimap.destroy();
3574
+ if (helpOverlay) helpOverlay.destroy();
3169
3575
  }
3170
3576
  };
3171
3577
  if (autorun) runner.start();