@stocksharp/diagram 0.4.1 → 0.6.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.
- package/README.md +105 -2
- package/dist/esm/canvas-renderer.js +116 -36
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/diagram/context-menu.js +319 -0
- package/dist/esm/diagram/context-menu.js.map +1 -0
- package/dist/esm/diagram/stocksharp-diagram.js +121 -9
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/esm/draw-surface.js +2 -0
- package/dist/esm/draw-surface.js.map +1 -0
- package/dist/esm/embed.js +7 -4
- package/dist/esm/embed.js.map +1 -1
- package/dist/esm/i18n.js +15 -4
- package/dist/esm/i18n.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/svg-surface.js +341 -0
- package/dist/esm/svg-surface.js.map +1 -0
- package/dist/ssdiagram.js +978 -188
- package/dist/ssdiagram.js.map +4 -4
- package/dist/types/canvas-renderer.d.ts +13 -0
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +42 -2
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/context-menu.d.ts +33 -0
- package/dist/types/diagram/context-menu.d.ts.map +1 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts +18 -5
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/dist/types/draw-surface.d.ts +52 -0
- package/dist/types/draw-surface.d.ts.map +1 -0
- package/dist/types/embed.d.ts.map +1 -1
- package/dist/types/i18n.d.ts +12 -0
- package/dist/types/i18n.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/svg-surface.d.ts +74 -0
- package/dist/types/svg-surface.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/canvas-renderer.ts +120 -27
- package/src/diagram/api.ts +53 -2
- package/src/diagram/context-menu.ts +363 -0
- package/src/diagram/stocksharp-diagram.ts +134 -10
- package/src/draw-surface.ts +61 -0
- package/src/embed.ts +7 -4
- package/src/i18n.ts +41 -6
- package/src/index.ts +10 -0
- 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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
1302
|
-
const tn = this.nodes.find((
|
|
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(
|
|
1406
|
-
return this.history.transaction(
|
|
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((
|
|
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,
|
|
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((
|
|
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
|
|
1793
|
-
const
|
|
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((
|
|
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(
|
|
2021
|
-
if (
|
|
2022
|
-
if (
|
|
2023
|
-
if (
|
|
2024
|
-
if (
|
|
2025
|
-
if (
|
|
2026
|
-
if (
|
|
2027
|
-
if (
|
|
2028
|
-
if (
|
|
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
|
|
2035
|
-
minX = Math.min(minX,
|
|
2036
|
-
minY = Math.min(minY,
|
|
2037
|
-
maxX = Math.max(maxX,
|
|
2038
|
-
maxY = Math.max(maxY,
|
|
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(
|
|
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(
|
|
2093
|
-
const rows = Math.max(
|
|
2094
|
-
|
|
2095
|
-
|
|
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 =
|
|
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(
|
|
2105
|
-
place(
|
|
2495
|
+
place(n2.inPorts, n2.x - off);
|
|
2496
|
+
place(n2.outPorts, n2.x + n2.w + off);
|
|
2106
2497
|
}
|
|
2107
2498
|
relayout() {
|
|
2108
|
-
for (const
|
|
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
|
|
2138
|
-
for (const p of [...
|
|
2139
|
-
if ((wx - p.cx) ** 2 + (wy - p.cy) ** 2 <= (PORT_R + 4) ** 2) return { node:
|
|
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
|
|
2161
|
-
if (wx >=
|
|
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
|
|
2171
|
-
if (exclude.has(
|
|
2172
|
-
out.push([
|
|
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((
|
|
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
|
|
2293
|
-
const ports = direction === "in" ?
|
|
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(
|
|
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:
|
|
2691
|
+
best = { node: n2, port: p };
|
|
2301
2692
|
}
|
|
2302
2693
|
}
|
|
2303
2694
|
}
|
|
2304
2695
|
return best;
|
|
2305
2696
|
}
|
|
2306
|
-
selectNode(
|
|
2697
|
+
selectNode(n2) {
|
|
2307
2698
|
this.selectedLink = null;
|
|
2308
2699
|
this.selectedPort = null;
|
|
2309
2700
|
this.selectedNodes.clear();
|
|
2310
|
-
if (
|
|
2311
|
-
this.selectedNode =
|
|
2312
|
-
this.emit("nodeSelected", { node:
|
|
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(
|
|
2707
|
+
toggleSelect(n2) {
|
|
2317
2708
|
this.selectedLink = null;
|
|
2318
2709
|
this.selectedPort = null;
|
|
2319
|
-
if (this.selectedNodes.has(
|
|
2320
|
-
this.selectedNodes.delete(
|
|
2321
|
-
if (this.selectedNode ===
|
|
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(
|
|
2324
|
-
this.selectedNode =
|
|
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((
|
|
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((
|
|
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
|
|
2413
|
-
if (f !== 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:
|
|
2808
|
+
to: t2,
|
|
2418
2809
|
toPort: l.to.portId,
|
|
2419
2810
|
metadata: l.metadata
|
|
2420
2811
|
});
|
|
@@ -2424,6 +2815,23 @@ var SSDiagram = (() => {
|
|
|
2424
2815
|
return added.map((node) => node.id);
|
|
2425
2816
|
}
|
|
2426
2817
|
// ---- input ------------------------------------------------------
|
|
2818
|
+
// Ends hover and everything hanging off it: highlight, cursor and the delayed tooltip.
|
|
2819
|
+
clearHover() {
|
|
2820
|
+
if (this.hoverPort !== null) this.emit("portHover", { node: this.hoverPort.node, port: this.hoverPort.port, hovering: false });
|
|
2821
|
+
if (this.hoverNode !== null) this.emit("nodeHover", { node: this.hoverNode, hovering: false });
|
|
2822
|
+
if (this.hoveredLink !== null) this.emit("linkHover", { link: this.hoveredLink, hovering: false });
|
|
2823
|
+
this.hoverPort = null;
|
|
2824
|
+
this.hoverNode = null;
|
|
2825
|
+
this.hoveredLink = null;
|
|
2826
|
+
this.tipTarget = null;
|
|
2827
|
+
this.tipShow = false;
|
|
2828
|
+
if (this.tipTimer !== null) {
|
|
2829
|
+
clearTimeout(this.tipTimer);
|
|
2830
|
+
this.tipTimer = null;
|
|
2831
|
+
}
|
|
2832
|
+
this.canvas.style.cursor = "default";
|
|
2833
|
+
this.scheduleDraw();
|
|
2834
|
+
}
|
|
2427
2835
|
cancelLongPress() {
|
|
2428
2836
|
if (this.lpTimer !== null) {
|
|
2429
2837
|
clearTimeout(this.lpTimer);
|
|
@@ -2456,7 +2864,7 @@ var SSDiagram = (() => {
|
|
|
2456
2864
|
this.relinking = null;
|
|
2457
2865
|
this.relinkCandidate = null;
|
|
2458
2866
|
this.linkSnap = null;
|
|
2459
|
-
this.
|
|
2867
|
+
this.clearHover();
|
|
2460
2868
|
this.emit("contextMenu", { x: pageX, y: pageY, link, node, port });
|
|
2461
2869
|
}
|
|
2462
2870
|
listen(target, type, listener, options) {
|
|
@@ -2718,7 +3126,7 @@ var SSDiagram = (() => {
|
|
|
2718
3126
|
const ry1 = Math.max(this.rubber.y0, this.rubber.y);
|
|
2719
3127
|
this.rubber = null;
|
|
2720
3128
|
if (rx1 - rx0 > 3 || ry1 - ry0 > 3) {
|
|
2721
|
-
const inRect = this.nodes.filter((
|
|
3129
|
+
const inRect = this.nodes.filter((n2) => n2.x < rx1 && n2.x + n2.w > rx0 && n2.y < ry1 && n2.y + n2.h > ry0);
|
|
2722
3130
|
const merge = e.shiftKey ? [...this.selectedNodes, ...inRect] : inRect;
|
|
2723
3131
|
this.setSelection([...new Set(merge)]);
|
|
2724
3132
|
}
|
|
@@ -2778,22 +3186,7 @@ var SSDiagram = (() => {
|
|
|
2778
3186
|
this.emitViewChanged(true);
|
|
2779
3187
|
this.scheduleDraw();
|
|
2780
3188
|
}, { passive: false });
|
|
2781
|
-
this.listen(this.canvas, "pointerleave", () =>
|
|
2782
|
-
if (this.hoverPort !== null) this.emit("portHover", { node: this.hoverPort.node, port: this.hoverPort.port, hovering: false });
|
|
2783
|
-
if (this.hoverNode !== null) this.emit("nodeHover", { node: this.hoverNode, hovering: false });
|
|
2784
|
-
if (this.hoveredLink !== null) this.emit("linkHover", { link: this.hoveredLink, hovering: false });
|
|
2785
|
-
this.hoverPort = null;
|
|
2786
|
-
this.hoverNode = null;
|
|
2787
|
-
this.hoveredLink = null;
|
|
2788
|
-
this.tipTarget = null;
|
|
2789
|
-
this.tipShow = false;
|
|
2790
|
-
if (this.tipTimer !== null) {
|
|
2791
|
-
clearTimeout(this.tipTimer);
|
|
2792
|
-
this.tipTimer = null;
|
|
2793
|
-
}
|
|
2794
|
-
this.canvas.style.cursor = "default";
|
|
2795
|
-
this.scheduleDraw();
|
|
2796
|
-
});
|
|
3189
|
+
this.listen(this.canvas, "pointerleave", () => this.clearHover());
|
|
2797
3190
|
this.listen(this.canvas, "dblclick", (e) => {
|
|
2798
3191
|
const [sx, sy] = localXY(e);
|
|
2799
3192
|
const [wx, wy] = this.toWorld(sx, sy);
|
|
@@ -2951,7 +3344,7 @@ var SSDiagram = (() => {
|
|
|
2951
3344
|
const a = this.endpoint(l, "from");
|
|
2952
3345
|
const b = this.endpoint(l, "to");
|
|
2953
3346
|
if (a === null || b === null) continue;
|
|
2954
|
-
const fp = this.nodes.find((
|
|
3347
|
+
const fp = this.nodes.find((n2) => n2.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
|
|
2955
3348
|
const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.portColor(fp.type) : this.portColor("");
|
|
2956
3349
|
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
3350
|
const color = state === "sel" ? this.selectionColor() : state === "hov" ? this.linkHoverColor() : baseColor;
|
|
@@ -2967,7 +3360,7 @@ var SSDiagram = (() => {
|
|
|
2967
3360
|
}
|
|
2968
3361
|
}
|
|
2969
3362
|
if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
|
|
2970
|
-
for (const
|
|
3363
|
+
for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
|
|
2971
3364
|
if (options.selection && this.permissions.createLinks && this.relinking === null)
|
|
2972
3365
|
this.drawSelectedLinkEndpoints();
|
|
2973
3366
|
if (options.transient && this.rubber !== null) {
|
|
@@ -2985,7 +3378,7 @@ var SSDiagram = (() => {
|
|
|
2985
3378
|
if (options.overview) this.drawOverview();
|
|
2986
3379
|
if (options.runtime) this.drawGlobalError();
|
|
2987
3380
|
if (options.transient) this.drawTooltip();
|
|
2988
|
-
if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((
|
|
3381
|
+
if (options.transient && (this.introStart !== null || this.globalErrorFlashStart !== null || this.nodes.some((n2) => n2.errorFlashStart !== null)))
|
|
2989
3382
|
this.scheduleDraw();
|
|
2990
3383
|
}
|
|
2991
3384
|
drawGlobalError() {
|
|
@@ -3049,17 +3442,17 @@ ${runtime.error}`;
|
|
|
3049
3442
|
isError = true;
|
|
3050
3443
|
}
|
|
3051
3444
|
} else if (this.hoverNode !== null) {
|
|
3052
|
-
const
|
|
3053
|
-
const errors = [
|
|
3445
|
+
const n2 = this.hoverNode;
|
|
3446
|
+
const errors = [n2.runtimeError, n2.loadError].filter((value) => value.length > 0);
|
|
3054
3447
|
if (errors.length > 0) {
|
|
3055
3448
|
text = errors.join("\n");
|
|
3056
3449
|
isError = true;
|
|
3057
|
-
} else if (this.showNodeMessages &&
|
|
3058
|
-
text =
|
|
3059
|
-
} else if (
|
|
3060
|
-
text =
|
|
3450
|
+
} else if (this.showNodeMessages && n2.message.length > 0) {
|
|
3451
|
+
text = n2.message;
|
|
3452
|
+
} else if (n2.description.length > 0) {
|
|
3453
|
+
text = n2.description;
|
|
3061
3454
|
} else {
|
|
3062
|
-
text =
|
|
3455
|
+
text = n2.typeId && n2.typeId !== n2.name ? `${n2.name} [${n2.typeId}]` : n2.name;
|
|
3063
3456
|
}
|
|
3064
3457
|
} else {
|
|
3065
3458
|
return;
|
|
@@ -3172,24 +3565,24 @@ ${runtime.error}`;
|
|
|
3172
3565
|
img.src = url;
|
|
3173
3566
|
return null;
|
|
3174
3567
|
}
|
|
3175
|
-
drawNode(
|
|
3568
|
+
drawNode(n2, selected, options) {
|
|
3176
3569
|
const ctx = this.ctx;
|
|
3177
|
-
const hasLoadError = options.runtime &&
|
|
3178
|
-
const hasRuntimeError = options.runtime &&
|
|
3179
|
-
const runtime = options.runtime ? this.runtimeState.nodes[
|
|
3180
|
-
const active = options.runtime && (this.runtimeState.activeNodeId ===
|
|
3181
|
-
roundRect(ctx,
|
|
3182
|
-
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" :
|
|
3570
|
+
const hasLoadError = options.runtime && n2.loadError.length > 0;
|
|
3571
|
+
const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
|
|
3572
|
+
const runtime = options.runtime ? this.runtimeState.nodes[n2.id] : void 0;
|
|
3573
|
+
const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
|
|
3574
|
+
roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
|
|
3575
|
+
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : n2.color;
|
|
3183
3576
|
ctx.fill();
|
|
3184
3577
|
ctx.lineWidth = selected ? 2 : 1.5;
|
|
3185
|
-
ctx.strokeStyle = selected ? this.selectionColor() :
|
|
3578
|
+
ctx.strokeStyle = selected ? this.selectionColor() : n2.border;
|
|
3186
3579
|
ctx.stroke();
|
|
3187
3580
|
if (hasLoadError || hasRuntimeError) {
|
|
3188
3581
|
let alpha = 1;
|
|
3189
|
-
if (options.transient && hasRuntimeError &&
|
|
3190
|
-
const elapsed = performance.now() -
|
|
3582
|
+
if (options.transient && hasRuntimeError && n2.errorFlashStart !== null) {
|
|
3583
|
+
const elapsed = performance.now() - n2.errorFlashStart;
|
|
3191
3584
|
if (elapsed >= ERROR_FLASH_MS) {
|
|
3192
|
-
|
|
3585
|
+
n2.errorFlashStart = null;
|
|
3193
3586
|
} else {
|
|
3194
3587
|
alpha = 0.12 + 0.88 * (0.5 + 0.5 * Math.sin(
|
|
3195
3588
|
elapsed / ERROR_FLASH_MS * Math.PI * 6 - Math.PI / 2
|
|
@@ -3198,30 +3591,30 @@ ${runtime.error}`;
|
|
|
3198
3591
|
}
|
|
3199
3592
|
ctx.save();
|
|
3200
3593
|
ctx.globalAlpha *= alpha;
|
|
3201
|
-
roundRect(ctx,
|
|
3594
|
+
roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
|
|
3202
3595
|
ctx.lineWidth = hasRuntimeError ? 3 : 2;
|
|
3203
3596
|
ctx.strokeStyle = ERROR_RED;
|
|
3204
3597
|
ctx.stroke();
|
|
3205
3598
|
ctx.restore();
|
|
3206
3599
|
}
|
|
3207
3600
|
const iconW = 18;
|
|
3208
|
-
if (
|
|
3209
|
-
const img = this.getIcon(
|
|
3601
|
+
if (n2.icon) {
|
|
3602
|
+
const img = this.getIcon(n2.icon);
|
|
3210
3603
|
if (img) {
|
|
3211
3604
|
try {
|
|
3212
|
-
ctx.drawImage(img,
|
|
3605
|
+
ctx.drawImage(img, n2.x + 8, n2.y + (n2.h - iconW) / 2, iconW, iconW);
|
|
3213
3606
|
} catch {
|
|
3214
3607
|
}
|
|
3215
3608
|
}
|
|
3216
3609
|
}
|
|
3217
|
-
const titleShift =
|
|
3610
|
+
const titleShift = n2.icon ? iconW + 4 : 0;
|
|
3218
3611
|
ctx.fillStyle = hasLoadError ? "#ffffff" : "#1b1b1b";
|
|
3219
3612
|
ctx.font = "600 12px Segoe UI, Tahoma, sans-serif";
|
|
3220
3613
|
ctx.textAlign = "center";
|
|
3221
3614
|
ctx.textBaseline = "middle";
|
|
3222
|
-
ctx.fillText(
|
|
3223
|
-
for (const p of
|
|
3224
|
-
for (const p of
|
|
3615
|
+
ctx.fillText(n2.name, n2.x + titleShift + (n2.w - titleShift) / 2, n2.y + n2.h / 2, n2.w - titleShift - 14);
|
|
3616
|
+
for (const p of n2.inPorts) this.drawPort(n2, p, options);
|
|
3617
|
+
for (const p of n2.outPorts) this.drawPort(n2, p, options);
|
|
3225
3618
|
}
|
|
3226
3619
|
drawPort(node, p, options) {
|
|
3227
3620
|
const ctx = this.ctx;
|
|
@@ -3319,9 +3712,9 @@ ${runtime.error}`;
|
|
|
3319
3712
|
ctx.setLineDash([]);
|
|
3320
3713
|
}
|
|
3321
3714
|
drawArrow(pts, color) {
|
|
3322
|
-
const
|
|
3323
|
-
const [px, py] = pts[
|
|
3324
|
-
const [ex, ey] = pts[
|
|
3715
|
+
const n2 = pts.length;
|
|
3716
|
+
const [px, py] = pts[n2 - 2];
|
|
3717
|
+
const [ex, ey] = pts[n2 - 1];
|
|
3325
3718
|
const ang = Math.atan2(ey - py, ex - px);
|
|
3326
3719
|
const ctx = this.ctx;
|
|
3327
3720
|
ctx.save();
|
|
@@ -3450,12 +3843,12 @@ ${runtime.error}`;
|
|
|
3450
3843
|
ctx.lineWidth = 1;
|
|
3451
3844
|
ctx.stroke();
|
|
3452
3845
|
ctx.clip();
|
|
3453
|
-
for (const
|
|
3454
|
-
ctx.fillStyle =
|
|
3455
|
-
ctx.fillRect(mx(
|
|
3456
|
-
ctx.strokeStyle =
|
|
3846
|
+
for (const n2 of this.nodes) {
|
|
3847
|
+
ctx.fillStyle = n2.color;
|
|
3848
|
+
ctx.fillRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3849
|
+
ctx.strokeStyle = n2.border;
|
|
3457
3850
|
ctx.lineWidth = 1;
|
|
3458
|
-
ctx.strokeRect(mx(
|
|
3851
|
+
ctx.strokeRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3459
3852
|
}
|
|
3460
3853
|
const [vx0, vy0] = this.toWorld(0, 0);
|
|
3461
3854
|
const [vx1, vy1] = this.toWorld(this.width, this.height);
|
|
@@ -3614,11 +4007,11 @@ ${runtime.error}`;
|
|
|
3614
4007
|
cols = Math.ceil((maxX - minX) / cell);
|
|
3615
4008
|
rows = Math.ceil((maxY - minY) / cell);
|
|
3616
4009
|
}
|
|
3617
|
-
const
|
|
3618
|
-
if (
|
|
4010
|
+
const n2 = cols * rows;
|
|
4011
|
+
if (n2 <= 0 || n2 > 8e4) return null;
|
|
3619
4012
|
const cx = (c) => minX + (c + 0.5) * cell;
|
|
3620
4013
|
const cy = (r) => minY + (r + 0.5) * cell;
|
|
3621
|
-
const blocked = new Uint8Array(
|
|
4014
|
+
const blocked = new Uint8Array(n2);
|
|
3622
4015
|
for (const r of rects) {
|
|
3623
4016
|
const c0 = Math.max(0, Math.floor((r[0] - minX) / cell));
|
|
3624
4017
|
const c1 = Math.min(cols - 1, Math.floor((r[0] + r[2] - minX) / cell));
|
|
@@ -3635,17 +4028,17 @@ ${runtime.error}`;
|
|
|
3635
4028
|
const gi = gr * cols + gc;
|
|
3636
4029
|
blocked[si] = 0;
|
|
3637
4030
|
blocked[gi] = 0;
|
|
3638
|
-
const g = new Float64Array(
|
|
3639
|
-
const prev = new Int32Array(
|
|
3640
|
-
const dir = new Int8Array(
|
|
4031
|
+
const g = new Float64Array(n2).fill(Infinity);
|
|
4032
|
+
const prev = new Int32Array(n2).fill(-1);
|
|
4033
|
+
const dir = new Int8Array(n2).fill(-1);
|
|
3641
4034
|
g[si] = 0;
|
|
3642
4035
|
const heap = [si];
|
|
3643
|
-
const f = new Float64Array(
|
|
4036
|
+
const f = new Float64Array(n2);
|
|
3644
4037
|
f[si] = (Math.abs(gc - sc) + Math.abs(gr - sr)) * A_STEP;
|
|
3645
4038
|
const swap = (i, j) => {
|
|
3646
|
-
const
|
|
4039
|
+
const t2 = heap[i];
|
|
3647
4040
|
heap[i] = heap[j];
|
|
3648
|
-
heap[j] =
|
|
4041
|
+
heap[j] = t2;
|
|
3649
4042
|
};
|
|
3650
4043
|
const push = (v) => {
|
|
3651
4044
|
heap.push(v);
|
|
@@ -3676,7 +4069,7 @@ ${runtime.error}`;
|
|
|
3676
4069
|
}
|
|
3677
4070
|
return top;
|
|
3678
4071
|
};
|
|
3679
|
-
const seen = new Uint8Array(
|
|
4072
|
+
const seen = new Uint8Array(n2);
|
|
3680
4073
|
while (heap.length > 0) {
|
|
3681
4074
|
const cur = pop();
|
|
3682
4075
|
if (cur === gi) break;
|
|
@@ -3734,13 +4127,308 @@ ${runtime.error}`;
|
|
|
3734
4127
|
const dx = x2 - x1;
|
|
3735
4128
|
const dy = y2 - y1;
|
|
3736
4129
|
const len2 = dx * dx + dy * dy;
|
|
3737
|
-
let
|
|
3738
|
-
|
|
3739
|
-
const cx = x1 +
|
|
3740
|
-
const cy = y1 +
|
|
4130
|
+
let t2 = len2 === 0 ? 0 : ((px - x1) * dx + (py - y1) * dy) / len2;
|
|
4131
|
+
t2 = clamp(t2, 0, 1);
|
|
4132
|
+
const cx = x1 + t2 * dx;
|
|
4133
|
+
const cy = y1 + t2 * dy;
|
|
3741
4134
|
return Math.hypot(px - cx, py - cy);
|
|
3742
4135
|
}
|
|
3743
4136
|
|
|
4137
|
+
// src/i18n.ts
|
|
4138
|
+
function t(key, fallback) {
|
|
4139
|
+
if (typeof window === "undefined") return fallback;
|
|
4140
|
+
const value = window.__designerI18n?.[key];
|
|
4141
|
+
return value === void 0 || value === "" ? fallback : value;
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
// src/diagram/context-menu.ts
|
|
4145
|
+
var ROOT_CLASS = "ssdiagram-context-menu";
|
|
4146
|
+
var ITEM_CLASS = "ssdiagram-context-menu-item";
|
|
4147
|
+
var SUBMENU_CLASS = "ssdiagram-context-menu-submenu";
|
|
4148
|
+
var SEPARATOR_CLASS = "ssdiagram-context-menu-separator";
|
|
4149
|
+
var LABEL_CLASS = "ssdiagram-context-menu-label";
|
|
4150
|
+
var ARROW_CLASS = "ssdiagram-context-menu-arrow";
|
|
4151
|
+
var CHECK_CLASS = "ssdiagram-context-menu-check";
|
|
4152
|
+
var SEPARATE_AFTER = /* @__PURE__ */ new Set(["redo", "paste", "delete", "overview"]);
|
|
4153
|
+
function label(id) {
|
|
4154
|
+
switch (id) {
|
|
4155
|
+
case "undo":
|
|
4156
|
+
return t("undo", "Undo");
|
|
4157
|
+
case "redo":
|
|
4158
|
+
return t("redo", "Redo");
|
|
4159
|
+
case "cut":
|
|
4160
|
+
return t("cut", "Cut");
|
|
4161
|
+
case "copy":
|
|
4162
|
+
return t("copy", "Copy");
|
|
4163
|
+
case "paste":
|
|
4164
|
+
return t("paste", "Paste");
|
|
4165
|
+
case "open":
|
|
4166
|
+
return t("ctxOpen", "Open");
|
|
4167
|
+
case "delete":
|
|
4168
|
+
return t("ctxDelete", "Delete");
|
|
4169
|
+
case "export":
|
|
4170
|
+
return t("ctxExportAs", "Export as");
|
|
4171
|
+
case "exportDocument":
|
|
4172
|
+
return t("ctxExportDocument", "Scheme");
|
|
4173
|
+
case "exportPng":
|
|
4174
|
+
return t("ctxExportPng", "PNG image");
|
|
4175
|
+
case "exportSvg":
|
|
4176
|
+
return t("ctxExportSvg", "SVG image");
|
|
4177
|
+
case "overview":
|
|
4178
|
+
return t("ctxOverview", "Overview");
|
|
4179
|
+
case "properties":
|
|
4180
|
+
return t("properties", "Properties");
|
|
4181
|
+
case "help":
|
|
4182
|
+
return t("ctxHelp", "Help");
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
var PANEL_STYLE = {
|
|
4186
|
+
position: "fixed",
|
|
4187
|
+
zIndex: "2147483000",
|
|
4188
|
+
minWidth: "176px",
|
|
4189
|
+
// Translations run longer than English -- German and Russian routinely half again. Without
|
|
4190
|
+
// a cap the panel just grows, and on a narrow viewport it leaves the screen entirely, which
|
|
4191
|
+
// no amount of repositioning can recover.
|
|
4192
|
+
maxWidth: "min(320px, calc(100vw - 16px))",
|
|
4193
|
+
padding: "4px",
|
|
4194
|
+
border: "1px solid var(--ssdiagram-menu-border, var(--ssdiagram-control-border, var(--t-border, #3a4250)))",
|
|
4195
|
+
borderRadius: "7px",
|
|
4196
|
+
background: "var(--ssdiagram-menu-background, var(--ssdiagram-control-background, var(--t-panel, #181a20)))",
|
|
4197
|
+
color: "var(--ssdiagram-menu-color, var(--ssdiagram-control-color, var(--t-text, #eaecef)))",
|
|
4198
|
+
boxShadow: "0 12px 32px rgba(0, 0, 0, 0.45)",
|
|
4199
|
+
font: 'var(--ssdiagram-menu-font, 12.5px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif)',
|
|
4200
|
+
boxSizing: "border-box",
|
|
4201
|
+
userSelect: "none"
|
|
4202
|
+
};
|
|
4203
|
+
var ITEM_STYLE = {
|
|
4204
|
+
display: "flex",
|
|
4205
|
+
alignItems: "center",
|
|
4206
|
+
gap: "10px",
|
|
4207
|
+
width: "100%",
|
|
4208
|
+
padding: "6px 10px",
|
|
4209
|
+
border: "0",
|
|
4210
|
+
borderRadius: "4px",
|
|
4211
|
+
background: "transparent",
|
|
4212
|
+
color: "inherit",
|
|
4213
|
+
font: "inherit",
|
|
4214
|
+
// Logical, not physical: on an RTL page these follow the writing direction instead of
|
|
4215
|
+
// pinning the text left and truncating from the wrong end.
|
|
4216
|
+
textAlign: "start",
|
|
4217
|
+
overflowWrap: "anywhere",
|
|
4218
|
+
cursor: "pointer",
|
|
4219
|
+
boxSizing: "border-box"
|
|
4220
|
+
};
|
|
4221
|
+
var HOVER_BACKGROUND = "var(--ssdiagram-menu-hover, var(--t-hover, rgba(255, 255, 255, 0.08)))";
|
|
4222
|
+
var DISABLED_COLOR = "var(--ssdiagram-menu-disabled-color, var(--t-text-dim, #848e9c))";
|
|
4223
|
+
var ContextMenuView = class {
|
|
4224
|
+
constructor(options) {
|
|
4225
|
+
this.root = null;
|
|
4226
|
+
this.submenu = null;
|
|
4227
|
+
this.submenuTriggers = /* @__PURE__ */ new Map();
|
|
4228
|
+
this.detach = [];
|
|
4229
|
+
this.container = options.container;
|
|
4230
|
+
this.execute = options.execute;
|
|
4231
|
+
}
|
|
4232
|
+
get isOpen() {
|
|
4233
|
+
return this.root !== null;
|
|
4234
|
+
}
|
|
4235
|
+
show(x, y, items) {
|
|
4236
|
+
this.hide();
|
|
4237
|
+
const owner = this.container.ownerDocument ?? document;
|
|
4238
|
+
const root = this.panel(owner);
|
|
4239
|
+
root.className = ROOT_CLASS;
|
|
4240
|
+
root.setAttribute("role", "menu");
|
|
4241
|
+
const gutter = items.some((item) => !("group" in item) && item.checked !== void 0);
|
|
4242
|
+
items.forEach((item, index) => {
|
|
4243
|
+
if ("group" in item) root.appendChild(this.groupEntry(owner, item, gutter));
|
|
4244
|
+
else root.appendChild(this.commandEntry(owner, item.command, item.enabled, true, item.checked, gutter));
|
|
4245
|
+
const id = "group" in item ? item.group : item.command;
|
|
4246
|
+
if (SEPARATE_AFTER.has(id) && index < items.length - 1) root.appendChild(this.separator(owner));
|
|
4247
|
+
});
|
|
4248
|
+
this.container.appendChild(root);
|
|
4249
|
+
this.root = root;
|
|
4250
|
+
this.place(root, x, y);
|
|
4251
|
+
this.watchForDismissal(owner);
|
|
4252
|
+
}
|
|
4253
|
+
hide() {
|
|
4254
|
+
for (const dispose of this.detach.splice(0)) dispose();
|
|
4255
|
+
this.submenu = null;
|
|
4256
|
+
this.submenuTriggers.clear();
|
|
4257
|
+
this.root?.remove();
|
|
4258
|
+
this.root = null;
|
|
4259
|
+
}
|
|
4260
|
+
destroy() {
|
|
4261
|
+
this.hide();
|
|
4262
|
+
}
|
|
4263
|
+
panel(owner) {
|
|
4264
|
+
const panel = owner.createElement("div");
|
|
4265
|
+
Object.assign(panel.style, PANEL_STYLE);
|
|
4266
|
+
return panel;
|
|
4267
|
+
}
|
|
4268
|
+
separator(owner) {
|
|
4269
|
+
const rule = owner.createElement("div");
|
|
4270
|
+
rule.className = SEPARATOR_CLASS;
|
|
4271
|
+
Object.assign(rule.style, {
|
|
4272
|
+
height: "1px",
|
|
4273
|
+
margin: "4px 6px",
|
|
4274
|
+
background: "var(--ssdiagram-menu-border, var(--t-border, #3a4250))"
|
|
4275
|
+
});
|
|
4276
|
+
return rule;
|
|
4277
|
+
}
|
|
4278
|
+
item(owner, text, enabled, arrow, checked, gutter) {
|
|
4279
|
+
const button = owner.createElement("button");
|
|
4280
|
+
button.type = "button";
|
|
4281
|
+
button.className = ITEM_CLASS;
|
|
4282
|
+
button.setAttribute("role", checked === void 0 ? "menuitem" : "menuitemcheckbox");
|
|
4283
|
+
if (checked !== void 0) button.setAttribute("aria-checked", String(checked));
|
|
4284
|
+
Object.assign(button.style, ITEM_STYLE);
|
|
4285
|
+
if (!enabled) {
|
|
4286
|
+
button.disabled = true;
|
|
4287
|
+
button.style.color = DISABLED_COLOR;
|
|
4288
|
+
button.style.cursor = "default";
|
|
4289
|
+
}
|
|
4290
|
+
if (gutter) {
|
|
4291
|
+
const tick = owner.createElement("span");
|
|
4292
|
+
tick.className = CHECK_CLASS;
|
|
4293
|
+
tick.textContent = checked === true ? "\u2713" : "";
|
|
4294
|
+
tick.setAttribute("aria-hidden", "true");
|
|
4295
|
+
Object.assign(tick.style, { flex: "none", width: "12px", textAlign: "center" });
|
|
4296
|
+
button.appendChild(tick);
|
|
4297
|
+
}
|
|
4298
|
+
const caption = owner.createElement("span");
|
|
4299
|
+
caption.className = LABEL_CLASS;
|
|
4300
|
+
caption.textContent = text;
|
|
4301
|
+
caption.style.flex = "1";
|
|
4302
|
+
button.appendChild(caption);
|
|
4303
|
+
if (arrow) {
|
|
4304
|
+
const chevron = owner.createElement("span");
|
|
4305
|
+
chevron.className = ARROW_CLASS;
|
|
4306
|
+
chevron.textContent = this.rtl() ? "\u2039" : "\u203A";
|
|
4307
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
4308
|
+
Object.assign(chevron.style, { flex: "none", opacity: "0.7", fontSize: "14px" });
|
|
4309
|
+
button.appendChild(chevron);
|
|
4310
|
+
}
|
|
4311
|
+
return button;
|
|
4312
|
+
}
|
|
4313
|
+
// `closesSubmenu` separates the two places an item can live. A top-level item shuts any
|
|
4314
|
+
// open submenu when the pointer arrives, the way menus everywhere behave. An item inside
|
|
4315
|
+
// the submenu must not: reaching it means the pointer has entered that very panel, and
|
|
4316
|
+
// closing it there would pull it out from under the pointer and drop the click on the
|
|
4317
|
+
// canvas behind.
|
|
4318
|
+
commandEntry(owner, command, enabled, closesSubmenu, checked, gutter) {
|
|
4319
|
+
const button = this.item(owner, label(command), enabled, false, checked, gutter);
|
|
4320
|
+
this.on(button, "pointerenter", () => {
|
|
4321
|
+
if (closesSubmenu) this.closeSubmenu();
|
|
4322
|
+
if (enabled) button.style.background = HOVER_BACKGROUND;
|
|
4323
|
+
});
|
|
4324
|
+
this.on(button, "pointerleave", () => {
|
|
4325
|
+
button.style.background = "transparent";
|
|
4326
|
+
});
|
|
4327
|
+
this.on(button, "click", () => {
|
|
4328
|
+
if (!enabled) return;
|
|
4329
|
+
this.hide();
|
|
4330
|
+
this.execute(command);
|
|
4331
|
+
});
|
|
4332
|
+
return button;
|
|
4333
|
+
}
|
|
4334
|
+
groupEntry(owner, group, gutter) {
|
|
4335
|
+
const host = owner.createElement("div");
|
|
4336
|
+
host.style.position = "relative";
|
|
4337
|
+
const button = this.item(owner, label(group.group), group.enabled, true, void 0, gutter);
|
|
4338
|
+
button.setAttribute("aria-haspopup", "menu");
|
|
4339
|
+
button.setAttribute("aria-expanded", "false");
|
|
4340
|
+
host.appendChild(button);
|
|
4341
|
+
const panel = this.panel(owner);
|
|
4342
|
+
panel.className = SUBMENU_CLASS;
|
|
4343
|
+
panel.setAttribute("role", "menu");
|
|
4344
|
+
panel.setAttribute("aria-label", label(group.group));
|
|
4345
|
+
Object.assign(panel.style, { position: "absolute", insetInlineStart: "100%", top: "-5px", display: "none" });
|
|
4346
|
+
const childGutter = group.commands.some((child) => child.checked !== void 0);
|
|
4347
|
+
for (const child of group.commands) {
|
|
4348
|
+
panel.appendChild(this.commandEntry(owner, child.command, child.enabled, false, child.checked, childGutter));
|
|
4349
|
+
}
|
|
4350
|
+
host.appendChild(panel);
|
|
4351
|
+
const open = () => {
|
|
4352
|
+
if (!group.enabled) return;
|
|
4353
|
+
button.style.background = HOVER_BACKGROUND;
|
|
4354
|
+
button.setAttribute("aria-expanded", "true");
|
|
4355
|
+
panel.style.display = "block";
|
|
4356
|
+
this.submenu = panel;
|
|
4357
|
+
this.keepOnScreen(panel);
|
|
4358
|
+
};
|
|
4359
|
+
this.on(button, "pointerenter", open);
|
|
4360
|
+
this.on(button, "click", open);
|
|
4361
|
+
this.on(host, "pointerleave", () => {
|
|
4362
|
+
if (this.submenu !== panel) return;
|
|
4363
|
+
button.style.background = "transparent";
|
|
4364
|
+
button.setAttribute("aria-expanded", "false");
|
|
4365
|
+
panel.style.display = "none";
|
|
4366
|
+
this.submenu = null;
|
|
4367
|
+
});
|
|
4368
|
+
this.submenuTriggers.set(panel, button);
|
|
4369
|
+
return host;
|
|
4370
|
+
}
|
|
4371
|
+
closeSubmenu() {
|
|
4372
|
+
if (this.submenu === null) return;
|
|
4373
|
+
this.submenu.style.display = "none";
|
|
4374
|
+
this.submenuTriggers.get(this.submenu)?.setAttribute("aria-expanded", "false");
|
|
4375
|
+
this.submenu = null;
|
|
4376
|
+
}
|
|
4377
|
+
// Reads the resolved writing direction so the submenu's side and its arrow agree with it.
|
|
4378
|
+
rtl() {
|
|
4379
|
+
if (typeof getComputedStyle !== "function") return false;
|
|
4380
|
+
try {
|
|
4381
|
+
return getComputedStyle(this.container).direction === "rtl";
|
|
4382
|
+
} catch {
|
|
4383
|
+
return false;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
/** Keeps the panel inside the viewport, flipping it left when it would run off the edge. */
|
|
4387
|
+
keepOnScreen(panel) {
|
|
4388
|
+
if (typeof panel.getBoundingClientRect !== "function" || typeof window === "undefined") return;
|
|
4389
|
+
const box = panel.getBoundingClientRect();
|
|
4390
|
+
if (box.right > window.innerWidth - 4 || box.left < 4) {
|
|
4391
|
+
panel.style.insetInlineStart = "auto";
|
|
4392
|
+
panel.style.insetInlineEnd = "100%";
|
|
4393
|
+
}
|
|
4394
|
+
if (box.bottom > window.innerHeight - 4) {
|
|
4395
|
+
panel.style.top = "auto";
|
|
4396
|
+
panel.style.bottom = "-5px";
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
place(root, x, y) {
|
|
4400
|
+
root.style.left = `${x}px`;
|
|
4401
|
+
root.style.top = `${y}px`;
|
|
4402
|
+
if (typeof root.getBoundingClientRect !== "function" || typeof window === "undefined") return;
|
|
4403
|
+
const box = root.getBoundingClientRect();
|
|
4404
|
+
if (box.right > window.innerWidth - 4) root.style.left = `${Math.max(4, x - box.width)}px`;
|
|
4405
|
+
if (box.bottom > window.innerHeight - 4) root.style.top = `${Math.max(4, y - box.height)}px`;
|
|
4406
|
+
}
|
|
4407
|
+
watchForDismissal(owner) {
|
|
4408
|
+
const dismiss = () => this.hide();
|
|
4409
|
+
const outside = (event) => {
|
|
4410
|
+
const root = this.root;
|
|
4411
|
+
const target = event.target;
|
|
4412
|
+
if (root !== null && target !== null && typeof root.contains === "function" && root.contains(target)) return;
|
|
4413
|
+
this.hide();
|
|
4414
|
+
};
|
|
4415
|
+
this.on(owner, "pointerdown", outside, true);
|
|
4416
|
+
this.on(owner, "keydown", (event) => {
|
|
4417
|
+
if (event.key === "Escape") this.hide();
|
|
4418
|
+
});
|
|
4419
|
+
if (typeof window !== "undefined") {
|
|
4420
|
+
this.on(window, "blur", dismiss);
|
|
4421
|
+
this.on(window, "resize", dismiss);
|
|
4422
|
+
this.on(window, "scroll", dismiss, true);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
on(target, type, listener, capture = false) {
|
|
4426
|
+
if (typeof target.addEventListener !== "function") return;
|
|
4427
|
+
target.addEventListener(type, listener, capture);
|
|
4428
|
+
this.detach.push(() => target.removeEventListener?.(type, listener, capture));
|
|
4429
|
+
}
|
|
4430
|
+
};
|
|
4431
|
+
|
|
3744
4432
|
// src/diagram/event-emitter.ts
|
|
3745
4433
|
var EventEmitter = class {
|
|
3746
4434
|
constructor() {
|
|
@@ -3895,6 +4583,22 @@ ${runtime.error}`;
|
|
|
3895
4583
|
};
|
|
3896
4584
|
|
|
3897
4585
|
// src/diagram/stocksharp-diagram.ts
|
|
4586
|
+
var CONTEXT_MENU = [
|
|
4587
|
+
"undo",
|
|
4588
|
+
"redo",
|
|
4589
|
+
"cut",
|
|
4590
|
+
"copy",
|
|
4591
|
+
"paste",
|
|
4592
|
+
"open",
|
|
4593
|
+
"delete",
|
|
4594
|
+
{ group: "export", commands: ["exportDocument", "exportPng", "exportSvg"] },
|
|
4595
|
+
"overview",
|
|
4596
|
+
"properties",
|
|
4597
|
+
"help"
|
|
4598
|
+
];
|
|
4599
|
+
var CONTEXT_CHECKED = {
|
|
4600
|
+
overview: (diagram) => diagram.getViewState().overviewVisible
|
|
4601
|
+
};
|
|
3898
4602
|
var StockSharpDiagram = class extends EventEmitter {
|
|
3899
4603
|
constructor(options) {
|
|
3900
4604
|
super();
|
|
@@ -3907,8 +4611,12 @@ ${runtime.error}`;
|
|
|
3907
4611
|
this.destroyed = false;
|
|
3908
4612
|
this.fullscreen = false;
|
|
3909
4613
|
this.fullscreenButtonVisible = true;
|
|
3910
|
-
|
|
4614
|
+
// null means the host never named the button, so its wording comes from the shared bundle.
|
|
4615
|
+
// Keeping the two apart is what lets an explicit label win while an untouched control still
|
|
4616
|
+
// follows the page's language instead of being stuck in English.
|
|
4617
|
+
this.fullscreenLabels = null;
|
|
3911
4618
|
this.contextActions = new DiagramActionRegistry();
|
|
4619
|
+
this.contextMenu = null;
|
|
3912
4620
|
this.div = options.div;
|
|
3913
4621
|
this.catalog = options.catalog;
|
|
3914
4622
|
this.overviewContainer = options.overviewContainer ?? null;
|
|
@@ -3921,10 +4629,11 @@ ${runtime.error}`;
|
|
|
3921
4629
|
gridSize: options.gridSize
|
|
3922
4630
|
});
|
|
3923
4631
|
this.fullscreenButtonVisible = options.showFullscreenButton ?? true;
|
|
3924
|
-
this.fullscreenLabels = options.fullscreenLabels ??
|
|
4632
|
+
this.fullscreenLabels = options.fullscreenLabels ?? null;
|
|
3925
4633
|
this.prepareFullscreenButtonHost();
|
|
3926
4634
|
this.fullscreenButton = this.createFullscreenButton();
|
|
3927
4635
|
this.updateFullscreenButton();
|
|
4636
|
+
this.setContextMenuEnabled(options.showContextMenu ?? true);
|
|
3928
4637
|
this.registerContextActions();
|
|
3929
4638
|
this.bindCanvasEvents();
|
|
3930
4639
|
this.disposables.push(this.catalog.on("portTypesChanged", () => this.applySocketTheme()));
|
|
@@ -4084,8 +4793,8 @@ ${runtime.error}`;
|
|
|
4084
4793
|
return this.canvas.updateNode(nodeId, { name: value });
|
|
4085
4794
|
}
|
|
4086
4795
|
/** Groups host-driven document edits into one undo/redo operation. */
|
|
4087
|
-
transaction(
|
|
4088
|
-
return this.canvas.withTransaction(
|
|
4796
|
+
transaction(label2, action) {
|
|
4797
|
+
return this.canvas.withTransaction(label2, action);
|
|
4089
4798
|
}
|
|
4090
4799
|
setShowNodeMessages(show) {
|
|
4091
4800
|
this.canvas.setShowNodeMessages(show);
|
|
@@ -4134,6 +4843,10 @@ ${runtime.error}`;
|
|
|
4134
4843
|
takeScreenshot(options = {}) {
|
|
4135
4844
|
return this.canvas.takeScreenshot(options);
|
|
4136
4845
|
}
|
|
4846
|
+
/** The same picture as takeScreenshot, as an SVG document. */
|
|
4847
|
+
takeSvg(options = {}) {
|
|
4848
|
+
return this.canvas.takeSvg(options);
|
|
4849
|
+
}
|
|
4137
4850
|
getSelection() {
|
|
4138
4851
|
return this.canvas.getSelection();
|
|
4139
4852
|
}
|
|
@@ -4165,8 +4878,22 @@ ${runtime.error}`;
|
|
|
4165
4878
|
this.fullscreenLabels = labels;
|
|
4166
4879
|
this.updateFullscreenButton();
|
|
4167
4880
|
}
|
|
4881
|
+
/**
|
|
4882
|
+
* Re-reads the shared bundle for text the control renders itself. Call it after changing
|
|
4883
|
+
* the page language: the context menu is rebuilt on every open and needs nothing, but the
|
|
4884
|
+
* fullscreen button is drawn once and would otherwise keep the old wording.
|
|
4885
|
+
*/
|
|
4886
|
+
refreshLabels() {
|
|
4887
|
+
this.updateFullscreenButton();
|
|
4888
|
+
}
|
|
4168
4889
|
getFullscreenLabels() {
|
|
4169
|
-
return { ...this.
|
|
4890
|
+
return { ...this.resolveFullscreenLabels() };
|
|
4891
|
+
}
|
|
4892
|
+
resolveFullscreenLabels() {
|
|
4893
|
+
return this.fullscreenLabels ?? {
|
|
4894
|
+
enter: t("fullscreenEnter", "Enter fullscreen"),
|
|
4895
|
+
exit: t("fullscreenExit", "Exit fullscreen")
|
|
4896
|
+
};
|
|
4170
4897
|
}
|
|
4171
4898
|
/** Updates only the control's state after the host changed its own layout. */
|
|
4172
4899
|
setFullscreenState(fullscreen) {
|
|
@@ -4266,11 +4993,44 @@ ${runtime.error}`;
|
|
|
4266
4993
|
const fallback = this.canvas.getClipboardDocument();
|
|
4267
4994
|
return fallback !== null && this.canvas.pasteDocument(fallback).length > 0;
|
|
4268
4995
|
}
|
|
4996
|
+
/**
|
|
4997
|
+
* Turns the built-in menu on or off. Off leaves `contextMenuRequested` untouched, so a
|
|
4998
|
+
* host that draws its own is only trading one menu for another, never losing the event.
|
|
4999
|
+
*/
|
|
5000
|
+
setContextMenuEnabled(enabled) {
|
|
5001
|
+
if (enabled === (this.contextMenu !== null)) return;
|
|
5002
|
+
if (!enabled) {
|
|
5003
|
+
this.contextMenu?.destroy();
|
|
5004
|
+
this.contextMenu = null;
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
this.contextMenu = new ContextMenuView({
|
|
5008
|
+
container: this.div,
|
|
5009
|
+
// Through the public entry point, so the built-in menu is exactly as privileged
|
|
5010
|
+
// as a host's own -- same guards, same contextCommand event.
|
|
5011
|
+
execute: (command) => {
|
|
5012
|
+
this.executeContextCommand(command);
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
5015
|
+
}
|
|
5016
|
+
isContextMenuEnabled() {
|
|
5017
|
+
return this.contextMenu !== null;
|
|
5018
|
+
}
|
|
4269
5019
|
getContextCommands() {
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
5020
|
+
const context = this.contextActionContext();
|
|
5021
|
+
const state = (command) => {
|
|
5022
|
+
const checked = CONTEXT_CHECKED[command];
|
|
5023
|
+
return {
|
|
5024
|
+
command,
|
|
5025
|
+
enabled: this.contextActions.canExecute(command, context),
|
|
5026
|
+
...checked === void 0 ? {} : { checked: checked(this) }
|
|
5027
|
+
};
|
|
5028
|
+
};
|
|
5029
|
+
return CONTEXT_MENU.map((entry) => {
|
|
5030
|
+
if (typeof entry === "string") return state(entry);
|
|
5031
|
+
const commands = entry.commands.map(state);
|
|
5032
|
+
return { group: entry.group, enabled: commands.some(({ enabled }) => enabled), commands };
|
|
5033
|
+
});
|
|
4274
5034
|
}
|
|
4275
5035
|
executeContextCommand(command) {
|
|
4276
5036
|
const context = this.contextActionContext();
|
|
@@ -4338,6 +5098,8 @@ ${runtime.error}`;
|
|
|
4338
5098
|
destroy() {
|
|
4339
5099
|
if (this.destroyed) return;
|
|
4340
5100
|
this.destroyed = true;
|
|
5101
|
+
this.contextMenu?.destroy();
|
|
5102
|
+
this.contextMenu = null;
|
|
4341
5103
|
for (const dispose of this.disposables.splice(0).reverse()) dispose();
|
|
4342
5104
|
this.canvas.destroy();
|
|
4343
5105
|
}
|
|
@@ -4390,11 +5152,12 @@ ${runtime.error}`;
|
|
|
4390
5152
|
}
|
|
4391
5153
|
updateFullscreenButton() {
|
|
4392
5154
|
const fullscreen = this.fullscreen;
|
|
4393
|
-
const
|
|
5155
|
+
const labels = this.resolveFullscreenLabels();
|
|
5156
|
+
const label2 = fullscreen ? labels.exit : labels.enter;
|
|
4394
5157
|
const path = fullscreen ? "M4 9h5V4M20 9h-5V4M4 15h5v5M20 15h-5v5" : "M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5";
|
|
4395
5158
|
this.fullscreenButton.className = fullscreen ? "ssdiagram-fullscreen-button is-active" : "ssdiagram-fullscreen-button";
|
|
4396
|
-
this.fullscreenButton.title =
|
|
4397
|
-
this.fullscreenButton.setAttribute("aria-label",
|
|
5159
|
+
this.fullscreenButton.title = label2;
|
|
5160
|
+
this.fullscreenButton.setAttribute("aria-label", label2);
|
|
4398
5161
|
this.fullscreenButton.setAttribute("aria-pressed", String(fullscreen));
|
|
4399
5162
|
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
5163
|
}
|
|
@@ -4490,6 +5253,7 @@ ${runtime.error}`;
|
|
|
4490
5253
|
}),
|
|
4491
5254
|
this.canvas.on("undoStackChanged", (state) => this.emit("undoStackChanged", state)),
|
|
4492
5255
|
this.canvas.on("contextMenu", ({ x, y, node, link, port }) => {
|
|
5256
|
+
const commands = this.getContextCommands();
|
|
4493
5257
|
this.emit("contextMenuRequested", {
|
|
4494
5258
|
x,
|
|
4495
5259
|
y,
|
|
@@ -4497,8 +5261,9 @@ ${runtime.error}`;
|
|
|
4497
5261
|
link: link === null ? null : this.fromCanvasLink(link),
|
|
4498
5262
|
port: port === null ? null : this.fromCanvasPort(port.port),
|
|
4499
5263
|
portDirection: port?.port.direction ?? null,
|
|
4500
|
-
commands
|
|
5264
|
+
commands
|
|
4501
5265
|
});
|
|
5266
|
+
this.contextMenu?.show(x, y, commands);
|
|
4502
5267
|
})
|
|
4503
5268
|
);
|
|
4504
5269
|
}
|
|
@@ -4533,6 +5298,30 @@ ${runtime.error}`;
|
|
|
4533
5298
|
canExecute: ({ selection }) => permissions().deleteSelection && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
|
|
4534
5299
|
execute: () => this.canvas.deleteSelection()
|
|
4535
5300
|
},
|
|
5301
|
+
// Export asks rather than does. Where the result goes -- a download, an upload,
|
|
5302
|
+
// a clipboard, a print preview -- is the host's decision, and so is which options
|
|
5303
|
+
// to render with, so the control reports the request and stops there.
|
|
5304
|
+
// Availability follows the diagram, not the selection: all three export the whole
|
|
5305
|
+
// picture, and an empty diagram has none to give. They stay on in read-only mode,
|
|
5306
|
+
// which forbids changing the diagram, not looking at it.
|
|
5307
|
+
exportDocument: {
|
|
5308
|
+
canExecute: ({ document: document2 }) => document2.nodes.length > 0,
|
|
5309
|
+
execute: () => this.emit("exportRequested", { format: "document" })
|
|
5310
|
+
},
|
|
5311
|
+
exportPng: {
|
|
5312
|
+
canExecute: ({ document: document2 }) => document2.nodes.length > 0,
|
|
5313
|
+
execute: () => this.emit("exportRequested", { format: "png" })
|
|
5314
|
+
},
|
|
5315
|
+
exportSvg: {
|
|
5316
|
+
canExecute: ({ document: document2 }) => document2.nodes.length > 0,
|
|
5317
|
+
execute: () => this.emit("exportRequested", { format: "svg" })
|
|
5318
|
+
},
|
|
5319
|
+
// Always available: it shows the overview as readily as it hides it, and an empty
|
|
5320
|
+
// diagram is exactly when someone may want the panel out of the way.
|
|
5321
|
+
overview: {
|
|
5322
|
+
canExecute: () => true,
|
|
5323
|
+
execute: () => this.setOverviewVisible(!this.getViewState().overviewVisible)
|
|
5324
|
+
},
|
|
4536
5325
|
properties: {
|
|
4537
5326
|
canExecute: ({ nodes }) => nodes.length > 0,
|
|
4538
5327
|
execute: ({ nodes }) => this.emit("nodeProperties", { nodes })
|
|
@@ -4550,6 +5339,7 @@ ${runtime.error}`;
|
|
|
4550
5339
|
const linkIds = new Set(selection.linkIds);
|
|
4551
5340
|
return {
|
|
4552
5341
|
selection,
|
|
5342
|
+
document: document2,
|
|
4553
5343
|
nodes: document2.nodes.filter((node) => nodeIds.has(node.id)).map((node) => this.fromCanvasInit(node)),
|
|
4554
5344
|
links: document2.links.filter((link) => linkIds.has(link.id)).map((link) => new Link({
|
|
4555
5345
|
id: link.id,
|
|
@@ -4708,8 +5498,8 @@ ${runtime.error}`;
|
|
|
4708
5498
|
// imported node ("Element type is missing from the palette"). Node.id
|
|
4709
5499
|
// keeps its original casing so save/load round-trips unchanged.
|
|
4710
5500
|
addNodeType(node) {
|
|
4711
|
-
const
|
|
4712
|
-
this.nodeTypes.set(
|
|
5501
|
+
const n2 = (node instanceof Node ? node : new Node(node)).clone();
|
|
5502
|
+
this.nodeTypes.set(n2.id.toLowerCase(), n2);
|
|
4713
5503
|
this.emitNodeTypesChanged();
|
|
4714
5504
|
}
|
|
4715
5505
|
removeNodeType(id) {
|
|
@@ -4853,11 +5643,11 @@ ${runtime.error}`;
|
|
|
4853
5643
|
const chev = document.createElement("span");
|
|
4854
5644
|
chev.className = "d-palette-chev";
|
|
4855
5645
|
chev.setAttribute("aria-hidden", "true");
|
|
4856
|
-
const
|
|
4857
|
-
|
|
4858
|
-
|
|
5646
|
+
const label2 = document.createElement("span");
|
|
5647
|
+
label2.className = "d-palette-group-name";
|
|
5648
|
+
label2.textContent = name;
|
|
4859
5649
|
head.appendChild(chev);
|
|
4860
|
-
head.appendChild(
|
|
5650
|
+
head.appendChild(label2);
|
|
4861
5651
|
head.addEventListener("click", () => {
|
|
4862
5652
|
if (this.collapsed.has(name)) this.collapsed.delete(name);
|
|
4863
5653
|
else this.collapsed.add(name);
|
|
@@ -4893,10 +5683,10 @@ ${runtime.error}`;
|
|
|
4893
5683
|
placeholder.setAttribute("aria-hidden", "true");
|
|
4894
5684
|
li.appendChild(placeholder);
|
|
4895
5685
|
}
|
|
4896
|
-
const
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
li.appendChild(
|
|
5686
|
+
const label2 = document.createElement("span");
|
|
5687
|
+
label2.className = "d-palette-label";
|
|
5688
|
+
label2.textContent = item.name;
|
|
5689
|
+
li.appendChild(label2);
|
|
4900
5690
|
li.addEventListener("click", () => this.selectNodeType(item.id));
|
|
4901
5691
|
li.addEventListener("dblclick", (event) => {
|
|
4902
5692
|
event.preventDefault();
|
|
@@ -4976,8 +5766,8 @@ ${runtime.error}`;
|
|
|
4976
5766
|
const m = hex.match(/^#?([0-9a-fA-F]{6})$/);
|
|
4977
5767
|
if (m === null)
|
|
4978
5768
|
return 0;
|
|
4979
|
-
const
|
|
4980
|
-
return 0.299 * (
|
|
5769
|
+
const n2 = parseInt(m[1], 16);
|
|
5770
|
+
return 0.299 * (n2 >> 16 & 255) + 0.587 * (n2 >> 8 & 255) + 0.114 * (n2 & 255);
|
|
4981
5771
|
}
|
|
4982
5772
|
function makePort(p) {
|
|
4983
5773
|
return new Port({
|
|
@@ -5018,23 +5808,23 @@ ${runtime.error}`;
|
|
|
5018
5808
|
add(usedOut, l.from, l.fromPort);
|
|
5019
5809
|
add(usedIn, l.to, l.toPort);
|
|
5020
5810
|
}
|
|
5021
|
-
const nodes = scheme.nodes.map((
|
|
5022
|
-
const
|
|
5023
|
-
const inPorts =
|
|
5024
|
-
const outPorts =
|
|
5025
|
-
const anchorType =
|
|
5026
|
-
for (const key of usedIn.get(
|
|
5811
|
+
const nodes = scheme.nodes.map((n2) => {
|
|
5812
|
+
const t2 = catalog.getNodeType(n2.typeId);
|
|
5813
|
+
const inPorts = t2 ? t2.inPorts.map(portOf) : [];
|
|
5814
|
+
const outPorts = t2 ? t2.outPorts.map(portOf) : [];
|
|
5815
|
+
const anchorType = t2?.inPorts.find((p) => p.isDynamic)?.type ?? "Any";
|
|
5816
|
+
for (const key of usedIn.get(n2.id) ?? [])
|
|
5027
5817
|
if (!inPorts.some((p) => p.id === key))
|
|
5028
5818
|
inPorts.push({ id: key, name: key, type: anchorType, maxLinks: 0 });
|
|
5029
|
-
for (const key of usedOut.get(
|
|
5819
|
+
for (const key of usedOut.get(n2.id) ?? [])
|
|
5030
5820
|
if (!outPorts.some((p) => p.id === key))
|
|
5031
5821
|
outPorts.push({ id: key, name: key, type: "Any", maxLinks: 0 });
|
|
5032
|
-
const data = new DiagramNode({ id:
|
|
5033
|
-
if (
|
|
5034
|
-
data.icon =
|
|
5035
|
-
if (!
|
|
5822
|
+
const data = new DiagramNode({ id: n2.id, typeId: n2.typeId, name: n2.name, x: n2.x, y: n2.y, inPorts, outPorts });
|
|
5823
|
+
if (t2?.icon)
|
|
5824
|
+
data.icon = t2.icon;
|
|
5825
|
+
if (!t2) {
|
|
5036
5826
|
data.isPlaceholder = true;
|
|
5037
|
-
nodeErrors[
|
|
5827
|
+
nodeErrors[n2.id] = missingTypeMessage(n2.typeId);
|
|
5038
5828
|
}
|
|
5039
5829
|
return data;
|
|
5040
5830
|
});
|
|
@@ -5242,7 +6032,7 @@ ${runtime.error}`;
|
|
|
5242
6032
|
diagram.setReadOnly(true);
|
|
5243
6033
|
diagram.setLinkValidator(() => true);
|
|
5244
6034
|
diagram.setOverviewVisible(false);
|
|
5245
|
-
const missingTypeTemplate = div.dataset.diagramMissingElement ?? 'Element type "{typeId}" is missing from the palette.';
|
|
6035
|
+
const missingTypeTemplate = div.dataset.diagramMissingElement ?? t("embedMissingElement", 'Element type "{typeId}" is missing from the palette.');
|
|
5246
6036
|
const converted = toDiagramNodes(
|
|
5247
6037
|
scheme,
|
|
5248
6038
|
catalog,
|
|
@@ -5327,9 +6117,9 @@ ${runtime.error}`;
|
|
|
5327
6117
|
function errorTexts(div) {
|
|
5328
6118
|
const parts = (div.dataset.diagramErrors ?? "").split("|");
|
|
5329
6119
|
return {
|
|
5330
|
-
load: parts[0] || "Diagram source could not be loaded.",
|
|
5331
|
-
empty: parts[1] || "Diagram is empty or malformed.",
|
|
5332
|
-
draw: parts[2] || "Diagram could not be rendered."
|
|
6120
|
+
load: parts[0] || t("embedErrorLoad", "Diagram source could not be loaded."),
|
|
6121
|
+
empty: parts[1] || t("embedErrorEmpty", "Diagram is empty or malformed."),
|
|
6122
|
+
draw: parts[2] || t("embedErrorDraw", "Diagram could not be rendered.")
|
|
5333
6123
|
};
|
|
5334
6124
|
}
|
|
5335
6125
|
function note(div, message, revision) {
|