html-overlay-node 0.1.10 → 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.
@@ -535,10 +535,13 @@ class Graph {
535
535
  const outCount = ((_b = def.outputs) == null ? void 0 : _b.length) || 0;
536
536
  const maxPorts = Math.max(inCount, outCount);
537
537
  const headerHeight = 26;
538
- const padding = 8;
539
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;
540
544
  let h = headerHeight + padding + maxPorts * portSpacing + padding;
541
- if (def.html) h += 16;
542
545
  return Math.max(h, 40);
543
546
  }
544
547
  }
@@ -590,7 +593,8 @@ const _CanvasRenderer = class _CanvasRenderer {
590
593
  port: "#4f46e5",
591
594
  portExec: "#10b981",
592
595
  edge: "rgba(255, 255, 255, 0.12)",
593
- edgeActive: "#6366f1",
596
+ edgeActive: "#34c38f",
597
+ // green for active edge animation
594
598
  accent: "#6366f1",
595
599
  accentBright: "#818cf8",
596
600
  accentGlow: "rgba(99, 102, 241, 0.25)"
@@ -669,12 +673,7 @@ const _CanvasRenderer = class _CanvasRenderer {
669
673
  ctx.closePath();
670
674
  ctx.fill();
671
675
  }
672
- _drawScreenText(text, lx, ly, {
673
- fontPx = 11,
674
- color = this.theme.text,
675
- align = "left",
676
- baseline = "alphabetic"
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();
@@ -731,8 +730,12 @@ const _CanvasRenderer = class _CanvasRenderer {
731
730
  selection = /* @__PURE__ */ new Set(),
732
731
  tempEdge = null,
733
732
  time = performance.now(),
733
+ activeNodes = /* @__PURE__ */ new Set(),
734
+ // Now explicitly passing active nodes
734
735
  activeEdges = /* @__PURE__ */ new Set(),
735
- drawEdges = true
736
+ activeEdgeTimes = /* @__PURE__ */ new Map(),
737
+ drawEdges = true,
738
+ loopActiveEdges = false
736
739
  } = {}) {
737
740
  var _a, _b, _c, _d;
738
741
  graph.updateWorldTransforms();
@@ -749,7 +752,7 @@ const _CanvasRenderer = class _CanvasRenderer {
749
752
  }
750
753
  }
751
754
  if (drawEdges) {
752
- ctx.lineWidth = 1.5 / this.scale;
755
+ ctx.lineWidth = 2.5 / this.scale;
753
756
  for (const e of graph.edges.values()) {
754
757
  const isActive = activeEdges && activeEdges.has(e.id);
755
758
  if (isActive) {
@@ -757,11 +760,14 @@ const _CanvasRenderer = class _CanvasRenderer {
757
760
  ctx.shadowColor = this.theme.edgeActive;
758
761
  ctx.shadowBlur = 8 / this.scale;
759
762
  ctx.strokeStyle = this.theme.edgeActive;
760
- ctx.lineWidth = 2 / this.scale;
763
+ ctx.lineWidth = 3 / this.scale;
761
764
  ctx.setLineDash([]);
762
765
  this._drawEdge(graph, e);
763
766
  ctx.restore();
764
- const dotT = time / 1e3 * 1.2 % 1;
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;
765
771
  const dotPos = this._getEdgeDotPosition(graph, e, dotT);
766
772
  if (dotPos) {
767
773
  ctx.save();
@@ -776,7 +782,7 @@ const _CanvasRenderer = class _CanvasRenderer {
776
782
  } else {
777
783
  ctx.setLineDash([]);
778
784
  ctx.strokeStyle = theme.edge;
779
- ctx.lineWidth = 1.5 / this.scale;
785
+ ctx.lineWidth = 2.5 / this.scale;
780
786
  this._drawEdge(graph, e);
781
787
  }
782
788
  }
@@ -787,16 +793,22 @@ const _CanvasRenderer = class _CanvasRenderer {
787
793
  const prevDash = this.ctx.getLineDash();
788
794
  this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
789
795
  this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
790
- this.ctx.lineWidth = 1.5 / this.scale;
796
+ this.ctx.lineWidth = 2.5 / this.scale;
791
797
  let ptsForArrow = null;
792
798
  if (this.edgeStyle === "line") {
793
799
  this._drawLine(a.x, a.y, b.x, b.y);
794
- ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
800
+ ptsForArrow = [
801
+ { x: a.x, y: a.y },
802
+ { x: b.x, y: b.y }
803
+ ];
795
804
  } else if (this.edgeStyle === "orthogonal") {
796
805
  ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
797
806
  } else {
798
807
  this._drawCurve(a.x, a.y, b.x, b.y);
799
- ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
808
+ ptsForArrow = [
809
+ { x: a.x, y: a.y },
810
+ { x: b.x, y: b.y }
811
+ ];
800
812
  }
801
813
  this.ctx.setLineDash(prevDash);
802
814
  if (ptsForArrow && ptsForArrow.length >= 2) {
@@ -820,6 +832,12 @@ const _CanvasRenderer = class _CanvasRenderer {
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) {
@@ -841,11 +859,11 @@ const _CanvasRenderer = class _CanvasRenderer {
841
859
  const categoryColor = node.color || (typeDef == null ? void 0 : typeDef.color) || theme.accent;
842
860
  if (selected) {
843
861
  ctx.save();
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;
862
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
863
+ ctx.shadowBlur = 8 / this.scale;
864
+ ctx.strokeStyle = "#ffffff";
865
+ ctx.lineWidth = 1.5 / this.scale;
866
+ const pad = 8 / this.scale;
849
867
  roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
850
868
  ctx.stroke();
851
869
  ctx.restore();
@@ -859,7 +877,7 @@ const _CanvasRenderer = class _CanvasRenderer {
859
877
  ctx.fill();
860
878
  ctx.restore();
861
879
  ctx.fillStyle = theme.node;
862
- ctx.strokeStyle = selected ? theme.accentBright : theme.nodeBorder;
880
+ ctx.strokeStyle = selected ? "rgba(255,255,255,0.4)" : theme.nodeBorder;
863
881
  ctx.lineWidth = 1 / this.scale;
864
882
  roundRect(ctx, x, y, w, h, r);
865
883
  ctx.fill();
@@ -873,7 +891,7 @@ const _CanvasRenderer = class _CanvasRenderer {
873
891
  ctx.globalAlpha = 0.25;
874
892
  ctx.fillRect(x, y, w, headerH);
875
893
  ctx.restore();
876
- ctx.strokeStyle = selected ? this._rgba(theme.accentBright, 0.3) : this._rgba(theme.nodeBorder, 0.6);
894
+ ctx.strokeStyle = selected ? "rgba(255,255,255,0.2)" : this._rgba(theme.nodeBorder, 0.6);
877
895
  ctx.lineWidth = 1 / this.scale;
878
896
  ctx.beginPath();
879
897
  ctx.moveTo(x, y + headerH);
@@ -919,6 +937,43 @@ const _CanvasRenderer = class _CanvasRenderer {
919
937
  }
920
938
  });
921
939
  }
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
+ }
922
977
  _drawPortShape(cx, cy, portType) {
923
978
  const { ctx, theme } = this;
924
979
  if (portType === "exec") {
@@ -966,14 +1021,14 @@ const _CanvasRenderer = class _CanvasRenderer {
966
1021
  }
967
1022
  /** Selection border for HTML overlay nodes, drawn on the edge canvas */
968
1023
  _drawHtmlSelectionBorder(node) {
969
- const { ctx, theme } = this;
1024
+ const { ctx } = this;
970
1025
  const { x, y, w, h } = node.computed;
971
1026
  const r = 2;
972
- const pad = 1.5 / this.scale;
1027
+ const pad = 2.5 / this.scale;
973
1028
  ctx.save();
974
- ctx.shadowColor = theme.accentGlow;
975
- ctx.shadowBlur = 14 / this.scale;
976
- ctx.strokeStyle = theme.accentBright;
1029
+ ctx.shadowColor = "rgba(255,255,255,0.3)";
1030
+ ctx.shadowBlur = 8 / this.scale;
1031
+ ctx.strokeStyle = "#ffffff";
977
1032
  ctx.lineWidth = 1.5 / this.scale;
978
1033
  roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r);
979
1034
  ctx.stroke();
@@ -981,24 +1036,41 @@ const _CanvasRenderer = class _CanvasRenderer {
981
1036
  }
982
1037
  /** Rotating dashed border drawn on the edge canvas for executing nodes */
983
1038
  _drawActiveNodeBorder(node, time) {
984
- const { ctx, theme } = this;
1039
+ const { ctx } = this;
985
1040
  const { x, y, w, h } = node.computed;
986
1041
  const r = 2;
987
- const pad = 2.5 / this.scale;
1042
+ const pad = 8 / this.scale;
988
1043
  const dashLen = 8 / this.scale;
989
1044
  const gapLen = 6 / this.scale;
990
1045
  const offset = -(time / 1e3) * (50 / this.scale);
991
1046
  ctx.save();
992
1047
  ctx.setLineDash([dashLen, gapLen]);
993
1048
  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;
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;
998
1053
  roundRect(ctx, x - pad, y - pad, w + pad * 2, h + pad * 2, r + pad);
999
1054
  ctx.stroke();
1000
1055
  ctx.restore();
1001
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
+ }
1002
1074
  _drawEdge(graph, e) {
1003
1075
  const from = graph.nodes.get(e.fromNode);
1004
1076
  const to = graph.nodes.get(e.toNode);
@@ -1088,7 +1160,8 @@ const _CanvasRenderer = class _CanvasRenderer {
1088
1160
  activeNodes = /* @__PURE__ */ new Set(),
1089
1161
  selection = /* @__PURE__ */ new Set(),
1090
1162
  time = performance.now(),
1091
- tempEdge = null
1163
+ tempEdge = null,
1164
+ loopActiveEdges = false
1092
1165
  } = {}) {
1093
1166
  var _a, _b;
1094
1167
  this._resetTransform();
@@ -1102,12 +1175,16 @@ const _CanvasRenderer = class _CanvasRenderer {
1102
1175
  ctx.shadowColor = theme.edgeActive;
1103
1176
  ctx.shadowBlur = 6 / this.scale;
1104
1177
  ctx.strokeStyle = theme.edgeActive;
1105
- ctx.lineWidth = 2 / this.scale;
1178
+ ctx.lineWidth = 3 / this.scale;
1106
1179
  ctx.setLineDash([]);
1107
1180
  this._drawEdge(graph, e);
1108
1181
  ctx.restore();
1182
+ const flowSpeed = this.theme.flowSpeed || 150;
1109
1183
  const activationTime = activeEdgeTimes.get(e.id) ?? time;
1110
- const dotT = Math.min(1, (time - activationTime) / 620);
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);
1111
1188
  const dotPos = this._getEdgeDotPosition(graph, e, dotT);
1112
1189
  if (dotPos) {
1113
1190
  ctx.save();
@@ -1122,7 +1199,7 @@ const _CanvasRenderer = class _CanvasRenderer {
1122
1199
  } else {
1123
1200
  ctx.setLineDash([]);
1124
1201
  ctx.strokeStyle = theme.edge;
1125
- ctx.lineWidth = 1.5 / this.scale;
1202
+ ctx.lineWidth = 2.5 / this.scale;
1126
1203
  this._drawEdge(graph, e);
1127
1204
  }
1128
1205
  }
@@ -1144,16 +1221,22 @@ const _CanvasRenderer = class _CanvasRenderer {
1144
1221
  const prevDash = this.ctx.getLineDash();
1145
1222
  this.ctx.setLineDash([5 / this.scale, 5 / this.scale]);
1146
1223
  this.ctx.strokeStyle = this._rgba(this.theme.accentBright, 0.7);
1147
- this.ctx.lineWidth = 1.5 / this.scale;
1224
+ this.ctx.lineWidth = 2.5 / this.scale;
1148
1225
  let ptsForArrow = null;
1149
1226
  if (this.edgeStyle === "line") {
1150
1227
  this._drawLine(a.x, a.y, b.x, b.y);
1151
- ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
1228
+ ptsForArrow = [
1229
+ { x: a.x, y: a.y },
1230
+ { x: b.x, y: b.y }
1231
+ ];
1152
1232
  } else if (this.edgeStyle === "orthogonal") {
1153
1233
  ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
1154
1234
  } else {
1155
1235
  this._drawCurve(a.x, a.y, b.x, b.y);
1156
- ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
1236
+ ptsForArrow = [
1237
+ { x: a.x, y: a.y },
1238
+ { x: b.x, y: b.y }
1239
+ ];
1157
1240
  }
1158
1241
  this.ctx.setLineDash(prevDash);
1159
1242
  if (ptsForArrow && ptsForArrow.length >= 2) {
@@ -1351,6 +1434,16 @@ const _Controller = class _Controller {
1351
1434
  this._onContextMenuEvt = this._onContextMenu.bind(this);
1352
1435
  this._onDblClickEvt = this._onDblClick.bind(this);
1353
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
+ });
1354
1447
  }
1355
1448
  destroy() {
1356
1449
  const c = this.renderer.canvas;
@@ -1642,7 +1735,8 @@ const _Controller = class _Controller {
1642
1735
  const dx = w.x - this.resizing.startX;
1643
1736
  const dy = w.y - this.resizing.startY;
1644
1737
  const minW = _Controller.MIN_NODE_WIDTH;
1645
- 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;
1646
1740
  n.size.width = Math.max(minW, this.resizing.startW + dx);
1647
1741
  n.size.height = Math.max(minH, this.resizing.startH + dy);
1648
1742
  (_a = this.hooks) == null ? void 0 : _a.emit("node:resize", n);
@@ -1900,17 +1994,22 @@ const _Controller = class _Controller {
1900
1994
  this.graph.updateWorldTransforms();
1901
1995
  this.render();
1902
1996
  }
1903
- render() {
1997
+ render(time = performance.now()) {
1904
1998
  var _a;
1905
1999
  const tEdge = this.renderTempEdge();
2000
+ const runner = this.graph.runner;
2001
+ const isStepMode = !!runner && runner.executionMode === "step";
1906
2002
  this.renderer.draw(this.graph, {
1907
2003
  selection: this.selection,
1908
2004
  tempEdge: null,
1909
2005
  // Don't draw temp edge on background
1910
2006
  boxSelecting: this.boxSelecting,
1911
2007
  activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
1912
- drawEdges: !this.edgeRenderer
2008
+ activeEdgeTimes: this.activeEdgeTimes,
2009
+ drawEdges: !this.edgeRenderer,
1913
2010
  // Only draw edges here if no separate edge renderer
2011
+ time,
2012
+ loopActiveEdges: isStepMode
1914
2013
  });
1915
2014
  (_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
1916
2015
  if (this.edgeRenderer) {
@@ -1922,8 +2021,9 @@ const _Controller = class _Controller {
1922
2021
  activeEdgeTimes: this.activeEdgeTimes,
1923
2022
  activeNodes: this.activeNodes,
1924
2023
  selection: this.selection,
1925
- time: performance.now(),
1926
- tempEdge: tEdge
2024
+ time,
2025
+ tempEdge: tEdge,
2026
+ loopActiveEdges: isStepMode
1927
2027
  });
1928
2028
  this.edgeRenderer._resetTransform();
1929
2029
  }
@@ -2317,6 +2417,10 @@ class Runner {
2317
2417
  this._raf = null;
2318
2418
  this._last = 0;
2319
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();
2320
2424
  }
2321
2425
  isRunning() {
2322
2426
  return this.running;
@@ -2359,6 +2463,7 @@ class Runner {
2359
2463
  runOnce(startNodeId, dt = 0) {
2360
2464
  const allConnectedNodes = /* @__PURE__ */ new Set();
2361
2465
  const execEdgeOrder = [];
2466
+ const runCache = /* @__PURE__ */ new Map();
2362
2467
  const queue = [{ nodeId: startNodeId, fromEdgeId: null }];
2363
2468
  const visited = /* @__PURE__ */ new Set();
2364
2469
  while (queue.length > 0) {
@@ -2376,15 +2481,15 @@ class Runner {
2376
2481
  const sourceNode = this.graph.nodes.get(edge.fromNode);
2377
2482
  if (sourceNode && !allConnectedNodes.has(edge.fromNode)) {
2378
2483
  allConnectedNodes.add(edge.fromNode);
2379
- this.executeNode(edge.fromNode, dt);
2484
+ this._executeNodeWithCache(edge.fromNode, dt, runCache);
2380
2485
  }
2381
2486
  }
2382
2487
  }
2383
2488
  }
2384
2489
  }
2385
- this.executeNode(currentNodeId, dt);
2386
- const execOutputs = node.outputs.filter((p) => p.portType === "exec");
2387
- for (const execOutput of execOutputs) {
2490
+ this._executeNodeWithCache(currentNodeId, dt, runCache);
2491
+ const execOutputPorts = node.outputs.filter((p) => p.portType === "exec");
2492
+ for (const execOutput of execOutputPorts) {
2388
2493
  for (const edge of this.graph.edges.values()) {
2389
2494
  if (edge.fromNode === currentNodeId && edge.fromPort === execOutput.id) {
2390
2495
  queue.push({ nodeId: edge.toNode, fromEdgeId: edge.id });
@@ -2400,6 +2505,130 @@ class Runner {
2400
2505
  }
2401
2506
  return { connectedNodes: allConnectedNodes, connectedEdges, execEdgeOrder };
2402
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
+ }
2631
+ }
2403
2632
  findAllNextExecNodes(nodeId) {
2404
2633
  const node = this.graph.nodes.get(nodeId);
2405
2634
  if (!node) return [];
@@ -2453,7 +2682,9 @@ class Runner {
2453
2682
  const dtMs = this._last ? t - this._last : 0;
2454
2683
  this._last = t;
2455
2684
  const dt = dtMs / 1e3;
2456
- this.step(this.cyclesPerFrame, dt);
2685
+ if (this.executionMode === "run") {
2686
+ this.step(this.cyclesPerFrame, dt);
2687
+ }
2457
2688
  (_b2 = (_a2 = this.hooks) == null ? void 0 : _a2.emit) == null ? void 0 : _b2.call(_a2, "runner:tick", {
2458
2689
  time: t,
2459
2690
  dt,
@@ -2587,6 +2818,7 @@ class HtmlOverlay {
2587
2818
  }
2588
2819
  seen.add(node.id);
2589
2820
  }
2821
+ this._drawStepOverlay(graph);
2590
2822
  for (const [id, el] of this.nodes) {
2591
2823
  if (!seen.has(id)) {
2592
2824
  el.remove();
@@ -2594,6 +2826,59 @@ class HtmlOverlay {
2594
2826
  }
2595
2827
  }
2596
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
+ }
2597
2882
  /**
2598
2883
  * Sync container transform with renderer state (lightweight update)
2599
2884
  * Called when zoom/pan occurs without needing full redraw
@@ -3165,7 +3450,7 @@ class HelpOverlay {
3165
3450
  _createElements() {
3166
3451
  this.toggleBtn = document.createElement("div");
3167
3452
  this.toggleBtn.id = "helpToggle";
3168
- this.toggleBtn.title = "Keyboard shortcuts (?)";
3453
+ this.toggleBtn.title = "단축키 (?)";
3169
3454
  this.toggleBtn.textContent = "?";
3170
3455
  this.container.appendChild(this.toggleBtn);
3171
3456
  this.overlay = document.createElement("div");
@@ -3463,35 +3748,13 @@ function createGraphEditor(target, {
3463
3748
  graph.runner = runner;
3464
3749
  graph.controller = controller;
3465
3750
  hooks.on("runner:tick", ({ time, dt }) => {
3466
- renderer.draw(graph, {
3467
- selection: controller.selection,
3468
- tempEdge: controller.connecting ? controller.renderTempEdge() : null,
3469
- // 필요시 helper
3470
- running: true,
3471
- time,
3472
- dt
3473
- });
3474
- htmlOverlay.draw(graph, controller.selection);
3751
+ controller.render(time);
3475
3752
  });
3476
3753
  hooks.on("runner:start", () => {
3477
- renderer.draw(graph, {
3478
- selection: controller.selection,
3479
- tempEdge: controller.connecting ? controller.renderTempEdge() : null,
3480
- running: true,
3481
- time: performance.now(),
3482
- dt: 0
3483
- });
3484
- htmlOverlay.draw(graph, controller.selection);
3754
+ controller.render(performance.now());
3485
3755
  });
3486
3756
  hooks.on("runner:stop", () => {
3487
- renderer.draw(graph, {
3488
- selection: controller.selection,
3489
- tempEdge: controller.connecting ? controller.renderTempEdge() : null,
3490
- running: false,
3491
- time: performance.now(),
3492
- dt: 0
3493
- });
3494
- htmlOverlay.draw(graph, controller.selection);
3757
+ controller.render(performance.now());
3495
3758
  });
3496
3759
  hooks.on("node:updated", () => {
3497
3760
  controller.render();
@@ -3545,6 +3808,8 @@ function createGraphEditor(target, {
3545
3808
  },
3546
3809
  graph,
3547
3810
  renderer,
3811
+ edgeRenderer,
3812
+ // Expose edge renderer for style changes
3548
3813
  controller,
3549
3814
  // Expose controller for snap-to-grid access
3550
3815
  runner,
@@ -3563,6 +3828,14 @@ function createGraphEditor(target, {
3563
3828
  render: () => controller.render(),
3564
3829
  start: () => runner.start(),
3565
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
+ },
3566
3839
  destroy: () => {
3567
3840
  runner.stop();
3568
3841
  ro.disconnect();