@stocksharp/diagram 0.4.0 → 0.5.0

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.
Files changed (40) hide show
  1. package/README.md +42 -0
  2. package/dist/esm/canvas-renderer.js +91 -16
  3. package/dist/esm/canvas-renderer.js.map +1 -1
  4. package/dist/esm/diagram/context-menu.js +272 -0
  5. package/dist/esm/diagram/context-menu.js.map +1 -0
  6. package/dist/esm/diagram/stocksharp-diagram.js +83 -5
  7. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  8. package/dist/esm/draw-surface.js +2 -0
  9. package/dist/esm/draw-surface.js.map +1 -0
  10. package/dist/esm/i18n.js.map +1 -1
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/svg-surface.js +341 -0
  13. package/dist/esm/svg-surface.js.map +1 -0
  14. package/dist/ssdiagram.js +879 -164
  15. package/dist/ssdiagram.js.map +4 -4
  16. package/dist/types/canvas-renderer.d.ts +12 -0
  17. package/dist/types/canvas-renderer.d.ts.map +1 -1
  18. package/dist/types/diagram/api.d.ts +36 -2
  19. package/dist/types/diagram/api.d.ts.map +1 -1
  20. package/dist/types/diagram/context-menu.d.ts +31 -0
  21. package/dist/types/diagram/context-menu.d.ts.map +1 -0
  22. package/dist/types/diagram/stocksharp-diagram.d.ts +11 -5
  23. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  24. package/dist/types/draw-surface.d.ts +52 -0
  25. package/dist/types/draw-surface.d.ts.map +1 -0
  26. package/dist/types/i18n.d.ts +4 -0
  27. package/dist/types/i18n.d.ts.map +1 -1
  28. package/dist/types/index.d.ts +2 -2
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/svg-surface.d.ts +74 -0
  31. package/dist/types/svg-surface.d.ts.map +1 -0
  32. package/package.json +1 -1
  33. package/src/canvas-renderer.ts +103 -16
  34. package/src/diagram/api.ts +46 -2
  35. package/src/diagram/context-menu.ts +298 -0
  36. package/src/diagram/stocksharp-diagram.ts +93 -6
  37. package/src/draw-surface.ts +61 -0
  38. package/src/i18n.ts +7 -0
  39. package/src/index.ts +8 -0
  40. package/src/svg-surface.ts +430 -0
package/dist/ssdiagram.js CHANGED
@@ -556,8 +556,8 @@ var SSDiagram = (() => {
556
556
  this.redoCommands.length = 0;
557
557
  this.notify();
558
558
  }
559
- transaction(label, action) {
560
- const frame = { label, commands: [] };
559
+ transaction(label2, action) {
560
+ const frame = { label: label2, commands: [] };
561
561
  this.transactions.push(frame);
562
562
  try {
563
563
  const result = action();
@@ -788,6 +788,326 @@ var SSDiagram = (() => {
788
788
  return value;
789
789
  }
790
790
 
791
+ // src/svg-surface.ts
792
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
793
+ function multiply(m, n2) {
794
+ return [
795
+ m[0] * n2[0] + m[2] * n2[1],
796
+ m[1] * n2[0] + m[3] * n2[1],
797
+ m[0] * n2[2] + m[2] * n2[3],
798
+ m[1] * n2[2] + m[3] * n2[3],
799
+ m[0] * n2[4] + m[2] * n2[5] + m[4],
800
+ m[1] * n2[4] + m[3] * n2[5] + m[5]
801
+ ];
802
+ }
803
+ function isIdentity(m) {
804
+ return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
805
+ }
806
+ function n(value) {
807
+ if (!Number.isFinite(value)) return "0";
808
+ return (Math.round(value * 1e3) / 1e3).toString();
809
+ }
810
+ function escapeXml(value) {
811
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
812
+ }
813
+ var ANCHOR = {
814
+ left: "start",
815
+ start: "start",
816
+ center: "middle",
817
+ right: "end",
818
+ end: "end"
819
+ };
820
+ var BASELINE = {
821
+ top: "text-before-edge",
822
+ hanging: "hanging",
823
+ middle: "central",
824
+ alphabetic: "alphabetic",
825
+ ideographic: "ideographic",
826
+ bottom: "text-after-edge"
827
+ };
828
+ var SvgSurface = class {
829
+ constructor(options) {
830
+ this.body = [];
831
+ this.defs = [];
832
+ this.stack = [];
833
+ this.state = {
834
+ fillStyle: "#000000",
835
+ strokeStyle: "#000000",
836
+ lineWidth: 1,
837
+ lineJoin: "miter",
838
+ lineCap: "butt",
839
+ globalAlpha: 1,
840
+ font: "10px sans-serif",
841
+ textAlign: "start",
842
+ textBaseline: "alphabetic",
843
+ dash: [],
844
+ matrix: IDENTITY,
845
+ clipId: null
846
+ };
847
+ /** Path being built by moveTo/lineTo/arc/arcTo, in the units the caller passes. */
848
+ this.path = [];
849
+ this.cursor = null;
850
+ this.subpathStart = null;
851
+ this.clipSeq = 0;
852
+ this.width = options.width;
853
+ this.height = options.height;
854
+ this.background = options.background;
855
+ this.metrics = options.metrics ?? null;
856
+ }
857
+ // ===== state =========================================================
858
+ get fillStyle() {
859
+ return this.state.fillStyle;
860
+ }
861
+ set fillStyle(value) {
862
+ if (typeof value === "string") this.state.fillStyle = value;
863
+ }
864
+ get strokeStyle() {
865
+ return this.state.strokeStyle;
866
+ }
867
+ set strokeStyle(value) {
868
+ if (typeof value === "string") this.state.strokeStyle = value;
869
+ }
870
+ get lineWidth() {
871
+ return this.state.lineWidth;
872
+ }
873
+ set lineWidth(value) {
874
+ this.state.lineWidth = value;
875
+ }
876
+ get lineJoin() {
877
+ return this.state.lineJoin;
878
+ }
879
+ set lineJoin(value) {
880
+ this.state.lineJoin = value;
881
+ }
882
+ get lineCap() {
883
+ return this.state.lineCap;
884
+ }
885
+ set lineCap(value) {
886
+ this.state.lineCap = value;
887
+ }
888
+ get globalAlpha() {
889
+ return this.state.globalAlpha;
890
+ }
891
+ set globalAlpha(value) {
892
+ this.state.globalAlpha = value;
893
+ }
894
+ get font() {
895
+ return this.state.font;
896
+ }
897
+ set font(value) {
898
+ this.state.font = value;
899
+ if (this.metrics !== null) this.metrics.font = value;
900
+ }
901
+ get textAlign() {
902
+ return this.state.textAlign;
903
+ }
904
+ set textAlign(value) {
905
+ this.state.textAlign = value;
906
+ }
907
+ get textBaseline() {
908
+ return this.state.textBaseline;
909
+ }
910
+ set textBaseline(value) {
911
+ this.state.textBaseline = value;
912
+ }
913
+ save() {
914
+ this.stack.push({ ...this.state, dash: [...this.state.dash], matrix: [...this.state.matrix] });
915
+ }
916
+ restore() {
917
+ const previous = this.stack.pop();
918
+ if (previous !== void 0) this.state = previous;
919
+ }
920
+ setLineDash(segments) {
921
+ this.state.dash = [...segments];
922
+ }
923
+ // ===== transform =====================================================
924
+ setTransform(a, b, c, d, e, f) {
925
+ this.state.matrix = [a, b, c, d, e, f];
926
+ }
927
+ translate(x, y) {
928
+ this.state.matrix = multiply(this.state.matrix, [1, 0, 0, 1, x, y]);
929
+ }
930
+ rotate(angle) {
931
+ const cos = Math.cos(angle);
932
+ const sin = Math.sin(angle);
933
+ this.state.matrix = multiply(this.state.matrix, [cos, sin, -sin, cos, 0, 0]);
934
+ }
935
+ // ===== paths =========================================================
936
+ beginPath() {
937
+ this.path = [];
938
+ this.cursor = null;
939
+ this.subpathStart = null;
940
+ }
941
+ closePath() {
942
+ if (this.cursor === null) return;
943
+ this.path.push("Z");
944
+ if (this.subpathStart !== null) this.cursor = [...this.subpathStart];
945
+ }
946
+ moveTo(x, y) {
947
+ this.path.push(`M${n(x)} ${n(y)}`);
948
+ this.cursor = [x, y];
949
+ this.subpathStart = [x, y];
950
+ }
951
+ lineTo(x, y) {
952
+ if (this.cursor === null) {
953
+ this.moveTo(x, y);
954
+ return;
955
+ }
956
+ this.path.push(`L${n(x)} ${n(y)}`);
957
+ this.cursor = [x, y];
958
+ }
959
+ arc(cx, cy, r, start, end, ccw = false) {
960
+ const sx = cx + r * Math.cos(start);
961
+ const sy = cy + r * Math.sin(start);
962
+ if (this.cursor === null) this.moveTo(sx, sy);
963
+ else this.lineTo(sx, sy);
964
+ let delta = end - start;
965
+ if (ccw) {
966
+ while (delta > 0) delta -= Math.PI * 2;
967
+ if (delta <= -Math.PI * 2) delta = -Math.PI * 2;
968
+ } else {
969
+ while (delta < 0) delta += Math.PI * 2;
970
+ if (delta >= Math.PI * 2) delta = Math.PI * 2;
971
+ }
972
+ const sweep = ccw ? 0 : 1;
973
+ if (Math.abs(delta) >= Math.PI * 2 - 1e-9) {
974
+ const mid = start + delta / 2;
975
+ const mx = cx + r * Math.cos(mid);
976
+ const my = cy + r * Math.sin(mid);
977
+ this.path.push(`A${n(r)} ${n(r)} 0 0 ${sweep} ${n(mx)} ${n(my)}`);
978
+ this.path.push(`A${n(r)} ${n(r)} 0 0 ${sweep} ${n(sx)} ${n(sy)}`);
979
+ this.cursor = [sx, sy];
980
+ return;
981
+ }
982
+ const ex = cx + r * Math.cos(start + delta);
983
+ const ey = cy + r * Math.sin(start + delta);
984
+ const large = Math.abs(delta) > Math.PI ? 1 : 0;
985
+ this.path.push(`A${n(r)} ${n(r)} 0 ${large} ${sweep} ${n(ex)} ${n(ey)}`);
986
+ this.cursor = [ex, ey];
987
+ }
988
+ arcTo(x1, y1, x2, y2, r) {
989
+ if (this.cursor === null) {
990
+ this.moveTo(x1, y1);
991
+ return;
992
+ }
993
+ const [x0, y0] = this.cursor;
994
+ const a = [x0 - x1, y0 - y1];
995
+ const b = [x2 - x1, y2 - y1];
996
+ const la = Math.hypot(a[0], a[1]);
997
+ const lb = Math.hypot(b[0], b[1]);
998
+ if (la === 0 || lb === 0 || r === 0) {
999
+ this.lineTo(x1, y1);
1000
+ return;
1001
+ }
1002
+ const ua = [a[0] / la, a[1] / la];
1003
+ const ub = [b[0] / lb, b[1] / lb];
1004
+ const cross = ua[0] * ub[1] - ua[1] * ub[0];
1005
+ if (Math.abs(cross) < 1e-9) {
1006
+ this.lineTo(x1, y1);
1007
+ return;
1008
+ }
1009
+ const angle = Math.acos(Math.max(-1, Math.min(1, ua[0] * ub[0] + ua[1] * ub[1])));
1010
+ const tangent = r / Math.tan(angle / 2);
1011
+ const t2 = Math.min(tangent, la, lb);
1012
+ const radius = t2 * Math.tan(angle / 2);
1013
+ const t1 = [x1 + ua[0] * t2, y1 + ua[1] * t2];
1014
+ const t22 = [x1 + ub[0] * t2, y1 + ub[1] * t2];
1015
+ this.lineTo(t1[0], t1[1]);
1016
+ const sweep = cross < 0 ? 1 : 0;
1017
+ this.path.push(`A${n(radius)} ${n(radius)} 0 0 ${sweep} ${n(t22[0])} ${n(t22[1])}`);
1018
+ this.cursor = [t22[0], t22[1]];
1019
+ }
1020
+ fill() {
1021
+ if (this.path.length === 0) return;
1022
+ this.emit(`<path d="${this.path.join(" ")}" ${this.fillAttrs()}/>`);
1023
+ }
1024
+ stroke() {
1025
+ if (this.path.length === 0) return;
1026
+ this.emit(`<path d="${this.path.join(" ")}" fill="none" ${this.strokeAttrs()}/>`);
1027
+ }
1028
+ clip() {
1029
+ if (this.path.length === 0) return;
1030
+ const id = `clip${++this.clipSeq}`;
1031
+ this.defs.push(`<clipPath id="${id}"><path d="${this.path.join(" ")}"/></clipPath>`);
1032
+ this.state.clipId = id;
1033
+ }
1034
+ // ===== rectangles ====================================================
1035
+ clearRect(x, y, w, h) {
1036
+ void x;
1037
+ void y;
1038
+ void w;
1039
+ void h;
1040
+ }
1041
+ fillRect(x, y, w, h) {
1042
+ this.emit(`<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" ${this.fillAttrs()}/>`);
1043
+ }
1044
+ strokeRect(x, y, w, h) {
1045
+ this.emit(`<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="none" ${this.strokeAttrs()}/>`);
1046
+ }
1047
+ // ===== text ==========================================================
1048
+ fillText(text, x, y) {
1049
+ if (text === "") return;
1050
+ const anchor = ANCHOR[this.state.textAlign] ?? "start";
1051
+ const baseline = BASELINE[this.state.textBaseline] ?? "alphabetic";
1052
+ this.emit(
1053
+ `<text x="${n(x)}" y="${n(y)}" style="font:${escapeXml(this.state.font)}" text-anchor="${anchor}" dominant-baseline="${baseline}" ${this.fillAttrs()}>${escapeXml(text)}</text>`
1054
+ );
1055
+ }
1056
+ measureText(text) {
1057
+ if (this.metrics !== null) return { width: this.metrics.measureText(text).width };
1058
+ const size = parseFloat(this.state.font) || 10;
1059
+ return { width: text.length * size * 0.55 };
1060
+ }
1061
+ // ===== images ========================================================
1062
+ drawImage(image, dx, dy, dw, dh) {
1063
+ const href = this.imageHref(image);
1064
+ if (href === null) return;
1065
+ this.emit(`<image x="${n(dx)}" y="${n(dy)}" width="${n(dw)}" height="${n(dh)}" href="${escapeXml(href)}"${this.commonAttrs()}/>`);
1066
+ }
1067
+ imageHref(image) {
1068
+ try {
1069
+ if (typeof HTMLCanvasElement !== "undefined" && image instanceof HTMLCanvasElement) return image.toDataURL();
1070
+ if (typeof HTMLImageElement !== "undefined" && image instanceof HTMLImageElement) return image.src;
1071
+ } catch {
1072
+ return null;
1073
+ }
1074
+ return null;
1075
+ }
1076
+ // ===== output ========================================================
1077
+ /** The finished document. Safe to call more than once; it does not consume the recording. */
1078
+ toSvg() {
1079
+ const background = this.background === void 0 ? "" : `<rect width="100%" height="100%" fill="${escapeXml(this.background)}"/>`;
1080
+ const defs = this.defs.length === 0 ? "" : `<defs>${this.defs.join("")}</defs>`;
1081
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(this.width)}" height="${n(this.height)}" viewBox="0 0 ${n(this.width)} ${n(this.height)}">${defs}${background}${this.body.join("")}</svg>`;
1082
+ }
1083
+ // ===== internals =====================================================
1084
+ emit(element) {
1085
+ this.body.push(element);
1086
+ }
1087
+ commonAttrs() {
1088
+ const parts = [];
1089
+ if (!isIdentity(this.state.matrix)) {
1090
+ parts.push(` transform="matrix(${this.state.matrix.map(n).join(" ")})"`);
1091
+ }
1092
+ if (this.state.globalAlpha !== 1) parts.push(` opacity="${n(this.state.globalAlpha)}"`);
1093
+ if (this.state.clipId !== null) parts.push(` clip-path="url(#${this.state.clipId})"`);
1094
+ return parts.join("");
1095
+ }
1096
+ fillAttrs() {
1097
+ return `fill="${escapeXml(this.state.fillStyle)}"${this.commonAttrs()}`;
1098
+ }
1099
+ strokeAttrs() {
1100
+ const parts = [
1101
+ `stroke="${escapeXml(this.state.strokeStyle)}"`,
1102
+ `stroke-width="${n(this.state.lineWidth)}"`
1103
+ ];
1104
+ if (this.state.lineJoin !== "miter") parts.push(`stroke-linejoin="${this.state.lineJoin}"`);
1105
+ if (this.state.lineCap !== "butt") parts.push(`stroke-linecap="${this.state.lineCap}"`);
1106
+ if (this.state.dash.length > 0) parts.push(`stroke-dasharray="${this.state.dash.map(n).join(" ")}"`);
1107
+ return parts.join(" ") + this.commonAttrs();
1108
+ }
1109
+ };
1110
+
791
1111
  // src/canvas-renderer.ts
792
1112
  var SCREEN_RENDER_OPTIONS = {
793
1113
  grid: true,
@@ -1142,7 +1462,7 @@ var SSDiagram = (() => {
1142
1462
  this.scheduleDraw();
1143
1463
  }
1144
1464
  removeDiagramNode(id) {
1145
- const node = this.nodes.find((n) => n.id === id);
1465
+ const node = this.nodes.find((n2) => n2.id === id);
1146
1466
  if (node === void 0) return;
1147
1467
  const snapshot = node.toInit(false);
1148
1468
  const cascaded = this.links.map((link, index) => ({ link, index })).filter(({ link }) => link.from === id || link.to === id).map(({ link, index }) => ({ init: link.toInit(), index }));
@@ -1178,11 +1498,11 @@ var SSDiagram = (() => {
1178
1498
  }
1179
1499
  }
1180
1500
  doRemoveNode(id) {
1181
- const node = this.nodes.find((n) => n.id === id);
1501
+ const node = this.nodes.find((n2) => n2.id === id);
1182
1502
  if (node === void 0) return;
1183
1503
  const removedLinks = this.links.filter((l) => l.from === id || l.to === id);
1184
1504
  this.links = this.links.filter((l) => l.from !== id && l.to !== id);
1185
- this.nodes = this.nodes.filter((n) => n !== node);
1505
+ this.nodes = this.nodes.filter((n2) => n2 !== node);
1186
1506
  let selectionChanged = this.selectedNodes.delete(node);
1187
1507
  if (this.selectedNode === node) {
1188
1508
  const remaining = [...this.selectedNodes];
@@ -1203,7 +1523,7 @@ var SSDiagram = (() => {
1203
1523
  this.scheduleDraw();
1204
1524
  }
1205
1525
  moveNode(id, x, y) {
1206
- const node = this.nodes.find((n) => n.id === id);
1526
+ const node = this.nodes.find((n2) => n2.id === id);
1207
1527
  if (node === void 0) return;
1208
1528
  const fromX = node.x, fromY = node.y;
1209
1529
  this.doMoveNode(id, x, y);
@@ -1242,7 +1562,7 @@ var SSDiagram = (() => {
1242
1562
  return true;
1243
1563
  }
1244
1564
  doMoveNode(id, x, y) {
1245
- const node = this.nodes.find((n) => n.id === id);
1565
+ const node = this.nodes.find((n2) => n2.id === id);
1246
1566
  if (node === void 0) return;
1247
1567
  node.x = x;
1248
1568
  node.y = y;
@@ -1298,8 +1618,8 @@ var SSDiagram = (() => {
1298
1618
  return ok;
1299
1619
  }
1300
1620
  doAddLink(init, validate = true, index = this.links.length) {
1301
- const fn = this.nodes.find((n) => n.id === init.from);
1302
- const tn = this.nodes.find((n) => n.id === init.to);
1621
+ const fn = this.nodes.find((n2) => n2.id === init.from);
1622
+ const tn = this.nodes.find((n2) => n2.id === init.to);
1303
1623
  if (fn === void 0 || tn === void 0) return false;
1304
1624
  const fp = fn.outPorts.find((p) => p.id === init.fromPort);
1305
1625
  const tp = tn.inPorts.find((p) => p.id === init.toPort);
@@ -1402,8 +1722,8 @@ var SSDiagram = (() => {
1402
1722
  // Group multiple mutations into one undo step (paste of N nodes,
1403
1723
  // bulk delete, multi-drag, etc.). Re-entrant: nested calls flatten
1404
1724
  // into the outer batch.
1405
- withTransaction(label, fn) {
1406
- return this.history.transaction(label, fn);
1725
+ withTransaction(label2, fn) {
1726
+ return this.history.transaction(label2, fn);
1407
1727
  }
1408
1728
  record(action) {
1409
1729
  this.history.recordApplied({
@@ -1416,7 +1736,7 @@ var SSDiagram = (() => {
1416
1736
  if (!this.permissions.deleteSelection) return;
1417
1737
  if (this.selectedNodes.size > 0) {
1418
1738
  this.withTransaction("delete selection", () => {
1419
- for (const id of [...this.selectedNodes].map((n) => n.id)) this.removeDiagramNode(id);
1739
+ for (const id of [...this.selectedNodes].map((n2) => n2.id)) this.removeDiagramNode(id);
1420
1740
  });
1421
1741
  this.selectedNodes.clear();
1422
1742
  this.selectedNode = null;
@@ -1595,7 +1915,7 @@ var SSDiagram = (() => {
1595
1915
  this.showNodeMessages = show;
1596
1916
  this.scheduleDraw();
1597
1917
  }
1598
- updateNodeState(nodeId, label, mutate) {
1918
+ updateNodeState(nodeId, label2, mutate) {
1599
1919
  const node = this.findNode(nodeId);
1600
1920
  if (node === void 0) return false;
1601
1921
  const before = node.toInit(false);
@@ -1613,7 +1933,7 @@ var SSDiagram = (() => {
1613
1933
  undo: () => {
1614
1934
  this.applyNodeSnapshot(nodeId, before);
1615
1935
  },
1616
- label
1936
+ label: label2
1617
1937
  });
1618
1938
  return true;
1619
1939
  }
@@ -1763,7 +2083,7 @@ var SSDiagram = (() => {
1763
2083
  }
1764
2084
  // Renderer operations consumed by the high-level component.
1765
2085
  findNode(id) {
1766
- return this.nodes.find((n) => n.id === id);
2086
+ return this.nodes.find((n2) => n2.id === id);
1767
2087
  }
1768
2088
  requestRedraw() {
1769
2089
  this.relayout();
@@ -1789,17 +2109,8 @@ var SSDiagram = (() => {
1789
2109
  copyContext.drawImage(this.canvas, 0, 0);
1790
2110
  return copy;
1791
2111
  }
1792
- const pixelRatio = this.positiveScreenshotNumber(options.pixelRatio ?? this.dpr, "pixelRatio");
1793
- const exportScale = scope === "content" ? this.positiveScreenshotNumber(options.scale ?? 1, "scale") : this.scale;
1794
- const padding = scope === "content" ? this.nonNegativeScreenshotNumber(options.padding ?? 32, "padding") : 0;
1795
- const bounds = scope === "content" ? this.graphBounds() : null;
1796
- const width = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxX ?? 1) - (bounds?.minX ?? 0)) * exportScale + padding * 2)) : this.width;
1797
- const height = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxY ?? 1) - (bounds?.minY ?? 0)) * exportScale + padding * 2)) : this.height;
1798
- const pixelWidth = Math.ceil(width * pixelRatio);
1799
- const pixelHeight = Math.ceil(height * pixelRatio);
1800
- if (pixelWidth > 16384 || pixelHeight > 16384 || pixelWidth * pixelHeight > 268435456) {
1801
- throw new RangeError(`ssdiagram: screenshot is too large (${pixelWidth}x${pixelHeight})`);
1802
- }
2112
+ const layout = this.exportLayout(scope, options);
2113
+ const { pixelRatio, exportScale, padding, bounds, width, height, pixelWidth, pixelHeight } = layout;
1803
2114
  const output = document.createElement("canvas");
1804
2115
  output.width = pixelWidth;
1805
2116
  output.height = pixelHeight;
@@ -1849,6 +2160,86 @@ var SSDiagram = (() => {
1849
2160
  }
1850
2161
  return output;
1851
2162
  }
2163
+ // Everything an export needs to know about size and placement, for either output. Shared on
2164
+ // purpose: a raster and a vector export of the same scope must frame the picture identically,
2165
+ // and two copies of this arithmetic would eventually disagree about padding or where the origin
2166
+ // sits. A block comment here would be lifted onto the next public member in the .d.ts.
2167
+ exportLayout(scope, options) {
2168
+ const pixelRatio = this.positiveScreenshotNumber(options.pixelRatio ?? this.dpr, "pixelRatio");
2169
+ const exportScale = scope === "content" ? this.positiveScreenshotNumber(options.scale ?? 1, "scale") : this.scale;
2170
+ const padding = scope === "content" ? this.nonNegativeScreenshotNumber(options.padding ?? 32, "padding") : 0;
2171
+ const bounds = scope === "content" ? this.graphBounds() : null;
2172
+ const width = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxX ?? 1) - (bounds?.minX ?? 0)) * exportScale + padding * 2)) : this.width;
2173
+ const height = scope === "content" ? Math.max(1, Math.ceil(((bounds?.maxY ?? 1) - (bounds?.minY ?? 0)) * exportScale + padding * 2)) : this.height;
2174
+ const pixelWidth = Math.ceil(width * pixelRatio);
2175
+ const pixelHeight = Math.ceil(height * pixelRatio);
2176
+ if (pixelWidth > 16384 || pixelHeight > 16384 || pixelWidth * pixelHeight > 268435456) {
2177
+ throw new RangeError(`ssdiagram: screenshot is too large (${pixelWidth}x${pixelHeight})`);
2178
+ }
2179
+ return { pixelRatio, exportScale, padding, bounds, width, height, pixelWidth, pixelHeight };
2180
+ }
2181
+ /**
2182
+ * The same picture takeScreenshot produces, as SVG.
2183
+ *
2184
+ * It is the renderer that draws it -- the surface underneath simply records instead of paints --
2185
+ * so the vector output cannot drift from what the screen and the raster export show. Text keeps
2186
+ * the on-screen metrics by measuring through a real 2D context.
2187
+ *
2188
+ * Scale and pixelRatio still frame the picture (they decide the viewBox), but nothing is
2189
+ * resampled: the result is resolution-independent, which is the point of asking for it.
2190
+ */
2191
+ takeSvg(options = {}) {
2192
+ if (this.destroyed) throw new Error("ssdiagram: cannot export a destroyed diagram");
2193
+ const scope = options.scope ?? "viewport";
2194
+ if (scope !== "viewport" && scope !== "content") {
2195
+ throw new RangeError(`ssdiagram: unsupported screenshot scope "${String(scope)}"`);
2196
+ }
2197
+ const { exportScale, padding, bounds, width, height } = this.exportLayout(scope, options);
2198
+ const metrics = this.canvas.getContext("2d");
2199
+ const surface = new SvgSurface({
2200
+ width,
2201
+ height,
2202
+ background: options.background ?? this.opts.background ?? "#1b1b1f",
2203
+ metrics: metrics ?? void 0
2204
+ });
2205
+ const previous = {
2206
+ ctx: this.ctx,
2207
+ width: this.width,
2208
+ height: this.height,
2209
+ dpr: this.dpr,
2210
+ scale: this.scale,
2211
+ offX: this.offX,
2212
+ offY: this.offY,
2213
+ overviewVisible: this.overviewVisible
2214
+ };
2215
+ try {
2216
+ this.ctx = surface;
2217
+ this.width = width;
2218
+ this.height = height;
2219
+ this.dpr = 1;
2220
+ this.scale = exportScale;
2221
+ this.offX = scope === "content" && bounds !== null ? padding - bounds.minX * exportScale : previous.offX;
2222
+ this.offY = scope === "content" && bounds !== null ? padding - bounds.minY * exportScale : previous.offY;
2223
+ this.overviewVisible = options.includeOverview ?? (scope === "viewport" && previous.overviewVisible);
2224
+ this.draw({
2225
+ grid: options.includeGrid ?? true,
2226
+ overview: this.overviewVisible,
2227
+ selection: options.includeSelection ?? scope === "viewport",
2228
+ runtime: options.includeRuntimeState ?? true,
2229
+ transient: false
2230
+ });
2231
+ } finally {
2232
+ this.ctx = previous.ctx;
2233
+ this.width = previous.width;
2234
+ this.height = previous.height;
2235
+ this.dpr = previous.dpr;
2236
+ this.scale = previous.scale;
2237
+ this.offX = previous.offX;
2238
+ this.offY = previous.offY;
2239
+ this.overviewVisible = previous.overviewVisible;
2240
+ }
2241
+ return surface.toSvg();
2242
+ }
1852
2243
  positiveScreenshotNumber(value, name) {
1853
2244
  if (!Number.isFinite(value) || value <= 0) {
1854
2245
  throw new RangeError(`ssdiagram: screenshot ${name} must be a positive finite number`);
@@ -1969,7 +2360,7 @@ var SSDiagram = (() => {
1969
2360
  return this.toScreen(wx, wy);
1970
2361
  }
1971
2362
  selectNodeById(id) {
1972
- const node = id === null ? null : this.nodes.find((n) => n.id === id) ?? null;
2363
+ const node = id === null ? null : this.nodes.find((n2) => n2.id === id) ?? null;
1973
2364
  this.selectNode(node);
1974
2365
  }
1975
2366
  setZoom(scale) {
@@ -2017,25 +2408,25 @@ var SSDiagram = (() => {
2017
2408
  this.opts.typeColors = { ...colors };
2018
2409
  this.scheduleDraw();
2019
2410
  }
2020
- setTheme(t) {
2021
- if (t.background !== void 0) this.opts.background = t.background;
2022
- if (t.gridColor !== void 0) this.opts.gridColor = t.gridColor;
2023
- if (t.linkMaxLightness !== void 0) this.opts.linkMaxLightness = t.linkMaxLightness;
2024
- if (t.typedLinkColors !== void 0) this.opts.typedLinkColors = t.typedLinkColors;
2025
- if (t.overviewBackground !== void 0) this.opts.overviewBackground = t.overviewBackground;
2026
- if (t.overviewBorderColor !== void 0) this.opts.overviewBorderColor = t.overviewBorderColor;
2027
- if (t.overviewViewportColor !== void 0) this.opts.overviewViewportColor = t.overviewViewportColor;
2028
- if (t.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t.overviewViewportFill;
2411
+ setTheme(t2) {
2412
+ if (t2.background !== void 0) this.opts.background = t2.background;
2413
+ if (t2.gridColor !== void 0) this.opts.gridColor = t2.gridColor;
2414
+ if (t2.linkMaxLightness !== void 0) this.opts.linkMaxLightness = t2.linkMaxLightness;
2415
+ if (t2.typedLinkColors !== void 0) this.opts.typedLinkColors = t2.typedLinkColors;
2416
+ if (t2.overviewBackground !== void 0) this.opts.overviewBackground = t2.overviewBackground;
2417
+ if (t2.overviewBorderColor !== void 0) this.opts.overviewBorderColor = t2.overviewBorderColor;
2418
+ if (t2.overviewViewportColor !== void 0) this.opts.overviewViewportColor = t2.overviewViewportColor;
2419
+ if (t2.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t2.overviewViewportFill;
2029
2420
  this.scheduleDraw();
2030
2421
  }
2031
2422
  graphBounds() {
2032
2423
  if (this.nodes.length === 0) return null;
2033
2424
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2034
- for (const n of this.nodes) {
2035
- minX = Math.min(minX, n.x);
2036
- minY = Math.min(minY, n.y);
2037
- maxX = Math.max(maxX, n.x + n.w);
2038
- maxY = Math.max(maxY, n.y + n.h);
2425
+ for (const n2 of this.nodes) {
2426
+ minX = Math.min(minX, n2.x);
2427
+ minY = Math.min(minY, n2.y);
2428
+ maxX = Math.max(maxX, n2.x + n2.w);
2429
+ maxY = Math.max(maxY, n2.y + n2.h);
2039
2430
  }
2040
2431
  return { minX, minY, maxX, maxY };
2041
2432
  }
@@ -2086,26 +2477,26 @@ var SSDiagram = (() => {
2086
2477
  this.canvas.remove();
2087
2478
  }
2088
2479
  // ---- geometry ---------------------------------------------------
2089
- layoutNode(n) {
2480
+ layoutNode(n2) {
2090
2481
  const ctx = this.ctx;
2091
2482
  ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
2092
- const titleW = ctx.measureText(n.name).width;
2093
- const rows = Math.max(n.inPorts.length, n.outPorts.length, 1);
2094
- n.w = Math.max(170, Math.ceil(titleW) + 28);
2095
- n.h = Math.max(48, rows * PORT_ROW_H + 16);
2483
+ const titleW = ctx.measureText(n2.name).width;
2484
+ const rows = Math.max(n2.inPorts.length, n2.outPorts.length, 1);
2485
+ n2.w = Math.max(170, Math.ceil(titleW) + 28);
2486
+ n2.h = Math.max(48, rows * PORT_ROW_H + 16);
2096
2487
  const off = PORT_SQ / 2;
2097
2488
  const place = (ports, x) => {
2098
- const startY = n.y + (n.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2489
+ const startY = n2.y + (n2.h - ports.length * PORT_ROW_H) / 2 + PORT_ROW_H / 2;
2099
2490
  ports.forEach((p, i) => {
2100
2491
  p.cx = x;
2101
2492
  p.cy = startY + i * PORT_ROW_H;
2102
2493
  });
2103
2494
  };
2104
- place(n.inPorts, n.x - off);
2105
- place(n.outPorts, n.x + n.w + off);
2495
+ place(n2.inPorts, n2.x - off);
2496
+ place(n2.outPorts, n2.x + n2.w + off);
2106
2497
  }
2107
2498
  relayout() {
2108
- for (const n of this.nodes) this.layoutNode(n);
2499
+ for (const n2 of this.nodes) this.layoutNode(n2);
2109
2500
  }
2110
2501
  toScreen(wx, wy) {
2111
2502
  return [wx * this.scale + this.offX, wy * this.scale + this.offY];
@@ -2134,9 +2525,9 @@ var SSDiagram = (() => {
2134
2525
  // ---- hit testing (world coords) ---------------------------------
2135
2526
  portAt(wx, wy) {
2136
2527
  for (let i = this.nodes.length - 1; i >= 0; i -= 1) {
2137
- const n = this.nodes[i];
2138
- for (const p of [...n.inPorts, ...n.outPorts]) {
2139
- if ((wx - p.cx) ** 2 + (wy - p.cy) ** 2 <= (PORT_R + 4) ** 2) return { node: n, port: p };
2528
+ const n2 = this.nodes[i];
2529
+ for (const p of [...n2.inPorts, ...n2.outPorts]) {
2530
+ if ((wx - p.cx) ** 2 + (wy - p.cy) ** 2 <= (PORT_R + 4) ** 2) return { node: n2, port: p };
2140
2531
  }
2141
2532
  }
2142
2533
  return null;
@@ -2157,8 +2548,8 @@ var SSDiagram = (() => {
2157
2548
  }
2158
2549
  nodeAt(wx, wy) {
2159
2550
  for (let i = this.nodes.length - 1; i >= 0; i -= 1) {
2160
- const n = this.nodes[i];
2161
- if (wx >= n.x && wx <= n.x + n.w && wy >= n.y && wy <= n.y + n.h) return n;
2551
+ const n2 = this.nodes[i];
2552
+ if (wx >= n2.x && wx <= n2.x + n2.w && wy >= n2.y && wy <= n2.y + n2.h) return n2;
2162
2553
  }
2163
2554
  return null;
2164
2555
  }
@@ -2167,9 +2558,9 @@ var SSDiagram = (() => {
2167
2558
  obstaclesFor(exclude) {
2168
2559
  const inf = 12;
2169
2560
  const out = [];
2170
- for (const n of this.nodes) {
2171
- if (exclude.has(n.id)) continue;
2172
- out.push([n.x - inf, n.y - inf, n.w + inf * 2, n.h + inf * 2]);
2561
+ for (const n2 of this.nodes) {
2562
+ if (exclude.has(n2.id)) continue;
2563
+ out.push([n2.x - inf, n2.y - inf, n2.w + inf * 2, n2.h + inf * 2]);
2173
2564
  }
2174
2565
  return out;
2175
2566
  }
@@ -2191,7 +2582,7 @@ var SSDiagram = (() => {
2191
2582
  return null;
2192
2583
  }
2193
2584
  endpoint(l, end) {
2194
- const node = this.nodes.find((n) => n.id === (end === "from" ? l.from : l.to));
2585
+ const node = this.nodes.find((n2) => n2.id === (end === "from" ? l.from : l.to));
2195
2586
  if (node === void 0) return null;
2196
2587
  const port = end === "from" ? node.outPorts.find((p) => p.id === l.fromPort) : node.inPorts.find((p) => p.id === l.toPort);
2197
2588
  if (port === void 0) return null;
@@ -2289,39 +2680,39 @@ var SSDiagram = (() => {
2289
2680
  }
2290
2681
  let best = null;
2291
2682
  let bestD = SNAP_PX;
2292
- for (const n of this.nodes) {
2293
- const ports = direction === "in" ? n.inPorts : n.outPorts;
2683
+ for (const n2 of this.nodes) {
2684
+ const ports = direction === "in" ? n2.inPorts : n2.outPorts;
2294
2685
  for (const p of ports) {
2295
- if (!accepts(n, p)) continue;
2686
+ if (!accepts(n2, p)) continue;
2296
2687
  const [sx, sy] = this.toScreen(p.cx, p.cy);
2297
2688
  const dpx = Math.hypot(sx - this.cursor.x, sy - this.cursor.y);
2298
2689
  if (dpx <= bestD) {
2299
2690
  bestD = dpx;
2300
- best = { node: n, port: p };
2691
+ best = { node: n2, port: p };
2301
2692
  }
2302
2693
  }
2303
2694
  }
2304
2695
  return best;
2305
2696
  }
2306
- selectNode(n) {
2697
+ selectNode(n2) {
2307
2698
  this.selectedLink = null;
2308
2699
  this.selectedPort = null;
2309
2700
  this.selectedNodes.clear();
2310
- if (n !== null) this.selectedNodes.add(n);
2311
- this.selectedNode = n;
2312
- this.emit("nodeSelected", { node: n, selected: n !== null });
2701
+ if (n2 !== null) this.selectedNodes.add(n2);
2702
+ this.selectedNode = n2;
2703
+ this.emit("nodeSelected", { node: n2, selected: n2 !== null });
2313
2704
  this.emitSelectionChanged();
2314
2705
  this.scheduleDraw();
2315
2706
  }
2316
- toggleSelect(n) {
2707
+ toggleSelect(n2) {
2317
2708
  this.selectedLink = null;
2318
2709
  this.selectedPort = null;
2319
- if (this.selectedNodes.has(n)) {
2320
- this.selectedNodes.delete(n);
2321
- if (this.selectedNode === n) this.selectedNode = null;
2710
+ if (this.selectedNodes.has(n2)) {
2711
+ this.selectedNodes.delete(n2);
2712
+ if (this.selectedNode === n2) this.selectedNode = null;
2322
2713
  } else {
2323
- this.selectedNodes.add(n);
2324
- this.selectedNode = n;
2714
+ this.selectedNodes.add(n2);
2715
+ this.selectedNode = n2;
2325
2716
  }
2326
2717
  this.emit("nodeSelected", { node: this.selectedNode, selected: this.selectedNodes.size > 0 });
2327
2718
  this.emitSelectionChanged();
@@ -2365,10 +2756,10 @@ var SSDiagram = (() => {
2365
2756
  this.clip = null;
2366
2757
  return null;
2367
2758
  }
2368
- const ids = new Set([...this.selectedNodes].map((n) => n.id));
2759
+ const ids = new Set([...this.selectedNodes].map((n2) => n2.id));
2369
2760
  const all = this.saveDocument();
2370
2761
  this.clip = createDiagramDocument({
2371
- nodes: all.nodes.filter((n) => ids.has(n.id)),
2762
+ nodes: all.nodes.filter((n2) => ids.has(n2.id)),
2372
2763
  links: all.links.filter((link) => ids.has(link.from.nodeId) && ids.has(link.to.nodeId))
2373
2764
  });
2374
2765
  return parseDiagramDocument(this.clip);
@@ -2409,12 +2800,12 @@ var SSDiagram = (() => {
2409
2800
  }
2410
2801
  for (const l of clipboard.links) {
2411
2802
  const f = map.get(l.from.nodeId);
2412
- const t = map.get(l.to.nodeId);
2413
- if (f !== void 0 && t !== void 0)
2803
+ const t2 = map.get(l.to.nodeId);
2804
+ if (f !== void 0 && t2 !== void 0)
2414
2805
  this.addLink({
2415
2806
  from: f,
2416
2807
  fromPort: l.from.portId,
2417
- to: t,
2808
+ to: t2,
2418
2809
  toPort: l.to.portId,
2419
2810
  metadata: l.metadata
2420
2811
  });
@@ -2718,7 +3109,7 @@ var SSDiagram = (() => {
2718
3109
  const ry1 = Math.max(this.rubber.y0, this.rubber.y);
2719
3110
  this.rubber = null;
2720
3111
  if (rx1 - rx0 > 3 || ry1 - ry0 > 3) {
2721
- const inRect = this.nodes.filter((n) => n.x < rx1 && n.x + n.w > rx0 && n.y < ry1 && n.y + n.h > ry0);
3112
+ const inRect = this.nodes.filter((n2) => n2.x < rx1 && n2.x + n2.w > rx0 && n2.y < ry1 && n2.y + n2.h > ry0);
2722
3113
  const merge = e.shiftKey ? [...this.selectedNodes, ...inRect] : inRect;
2723
3114
  this.setSelection([...new Set(merge)]);
2724
3115
  }
@@ -2951,7 +3342,7 @@ var SSDiagram = (() => {
2951
3342
  const a = this.endpoint(l, "from");
2952
3343
  const b = this.endpoint(l, "to");
2953
3344
  if (a === null || b === null) continue;
2954
- const fp = this.nodes.find((n) => n.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
3345
+ const fp = this.nodes.find((n2) => n2.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
2955
3346
  const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.portColor(fp.type) : this.portColor("");
2956
3347
  const state = options.selection && l === this.selectedLink ? "sel" : options.transient && (l === this.hoveredLink || hoveredNode !== null && (l.from === hoveredNode.id || l.to === hoveredNode.id)) ? "hov" : "norm";
2957
3348
  const color = state === "sel" ? this.selectionColor() : state === "hov" ? this.linkHoverColor() : baseColor;
@@ -2967,7 +3358,7 @@ var SSDiagram = (() => {
2967
3358
  }
2968
3359
  }
2969
3360
  if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
2970
- for (const n of this.nodes) this.drawNode(n, options.selection && this.selectedNodes.has(n), options);
3361
+ for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
2971
3362
  if (options.selection && this.permissions.createLinks && this.relinking === null)
2972
3363
  this.drawSelectedLinkEndpoints();
2973
3364
  if (options.transient && this.rubber !== null) {
@@ -2985,7 +3376,7 @@ var SSDiagram = (() => {
2985
3376
  if (options.overview) this.drawOverview();
2986
3377
  if (options.runtime) this.drawGlobalError();
2987
3378
  if (options.transient) this.drawTooltip();
2988
- if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((n) => n.errorFlashStart !== null)))
3379
+ if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((n2) => n2.errorFlashStart !== null)))
2989
3380
  this.scheduleDraw();
2990
3381
  }
2991
3382
  drawGlobalError() {
@@ -3049,17 +3440,17 @@ ${runtime.error}`;
3049
3440
  isError = true;
3050
3441
  }
3051
3442
  } else if (this.hoverNode !== null) {
3052
- const n = this.hoverNode;
3053
- const errors = [n.runtimeError, n.loadError].filter((value) => value.length > 0);
3443
+ const n2 = this.hoverNode;
3444
+ const errors = [n2.runtimeError, n2.loadError].filter((value) => value.length > 0);
3054
3445
  if (errors.length > 0) {
3055
3446
  text = errors.join("\n");
3056
3447
  isError = true;
3057
- } else if (this.showNodeMessages && n.message.length > 0) {
3058
- text = n.message;
3059
- } else if (n.description.length > 0) {
3060
- text = n.description;
3448
+ } else if (this.showNodeMessages && n2.message.length > 0) {
3449
+ text = n2.message;
3450
+ } else if (n2.description.length > 0) {
3451
+ text = n2.description;
3061
3452
  } else {
3062
- text = n.typeId && n.typeId !== n.name ? `${n.name} [${n.typeId}]` : n.name;
3453
+ text = n2.typeId && n2.typeId !== n2.name ? `${n2.name} [${n2.typeId}]` : n2.name;
3063
3454
  }
3064
3455
  } else {
3065
3456
  return;
@@ -3172,24 +3563,24 @@ ${runtime.error}`;
3172
3563
  img.src = url;
3173
3564
  return null;
3174
3565
  }
3175
- drawNode(n, selected, options) {
3566
+ drawNode(n2, selected, options) {
3176
3567
  const ctx = this.ctx;
3177
- const hasLoadError = options.runtime && n.loadError.length > 0;
3178
- const hasRuntimeError = options.runtime && n.runtimeError.length > 0;
3179
- const runtime = options.runtime ? this.runtimeState.nodes[n.id] : void 0;
3180
- const active = options.runtime && (this.runtimeState.activeNodeId === n.id || runtime?.active === true);
3181
- roundRect(ctx, n.x, n.y, n.w, n.h, 6);
3182
- ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : n.color;
3568
+ const hasLoadError = options.runtime && n2.loadError.length > 0;
3569
+ const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
3570
+ const runtime = options.runtime ? this.runtimeState.nodes[n2.id] : void 0;
3571
+ const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
3572
+ roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
3573
+ ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : n2.color;
3183
3574
  ctx.fill();
3184
3575
  ctx.lineWidth = selected ? 2 : 1.5;
3185
- ctx.strokeStyle = selected ? this.selectionColor() : n.border;
3576
+ ctx.strokeStyle = selected ? this.selectionColor() : n2.border;
3186
3577
  ctx.stroke();
3187
3578
  if (hasLoadError || hasRuntimeError) {
3188
3579
  let alpha = 1;
3189
- if (options.transient && hasRuntimeError && n.errorFlashStart !== null) {
3190
- const elapsed = performance.now() - n.errorFlashStart;
3580
+ if (options.transient && hasRuntimeError && n2.errorFlashStart !== null) {
3581
+ const elapsed = performance.now() - n2.errorFlashStart;
3191
3582
  if (elapsed >= ERROR_FLASH_MS) {
3192
- n.errorFlashStart = null;
3583
+ n2.errorFlashStart = null;
3193
3584
  } else {
3194
3585
  alpha = 0.12 + 0.88 * (0.5 + 0.5 * Math.sin(
3195
3586
  elapsed / ERROR_FLASH_MS * Math.PI * 6 - Math.PI / 2
@@ -3198,30 +3589,30 @@ ${runtime.error}`;
3198
3589
  }
3199
3590
  ctx.save();
3200
3591
  ctx.globalAlpha *= alpha;
3201
- roundRect(ctx, n.x, n.y, n.w, n.h, 6);
3592
+ roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
3202
3593
  ctx.lineWidth = hasRuntimeError ? 3 : 2;
3203
3594
  ctx.strokeStyle = ERROR_RED;
3204
3595
  ctx.stroke();
3205
3596
  ctx.restore();
3206
3597
  }
3207
3598
  const iconW = 18;
3208
- if (n.icon) {
3209
- const img = this.getIcon(n.icon);
3599
+ if (n2.icon) {
3600
+ const img = this.getIcon(n2.icon);
3210
3601
  if (img) {
3211
3602
  try {
3212
- ctx.drawImage(img, n.x + 8, n.y + (n.h - iconW) / 2, iconW, iconW);
3603
+ ctx.drawImage(img, n2.x + 8, n2.y + (n2.h - iconW) / 2, iconW, iconW);
3213
3604
  } catch {
3214
3605
  }
3215
3606
  }
3216
3607
  }
3217
- const titleShift = n.icon ? iconW + 4 : 0;
3608
+ const titleShift = n2.icon ? iconW + 4 : 0;
3218
3609
  ctx.fillStyle = hasLoadError ? "#ffffff" : "#1b1b1b";
3219
3610
  ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
3220
3611
  ctx.textAlign = "center";
3221
3612
  ctx.textBaseline = "middle";
3222
- ctx.fillText(n.name, n.x + titleShift + (n.w - titleShift) / 2, n.y + n.h / 2, n.w - titleShift - 14);
3223
- for (const p of n.inPorts) this.drawPort(n, p, options);
3224
- for (const p of n.outPorts) this.drawPort(n, p, options);
3613
+ ctx.fillText(n2.name, n2.x + titleShift + (n2.w - titleShift) / 2, n2.y + n2.h / 2, n2.w - titleShift - 14);
3614
+ for (const p of n2.inPorts) this.drawPort(n2, p, options);
3615
+ for (const p of n2.outPorts) this.drawPort(n2, p, options);
3225
3616
  }
3226
3617
  drawPort(node, p, options) {
3227
3618
  const ctx = this.ctx;
@@ -3319,9 +3710,9 @@ ${runtime.error}`;
3319
3710
  ctx.setLineDash([]);
3320
3711
  }
3321
3712
  drawArrow(pts, color) {
3322
- const n = pts.length;
3323
- const [px, py] = pts[n - 2];
3324
- const [ex, ey] = pts[n - 1];
3713
+ const n2 = pts.length;
3714
+ const [px, py] = pts[n2 - 2];
3715
+ const [ex, ey] = pts[n2 - 1];
3325
3716
  const ang = Math.atan2(ey - py, ex - px);
3326
3717
  const ctx = this.ctx;
3327
3718
  ctx.save();
@@ -3450,12 +3841,12 @@ ${runtime.error}`;
3450
3841
  ctx.lineWidth = 1;
3451
3842
  ctx.stroke();
3452
3843
  ctx.clip();
3453
- for (const n of this.nodes) {
3454
- ctx.fillStyle = n.color;
3455
- ctx.fillRect(mx(n.x), my(n.y), Math.max(2, n.w * s), Math.max(2, n.h * s));
3456
- ctx.strokeStyle = n.border;
3844
+ for (const n2 of this.nodes) {
3845
+ ctx.fillStyle = n2.color;
3846
+ ctx.fillRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
3847
+ ctx.strokeStyle = n2.border;
3457
3848
  ctx.lineWidth = 1;
3458
- ctx.strokeRect(mx(n.x), my(n.y), Math.max(2, n.w * s), Math.max(2, n.h * s));
3849
+ ctx.strokeRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
3459
3850
  }
3460
3851
  const [vx0, vy0] = this.toWorld(0, 0);
3461
3852
  const [vx1, vy1] = this.toWorld(this.width, this.height);
@@ -3614,11 +4005,11 @@ ${runtime.error}`;
3614
4005
  cols = Math.ceil((maxX - minX) / cell);
3615
4006
  rows = Math.ceil((maxY - minY) / cell);
3616
4007
  }
3617
- const n = cols * rows;
3618
- if (n <= 0 || n > 8e4) return null;
4008
+ const n2 = cols * rows;
4009
+ if (n2 <= 0 || n2 > 8e4) return null;
3619
4010
  const cx = (c) => minX + (c + 0.5) * cell;
3620
4011
  const cy = (r) => minY + (r + 0.5) * cell;
3621
- const blocked = new Uint8Array(n);
4012
+ const blocked = new Uint8Array(n2);
3622
4013
  for (const r of rects) {
3623
4014
  const c0 = Math.max(0, Math.floor((r[0] - minX) / cell));
3624
4015
  const c1 = Math.min(cols - 1, Math.floor((r[0] + r[2] - minX) / cell));
@@ -3635,17 +4026,17 @@ ${runtime.error}`;
3635
4026
  const gi = gr * cols + gc;
3636
4027
  blocked[si] = 0;
3637
4028
  blocked[gi] = 0;
3638
- const g = new Float64Array(n).fill(Infinity);
3639
- const prev = new Int32Array(n).fill(-1);
3640
- const dir = new Int8Array(n).fill(-1);
4029
+ const g = new Float64Array(n2).fill(Infinity);
4030
+ const prev = new Int32Array(n2).fill(-1);
4031
+ const dir = new Int8Array(n2).fill(-1);
3641
4032
  g[si] = 0;
3642
4033
  const heap = [si];
3643
- const f = new Float64Array(n);
4034
+ const f = new Float64Array(n2);
3644
4035
  f[si] = (Math.abs(gc - sc) + Math.abs(gr - sr)) * A_STEP;
3645
4036
  const swap = (i, j) => {
3646
- const t = heap[i];
4037
+ const t2 = heap[i];
3647
4038
  heap[i] = heap[j];
3648
- heap[j] = t;
4039
+ heap[j] = t2;
3649
4040
  };
3650
4041
  const push = (v) => {
3651
4042
  heap.push(v);
@@ -3676,7 +4067,7 @@ ${runtime.error}`;
3676
4067
  }
3677
4068
  return top;
3678
4069
  };
3679
- const seen = new Uint8Array(n);
4070
+ const seen = new Uint8Array(n2);
3680
4071
  while (heap.length > 0) {
3681
4072
  const cur = pop();
3682
4073
  if (cur === gi) break;
@@ -3734,13 +4125,267 @@ ${runtime.error}`;
3734
4125
  const dx = x2 - x1;
3735
4126
  const dy = y2 - y1;
3736
4127
  const len2 = dx * dx + dy * dy;
3737
- let t = len2 === 0 ? 0 : ((px - x1) * dx + (py - y1) * dy) / len2;
3738
- t = clamp(t, 0, 1);
3739
- const cx = x1 + t * dx;
3740
- const cy = y1 + t * dy;
4128
+ let t2 = len2 === 0 ? 0 : ((px - x1) * dx + (py - y1) * dy) / len2;
4129
+ t2 = clamp(t2, 0, 1);
4130
+ const cx = x1 + t2 * dx;
4131
+ const cy = y1 + t2 * dy;
3741
4132
  return Math.hypot(px - cx, py - cy);
3742
4133
  }
3743
4134
 
4135
+ // src/i18n.ts
4136
+ var i18n = typeof window === "undefined" ? {} : window.__designerI18n ?? {};
4137
+ function t(key, fallback) {
4138
+ return i18n[key] ?? fallback;
4139
+ }
4140
+
4141
+ // src/diagram/context-menu.ts
4142
+ var ROOT_CLASS = "ssdiagram-context-menu";
4143
+ var ITEM_CLASS = "ssdiagram-context-menu-item";
4144
+ var SUBMENU_CLASS = "ssdiagram-context-menu-submenu";
4145
+ var SEPARATOR_CLASS = "ssdiagram-context-menu-separator";
4146
+ var LABEL_CLASS = "ssdiagram-context-menu-label";
4147
+ var ARROW_CLASS = "ssdiagram-context-menu-arrow";
4148
+ var SEPARATE_AFTER = /* @__PURE__ */ new Set(["redo", "paste", "delete"]);
4149
+ function label(id) {
4150
+ switch (id) {
4151
+ case "undo":
4152
+ return t("undo", "Undo");
4153
+ case "redo":
4154
+ return t("redo", "Redo");
4155
+ case "cut":
4156
+ return t("cut", "Cut");
4157
+ case "copy":
4158
+ return t("copy", "Copy");
4159
+ case "paste":
4160
+ return t("paste", "Paste");
4161
+ case "open":
4162
+ return t("ctxOpen", "Open");
4163
+ case "delete":
4164
+ return t("delete", "Delete");
4165
+ case "export":
4166
+ return t("ctxExportAs", "Export as");
4167
+ case "exportDocument":
4168
+ return t("ctxExportDocument", "Scheme");
4169
+ case "exportPng":
4170
+ return t("ctxExportPng", "PNG image");
4171
+ case "exportSvg":
4172
+ return t("ctxExportSvg", "SVG image");
4173
+ case "properties":
4174
+ return t("properties", "Properties");
4175
+ case "help":
4176
+ return t("ctxHelp", "Help");
4177
+ }
4178
+ }
4179
+ var PANEL_STYLE = {
4180
+ position: "fixed",
4181
+ zIndex: "2147483000",
4182
+ minWidth: "176px",
4183
+ padding: "4px",
4184
+ border: "1px solid var(--ssdiagram-menu-border, var(--ssdiagram-control-border, var(--t-border, #3a4250)))",
4185
+ borderRadius: "7px",
4186
+ background: "var(--ssdiagram-menu-background, var(--ssdiagram-control-background, var(--t-panel, #181a20)))",
4187
+ color: "var(--ssdiagram-menu-color, var(--ssdiagram-control-color, var(--t-text, #eaecef)))",
4188
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.45)",
4189
+ font: 'var(--ssdiagram-menu-font, 12.5px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif)',
4190
+ boxSizing: "border-box",
4191
+ userSelect: "none"
4192
+ };
4193
+ var ITEM_STYLE = {
4194
+ display: "flex",
4195
+ alignItems: "center",
4196
+ gap: "10px",
4197
+ width: "100%",
4198
+ padding: "6px 10px",
4199
+ border: "0",
4200
+ borderRadius: "4px",
4201
+ background: "transparent",
4202
+ color: "inherit",
4203
+ font: "inherit",
4204
+ textAlign: "left",
4205
+ whiteSpace: "nowrap",
4206
+ cursor: "pointer",
4207
+ boxSizing: "border-box"
4208
+ };
4209
+ var HOVER_BACKGROUND = "var(--ssdiagram-menu-hover, var(--t-hover, rgba(255, 255, 255, 0.08)))";
4210
+ var DISABLED_COLOR = "var(--ssdiagram-menu-disabled-color, var(--t-text-dim, #848e9c))";
4211
+ var ContextMenuView = class {
4212
+ constructor(options) {
4213
+ this.root = null;
4214
+ this.submenu = null;
4215
+ this.detach = [];
4216
+ this.container = options.container;
4217
+ this.execute = options.execute;
4218
+ }
4219
+ get isOpen() {
4220
+ return this.root !== null;
4221
+ }
4222
+ show(x, y, items) {
4223
+ this.hide();
4224
+ const owner = this.container.ownerDocument ?? document;
4225
+ const root = this.panel(owner);
4226
+ root.className = ROOT_CLASS;
4227
+ root.setAttribute("role", "menu");
4228
+ items.forEach((item, index) => {
4229
+ if ("group" in item) root.appendChild(this.groupEntry(owner, item));
4230
+ else root.appendChild(this.commandEntry(owner, item.command, item.enabled, true));
4231
+ const id = "group" in item ? item.group : item.command;
4232
+ if (SEPARATE_AFTER.has(id) && index < items.length - 1) root.appendChild(this.separator(owner));
4233
+ });
4234
+ this.container.appendChild(root);
4235
+ this.root = root;
4236
+ this.place(root, x, y);
4237
+ this.watchForDismissal(owner);
4238
+ }
4239
+ hide() {
4240
+ for (const dispose of this.detach.splice(0)) dispose();
4241
+ this.submenu = null;
4242
+ this.root?.remove();
4243
+ this.root = null;
4244
+ }
4245
+ destroy() {
4246
+ this.hide();
4247
+ }
4248
+ panel(owner) {
4249
+ const panel = owner.createElement("div");
4250
+ Object.assign(panel.style, PANEL_STYLE);
4251
+ return panel;
4252
+ }
4253
+ separator(owner) {
4254
+ const rule = owner.createElement("div");
4255
+ rule.className = SEPARATOR_CLASS;
4256
+ Object.assign(rule.style, {
4257
+ height: "1px",
4258
+ margin: "4px 6px",
4259
+ background: "var(--ssdiagram-menu-border, var(--t-border, #3a4250))"
4260
+ });
4261
+ return rule;
4262
+ }
4263
+ item(owner, text, enabled, arrow) {
4264
+ const button = owner.createElement("button");
4265
+ button.type = "button";
4266
+ button.className = ITEM_CLASS;
4267
+ button.setAttribute("role", "menuitem");
4268
+ Object.assign(button.style, ITEM_STYLE);
4269
+ if (!enabled) {
4270
+ button.disabled = true;
4271
+ button.style.color = DISABLED_COLOR;
4272
+ button.style.cursor = "default";
4273
+ }
4274
+ const caption = owner.createElement("span");
4275
+ caption.className = LABEL_CLASS;
4276
+ caption.textContent = text;
4277
+ caption.style.flex = "1";
4278
+ button.appendChild(caption);
4279
+ if (arrow) {
4280
+ const chevron = owner.createElement("span");
4281
+ chevron.className = ARROW_CLASS;
4282
+ chevron.textContent = "\u203A";
4283
+ chevron.setAttribute("aria-hidden", "true");
4284
+ Object.assign(chevron.style, { flex: "none", opacity: "0.7", fontSize: "14px" });
4285
+ button.appendChild(chevron);
4286
+ }
4287
+ return button;
4288
+ }
4289
+ // `closesSubmenu` separates the two places an item can live. A top-level item shuts any
4290
+ // open submenu when the pointer arrives, the way menus everywhere behave. An item inside
4291
+ // the submenu must not: reaching it means the pointer has entered that very panel, and
4292
+ // closing it there would pull it out from under the pointer and drop the click on the
4293
+ // canvas behind.
4294
+ commandEntry(owner, command, enabled, closesSubmenu) {
4295
+ const button = this.item(owner, label(command), enabled, false);
4296
+ this.on(button, "pointerenter", () => {
4297
+ if (closesSubmenu) this.closeSubmenu();
4298
+ if (enabled) button.style.background = HOVER_BACKGROUND;
4299
+ });
4300
+ this.on(button, "pointerleave", () => {
4301
+ button.style.background = "transparent";
4302
+ });
4303
+ this.on(button, "click", () => {
4304
+ if (!enabled) return;
4305
+ this.hide();
4306
+ this.execute(command);
4307
+ });
4308
+ return button;
4309
+ }
4310
+ groupEntry(owner, group) {
4311
+ const host = owner.createElement("div");
4312
+ host.style.position = "relative";
4313
+ const button = this.item(owner, label(group.group), group.enabled, true);
4314
+ host.appendChild(button);
4315
+ const panel = this.panel(owner);
4316
+ panel.className = SUBMENU_CLASS;
4317
+ panel.setAttribute("role", "menu");
4318
+ Object.assign(panel.style, { position: "absolute", left: "100%", top: "-5px", display: "none" });
4319
+ for (const child of group.commands) panel.appendChild(this.commandEntry(owner, child.command, child.enabled, false));
4320
+ host.appendChild(panel);
4321
+ const open = () => {
4322
+ if (!group.enabled) return;
4323
+ button.style.background = HOVER_BACKGROUND;
4324
+ panel.style.display = "block";
4325
+ this.submenu = panel;
4326
+ this.keepOnScreen(panel);
4327
+ };
4328
+ this.on(button, "pointerenter", open);
4329
+ this.on(button, "click", open);
4330
+ this.on(host, "pointerleave", () => {
4331
+ if (this.submenu !== panel) return;
4332
+ button.style.background = "transparent";
4333
+ panel.style.display = "none";
4334
+ this.submenu = null;
4335
+ });
4336
+ return host;
4337
+ }
4338
+ closeSubmenu() {
4339
+ if (this.submenu === null) return;
4340
+ this.submenu.style.display = "none";
4341
+ this.submenu = null;
4342
+ }
4343
+ /** Keeps the panel inside the viewport, flipping it left when it would run off the edge. */
4344
+ keepOnScreen(panel) {
4345
+ if (typeof panel.getBoundingClientRect !== "function" || typeof window === "undefined") return;
4346
+ const box = panel.getBoundingClientRect();
4347
+ if (box.right > window.innerWidth - 4) {
4348
+ panel.style.left = "auto";
4349
+ panel.style.right = "100%";
4350
+ }
4351
+ if (box.bottom > window.innerHeight - 4) {
4352
+ panel.style.top = "auto";
4353
+ panel.style.bottom = "-5px";
4354
+ }
4355
+ }
4356
+ place(root, x, y) {
4357
+ root.style.left = `${x}px`;
4358
+ root.style.top = `${y}px`;
4359
+ if (typeof root.getBoundingClientRect !== "function" || typeof window === "undefined") return;
4360
+ const box = root.getBoundingClientRect();
4361
+ if (box.right > window.innerWidth - 4) root.style.left = `${Math.max(4, x - box.width)}px`;
4362
+ if (box.bottom > window.innerHeight - 4) root.style.top = `${Math.max(4, y - box.height)}px`;
4363
+ }
4364
+ watchForDismissal(owner) {
4365
+ const dismiss = () => this.hide();
4366
+ const outside = (event) => {
4367
+ const root = this.root;
4368
+ const target = event.target;
4369
+ if (root !== null && target !== null && typeof root.contains === "function" && root.contains(target)) return;
4370
+ this.hide();
4371
+ };
4372
+ this.on(owner, "pointerdown", outside, true);
4373
+ this.on(owner, "keydown", (event) => {
4374
+ if (event.key === "Escape") this.hide();
4375
+ });
4376
+ if (typeof window !== "undefined") {
4377
+ this.on(window, "blur", dismiss);
4378
+ this.on(window, "resize", dismiss);
4379
+ this.on(window, "scroll", dismiss, true);
4380
+ }
4381
+ }
4382
+ on(target, type, listener, capture = false) {
4383
+ if (typeof target.addEventListener !== "function") return;
4384
+ target.addEventListener(type, listener, capture);
4385
+ this.detach.push(() => target.removeEventListener?.(type, listener, capture));
4386
+ }
4387
+ };
4388
+
3744
4389
  // src/diagram/event-emitter.ts
3745
4390
  var EventEmitter = class {
3746
4391
  constructor() {
@@ -3895,6 +4540,18 @@ ${runtime.error}`;
3895
4540
  };
3896
4541
 
3897
4542
  // src/diagram/stocksharp-diagram.ts
4543
+ var CONTEXT_MENU = [
4544
+ "undo",
4545
+ "redo",
4546
+ "cut",
4547
+ "copy",
4548
+ "paste",
4549
+ "open",
4550
+ "delete",
4551
+ { group: "export", commands: ["exportDocument", "exportPng", "exportSvg"] },
4552
+ "properties",
4553
+ "help"
4554
+ ];
3898
4555
  var StockSharpDiagram = class extends EventEmitter {
3899
4556
  constructor(options) {
3900
4557
  super();
@@ -3909,6 +4566,7 @@ ${runtime.error}`;
3909
4566
  this.fullscreenButtonVisible = true;
3910
4567
  this.fullscreenLabels = { enter: "Enter fullscreen", exit: "Exit fullscreen" };
3911
4568
  this.contextActions = new DiagramActionRegistry();
4569
+ this.contextMenu = null;
3912
4570
  this.div = options.div;
3913
4571
  this.catalog = options.catalog;
3914
4572
  this.overviewContainer = options.overviewContainer ?? null;
@@ -3925,6 +4583,7 @@ ${runtime.error}`;
3925
4583
  this.prepareFullscreenButtonHost();
3926
4584
  this.fullscreenButton = this.createFullscreenButton();
3927
4585
  this.updateFullscreenButton();
4586
+ this.setContextMenuEnabled(options.showContextMenu ?? true);
3928
4587
  this.registerContextActions();
3929
4588
  this.bindCanvasEvents();
3930
4589
  this.disposables.push(this.catalog.on("portTypesChanged", () => this.applySocketTheme()));
@@ -4084,8 +4743,8 @@ ${runtime.error}`;
4084
4743
  return this.canvas.updateNode(nodeId, { name: value });
4085
4744
  }
4086
4745
  /** Groups host-driven document edits into one undo/redo operation. */
4087
- transaction(label, action) {
4088
- return this.canvas.withTransaction(label, action);
4746
+ transaction(label2, action) {
4747
+ return this.canvas.withTransaction(label2, action);
4089
4748
  }
4090
4749
  setShowNodeMessages(show) {
4091
4750
  this.canvas.setShowNodeMessages(show);
@@ -4134,6 +4793,10 @@ ${runtime.error}`;
4134
4793
  takeScreenshot(options = {}) {
4135
4794
  return this.canvas.takeScreenshot(options);
4136
4795
  }
4796
+ /** The same picture as takeScreenshot, as an SVG document. */
4797
+ takeSvg(options = {}) {
4798
+ return this.canvas.takeSvg(options);
4799
+ }
4137
4800
  getSelection() {
4138
4801
  return this.canvas.getSelection();
4139
4802
  }
@@ -4266,11 +4929,40 @@ ${runtime.error}`;
4266
4929
  const fallback = this.canvas.getClipboardDocument();
4267
4930
  return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
4268
4931
  }
4932
+ /**
4933
+ * Turns the built-in menu on or off. Off leaves `contextMenuRequested` untouched, so a
4934
+ * host that draws its own is only trading one menu for another, never losing the event.
4935
+ */
4936
+ setContextMenuEnabled(enabled) {
4937
+ if (enabled === (this.contextMenu !== null)) return;
4938
+ if (!enabled) {
4939
+ this.contextMenu?.destroy();
4940
+ this.contextMenu = null;
4941
+ return;
4942
+ }
4943
+ this.contextMenu = new ContextMenuView({
4944
+ container: this.div,
4945
+ // Through the public entry point, so the built-in menu is exactly as privileged
4946
+ // as a host's own -- same guards, same contextCommand event.
4947
+ execute: (command) => {
4948
+ this.executeContextCommand(command);
4949
+ }
4950
+ });
4951
+ }
4952
+ isContextMenuEnabled() {
4953
+ return this.contextMenu !== null;
4954
+ }
4269
4955
  getContextCommands() {
4270
- return this.contextActions.states(this.contextActionContext()).map(({ id, enabled }) => ({
4271
- command: id,
4272
- enabled
4273
- }));
4956
+ const context = this.contextActionContext();
4957
+ const state = (command) => ({
4958
+ command,
4959
+ enabled: this.contextActions.canExecute(command, context)
4960
+ });
4961
+ return CONTEXT_MENU.map((entry) => {
4962
+ if (typeof entry === "string") return state(entry);
4963
+ const commands = entry.commands.map(state);
4964
+ return { group: entry.group, enabled: commands.some(({ enabled }) => enabled), commands };
4965
+ });
4274
4966
  }
4275
4967
  executeContextCommand(command) {
4276
4968
  const context = this.contextActionContext();
@@ -4338,6 +5030,8 @@ ${runtime.error}`;
4338
5030
  destroy() {
4339
5031
  if (this.destroyed) return;
4340
5032
  this.destroyed = true;
5033
+ this.contextMenu?.destroy();
5034
+ this.contextMenu = null;
4341
5035
  for (const dispose of this.disposables.splice(0).reverse()) dispose();
4342
5036
  this.canvas.destroy();
4343
5037
  }
@@ -4390,11 +5084,11 @@ ${runtime.error}`;
4390
5084
  }
4391
5085
  updateFullscreenButton() {
4392
5086
  const fullscreen = this.fullscreen;
4393
- const label = fullscreen ? this.fullscreenLabels.exit : this.fullscreenLabels.enter;
5087
+ const label2 = fullscreen ? this.fullscreenLabels.exit : this.fullscreenLabels.enter;
4394
5088
  const path = fullscreen ? "M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5" : "M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5";
4395
5089
  this.fullscreenButton.className = fullscreen ? "ssdiagram-fullscreen-button is-active" : "ssdiagram-fullscreen-button";
4396
- this.fullscreenButton.title = label;
4397
- this.fullscreenButton.setAttribute("aria-label", label);
5090
+ this.fullscreenButton.title = label2;
5091
+ this.fullscreenButton.setAttribute("aria-label", label2);
4398
5092
  this.fullscreenButton.setAttribute("aria-pressed", String(fullscreen));
4399
5093
  this.fullscreenButton.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${path}"/></svg>`;
4400
5094
  }
@@ -4490,6 +5184,7 @@ ${runtime.error}`;
4490
5184
  }),
4491
5185
  this.canvas.on("undoStackChanged", (state) => this.emit("undoStackChanged", state)),
4492
5186
  this.canvas.on("contextMenu", ({ x, y, node, link, port }) => {
5187
+ const commands = this.getContextCommands();
4493
5188
  this.emit("contextMenuRequested", {
4494
5189
  x,
4495
5190
  y,
@@ -4497,8 +5192,9 @@ ${runtime.error}`;
4497
5192
  link: link === null ? null : this.fromCanvasLink(link),
4498
5193
  port: port === null ? null : this.fromCanvasPort(port.port),
4499
5194
  portDirection: port?.port.direction ?? null,
4500
- commands: this.getContextCommands()
5195
+ commands
4501
5196
  });
5197
+ this.contextMenu?.show(x, y, commands);
4502
5198
  })
4503
5199
  );
4504
5200
  }
@@ -4533,6 +5229,24 @@ ${runtime.error}`;
4533
5229
  canExecute: ({ selection }) => permissions().deleteSelection && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
4534
5230
  execute: () => this.canvas.deleteSelection()
4535
5231
  },
5232
+ // Export asks rather than does. Where the result goes -- a download, an upload,
5233
+ // a clipboard, a print preview -- is the host's decision, and so is which options
5234
+ // to render with, so the control reports the request and stops there.
5235
+ // Availability follows the diagram, not the selection: all three export the whole
5236
+ // picture, and an empty diagram has none to give. They stay on in read-only mode,
5237
+ // which forbids changing the diagram, not looking at it.
5238
+ exportDocument: {
5239
+ canExecute: ({ document: document2 }) => document2.nodes.length > 0,
5240
+ execute: () => this.emit("exportRequested", { format: "document" })
5241
+ },
5242
+ exportPng: {
5243
+ canExecute: ({ document: document2 }) => document2.nodes.length > 0,
5244
+ execute: () => this.emit("exportRequested", { format: "png" })
5245
+ },
5246
+ exportSvg: {
5247
+ canExecute: ({ document: document2 }) => document2.nodes.length > 0,
5248
+ execute: () => this.emit("exportRequested", { format: "svg" })
5249
+ },
4536
5250
  properties: {
4537
5251
  canExecute: ({ nodes }) => nodes.length > 0,
4538
5252
  execute: ({ nodes }) => this.emit("nodeProperties", { nodes })
@@ -4550,6 +5264,7 @@ ${runtime.error}`;
4550
5264
  const linkIds = new Set(selection.linkIds);
4551
5265
  return {
4552
5266
  selection,
5267
+ document: document2,
4553
5268
  nodes: document2.nodes.filter((node) => nodeIds.has(node.id)).map((node) => this.fromCanvasInit(node)),
4554
5269
  links: document2.links.filter((link) => linkIds.has(link.id)).map((link) => new Link({
4555
5270
  id: link.id,
@@ -4708,8 +5423,8 @@ ${runtime.error}`;
4708
5423
  // imported node ("Element type is missing from the palette"). Node.id
4709
5424
  // keeps its original casing so save/load round-trips unchanged.
4710
5425
  addNodeType(node) {
4711
- const n = (node instanceof Node ? node : new Node(node)).clone();
4712
- this.nodeTypes.set(n.id.toLowerCase(), n);
5426
+ const n2 = (node instanceof Node ? node : new Node(node)).clone();
5427
+ this.nodeTypes.set(n2.id.toLowerCase(), n2);
4713
5428
  this.emitNodeTypesChanged();
4714
5429
  }
4715
5430
  removeNodeType(id) {
@@ -4853,11 +5568,11 @@ ${runtime.error}`;
4853
5568
  const chev = document.createElement("span");
4854
5569
  chev.className = "d-palette-chev";
4855
5570
  chev.setAttribute("aria-hidden", "true");
4856
- const label = document.createElement("span");
4857
- label.className = "d-palette-group-name";
4858
- label.textContent = name;
5571
+ const label2 = document.createElement("span");
5572
+ label2.className = "d-palette-group-name";
5573
+ label2.textContent = name;
4859
5574
  head.appendChild(chev);
4860
- head.appendChild(label);
5575
+ head.appendChild(label2);
4861
5576
  head.addEventListener("click", () => {
4862
5577
  if (this.collapsed.has(name)) this.collapsed.delete(name);
4863
5578
  else this.collapsed.add(name);
@@ -4893,10 +5608,10 @@ ${runtime.error}`;
4893
5608
  placeholder.setAttribute("aria-hidden", "true");
4894
5609
  li.appendChild(placeholder);
4895
5610
  }
4896
- const label = document.createElement("span");
4897
- label.className = "d-palette-label";
4898
- label.textContent = item.name;
4899
- li.appendChild(label);
5611
+ const label2 = document.createElement("span");
5612
+ label2.className = "d-palette-label";
5613
+ label2.textContent = item.name;
5614
+ li.appendChild(label2);
4900
5615
  li.addEventListener("click", () => this.selectNodeType(item.id));
4901
5616
  li.addEventListener("dblclick", (event) => {
4902
5617
  event.preventDefault();
@@ -4976,8 +5691,8 @@ ${runtime.error}`;
4976
5691
  const m = hex.match(/^#?([0-9a-fA-F]{6})$/);
4977
5692
  if (m === null)
4978
5693
  return 0;
4979
- const n = parseInt(m[1], 16);
4980
- return 0.299 * (n >> 16 & 255) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255);
5694
+ const n2 = parseInt(m[1], 16);
5695
+ return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
4981
5696
  }
4982
5697
  function makePort(p) {
4983
5698
  return new Port({
@@ -5018,23 +5733,23 @@ ${runtime.error}`;
5018
5733
  add(usedOut, l.from, l.fromPort);
5019
5734
  add(usedIn, l.to, l.toPort);
5020
5735
  }
5021
- const nodes = scheme.nodes.map((n) => {
5022
- const t = catalog.getNodeType(n.typeId);
5023
- const inPorts = t ? t.inPorts.map(portOf) : [];
5024
- const outPorts = t ? t.outPorts.map(portOf) : [];
5025
- const anchorType = t?.inPorts.find((p) => p.isDynamic)?.type ?? "Any";
5026
- for (const key of usedIn.get(n.id) ?? [])
5736
+ const nodes = scheme.nodes.map((n2) => {
5737
+ const t2 = catalog.getNodeType(n2.typeId);
5738
+ const inPorts = t2 ? t2.inPorts.map(portOf) : [];
5739
+ const outPorts = t2 ? t2.outPorts.map(portOf) : [];
5740
+ const anchorType = t2?.inPorts.find((p) => p.isDynamic)?.type ?? "Any";
5741
+ for (const key of usedIn.get(n2.id) ?? [])
5027
5742
  if (!inPorts.some((p) => p.id === key))
5028
5743
  inPorts.push({ id: key, name: key, type: anchorType, maxLinks: 0 });
5029
- for (const key of usedOut.get(n.id) ?? [])
5744
+ for (const key of usedOut.get(n2.id) ?? [])
5030
5745
  if (!outPorts.some((p) => p.id === key))
5031
5746
  outPorts.push({ id: key, name: key, type: "Any", maxLinks: 0 });
5032
- const data = new DiagramNode({ id: n.id, typeId: n.typeId, name: n.name, x: n.x, y: n.y, inPorts, outPorts });
5033
- if (t?.icon)
5034
- data.icon = t.icon;
5035
- if (!t) {
5747
+ const data = new DiagramNode({ id: n2.id, typeId: n2.typeId, name: n2.name, x: n2.x, y: n2.y, inPorts, outPorts });
5748
+ if (t2?.icon)
5749
+ data.icon = t2.icon;
5750
+ if (!t2) {
5036
5751
  data.isPlaceholder = true;
5037
- nodeErrors[n.id] = missingTypeMessage(n.typeId);
5752
+ nodeErrors[n2.id] = missingTypeMessage(n2.typeId);
5038
5753
  }
5039
5754
  return data;
5040
5755
  });