html-overlay-node 0.1.5 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,27 +153,36 @@ class Node {
153
153
  * @param {string} [portType="data"] - Port type: "exec" or "data"
154
154
  * @returns {Object} The created port
155
155
  */
156
+ /**
157
+ * Recalculate minimum size based on ports
158
+ */
159
+ _updateMinSize() {
160
+ const HEADER_HEIGHT = 28;
161
+ const PORT_SPACING = 24;
162
+ const BOTTOM_PADDING = 10;
163
+ const inHeight = HEADER_HEIGHT + 10 + this.inputs.length * PORT_SPACING + BOTTOM_PADDING;
164
+ const outHeight = HEADER_HEIGHT + 10 + this.outputs.length * PORT_SPACING + BOTTOM_PADDING;
165
+ const minHeight = Math.max(inHeight, outHeight, 60);
166
+ if (this.size.height < minHeight) {
167
+ this.size.height = minHeight;
168
+ }
169
+ }
156
170
  addInput(name, datatype = "any", portType = "data") {
157
- if (!name || typeof name !== "string") {
158
- throw new Error("Input port name must be a non-empty string");
171
+ if (typeof name !== "string" || portType === "data" && !name) {
172
+ throw new Error("Input port name must be a string (non-empty for data ports)");
159
173
  }
160
174
  const port = { id: randomUUID(), name, datatype, portType, dir: "in" };
161
175
  this.inputs.push(port);
176
+ this._updateMinSize();
162
177
  return port;
163
178
  }
164
- /**
165
- * Add an output port to this node
166
- * @param {string} name - Port name
167
- * @param {string} [datatype="any"] - Data type for the port
168
- * @param {string} [portType="data"] - Port type: "exec" or "data"
169
- * @returns {Object} The created port
170
- */
171
179
  addOutput(name, datatype = "any", portType = "data") {
172
- if (!name || typeof name !== "string") {
173
- throw new Error("Output port name must be a non-empty string");
180
+ if (typeof name !== "string" || portType === "data" && !name) {
181
+ throw new Error("Output port name must be a string (non-empty for data ports)");
174
182
  }
175
183
  const port = { id: randomUUID(), name, datatype, portType, dir: "out" };
176
184
  this.outputs.push(port);
185
+ this._updateMinSize();
177
186
  return port;
178
187
  }
179
188
  }
@@ -188,8 +197,8 @@ class Edge {
188
197
  * @param {string} options.toPort - Target port ID
189
198
  */
190
199
  constructor({ id, fromNode, fromPort, toNode, toPort }) {
191
- if (!fromNode || !fromPort || !toNode || !toPort) {
192
- throw new Error("Edge requires fromNode, fromPort, toNode, and toPort");
200
+ if (fromNode == null || fromPort == null || toNode == null || toPort == null) {
201
+ throw new Error("Edge requires fromNode, fromPort, toNode, and toPort (null/undefined not allowed)");
193
202
  }
194
203
  this.id = id ?? randomUUID();
195
204
  this.fromNode = fromNode;
@@ -518,17 +527,19 @@ class Graph {
518
527
  }
519
528
  }
520
529
  function portRect(node, port, idx, dir) {
521
- const { x: nx, y: ny, w: width, h: height } = node.computed || {
530
+ const {
531
+ x: nx,
532
+ y: ny,
533
+ w: width,
534
+ h: height
535
+ } = node.computed || {
522
536
  x: node.pos.x,
523
537
  y: node.pos.y,
524
538
  w: node.size.width,
525
539
  h: node.size.height
526
540
  };
527
- const portCount = dir === "in" ? node.inputs.length : node.outputs.length;
528
541
  const headerHeight = 28;
529
- const availableHeight = (height || node.size.height) - headerHeight - 16;
530
- const spacing = availableHeight / (portCount + 1);
531
- const y = ny + headerHeight + spacing * (idx + 1);
542
+ const y = ny + headerHeight + 10 + idx * 24;
532
543
  const portWidth = 12;
533
544
  const portHeight = 12;
534
545
  if (dir === "in") {
@@ -591,31 +602,37 @@ const _CanvasRenderer = class _CanvasRenderer {
591
602
  this.canvas.width = w;
592
603
  this.canvas.height = h;
593
604
  }
594
- setTransform({
595
- scale = this.scale,
596
- offsetX = this.offsetX,
597
- offsetY = this.offsetY
598
- } = {}) {
605
+ setTransform({ scale = this.scale, offsetX = this.offsetX, offsetY = this.offsetY } = {}) {
606
+ var _a;
599
607
  this.scale = Math.min(this.maxScale, Math.max(this.minScale, scale));
600
608
  this.offsetX = offsetX;
601
609
  this.offsetY = offsetY;
610
+ (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
611
+ }
612
+ /**
613
+ * Set callback to be called when transform changes (zoom/pan)
614
+ * @param {Function} callback - Function to call on transform change
615
+ */
616
+ setTransformChangeCallback(callback) {
617
+ this._onTransformChange = callback;
602
618
  }
603
619
  panBy(dx, dy) {
620
+ var _a;
604
621
  this.offsetX += dx;
605
622
  this.offsetY += dy;
623
+ (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
606
624
  }
607
625
  zoomAt(factor, cx, cy) {
626
+ var _a;
608
627
  const prev = this.scale;
609
- const next = Math.min(
610
- this.maxScale,
611
- Math.max(this.minScale, prev * factor)
612
- );
628
+ const next = Math.min(this.maxScale, Math.max(this.minScale, prev * factor));
613
629
  if (next === prev) return;
614
630
  const wx = (cx - this.offsetX) / prev;
615
631
  const wy = (cy - this.offsetY) / prev;
616
632
  this.offsetX = cx - wx * next;
617
633
  this.offsetY = cy - wy * next;
618
634
  this.scale = next;
635
+ (_a = this._onTransformChange) == null ? void 0 : _a.call(this);
619
636
  }
620
637
  screenToWorld(x, y) {
621
638
  return {
@@ -631,7 +648,9 @@ const _CanvasRenderer = class _CanvasRenderer {
631
648
  }
632
649
  _applyTransform() {
633
650
  const { ctx } = this;
634
- ctx.setTransform(this.scale, 0, 0, this.scale, this.offsetX, this.offsetY);
651
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
652
+ ctx.translate(this.offsetX, this.offsetY);
653
+ ctx.scale(this.scale, this.scale);
635
654
  }
636
655
  _resetTransform() {
637
656
  this.ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -643,14 +662,8 @@ const _CanvasRenderer = class _CanvasRenderer {
643
662
  const ang = Math.atan2(y2 - y1, x2 - x1);
644
663
  ctx.beginPath();
645
664
  ctx.moveTo(x2, y2);
646
- ctx.lineTo(
647
- x2 - s * Math.cos(ang - Math.PI / 6),
648
- y2 - s * Math.sin(ang - Math.PI / 6)
649
- );
650
- ctx.lineTo(
651
- x2 - s * Math.cos(ang + Math.PI / 6),
652
- y2 - s * Math.sin(ang + Math.PI / 6)
653
- );
665
+ ctx.lineTo(x2 - s * Math.cos(ang - Math.PI / 6), y2 - s * Math.sin(ang - Math.PI / 6));
666
+ ctx.lineTo(x2 - s * Math.cos(ang + Math.PI / 6), y2 - s * Math.sin(ang + Math.PI / 6));
654
667
  ctx.closePath();
655
668
  ctx.fill();
656
669
  }
@@ -710,7 +723,8 @@ const _CanvasRenderer = class _CanvasRenderer {
710
723
  time = performance.now(),
711
724
  dt = 0,
712
725
  groups = null,
713
- activeEdges = /* @__PURE__ */ new Set()
726
+ activeEdges = /* @__PURE__ */ new Set(),
727
+ drawEdges = true
714
728
  } = {}) {
715
729
  var _a, _b, _c, _d, _e, _f;
716
730
  graph.updateWorldTransforms();
@@ -722,37 +736,39 @@ const _CanvasRenderer = class _CanvasRenderer {
722
736
  if (n.type === "core/Group") {
723
737
  const sel = selection.has(n.id);
724
738
  const def = (_b = (_a = this.registry) == null ? void 0 : _a.types) == null ? void 0 : _b.get(n.type);
725
- if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
739
+ if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme, renderer: this });
726
740
  else this._drawNode(n, sel);
727
741
  }
728
742
  }
729
- ctx.lineWidth = 1.5 / this.scale;
730
- let dashArray = null;
731
- let dashOffset = 0;
732
- if (running) {
733
- const speed = 120;
734
- const phase = time / 1e3 * speed / this.scale % _CanvasRenderer.FONT_SIZE;
735
- dashArray = [6 / this.scale, 6 / this.scale];
736
- dashOffset = -phase;
737
- }
738
- for (const e of graph.edges.values()) {
739
- const shouldAnimate = activeEdges && activeEdges.size > 0 && activeEdges.has(e.id);
740
- if (running && shouldAnimate && dashArray) {
741
- ctx.setLineDash(dashArray);
742
- ctx.lineDashOffset = dashOffset;
743
- } else {
744
- ctx.setLineDash([]);
745
- ctx.lineDashOffset = 0;
746
- }
747
- const isActive = activeEdges && activeEdges.has(e.id);
748
- if (isActive) {
749
- ctx.strokeStyle = "#00ffff";
750
- ctx.lineWidth = 3 * this.scale;
751
- } else {
752
- ctx.strokeStyle = theme.edge;
753
- ctx.lineWidth = 1.5 / this.scale;
743
+ 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
+ }
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
+ const isActive = activeEdges && activeEdges.has(e.id);
763
+ if (isActive) {
764
+ ctx.strokeStyle = "#00ffff";
765
+ ctx.lineWidth = 3 / this.scale;
766
+ } else {
767
+ ctx.strokeStyle = theme.edge;
768
+ ctx.lineWidth = 1.5 / this.scale;
769
+ }
770
+ this._drawEdge(graph, e);
754
771
  }
755
- this._drawEdge(graph, e);
756
772
  }
757
773
  if (tempEdge) {
758
774
  const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
@@ -762,12 +778,18 @@ const _CanvasRenderer = class _CanvasRenderer {
762
778
  let ptsForArrow = null;
763
779
  if (this.edgeStyle === "line") {
764
780
  this._drawLine(a.x, a.y, b.x, b.y);
765
- ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
781
+ ptsForArrow = [
782
+ { x: a.x, y: a.y },
783
+ { x: b.x, y: b.y }
784
+ ];
766
785
  } else if (this.edgeStyle === "orthogonal") {
767
786
  ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
768
787
  } else {
769
788
  this._drawCurve(a.x, a.y, b.x, b.y);
770
- ptsForArrow = [{ x: a.x, y: a.y }, { x: b.x, y: b.y }];
789
+ ptsForArrow = [
790
+ { x: a.x, y: a.y },
791
+ { x: b.x, y: b.y }
792
+ ];
771
793
  }
772
794
  this.ctx.setLineDash(prevDash);
773
795
  if (ptsForArrow && ptsForArrow.length >= 2) {
@@ -783,8 +805,10 @@ const _CanvasRenderer = class _CanvasRenderer {
783
805
  const sel = selection.has(n.id);
784
806
  const def = (_d = (_c = this.registry) == null ? void 0 : _c.types) == null ? void 0 : _d.get(n.type);
785
807
  const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
786
- this._drawNode(n, sel, hasHtmlOverlay);
787
- if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme });
808
+ if (!hasHtmlOverlay) {
809
+ this._drawNode(n, sel, true);
810
+ if (def == null ? void 0 : def.onDraw) def.onDraw(n, { ctx, theme, renderer: this });
811
+ }
788
812
  }
789
813
  }
790
814
  for (const n of graph.nodes.values()) {
@@ -951,7 +975,7 @@ const _CanvasRenderer = class _CanvasRenderer {
951
975
  const iIn = to.inputs.findIndex((p) => p.id === e.toPort);
952
976
  const pr1 = portRect(from, null, iOut, "out");
953
977
  const pr2 = portRect(to, null, iIn, "in");
954
- const x1 = pr1.x, y1 = pr1.y + 7, x2 = pr2.x, y2 = pr2.y + 7;
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;
955
979
  if (this.edgeStyle === "line") {
956
980
  this._drawLine(x1, y1, x2, y2);
957
981
  } else if (this.edgeStyle === "orthogonal") {
@@ -971,8 +995,7 @@ const _CanvasRenderer = class _CanvasRenderer {
971
995
  const { ctx } = this;
972
996
  ctx.beginPath();
973
997
  ctx.moveTo(points[0].x, points[0].y);
974
- for (let i = 1; i < points.length; i++)
975
- ctx.lineTo(points[i].x, points[i].y);
998
+ for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
976
999
  ctx.stroke();
977
1000
  }
978
1001
  _drawOrthogonal(x1, y1, x2, y2) {
@@ -1003,6 +1026,72 @@ const _CanvasRenderer = class _CanvasRenderer {
1003
1026
  ctx.bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
1004
1027
  ctx.stroke();
1005
1028
  }
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 } = {}) {
1035
+ this._resetTransform();
1036
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1037
+ this._applyTransform();
1038
+ 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
+ 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;
1056
+ } else {
1057
+ ctx.setLineDash([]);
1058
+ ctx.strokeStyle = theme.edge;
1059
+ ctx.lineWidth = 1.5 / this.scale;
1060
+ }
1061
+ this._drawEdge(graph, e);
1062
+ }
1063
+ if (tempEdge) {
1064
+ const a = this.screenToWorld(tempEdge.x1, tempEdge.y1);
1065
+ const b = this.screenToWorld(tempEdge.x2, tempEdge.y2);
1066
+ const prevDash = this.ctx.getLineDash();
1067
+ this.ctx.setLineDash([6 / this.scale, 6 / this.scale]);
1068
+ let ptsForArrow = null;
1069
+ if (this.edgeStyle === "line") {
1070
+ 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
+ ];
1075
+ } else if (this.edgeStyle === "orthogonal") {
1076
+ ptsForArrow = this._drawOrthogonal(a.x, a.y, b.x, b.y);
1077
+ } else {
1078
+ 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
+ ];
1083
+ }
1084
+ this.ctx.setLineDash(prevDash);
1085
+ if (ptsForArrow && ptsForArrow.length >= 2) {
1086
+ const p1 = ptsForArrow[ptsForArrow.length - 2];
1087
+ 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);
1091
+ }
1092
+ }
1093
+ this._resetTransform();
1094
+ }
1006
1095
  };
1007
1096
  __publicField(_CanvasRenderer, "FONT_SIZE", 12);
1008
1097
  __publicField(_CanvasRenderer, "SELECTED_NODE_COLOR", "#6cf");
@@ -1126,12 +1215,13 @@ class CommandStack {
1126
1215
  }
1127
1216
  }
1128
1217
  const _Controller = class _Controller {
1129
- constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, portRenderer }) {
1218
+ constructor({ graph, renderer, hooks, htmlOverlay, contextMenu, edgeRenderer, portRenderer }) {
1130
1219
  this.graph = graph;
1131
1220
  this.renderer = renderer;
1132
1221
  this.hooks = hooks;
1133
1222
  this.htmlOverlay = htmlOverlay;
1134
1223
  this.contextMenu = contextMenu;
1224
+ this.edgeRenderer = edgeRenderer;
1135
1225
  this.portRenderer = portRenderer;
1136
1226
  this.stack = new CommandStack();
1137
1227
  this.selection = /* @__PURE__ */ new Set();
@@ -1282,13 +1372,11 @@ const _Controller = class _Controller {
1282
1372
  for (const n of this.graph.nodes.values()) {
1283
1373
  for (let i = 0; i < n.inputs.length; i++) {
1284
1374
  const r = portRect(n, n.inputs[i], i, "in");
1285
- if (rectHas(r, x, y))
1286
- return { node: n, port: n.inputs[i], dir: "in", idx: i };
1375
+ if (rectHas(r, x, y)) return { node: n, port: n.inputs[i], dir: "in", idx: i };
1287
1376
  }
1288
1377
  for (let i = 0; i < n.outputs.length; i++) {
1289
1378
  const r = portRect(n, n.outputs[i], i, "out");
1290
- if (rectHas(r, x, y))
1291
- return { node: n, port: n.outputs[i], dir: "out", idx: i };
1379
+ if (rectHas(r, x, y)) return { node: n, port: n.outputs[i], dir: "out", idx: i };
1292
1380
  }
1293
1381
  }
1294
1382
  return null;
@@ -1537,13 +1625,7 @@ const _Controller = class _Controller {
1537
1625
  const portIn = this._findPortAtWorld(w.x, w.y);
1538
1626
  if (portIn && portIn.dir === "in") {
1539
1627
  this.stack.exec(
1540
- AddEdgeCmd(
1541
- this.graph,
1542
- from.fromNode,
1543
- from.fromPort,
1544
- portIn.node.id,
1545
- portIn.port.id
1546
- )
1628
+ AddEdgeCmd(this.graph, from.fromNode, from.fromPort, portIn.node.id, portIn.port.id)
1547
1629
  );
1548
1630
  }
1549
1631
  this.connecting = null;
@@ -1711,16 +1793,31 @@ const _Controller = class _Controller {
1711
1793
  this.render();
1712
1794
  }
1713
1795
  render() {
1714
- var _a, _b, _c;
1796
+ var _a;
1715
1797
  const tEdge = this.renderTempEdge();
1716
1798
  this.renderer.draw(this.graph, {
1717
1799
  selection: this.selection,
1718
- tempEdge: tEdge,
1800
+ tempEdge: null,
1801
+ // Don't draw temp edge on background
1719
1802
  boxSelecting: this.boxSelecting,
1720
- activeEdges: this.activeEdges || /* @__PURE__ */ new Set()
1721
- // For animation
1803
+ activeEdges: this.activeEdges || /* @__PURE__ */ new Set(),
1804
+ drawEdges: !this.edgeRenderer
1805
+ // Only draw edges here if no separate edge renderer
1722
1806
  });
1723
1807
  (_a = this.htmlOverlay) == null ? void 0 : _a.draw(this.graph, this.selection);
1808
+ if (this.edgeRenderer) {
1809
+ const edgeCtx = this.edgeRenderer.ctx;
1810
+ edgeCtx.clearRect(0, 0, this.edgeRenderer.canvas.width, this.edgeRenderer.canvas.height);
1811
+ this.edgeRenderer._applyTransform();
1812
+ 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
1818
+ });
1819
+ this.edgeRenderer._resetTransform();
1820
+ }
1724
1821
  if (this.boxSelecting) {
1725
1822
  const { startX, startY, currentX, currentY } = this.boxSelecting;
1726
1823
  const minX = Math.min(startX, currentX);
@@ -1729,14 +1826,28 @@ const _Controller = class _Controller {
1729
1826
  const height = Math.abs(currentY - startY);
1730
1827
  const screenStart = this.renderer.worldToScreen(minX, minY);
1731
1828
  const screenEnd = this.renderer.worldToScreen(minX + width, minY + height);
1732
- const ctx = this.renderer.ctx;
1829
+ const ctx = this.edgeRenderer ? this.edgeRenderer.ctx : this.renderer.ctx;
1733
1830
  ctx.save();
1734
- this.renderer._resetTransform();
1831
+ if (this.edgeRenderer) {
1832
+ this.edgeRenderer._resetTransform();
1833
+ } else {
1834
+ this.renderer._resetTransform();
1835
+ }
1735
1836
  ctx.strokeStyle = "#6cf";
1736
1837
  ctx.fillStyle = "rgba(102, 204, 255, 0.1)";
1737
1838
  ctx.lineWidth = 2;
1738
- ctx.strokeRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
1739
- ctx.fillRect(screenStart.x, screenStart.y, screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
1839
+ ctx.strokeRect(
1840
+ screenStart.x,
1841
+ screenStart.y,
1842
+ screenEnd.x - screenStart.x,
1843
+ screenEnd.y - screenStart.y
1844
+ );
1845
+ ctx.fillRect(
1846
+ screenStart.x,
1847
+ screenStart.y,
1848
+ screenEnd.x - screenStart.x,
1849
+ screenEnd.y - screenStart.y
1850
+ );
1740
1851
  ctx.restore();
1741
1852
  }
1742
1853
  if (this.portRenderer) {
@@ -1748,11 +1859,7 @@ const _Controller = class _Controller {
1748
1859
  this.portRenderer._applyTransform();
1749
1860
  for (const n of this.graph.nodes.values()) {
1750
1861
  if (n.type !== "core/Group") {
1751
- const def = (_c = (_b = this.portRenderer.registry) == null ? void 0 : _b.types) == null ? void 0 : _c.get(n.type);
1752
- const hasHtmlOverlay = !!(def == null ? void 0 : def.html);
1753
- if (hasHtmlOverlay) {
1754
- this.portRenderer._drawPorts(n);
1755
- }
1862
+ this.portRenderer._drawPorts(n);
1756
1863
  }
1757
1864
  }
1758
1865
  this.portRenderer._resetTransform();
@@ -1760,10 +1867,7 @@ const _Controller = class _Controller {
1760
1867
  }
1761
1868
  renderTempEdge() {
1762
1869
  if (!this.connecting) return null;
1763
- const a = this._portAnchorScreen(
1764
- this.connecting.fromNode,
1765
- this.connecting.fromPort
1766
- );
1870
+ const a = this._portAnchorScreen(this.connecting.fromNode, this.connecting.fromPort);
1767
1871
  return {
1768
1872
  x1: a.x,
1769
1873
  y1: a.y,
@@ -1775,7 +1879,7 @@ const _Controller = class _Controller {
1775
1879
  const n = this.graph.nodes.get(nodeId);
1776
1880
  const iOut = n.outputs.findIndex((p) => p.id === portId);
1777
1881
  const r = portRect(n, null, iOut, "out");
1778
- return this.renderer.worldToScreen(r.x, r.y + 7);
1882
+ return this.renderer.worldToScreen(r.x + r.w / 2, r.y + r.h / 2);
1779
1883
  }
1780
1884
  };
1781
1885
  __publicField(_Controller, "MIN_NODE_WIDTH", 80);
@@ -1964,7 +2068,8 @@ class ContextMenu {
1964
2068
  itemEl._hideTimeout = null;
1965
2069
  }
1966
2070
  if (item.submenu) {
1967
- this._showSubmenu(item.submenu, itemEl);
2071
+ const submenuItems = typeof item.submenu === "function" ? item.submenu() : item.submenu;
2072
+ this._showSubmenu(submenuItems, itemEl);
1968
2073
  }
1969
2074
  });
1970
2075
  itemEl.addEventListener("mouseleave", (e) => {
@@ -2142,6 +2247,7 @@ class Runner {
2142
2247
  }
2143
2248
  /**
2144
2249
  * Execute connected nodes once from a starting node
2250
+ * Uses queue-based traversal to support branching exec flows
2145
2251
  * @param {string} startNodeId - ID of the node to start from
2146
2252
  * @param {number} dt - Delta time
2147
2253
  */
@@ -2149,12 +2255,16 @@ class Runner {
2149
2255
  console.log("[Runner.runOnce] Starting exec flow from node:", startNodeId);
2150
2256
  const executedNodes = [];
2151
2257
  const allConnectedNodes = /* @__PURE__ */ new Set();
2152
- let currentNodeId = startNodeId;
2153
- while (currentNodeId) {
2258
+ const queue = [startNodeId];
2259
+ const visited = /* @__PURE__ */ new Set();
2260
+ while (queue.length > 0) {
2261
+ const currentNodeId = queue.shift();
2262
+ if (visited.has(currentNodeId)) continue;
2263
+ visited.add(currentNodeId);
2154
2264
  const node = this.graph.nodes.get(currentNodeId);
2155
2265
  if (!node) {
2156
2266
  console.warn(`[Runner.runOnce] Node not found: ${currentNodeId}`);
2157
- break;
2267
+ continue;
2158
2268
  }
2159
2269
  executedNodes.push(currentNodeId);
2160
2270
  allConnectedNodes.add(currentNodeId);
@@ -2173,7 +2283,8 @@ class Runner {
2173
2283
  }
2174
2284
  }
2175
2285
  this.executeNode(currentNodeId, dt);
2176
- currentNodeId = this.findNextExecNode(currentNodeId);
2286
+ const nextNodes = this.findAllNextExecNodes(currentNodeId);
2287
+ queue.push(...nextNodes);
2177
2288
  }
2178
2289
  console.log("[Runner.runOnce] Executed nodes:", executedNodes.length);
2179
2290
  const connectedEdges = /* @__PURE__ */ new Set();
@@ -2186,21 +2297,25 @@ class Runner {
2186
2297
  return { connectedNodes: allConnectedNodes, connectedEdges };
2187
2298
  }
2188
2299
  /**
2189
- * Find the next node to execute by following exec output
2300
+ * Find all nodes connected via exec outputs
2301
+ * Supports multiple connections from a single exec output
2190
2302
  * @param {string} nodeId - Current node ID
2191
- * @returns {string|null} Next node ID or null
2303
+ * @returns {string[]} Array of next node IDs
2192
2304
  */
2193
- findNextExecNode(nodeId) {
2305
+ findAllNextExecNodes(nodeId) {
2194
2306
  const node = this.graph.nodes.get(nodeId);
2195
- if (!node) return null;
2196
- const execOutput = node.outputs.find((p) => p.portType === "exec");
2197
- if (!execOutput) return null;
2198
- for (const edge of this.graph.edges.values()) {
2199
- if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
2200
- return edge.toNode;
2307
+ if (!node) return [];
2308
+ const execOutputs = node.outputs.filter((p) => p.portType === "exec");
2309
+ if (execOutputs.length === 0) return [];
2310
+ const nextNodes = [];
2311
+ for (const execOutput of execOutputs) {
2312
+ for (const edge of this.graph.edges.values()) {
2313
+ if (edge.fromNode === nodeId && edge.fromPort === execOutput.id) {
2314
+ nextNodes.push(edge.toNode);
2315
+ }
2201
2316
  }
2202
2317
  }
2203
- return null;
2318
+ return nextNodes;
2204
2319
  }
2205
2320
  /**
2206
2321
  * Execute a single node
@@ -2331,7 +2446,7 @@ class HtmlOverlay {
2331
2446
  return container;
2332
2447
  }
2333
2448
  /** 노드용 엘리먼트 생성(한 번만) */
2334
- _ensureNodeElement(node, def) {
2449
+ _ensureNodeElement(node, def, graph) {
2335
2450
  var _a;
2336
2451
  let el = this.nodes.get(node.id);
2337
2452
  if (!el) {
@@ -2340,7 +2455,7 @@ class HtmlOverlay {
2340
2455
  } else if (def.html) {
2341
2456
  el = this._createDefaultNodeLayout(node);
2342
2457
  if (def.html.init) {
2343
- def.html.init(node, el, el._domParts);
2458
+ def.html.init(node, el, { ...el._domParts, graph });
2344
2459
  }
2345
2460
  } else {
2346
2461
  return null;
@@ -2363,7 +2478,7 @@ class HtmlOverlay {
2363
2478
  const def = this.registry.types.get(node.type);
2364
2479
  const hasHtml = !!(def == null ? void 0 : def.html);
2365
2480
  if (!hasHtml) continue;
2366
- const el = this._ensureNodeElement(node, def);
2481
+ const el = this._ensureNodeElement(node, def, graph);
2367
2482
  if (!el) continue;
2368
2483
  el.style.left = `${node.computed.x}px`;
2369
2484
  el.style.top = `${node.computed.y}px`;
@@ -2386,6 +2501,15 @@ class HtmlOverlay {
2386
2501
  }
2387
2502
  }
2388
2503
  }
2504
+ /**
2505
+ * Sync container transform with renderer state (lightweight update)
2506
+ * Called when zoom/pan occurs without needing full redraw
2507
+ */
2508
+ syncTransform() {
2509
+ const { scale, offsetX, offsetY } = this.renderer;
2510
+ this.container.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
2511
+ this.container.style.transformOrigin = "0 0";
2512
+ }
2389
2513
  clear() {
2390
2514
  for (const [, el] of this.nodes) {
2391
2515
  el.remove();
@@ -2725,14 +2849,87 @@ class PropertyPanel {
2725
2849
  }
2726
2850
  }
2727
2851
  }
2852
+ function setupDefaultContextMenu(contextMenu, { controller, graph, hooks }) {
2853
+ const getNodeTypes = () => {
2854
+ const nodeTypes = [];
2855
+ for (const [key, typeDef] of graph.registry.types.entries()) {
2856
+ nodeTypes.push({
2857
+ id: `add-${key}`,
2858
+ label: typeDef.title || key,
2859
+ action: () => {
2860
+ const worldPos = contextMenu.worldPosition || { x: 100, y: 100 };
2861
+ const node = graph.addNode(key, {
2862
+ x: worldPos.x,
2863
+ y: worldPos.y
2864
+ });
2865
+ hooks == null ? void 0 : hooks.emit("node:updated", node);
2866
+ controller.render();
2867
+ }
2868
+ });
2869
+ }
2870
+ return nodeTypes;
2871
+ };
2872
+ contextMenu.addItem("add-node", "Add Node", {
2873
+ condition: (target) => !target,
2874
+ submenu: getNodeTypes,
2875
+ // Pass function instead of array
2876
+ order: 5
2877
+ });
2878
+ contextMenu.addItem("delete-node", "Delete Node", {
2879
+ condition: (target) => target && target.type !== "core/Group",
2880
+ action: (target) => {
2881
+ const cmd = RemoveNodeCmd(graph, target);
2882
+ controller.stack.exec(cmd);
2883
+ hooks == null ? void 0 : hooks.emit("node:updated", target);
2884
+ },
2885
+ order: 10
2886
+ });
2887
+ const colors = [
2888
+ { name: "Default", color: "#39424e" },
2889
+ { name: "Slate", color: "#4a5568" },
2890
+ { name: "Gray", color: "#2d3748" },
2891
+ { name: "Blue", color: "#1a365d" },
2892
+ { name: "Green", color: "#22543d" },
2893
+ { name: "Red", color: "#742a2a" },
2894
+ { name: "Purple", color: "#44337a" }
2895
+ ];
2896
+ contextMenu.addItem("change-group-color", "Change Color", {
2897
+ condition: (target) => target && target.type === "core/Group",
2898
+ submenu: colors.map((colorInfo) => ({
2899
+ id: `color-${colorInfo.color}`,
2900
+ label: colorInfo.name,
2901
+ color: colorInfo.color,
2902
+ action: (target) => {
2903
+ const currentColor = target.state.color || "#39424e";
2904
+ const cmd = ChangeGroupColorCmd(target, currentColor, colorInfo.color);
2905
+ controller.stack.exec(cmd);
2906
+ hooks == null ? void 0 : hooks.emit("node:updated", target);
2907
+ }
2908
+ })),
2909
+ order: 20
2910
+ });
2911
+ contextMenu.addItem("delete-group", "Delete Group", {
2912
+ condition: (target) => target && target.type === "core/Group",
2913
+ action: (target) => {
2914
+ const cmd = RemoveNodeCmd(graph, target);
2915
+ controller.stack.exec(cmd);
2916
+ hooks == null ? void 0 : hooks.emit("node:updated", target);
2917
+ },
2918
+ order: 20
2919
+ });
2920
+ }
2728
2921
  function createGraphEditor(target, {
2729
2922
  theme,
2730
2923
  hooks: customHooks,
2731
2924
  autorun = true,
2732
2925
  showMinimap = true,
2733
2926
  enablePropertyPanel = true,
2734
- propertyPanelContainer = null
2927
+ propertyPanelContainer = null,
2928
+ setupDefaultContextMenu: setupDefaultContextMenu$1 = true,
2929
+ setupContextMenu = null,
2930
+ plugins = []
2735
2931
  } = {}) {
2932
+ var _a;
2736
2933
  let canvas;
2737
2934
  let container;
2738
2935
  if (typeof target === "string") {
@@ -2780,6 +2977,46 @@ function createGraphEditor(target, {
2780
2977
  const graph = new Graph({ hooks, registry });
2781
2978
  const renderer = new CanvasRenderer(canvas, { theme, registry });
2782
2979
  const htmlOverlay = new HtmlOverlay(canvas.parentElement, renderer, registry);
2980
+ renderer.setTransformChangeCallback(() => {
2981
+ htmlOverlay.syncTransform();
2982
+ });
2983
+ const edgeCanvas = document.createElement("canvas");
2984
+ edgeCanvas.id = "edge-canvas";
2985
+ Object.assign(edgeCanvas.style, {
2986
+ position: "absolute",
2987
+ top: "0",
2988
+ left: "0",
2989
+ pointerEvents: "none",
2990
+ // Pass through clicks
2991
+ zIndex: "15"
2992
+ // Above HTML overlay (10), below port canvas (20)
2993
+ });
2994
+ canvas.parentElement.appendChild(edgeCanvas);
2995
+ const edgeRenderer = new CanvasRenderer(edgeCanvas, { theme, registry });
2996
+ Object.defineProperty(edgeRenderer, "scale", {
2997
+ get() {
2998
+ return renderer.scale;
2999
+ },
3000
+ set(v) {
3001
+ renderer.scale = v;
3002
+ }
3003
+ });
3004
+ Object.defineProperty(edgeRenderer, "offsetX", {
3005
+ get() {
3006
+ return renderer.offsetX;
3007
+ },
3008
+ set(v) {
3009
+ renderer.offsetX = v;
3010
+ }
3011
+ });
3012
+ Object.defineProperty(edgeRenderer, "offsetY", {
3013
+ get() {
3014
+ return renderer.offsetY;
3015
+ },
3016
+ set(v) {
3017
+ renderer.offsetY = v;
3018
+ }
3019
+ });
2783
3020
  const portCanvas = document.createElement("canvas");
2784
3021
  portCanvas.id = "port-canvas";
2785
3022
  Object.assign(portCanvas.style, {
@@ -2789,7 +3026,7 @@ function createGraphEditor(target, {
2789
3026
  pointerEvents: "none",
2790
3027
  // Pass through clicks
2791
3028
  zIndex: "20"
2792
- // Above HTML overlay (z-index 10)
3029
+ // Above edge canvas (15)
2793
3030
  });
2794
3031
  canvas.parentElement.appendChild(portCanvas);
2795
3032
  const portRenderer = new CanvasRenderer(portCanvas, { theme, registry });
@@ -2797,7 +3034,7 @@ function createGraphEditor(target, {
2797
3034
  portRenderer.scale = renderer.scale;
2798
3035
  portRenderer.offsetX = renderer.offsetX;
2799
3036
  portRenderer.offsetY = renderer.offsetY;
2800
- const controller = new Controller({ graph, renderer, hooks, htmlOverlay, portRenderer });
3037
+ const controller = new Controller({ graph, renderer, hooks, htmlOverlay, edgeRenderer, portRenderer });
2801
3038
  const contextMenu = new ContextMenu({
2802
3039
  graph,
2803
3040
  hooks,
@@ -2822,6 +3059,8 @@ function createGraphEditor(target, {
2822
3059
  });
2823
3060
  }
2824
3061
  const runner = new Runner({ graph, registry, hooks });
3062
+ graph.runner = runner;
3063
+ graph.controller = controller;
2825
3064
  hooks.on("runner:tick", ({ time, dt }) => {
2826
3065
  renderer.draw(graph, {
2827
3066
  selection: controller.selection,
@@ -2856,695 +3095,33 @@ function createGraphEditor(target, {
2856
3095
  hooks.on("node:updated", () => {
2857
3096
  controller.render();
2858
3097
  });
2859
- registry.register("core/Note", {
2860
- title: "Note",
2861
- size: { w: 180, h: 80 },
2862
- inputs: [{ name: "in", datatype: "any" }],
2863
- outputs: [{ name: "out", datatype: "any" }],
2864
- onCreate(node) {
2865
- node.state.text = "hello";
2866
- },
2867
- onExecute(node, { dt, getInput, setOutput }) {
2868
- const incoming = getInput("in");
2869
- const out = (incoming ?? node.state.text ?? "").toString().toUpperCase();
2870
- setOutput(
2871
- "out",
2872
- out + ` · ${Math.floor(performance.now() / 1e3 % 100)}`
2873
- );
2874
- },
2875
- onDraw(node, { ctx, theme: theme2 }) {
2876
- const { x, y } = node.pos;
2877
- const { width: w } = node.size;
2878
- }
2879
- });
2880
- registry.register("core/HtmlNote", {
2881
- title: "HTML Note",
2882
- size: { w: 200, h: 150 },
2883
- inputs: [{ name: "in", datatype: "any" }],
2884
- outputs: [{ name: "out", datatype: "any" }],
2885
- // HTML Overlay Configuration
2886
- html: {
2887
- // 초기화: 헤더/바디 구성
2888
- init(node, el, { header, body }) {
2889
- el.style.backgroundColor = "#222";
2890
- el.style.borderRadius = "8px";
2891
- el.style.border = "1px solid #444";
2892
- el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
2893
- header.style.backgroundColor = "#333";
2894
- header.style.borderBottom = "1px solid #444";
2895
- header.style.color = "#eee";
2896
- header.style.fontSize = "12px";
2897
- header.style.fontWeight = "bold";
2898
- header.textContent = "My HTML Node";
2899
- body.style.padding = "8px";
2900
- body.style.color = "#ccc";
2901
- body.style.fontSize = "12px";
2902
- const contentDiv = document.createElement("div");
2903
- contentDiv.textContent = "Event Name";
2904
- body.appendChild(contentDiv);
2905
- const input = document.createElement("input");
2906
- Object.assign(input.style, {
2907
- marginTop: "4px",
2908
- padding: "4px",
2909
- background: "#111",
2910
- border: "1px solid #555",
2911
- color: "#fff",
2912
- borderRadius: "4px",
2913
- pointerEvents: "auto"
2914
- });
2915
- input.placeholder = "Type here...";
2916
- input.addEventListener("input", (e) => {
2917
- node.state.text = e.target.value;
2918
- });
2919
- input.addEventListener("mousedown", (e) => e.stopPropagation());
2920
- body.appendChild(input);
2921
- el._input = input;
2922
- },
2923
- // 매 프레임(또는 필요시) 업데이트
2924
- update(node, el, { header, _body, selected }) {
2925
- el.style.borderColor = selected ? "#6cf" : "#444";
2926
- header.style.backgroundColor = selected ? "#3a4a5a" : "#333";
2927
- if (el._input.value !== (node.state.text || "")) {
2928
- el._input.value = node.state.text || "";
3098
+ if (setupDefaultContextMenu$1) {
3099
+ setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3100
+ }
3101
+ if (setupContextMenu) {
3102
+ setupContextMenu(contextMenu, { controller, graph, hooks });
3103
+ }
3104
+ if (plugins && plugins.length > 0) {
3105
+ for (const plugin of plugins) {
3106
+ if (typeof plugin.install === "function") {
3107
+ try {
3108
+ plugin.install({ graph, registry, hooks, runner, controller, contextMenu }, plugin.options || {});
3109
+ } catch (err) {
3110
+ console.error(`[createGraphEditor] Failed to install plugin "${plugin.name || "unknown"}":`, err);
3111
+ (_a = hooks == null ? void 0 : hooks.emit) == null ? void 0 : _a.call(hooks, "error", err);
2929
3112
  }
3113
+ } else {
3114
+ console.warn(`[createGraphEditor] Plugin "${plugin.name || "unknown"}" does not have an install() method`);
2930
3115
  }
2931
- },
2932
- onCreate(node) {
2933
- node.state.text = "";
2934
- },
2935
- onExecute(node, { getInput, setOutput }) {
2936
- const incoming = getInput("in");
2937
- setOutput("out", incoming);
2938
- }
2939
- // onDraw는 생략 가능 (HTML이 덮으니까)
2940
- // 하지만 포트 등은 그려야 할 수도 있음.
2941
- // 현재 구조상 CanvasRenderer가 기본 노드를 그리므로,
2942
- // 투명하게 하거나 겹쳐서 그릴 수 있음.
2943
- });
2944
- registry.register("core/TodoNode", {
2945
- title: "Todo List",
2946
- size: { w: 240, h: 300 },
2947
- inputs: [{ name: "in", datatype: "any" }],
2948
- outputs: [{ name: "out", datatype: "any" }],
2949
- html: {
2950
- init(node, el, { header, body }) {
2951
- el.style.backgroundColor = "#1e1e24";
2952
- el.style.borderRadius = "8px";
2953
- el.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)";
2954
- el.style.border = "1px solid #333";
2955
- header.style.backgroundColor = "#2a2a31";
2956
- header.style.padding = "8px";
2957
- header.style.fontWeight = "bold";
2958
- header.style.color = "#e9e9ef";
2959
- header.textContent = node.title;
2960
- body.style.display = "flex";
2961
- body.style.flexDirection = "column";
2962
- body.style.padding = "8px";
2963
- body.style.color = "#e9e9ef";
2964
- const inputRow = document.createElement("div");
2965
- Object.assign(inputRow.style, { display: "flex", gap: "4px", marginBottom: "8px" });
2966
- const input = document.createElement("input");
2967
- Object.assign(input.style, {
2968
- flex: "1",
2969
- padding: "6px",
2970
- borderRadius: "4px",
2971
- border: "1px solid #444",
2972
- background: "#141417",
2973
- color: "#fff",
2974
- pointerEvents: "auto"
2975
- });
2976
- input.placeholder = "Add task...";
2977
- const addBtn = document.createElement("button");
2978
- addBtn.textContent = "+";
2979
- Object.assign(addBtn.style, {
2980
- padding: "0 12px",
2981
- cursor: "pointer",
2982
- background: "#4f5b66",
2983
- color: "#fff",
2984
- border: "none",
2985
- borderRadius: "4px",
2986
- pointerEvents: "auto"
2987
- });
2988
- inputRow.append(input, addBtn);
2989
- const list = document.createElement("ul");
2990
- Object.assign(list.style, {
2991
- listStyle: "none",
2992
- padding: "0",
2993
- margin: "0",
2994
- overflow: "hidden",
2995
- flex: "1"
2996
- });
2997
- body.append(inputRow, list);
2998
- const addTodo = () => {
2999
- const text = input.value.trim();
3000
- if (!text) return;
3001
- const todos = node.state.todos || [];
3002
- node.state.todos = [...todos, { id: Date.now(), text, done: false }];
3003
- input.value = "";
3004
- hooks.emit("node:updated", node);
3005
- };
3006
- addBtn.onclick = addTodo;
3007
- input.onkeydown = (e) => {
3008
- if (e.key === "Enter") addTodo();
3009
- e.stopPropagation();
3010
- };
3011
- input.onmousedown = (e) => e.stopPropagation();
3012
- el._refs = { list };
3013
- },
3014
- update(node, el, { selected }) {
3015
- el.style.borderColor = selected ? "#6cf" : "#333";
3016
- const { list } = el._refs;
3017
- const todos = node.state.todos || [];
3018
- list.innerHTML = "";
3019
- todos.forEach((todo) => {
3020
- const li = document.createElement("li");
3021
- Object.assign(li.style, {
3022
- display: "flex",
3023
- alignItems: "center",
3024
- padding: "6px 0",
3025
- borderBottom: "1px solid #2a2a31"
3026
- });
3027
- const chk = document.createElement("input");
3028
- chk.type = "checkbox";
3029
- chk.checked = todo.done;
3030
- chk.style.marginRight = "8px";
3031
- chk.style.pointerEvents = "auto";
3032
- chk.onchange = () => {
3033
- todo.done = chk.checked;
3034
- hooks.emit("node:updated", node);
3035
- };
3036
- chk.onmousedown = (e) => e.stopPropagation();
3037
- const span = document.createElement("span");
3038
- span.textContent = todo.text;
3039
- span.style.flex = "1";
3040
- span.style.textDecoration = todo.done ? "line-through" : "none";
3041
- span.style.color = todo.done ? "#777" : "#eee";
3042
- const del = document.createElement("button");
3043
- del.textContent = "×";
3044
- Object.assign(del.style, {
3045
- background: "none",
3046
- border: "none",
3047
- color: "#f44",
3048
- cursor: "pointer",
3049
- fontSize: "16px",
3050
- pointerEvents: "auto"
3051
- });
3052
- del.onclick = () => {
3053
- node.state.todos = node.state.todos.filter((t) => t.id !== todo.id);
3054
- hooks.emit("node:updated", node);
3055
- };
3056
- del.onmousedown = (e) => e.stopPropagation();
3057
- li.append(chk, span, del);
3058
- list.appendChild(li);
3059
- });
3060
- }
3061
- },
3062
- onCreate(node) {
3063
- node.state.todos = [
3064
- { id: 1, text: "Welcome to Free Node", done: false },
3065
- { id: 2, text: "Try adding a task", done: true }
3066
- ];
3067
- }
3068
- });
3069
- registry.register("math/Add", {
3070
- title: "Add",
3071
- size: { w: 140, h: 100 },
3072
- inputs: [
3073
- { name: "exec", portType: "exec" },
3074
- { name: "a", portType: "data", datatype: "number" },
3075
- { name: "b", portType: "data", datatype: "number" }
3076
- ],
3077
- outputs: [
3078
- { name: "exec", portType: "exec" },
3079
- { name: "result", portType: "data", datatype: "number" }
3080
- ],
3081
- onCreate(node) {
3082
- node.state.a = 0;
3083
- node.state.b = 0;
3084
- },
3085
- onExecute(node, { getInput, setOutput }) {
3086
- const a = getInput("a") ?? 0;
3087
- const b = getInput("b") ?? 0;
3088
- const result = a + b;
3089
- console.log("[Add] a:", a, "b:", b, "result:", result);
3090
- setOutput("result", result);
3091
- }
3092
- });
3093
- registry.register("math/Subtract", {
3094
- title: "Subtract",
3095
- size: { w: 140, h: 80 },
3096
- inputs: [
3097
- { name: "a", datatype: "number" },
3098
- { name: "b", datatype: "number" }
3099
- ],
3100
- outputs: [{ name: "result", datatype: "number" }],
3101
- onExecute(node, { getInput, setOutput }) {
3102
- const a = getInput("a") ?? 0;
3103
- const b = getInput("b") ?? 0;
3104
- setOutput("result", a - b);
3105
- }
3106
- });
3107
- registry.register("math/Multiply", {
3108
- title: "Multiply",
3109
- size: { w: 140, h: 100 },
3110
- inputs: [
3111
- { name: "exec", portType: "exec" },
3112
- { name: "a", portType: "data", datatype: "number" },
3113
- { name: "b", portType: "data", datatype: "number" }
3114
- ],
3115
- outputs: [
3116
- { name: "exec", portType: "exec" },
3117
- { name: "result", portType: "data", datatype: "number" }
3118
- ],
3119
- onExecute(node, { getInput, setOutput }) {
3120
- const a = getInput("a") ?? 0;
3121
- const b = getInput("b") ?? 0;
3122
- const result = a * b;
3123
- console.log("[Multiply] a:", a, "b:", b, "result:", result);
3124
- setOutput("result", result);
3125
- }
3126
- });
3127
- registry.register("math/Divide", {
3128
- title: "Divide",
3129
- size: { w: 140, h: 80 },
3130
- inputs: [
3131
- { name: "a", datatype: "number" },
3132
- { name: "b", datatype: "number" }
3133
- ],
3134
- outputs: [{ name: "result", datatype: "number" }],
3135
- onExecute(node, { getInput, setOutput }) {
3136
- const a = getInput("a") ?? 0;
3137
- const b = getInput("b") ?? 1;
3138
- setOutput("result", b !== 0 ? a / b : 0);
3139
- }
3140
- });
3141
- registry.register("logic/AND", {
3142
- title: "AND",
3143
- size: { w: 120, h: 100 },
3144
- inputs: [
3145
- { name: "exec", portType: "exec" },
3146
- { name: "a", portType: "data", datatype: "boolean" },
3147
- { name: "b", portType: "data", datatype: "boolean" }
3148
- ],
3149
- outputs: [
3150
- { name: "exec", portType: "exec" },
3151
- { name: "result", portType: "data", datatype: "boolean" }
3152
- ],
3153
- onExecute(node, { getInput, setOutput }) {
3154
- const a = getInput("a") ?? false;
3155
- const b = getInput("b") ?? false;
3156
- console.log("[AND] Inputs - a:", a, "b:", b);
3157
- const result = a && b;
3158
- console.log("[AND] Result:", result);
3159
- setOutput("result", result);
3160
- }
3161
- });
3162
- registry.register("logic/OR", {
3163
- title: "OR",
3164
- size: { w: 120, h: 80 },
3165
- inputs: [
3166
- { name: "a", datatype: "boolean" },
3167
- { name: "b", datatype: "boolean" }
3168
- ],
3169
- outputs: [{ name: "result", datatype: "boolean" }],
3170
- onExecute(node, { getInput, setOutput }) {
3171
- const a = getInput("a") ?? false;
3172
- const b = getInput("b") ?? false;
3173
- setOutput("result", a || b);
3174
- }
3175
- });
3176
- registry.register("logic/NOT", {
3177
- title: "NOT",
3178
- size: { w: 120, h: 70 },
3179
- inputs: [{ name: "in", datatype: "boolean" }],
3180
- outputs: [{ name: "out", datatype: "boolean" }],
3181
- onExecute(node, { getInput, setOutput }) {
3182
- const val = getInput("in") ?? false;
3183
- setOutput("out", !val);
3184
- }
3185
- });
3186
- registry.register("value/Number", {
3187
- title: "Number",
3188
- size: { w: 140, h: 60 },
3189
- outputs: [{ name: "value", portType: "data", datatype: "number" }],
3190
- onCreate(node) {
3191
- node.state.value = 0;
3192
- },
3193
- onExecute(node, { setOutput }) {
3194
- console.log("[Number] Outputting value:", node.state.value ?? 0);
3195
- setOutput("value", node.state.value ?? 0);
3196
- },
3197
- html: {
3198
- init(node, el, { header, body }) {
3199
- el.style.backgroundColor = "#1e1e24";
3200
- el.style.border = "1px solid #444";
3201
- el.style.borderRadius = "8px";
3202
- header.style.backgroundColor = "#2a2a31";
3203
- header.style.borderBottom = "1px solid #444";
3204
- header.style.color = "#eee";
3205
- header.style.fontSize = "12px";
3206
- header.textContent = "Number";
3207
- body.style.padding = "12px";
3208
- body.style.display = "flex";
3209
- body.style.alignItems = "center";
3210
- body.style.justifyContent = "center";
3211
- const input = document.createElement("input");
3212
- input.type = "number";
3213
- input.value = node.state.value ?? 0;
3214
- Object.assign(input.style, {
3215
- width: "100%",
3216
- padding: "6px",
3217
- background: "#141417",
3218
- border: "1px solid #444",
3219
- borderRadius: "4px",
3220
- color: "#fff",
3221
- fontSize: "14px",
3222
- textAlign: "center",
3223
- pointerEvents: "auto"
3224
- });
3225
- input.addEventListener("change", (e) => {
3226
- node.state.value = parseFloat(e.target.value) || 0;
3227
- });
3228
- input.addEventListener("mousedown", (e) => e.stopPropagation());
3229
- input.addEventListener("keydown", (e) => e.stopPropagation());
3230
- body.appendChild(input);
3231
- },
3232
- update(node, el, { header, body, selected }) {
3233
- el.style.borderColor = selected ? "#6cf" : "#444";
3234
- header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
3235
- }
3236
- },
3237
- onDraw(node, { ctx, theme: theme2 }) {
3238
- const { x, y } = node.computed;
3239
- ctx.fillStyle = "#8f8";
3240
- ctx.font = "14px sans-serif";
3241
- ctx.textAlign = "center";
3242
- ctx.fillText(String(node.state.value ?? 0), x + 70, y + 42);
3243
- }
3244
- });
3245
- registry.register("value/String", {
3246
- title: "String",
3247
- size: { w: 160, h: 60 },
3248
- outputs: [{ name: "value", datatype: "string" }],
3249
- onCreate(node) {
3250
- node.state.value = "Hello";
3251
- },
3252
- onExecute(node, { setOutput }) {
3253
- setOutput("value", node.state.value ?? "");
3254
- },
3255
- onDraw(node, { ctx, theme: theme2 }) {
3256
- const { x, y } = node.computed;
3257
- ctx.fillStyle = "#8f8";
3258
- ctx.font = "12px sans-serif";
3259
- ctx.textAlign = "center";
3260
- const text = String(node.state.value ?? "");
3261
- const displayText = text.length > 15 ? text.substring(0, 15) + "..." : text;
3262
- ctx.fillText(displayText, x + 80, y + 42);
3263
- }
3264
- });
3265
- registry.register("value/Boolean", {
3266
- title: "Boolean",
3267
- size: { w: 140, h: 60 },
3268
- outputs: [{ name: "value", portType: "data", datatype: "boolean" }],
3269
- onCreate(node) {
3270
- node.state.value = true;
3271
- },
3272
- onExecute(node, { setOutput }) {
3273
- console.log("[Boolean] Outputting value:", node.state.value ?? false);
3274
- setOutput("value", node.state.value ?? false);
3275
- },
3276
- onDraw(node, { ctx, theme: theme2 }) {
3277
- const { x, y } = node.computed;
3278
- ctx.fillStyle = node.state.value ? "#8f8" : "#f88";
3279
- ctx.font = "14px sans-serif";
3280
- ctx.textAlign = "center";
3281
- ctx.fillText(String(node.state.value), x + 70, y + 42);
3282
- }
3283
- });
3284
- registry.register("util/Print", {
3285
- title: "Print",
3286
- size: { w: 140, h: 80 },
3287
- inputs: [
3288
- { name: "exec", portType: "exec" },
3289
- { name: "value", portType: "data", datatype: "any" }
3290
- ],
3291
- onCreate(node) {
3292
- node.state.lastValue = null;
3293
- },
3294
- onExecute(node, { getInput }) {
3295
- const val = getInput("value");
3296
- if (val !== node.state.lastValue) {
3297
- console.log("[Print]", val);
3298
- node.state.lastValue = val;
3299
- }
3300
- }
3301
- });
3302
- registry.register("util/Watch", {
3303
- title: "Watch",
3304
- size: { w: 180, h: 110 },
3305
- inputs: [
3306
- { name: "exec", portType: "exec" },
3307
- { name: "value", portType: "data", datatype: "any" }
3308
- ],
3309
- outputs: [
3310
- { name: "exec", portType: "exec" },
3311
- { name: "value", portType: "data", datatype: "any" }
3312
- ],
3313
- onCreate(node) {
3314
- node.state.displayValue = "---";
3315
- },
3316
- onExecute(node, { getInput, setOutput }) {
3317
- const val = getInput("value");
3318
- console.log("[Watch] onExecute called, value:", val);
3319
- node.state.displayValue = String(val ?? "---");
3320
- setOutput("value", val);
3321
- },
3322
- onDraw(node, { ctx, theme: theme2 }) {
3323
- const { x, y } = node.computed;
3324
- ctx.fillStyle = "#fa3";
3325
- ctx.font = "11px monospace";
3326
- ctx.textAlign = "left";
3327
- const text = String(node.state.displayValue ?? "---");
3328
- const displayText = text.length > 20 ? text.substring(0, 20) + "..." : text;
3329
- ctx.fillText(displayText, x + 8, y + 50);
3330
- }
3331
- });
3332
- registry.register("util/Timer", {
3333
- title: "Timer",
3334
- size: { w: 140, h: 60 },
3335
- outputs: [{ name: "time", datatype: "number" }],
3336
- onCreate(node) {
3337
- node.state.startTime = performance.now();
3338
- },
3339
- onExecute(node, { setOutput }) {
3340
- const elapsed = (performance.now() - (node.state.startTime ?? 0)) / 1e3;
3341
- setOutput("time", elapsed.toFixed(2));
3342
- }
3343
- });
3344
- registry.register("util/Trigger", {
3345
- title: "Trigger",
3346
- size: { w: 140, h: 80 },
3347
- outputs: [{ name: "exec", portType: "exec" }],
3348
- // Changed to exec port
3349
- html: {
3350
- init(node, el, { header, body }) {
3351
- el.style.backgroundColor = "#1e1e24";
3352
- el.style.border = "1px solid #444";
3353
- el.style.borderRadius = "8px";
3354
- header.style.backgroundColor = "#2a2a31";
3355
- header.style.borderBottom = "1px solid #444";
3356
- header.style.color = "#eee";
3357
- header.style.fontSize = "12px";
3358
- header.textContent = "Trigger";
3359
- body.style.padding = "12px";
3360
- body.style.display = "flex";
3361
- body.style.alignItems = "center";
3362
- body.style.justifyContent = "center";
3363
- const button = document.createElement("button");
3364
- button.textContent = "Fire!";
3365
- Object.assign(button.style, {
3366
- padding: "8px 16px",
3367
- background: "#4a9eff",
3368
- border: "none",
3369
- borderRadius: "4px",
3370
- color: "#fff",
3371
- fontWeight: "bold",
3372
- cursor: "pointer",
3373
- pointerEvents: "auto",
3374
- transition: "background 0.2s"
3375
- });
3376
- button.addEventListener("mousedown", (e) => {
3377
- e.stopPropagation();
3378
- button.style.background = "#2a7ede";
3379
- });
3380
- button.addEventListener("mouseup", () => {
3381
- button.style.background = "#4a9eff";
3382
- });
3383
- button.addEventListener("click", (e) => {
3384
- e.stopPropagation();
3385
- node.state.triggered = true;
3386
- console.log("[Trigger] Button clicked!");
3387
- if (node.__runnerRef && node.__controllerRef) {
3388
- console.log("[Trigger] Runner and controller found");
3389
- const runner2 = node.__runnerRef;
3390
- const controller2 = node.__controllerRef;
3391
- const graph2 = controller2.graph;
3392
- console.log("[Trigger] Calling runner.runOnce with node.id:", node.id);
3393
- const result = runner2.runOnce(node.id, 0);
3394
- const connectedEdges = result.connectedEdges;
3395
- const startTime = performance.now();
3396
- const animationDuration = 500;
3397
- const animate = () => {
3398
- var _a;
3399
- const elapsed = performance.now() - startTime;
3400
- if (elapsed < animationDuration) {
3401
- controller2.renderer.draw(graph2, {
3402
- selection: controller2.selection,
3403
- tempEdge: null,
3404
- running: true,
3405
- time: performance.now(),
3406
- dt: 0,
3407
- activeEdges: connectedEdges
3408
- // Only animate connected edges
3409
- });
3410
- (_a = controller2.htmlOverlay) == null ? void 0 : _a.draw(graph2, controller2.selection);
3411
- requestAnimationFrame(animate);
3412
- } else {
3413
- controller2.render();
3414
- node.state.triggered = false;
3415
- }
3416
- };
3417
- animate();
3418
- }
3419
- });
3420
- body.appendChild(button);
3421
- },
3422
- update(node, el, { header, body, selected }) {
3423
- el.style.borderColor = selected ? "#6cf" : "#444";
3424
- header.style.backgroundColor = selected ? "#3a4a5a" : "#2a2a31";
3425
- }
3426
- },
3427
- onCreate(node) {
3428
- node.state.triggered = false;
3429
- },
3430
- onExecute(node, { setOutput }) {
3431
- console.log("[Trigger] Outputting triggered:", node.state.triggered);
3432
- setOutput("triggered", node.state.triggered);
3433
- }
3434
- });
3435
- registry.register("core/Group", {
3436
- title: "Group",
3437
- size: { w: 240, h: 160 },
3438
- onDraw(node, { ctx, theme: theme2 }) {
3439
- const { x, y, w, h } = node.computed;
3440
- const headerH = 24;
3441
- const color = node.state.color || "#39424e";
3442
- const bgAlpha = 0.5;
3443
- const textColor = theme2.text || "#e9e9ef";
3444
- const rgba = (hex, a) => {
3445
- const c = hex.replace("#", "");
3446
- const n = parseInt(
3447
- c.length === 3 ? c.split("").map((x2) => x2 + x2).join("") : c,
3448
- 16
3449
- );
3450
- const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
3451
- return `rgba(${r},${g},${b},${a})`;
3452
- };
3453
- const roundRect2 = (ctx2, x2, y2, w2, h2, r) => {
3454
- if (w2 < 2 * r) r = w2 / 2;
3455
- if (h2 < 2 * r) r = h2 / 2;
3456
- ctx2.beginPath();
3457
- ctx2.moveTo(x2 + r, y2);
3458
- ctx2.arcTo(x2 + w2, y2, x2 + w2, y2 + h2, r);
3459
- ctx2.arcTo(x2 + w2, y2 + h2, x2, y2 + h2, r);
3460
- ctx2.arcTo(x2, y2 + h2, x2, y2, r);
3461
- ctx2.arcTo(x2, y2, x2 + w2, y2, r);
3462
- ctx2.closePath();
3463
- };
3464
- ctx.fillStyle = rgba(color, bgAlpha);
3465
- roundRect2(ctx, x, y, w, h, 10);
3466
- ctx.fill();
3467
- ctx.fillStyle = rgba(color, 0.3);
3468
- ctx.beginPath();
3469
- ctx.roundRect(x, y, w, headerH, [10, 10, 0, 0]);
3470
- ctx.fill();
3471
- ctx.fillStyle = textColor;
3472
- ctx.font = "600 13px system-ui";
3473
- ctx.textBaseline = "top";
3474
- ctx.fillText(node.title, x + 12, y + 6);
3475
- }
3476
- });
3477
- function setupDefaultContextMenu(contextMenu2, { controller: controller2, graph: graph2, hooks: hooks2 }) {
3478
- const nodeTypes = [];
3479
- for (const [key, typeDef] of graph2.registry.types.entries()) {
3480
- nodeTypes.push({
3481
- id: `add-${key}`,
3482
- label: typeDef.title || key,
3483
- action: () => {
3484
- const worldPos = contextMenu2.worldPosition || { x: 100, y: 100 };
3485
- const node = graph2.addNode(key, {
3486
- x: worldPos.x,
3487
- y: worldPos.y
3488
- });
3489
- hooks2 == null ? void 0 : hooks2.emit("node:updated", node);
3490
- controller2.render();
3491
- }
3492
- });
3493
3116
  }
3494
- contextMenu2.addItem("add-node", "Add Node", {
3495
- condition: (target2) => !target2,
3496
- submenu: nodeTypes,
3497
- order: 5
3498
- });
3499
- contextMenu2.addItem("delete-node", "Delete Node", {
3500
- condition: (target2) => target2 && target2.type !== "core/Group",
3501
- action: (target2) => {
3502
- const cmd = RemoveNodeCmd(graph2, target2);
3503
- controller2.stack.exec(cmd);
3504
- hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3505
- },
3506
- order: 10
3507
- });
3508
- const colors = [
3509
- { name: "Default", color: "#39424e" },
3510
- { name: "Slate", color: "#4a5568" },
3511
- { name: "Gray", color: "#2d3748" },
3512
- { name: "Blue", color: "#1a365d" },
3513
- { name: "Green", color: "#22543d" },
3514
- { name: "Red", color: "#742a2a" },
3515
- { name: "Purple", color: "#44337a" }
3516
- ];
3517
- contextMenu2.addItem("change-group-color", "Change Color", {
3518
- condition: (target2) => target2 && target2.type === "core/Group",
3519
- submenu: colors.map((colorInfo) => ({
3520
- id: `color-${colorInfo.color}`,
3521
- label: colorInfo.name,
3522
- color: colorInfo.color,
3523
- action: (target2) => {
3524
- const currentColor = target2.state.color || "#39424e";
3525
- const cmd = ChangeGroupColorCmd(target2, currentColor, colorInfo.color);
3526
- controller2.stack.exec(cmd);
3527
- hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3528
- }
3529
- })),
3530
- order: 20
3531
- });
3532
- contextMenu2.addItem("delete-group", "Delete Group", {
3533
- condition: (target2) => target2 && target2.type === "core/Group",
3534
- action: (target2) => {
3535
- const cmd = RemoveNodeCmd(graph2, target2);
3536
- controller2.stack.exec(cmd);
3537
- hooks2 == null ? void 0 : hooks2.emit("node:updated", target2);
3538
- },
3539
- order: 20
3540
- });
3541
3117
  }
3542
- setupDefaultContextMenu(contextMenu, { controller, graph, hooks });
3543
3118
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
3119
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3544
3120
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3545
3121
  controller.render();
3546
3122
  const ro = new ResizeObserver(() => {
3547
3123
  renderer.resize(canvas.clientWidth, canvas.clientHeight);
3124
+ edgeRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3548
3125
  portRenderer.resize(canvas.clientWidth, canvas.clientHeight);
3549
3126
  controller.render();
3550
3127
  });