@stocksharp/diagram 1.4.0 → 1.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.
- package/README.md +84 -1
- package/dist/esm/canvas-renderer.js +166 -25
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/color.js +42 -0
- package/dist/esm/color.js.map +1 -1
- package/dist/esm/core/document.js +11 -0
- package/dist/esm/core/document.js.map +1 -1
- package/dist/esm/core/model.js.map +1 -1
- package/dist/esm/diagram/stocksharp-diagram.js +23 -3
- package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
- package/dist/esm/embed.js +104 -20
- package/dist/esm/embed.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/ssdiagram-headless.js +195 -21
- package/dist/ssdiagram-headless.js.map +2 -2
- package/dist/ssdiagram.js +300 -43
- package/dist/ssdiagram.js.map +3 -3
- package/dist/types/canvas-renderer.d.ts +25 -3
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/color.d.ts +7 -0
- package/dist/types/color.d.ts.map +1 -1
- package/dist/types/core/model.d.ts +7 -0
- package/dist/types/core/model.d.ts.map +1 -1
- package/dist/types/diagram/api.d.ts +6 -0
- package/dist/types/diagram/api.d.ts.map +1 -1
- package/dist/types/diagram/stocksharp-diagram.d.ts +11 -0
- package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
- package/dist/types/embed.d.ts +21 -0
- package/dist/types/embed.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/canvas-renderer.ts +169 -25
- package/src/color.ts +48 -0
- package/src/core/document.ts +12 -0
- package/src/core/model.ts +7 -0
- package/src/diagram/api.ts +6 -0
- package/src/diagram/stocksharp-diagram.ts +27 -3
- package/src/embed.ts +147 -29
- package/src/index.ts +2 -0
|
@@ -458,6 +458,42 @@ var SSDiagramHeadless = (() => {
|
|
|
458
458
|
return color;
|
|
459
459
|
return `hsl(${Math.round(hue * 10) / 10}, ${Math.round(saturation * 1e3) / 10}%, ${Math.round(maxLightness * 1e3) / 10}%)`;
|
|
460
460
|
}
|
|
461
|
+
function resolveCssVariables(value, lookup) {
|
|
462
|
+
return resolveAt(value, lookup, 0);
|
|
463
|
+
}
|
|
464
|
+
function resolveAt(value, lookup, depth) {
|
|
465
|
+
const start = value.indexOf("var(");
|
|
466
|
+
if (start < 0)
|
|
467
|
+
return value;
|
|
468
|
+
if (depth > 8)
|
|
469
|
+
return null;
|
|
470
|
+
let level = 0;
|
|
471
|
+
let end = -1;
|
|
472
|
+
for (let i = start + 3; i < value.length; i += 1) {
|
|
473
|
+
if (value[i] === "(")
|
|
474
|
+
level += 1;
|
|
475
|
+
else if (value[i] === ")" && --level === 0) {
|
|
476
|
+
end = i;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (end < 0)
|
|
481
|
+
return null;
|
|
482
|
+
const inner = value.slice(start + 4, end);
|
|
483
|
+
const comma = inner.indexOf(",");
|
|
484
|
+
const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
|
|
485
|
+
const fallback = comma < 0 ? "" : inner.slice(comma + 1).trim();
|
|
486
|
+
let replacement = name.startsWith("--") ? lookup(name).trim() : "";
|
|
487
|
+
if (replacement === "")
|
|
488
|
+
replacement = fallback;
|
|
489
|
+
if (replacement === "")
|
|
490
|
+
return null;
|
|
491
|
+
const resolved = resolveAt(replacement, lookup, depth + 1);
|
|
492
|
+
const rest = resolveAt(value.slice(end + 1), lookup, depth);
|
|
493
|
+
if (resolved === null || rest === null)
|
|
494
|
+
return null;
|
|
495
|
+
return value.slice(0, start) + resolved + rest;
|
|
496
|
+
}
|
|
461
497
|
|
|
462
498
|
// src/core/json.ts
|
|
463
499
|
function setJsonKey(target, key, value) {
|
|
@@ -555,10 +591,12 @@ var SSDiagramHeadless = (() => {
|
|
|
555
591
|
function normalizeNode(value, path) {
|
|
556
592
|
const input = requireStructure(value, path);
|
|
557
593
|
const id = requireIdentifier(input.id, `${path}.id`);
|
|
594
|
+
const subtitle = requireOptionalText(input.subtitle, `${path}.subtitle`);
|
|
558
595
|
return {
|
|
559
596
|
id,
|
|
560
597
|
typeId: requireIdentifier(input.typeId ?? id, `${path}.typeId`),
|
|
561
598
|
name: requireString(input.name, `${path}.name`),
|
|
599
|
+
...subtitle === "" ? {} : { subtitle },
|
|
562
600
|
description: requireString(input.description ?? "", `${path}.description`),
|
|
563
601
|
groupName: requireString(input.groupName ?? "Common", `${path}.groupName`),
|
|
564
602
|
x: requireFiniteNumber(input.x ?? 0, `${path}.x`),
|
|
@@ -609,14 +647,19 @@ var SSDiagramHeadless = (() => {
|
|
|
609
647
|
}
|
|
610
648
|
function normalizeLink(value, path) {
|
|
611
649
|
const input = requireStructure(value, path);
|
|
650
|
+
const label = requireOptionalText(input.label, `${path}.label`);
|
|
612
651
|
return {
|
|
613
652
|
id: requireIdentifier(input.id, `${path}.id`),
|
|
614
653
|
from: normalizeEndpoint(input.from, `${path}.from`),
|
|
615
654
|
to: normalizeEndpoint(input.to, `${path}.to`),
|
|
616
655
|
style: requireLinkStyle(input.style, `${path}.style`),
|
|
656
|
+
...label === "" ? {} : { label },
|
|
617
657
|
metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`)
|
|
618
658
|
};
|
|
619
659
|
}
|
|
660
|
+
function requireOptionalText(value, path) {
|
|
661
|
+
return value === void 0 || value === null ? "" : requireString(value, path);
|
|
662
|
+
}
|
|
620
663
|
function requireLinkStyle(value, path) {
|
|
621
664
|
if (value === void 0 || value === null) return "solid";
|
|
622
665
|
if (value !== "solid" && value !== "dashed") {
|
|
@@ -656,6 +699,7 @@ var SSDiagramHeadless = (() => {
|
|
|
656
699
|
id: requireString(node.id, `${path}.id`),
|
|
657
700
|
typeId: requireString(node.typeId, `${path}.typeId`),
|
|
658
701
|
name: requireString(node.name, `${path}.name`),
|
|
702
|
+
subtitle: node.subtitle,
|
|
659
703
|
description: requireString(node.description, `${path}.description`),
|
|
660
704
|
groupName: requireString(node.groupName, `${path}.groupName`),
|
|
661
705
|
x: requireNumber(node.x, `${path}.x`),
|
|
@@ -711,6 +755,7 @@ var SSDiagramHeadless = (() => {
|
|
|
711
755
|
from: parseEndpoint(link.from, `${path}.from`),
|
|
712
756
|
to: parseEndpoint(link.to, `${path}.to`),
|
|
713
757
|
style: link.style === void 0 ? void 0 : requireLinkStyle(link.style, `${path}.style`),
|
|
758
|
+
label: link.label,
|
|
714
759
|
metadata: cloneJsonObject(link.metadata, `${path}.metadata`)
|
|
715
760
|
}, path);
|
|
716
761
|
}
|
|
@@ -1141,6 +1186,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1141
1186
|
this.id = id;
|
|
1142
1187
|
this.typeId = init.typeId ?? id;
|
|
1143
1188
|
this.name = init.name;
|
|
1189
|
+
this.subtitle = init.subtitle ?? "";
|
|
1144
1190
|
this.description = init.description ?? "";
|
|
1145
1191
|
this.groupName = init.groupName ?? "Common";
|
|
1146
1192
|
this.color = init.color ?? "#d7d7d7";
|
|
@@ -1166,6 +1212,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1166
1212
|
id: this.id,
|
|
1167
1213
|
typeId: this.typeId,
|
|
1168
1214
|
name: this.name,
|
|
1215
|
+
...this.subtitle === "" ? {} : { subtitle: this.subtitle },
|
|
1169
1216
|
description: this.description,
|
|
1170
1217
|
groupName: this.groupName,
|
|
1171
1218
|
color: this.color,
|
|
@@ -1187,12 +1234,13 @@ var SSDiagramHeadless = (() => {
|
|
|
1187
1234
|
}
|
|
1188
1235
|
};
|
|
1189
1236
|
var LinkModel = class {
|
|
1190
|
-
constructor(from, fromPort, to, toPort, id = "", metadata, style = "solid") {
|
|
1237
|
+
constructor(from, fromPort, to, toPort, id = "", metadata, style = "solid", label = "") {
|
|
1191
1238
|
this.from = from;
|
|
1192
1239
|
this.fromPort = fromPort;
|
|
1193
1240
|
this.to = to;
|
|
1194
1241
|
this.toPort = toPort;
|
|
1195
1242
|
this.style = style;
|
|
1243
|
+
this.label = label;
|
|
1196
1244
|
this.id = id;
|
|
1197
1245
|
this.metadata = copyJsonObject(metadata);
|
|
1198
1246
|
}
|
|
@@ -1207,6 +1255,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1207
1255
|
to: this.to,
|
|
1208
1256
|
toPort: this.toPort,
|
|
1209
1257
|
style: this.style,
|
|
1258
|
+
...this.label === "" ? {} : { label: this.label },
|
|
1210
1259
|
metadata: copyJsonObject(this.metadata)
|
|
1211
1260
|
};
|
|
1212
1261
|
}
|
|
@@ -1227,6 +1276,15 @@ var SSDiagramHeadless = (() => {
|
|
|
1227
1276
|
var INTRO_RISE = 70;
|
|
1228
1277
|
var ERROR_FLASH_MS = 1100;
|
|
1229
1278
|
var ERROR_RED = "#f6465d";
|
|
1279
|
+
var DEFAULT_NODE_COLOR = "#d7d7d7";
|
|
1280
|
+
var DEFAULT_NODE_BORDER = "#8c8c8c";
|
|
1281
|
+
var DEFAULT_ZONE_TINT = "#8a8f98";
|
|
1282
|
+
var TITLE_FONT = "600 12px Segoe UI, Tahoma, sans-serif";
|
|
1283
|
+
var SUBTITLE_FONT = "11px Segoe UI, Tahoma, sans-serif";
|
|
1284
|
+
var SUBTITLE_MIN_H = 62;
|
|
1285
|
+
var LINK_LABEL_FONT = "600 10px Segoe UI, Tahoma, sans-serif";
|
|
1286
|
+
var ZONE_CAPTION_TOP = 9;
|
|
1287
|
+
var ZONE_CAPTION_H = 14;
|
|
1230
1288
|
var ERROR_BACKGROUND = "#7d2632";
|
|
1231
1289
|
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1232
1290
|
function hashHue(s) {
|
|
@@ -1324,10 +1382,14 @@ var SSDiagramHeadless = (() => {
|
|
|
1324
1382
|
this.ovGeo = null;
|
|
1325
1383
|
this.validator = null;
|
|
1326
1384
|
this.handlers = /* @__PURE__ */ new Map();
|
|
1385
|
+
// Computed style of the host, read at most once per frame: colours written as var(--token)
|
|
1386
|
+
// resolve against it, so a frame drawn after a theme switch picks up the new values.
|
|
1387
|
+
this.hostStyle = null;
|
|
1327
1388
|
this.opts = opts;
|
|
1328
1389
|
if (opts.typeColors !== void 0) this.setTypeColorsInternal(opts.typeColors);
|
|
1329
1390
|
this.host = opts.host;
|
|
1330
1391
|
this.gridSnapEnabled = opts.gridSnap ?? false;
|
|
1392
|
+
this.pageScroll = opts.pageScroll ?? false;
|
|
1331
1393
|
this.gridSize = this.normalizeGridSize(opts.gridSize ?? DEFAULT_GRID_SIZE);
|
|
1332
1394
|
this.history = new DiagramCommandHistory(() => {
|
|
1333
1395
|
this.emit("undoStackChanged", { canUndo: this.canUndo(), canRedo: this.canRedo() });
|
|
@@ -1336,7 +1398,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1336
1398
|
this.canvas.style.display = "block";
|
|
1337
1399
|
this.canvas.tabIndex = 0;
|
|
1338
1400
|
this.canvas.style.outline = "none";
|
|
1339
|
-
this.
|
|
1401
|
+
this.applyTouchAction();
|
|
1340
1402
|
this.canvas.style.userSelect = "none";
|
|
1341
1403
|
this.canvas.style.webkitUserSelect = "none";
|
|
1342
1404
|
this.canvas.style.webkitTouchCallout = "none";
|
|
@@ -1390,6 +1452,19 @@ var SSDiagramHeadless = (() => {
|
|
|
1390
1452
|
this.gridSize = nextSize;
|
|
1391
1453
|
this.scheduleDraw();
|
|
1392
1454
|
}
|
|
1455
|
+
/** See DiagramOptions.pageScroll. */
|
|
1456
|
+
setPageScroll(enabled) {
|
|
1457
|
+
this.pageScroll = enabled;
|
|
1458
|
+
this.applyTouchAction();
|
|
1459
|
+
}
|
|
1460
|
+
isPageScrollEnabled() {
|
|
1461
|
+
return this.pageScroll;
|
|
1462
|
+
}
|
|
1463
|
+
// pan-y hands a vertical swipe to the browser, which scrolls the page; a horizontal one and a
|
|
1464
|
+
// two-finger pinch still reach the canvas.
|
|
1465
|
+
applyTouchAction() {
|
|
1466
|
+
this.canvas.style.touchAction = this.pageScroll ? "pan-y" : "none";
|
|
1467
|
+
}
|
|
1393
1468
|
getGridSnap() {
|
|
1394
1469
|
return { enabled: this.gridSnapEnabled, size: this.gridSize };
|
|
1395
1470
|
}
|
|
@@ -1605,7 +1680,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1605
1680
|
this.emit("linkValidation", { fromNode: fn, from: fp, toNode: tn, to: tp, ...validation });
|
|
1606
1681
|
if (!validation.allowed) return false;
|
|
1607
1682
|
}
|
|
1608
|
-
const link = new LinkModel(init.from, init.fromPort, init.to, init.toPort, init.id ?? this.nextLinkId(), init.metadata, init.style ?? "solid");
|
|
1683
|
+
const link = new LinkModel(init.from, init.fromPort, init.to, init.toPort, init.id ?? this.nextLinkId(), init.metadata, init.style ?? "solid", init.label ?? "");
|
|
1609
1684
|
if (this.links.some((l) => l.id === link.id)) return false;
|
|
1610
1685
|
this.links.splice(clamp(Math.trunc(index), 0, this.links.length), 0, link);
|
|
1611
1686
|
this.emit("linkAdded", { link });
|
|
@@ -1877,6 +1952,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1877
1952
|
updateNode(nodeId, patch) {
|
|
1878
1953
|
return this.updateNodeState(nodeId, "update node", (node) => {
|
|
1879
1954
|
if (patch.name !== void 0) node.name = patch.name;
|
|
1955
|
+
if (patch.subtitle !== void 0) node.subtitle = patch.subtitle;
|
|
1880
1956
|
if (patch.description !== void 0) node.description = patch.description;
|
|
1881
1957
|
if (patch.color !== void 0) node.color = patch.color;
|
|
1882
1958
|
if (patch.border !== void 0) node.border = patch.border;
|
|
@@ -1922,6 +1998,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1922
1998
|
const restored = new NodeModel(snapshot, nodeId);
|
|
1923
1999
|
node.typeId = restored.typeId;
|
|
1924
2000
|
node.name = restored.name;
|
|
2001
|
+
node.subtitle = restored.subtitle;
|
|
1925
2002
|
node.description = restored.description;
|
|
1926
2003
|
node.groupName = restored.groupName;
|
|
1927
2004
|
node.color = restored.color;
|
|
@@ -1952,6 +2029,10 @@ var SSDiagramHeadless = (() => {
|
|
|
1952
2029
|
return true;
|
|
1953
2030
|
}
|
|
1954
2031
|
load(nodes, links) {
|
|
2032
|
+
this.loadModel(nodes, links, []);
|
|
2033
|
+
}
|
|
2034
|
+
// Zones are placed before the view is fitted, so the first frame frames them too.
|
|
2035
|
+
loadModel(nodes, links, zones) {
|
|
1955
2036
|
this.clear();
|
|
1956
2037
|
this.idSeq = 1;
|
|
1957
2038
|
this.linkSeq = 1;
|
|
@@ -1978,9 +2059,10 @@ var SSDiagramHeadless = (() => {
|
|
|
1978
2059
|
if (this.links.some((existing) => existing.id === id)) {
|
|
1979
2060
|
throw new Error(`ssdiagram: duplicate link id "${id}"`);
|
|
1980
2061
|
}
|
|
1981
|
-
const model = new LinkModel(link.from, link.fromPort, link.to, link.toPort, id, link.metadata, link.style ?? "solid");
|
|
2062
|
+
const model = new LinkModel(link.from, link.fromPort, link.to, link.toPort, id, link.metadata, link.style ?? "solid", link.label ?? "");
|
|
1982
2063
|
if (!this.links.some((existing) => existing.key() === model.key())) this.links.push(model);
|
|
1983
2064
|
}
|
|
2065
|
+
this.zones = zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
|
|
1984
2066
|
for (const node of this.nodes) {
|
|
1985
2067
|
if (node.loadError.length === 0) continue;
|
|
1986
2068
|
const state = createDiagramNodeRuntimeState();
|
|
@@ -2003,13 +2085,14 @@ var SSDiagramHeadless = (() => {
|
|
|
2003
2085
|
to: l.to,
|
|
2004
2086
|
toPort: l.toPort,
|
|
2005
2087
|
style: l.style,
|
|
2088
|
+
...l.label === "" ? {} : { label: l.label },
|
|
2006
2089
|
metadata: copyJsonObject(l.metadata)
|
|
2007
2090
|
}))
|
|
2008
2091
|
};
|
|
2009
2092
|
}
|
|
2010
2093
|
loadDocument(source) {
|
|
2011
2094
|
const document2 = parseDiagramDocument(source);
|
|
2012
|
-
this.
|
|
2095
|
+
this.loadModel(
|
|
2013
2096
|
document2.nodes,
|
|
2014
2097
|
document2.links.map((link) => ({
|
|
2015
2098
|
id: link.id,
|
|
@@ -2018,10 +2101,11 @@ var SSDiagramHeadless = (() => {
|
|
|
2018
2101
|
to: link.to.nodeId,
|
|
2019
2102
|
toPort: link.to.portId,
|
|
2020
2103
|
style: link.style,
|
|
2104
|
+
label: link.label,
|
|
2021
2105
|
metadata: link.metadata
|
|
2022
|
-
}))
|
|
2106
|
+
})),
|
|
2107
|
+
document2.zones
|
|
2023
2108
|
);
|
|
2024
|
-
this.zones = document2.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
|
|
2025
2109
|
this.documentMetadata = copyJsonObject(document2.metadata);
|
|
2026
2110
|
}
|
|
2027
2111
|
saveDocument() {
|
|
@@ -2032,6 +2116,7 @@ var SSDiagramHeadless = (() => {
|
|
|
2032
2116
|
from: { nodeId: link.from, portId: link.fromPort },
|
|
2033
2117
|
to: { nodeId: link.to, portId: link.toPort },
|
|
2034
2118
|
style: link.style,
|
|
2119
|
+
label: link.label,
|
|
2035
2120
|
metadata: link.metadata
|
|
2036
2121
|
})),
|
|
2037
2122
|
zones: this.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) })),
|
|
@@ -2458,8 +2543,10 @@ var SSDiagramHeadless = (() => {
|
|
|
2458
2543
|
if (t.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t.overviewViewportFill;
|
|
2459
2544
|
this.scheduleDraw();
|
|
2460
2545
|
}
|
|
2546
|
+
// Zones count: fitting or exporting only the nodes cut off the places they sit in, captions
|
|
2547
|
+
// included. A caption that is taller than its zone still has to fit.
|
|
2461
2548
|
graphBounds() {
|
|
2462
|
-
if (this.nodes.length === 0) return null;
|
|
2549
|
+
if (this.nodes.length === 0 && this.zones.length === 0) return null;
|
|
2463
2550
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2464
2551
|
for (const n2 of this.nodes) {
|
|
2465
2552
|
minX = Math.min(minX, n2.x);
|
|
@@ -2467,6 +2554,13 @@ var SSDiagramHeadless = (() => {
|
|
|
2467
2554
|
maxX = Math.max(maxX, n2.x + n2.w);
|
|
2468
2555
|
maxY = Math.max(maxY, n2.y + n2.h);
|
|
2469
2556
|
}
|
|
2557
|
+
for (const zone of this.zones) {
|
|
2558
|
+
const bottom = zone.name.length > 0 ? Math.max(zone.height, ZONE_CAPTION_TOP + ZONE_CAPTION_H) : zone.height;
|
|
2559
|
+
minX = Math.min(minX, zone.x - 1);
|
|
2560
|
+
minY = Math.min(minY, zone.y - 1);
|
|
2561
|
+
maxX = Math.max(maxX, zone.x + zone.width + 1);
|
|
2562
|
+
maxY = Math.max(maxY, zone.y + bottom + 1);
|
|
2563
|
+
}
|
|
2470
2564
|
return { minX, minY, maxX, maxY };
|
|
2471
2565
|
}
|
|
2472
2566
|
resize(w, h) {
|
|
@@ -2518,11 +2612,16 @@ var SSDiagramHeadless = (() => {
|
|
|
2518
2612
|
// ---- geometry ---------------------------------------------------
|
|
2519
2613
|
layoutNode(n2) {
|
|
2520
2614
|
const ctx = this.ctx;
|
|
2521
|
-
ctx.font =
|
|
2615
|
+
ctx.font = TITLE_FONT;
|
|
2522
2616
|
const titleW = ctx.measureText(n2.name).width;
|
|
2617
|
+
let subtitleW = 0;
|
|
2618
|
+
if (n2.subtitle.length > 0) {
|
|
2619
|
+
ctx.font = SUBTITLE_FONT;
|
|
2620
|
+
subtitleW = ctx.measureText(n2.subtitle).width;
|
|
2621
|
+
}
|
|
2523
2622
|
const rows = Math.max(n2.inPorts.length, n2.outPorts.length, 1);
|
|
2524
|
-
n2.w = Math.max(170, Math.ceil(titleW) + 28);
|
|
2525
|
-
n2.h = Math.max(48, rows * PORT_ROW_H + 16);
|
|
2623
|
+
n2.w = Math.max(170, Math.ceil(titleW) + 28, Math.ceil(subtitleW) + 28);
|
|
2624
|
+
n2.h = Math.max(n2.subtitle.length > 0 ? SUBTITLE_MIN_H : 48, rows * PORT_ROW_H + 16);
|
|
2526
2625
|
const off = PORT_SQ / 2;
|
|
2527
2626
|
const place = (ports, x) => {
|
|
2528
2627
|
const stack = ports.some(isGrowAnchor) ? [...ports.filter((p) => !isGrowAnchor(p)), ...ports.filter(isGrowAnchor)] : ports;
|
|
@@ -2881,6 +2980,7 @@ var SSDiagramHeadless = (() => {
|
|
|
2881
2980
|
// The clipboard is built from saveDocument(), so it carries the style;
|
|
2882
2981
|
// rebuilding without it silently turned every pasted dashed link solid.
|
|
2883
2982
|
style: l.style,
|
|
2983
|
+
label: l.label,
|
|
2884
2984
|
metadata: l.metadata
|
|
2885
2985
|
});
|
|
2886
2986
|
}
|
|
@@ -3266,6 +3366,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3266
3366
|
this.listen(window, "pointerup", finish);
|
|
3267
3367
|
this.listen(window, "pointercancel", finish);
|
|
3268
3368
|
this.listen(this.canvas, "wheel", (e) => {
|
|
3369
|
+
if (this.pageScroll && !e.ctrlKey && !e.metaKey) return;
|
|
3269
3370
|
e.preventDefault();
|
|
3270
3371
|
const [sx, sy] = localXY(e);
|
|
3271
3372
|
const [wx, wy] = this.toWorld(sx, sy);
|
|
@@ -3411,6 +3512,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3411
3512
|
}
|
|
3412
3513
|
draw(options = SCREEN_RENDER_OPTIONS) {
|
|
3413
3514
|
const ctx = this.ctx;
|
|
3515
|
+
this.hostStyle = null;
|
|
3414
3516
|
ctx.clearRect(0, 0, this.width, this.height);
|
|
3415
3517
|
const background = this.opts.background ?? "#1b1b1f";
|
|
3416
3518
|
ctx.fillStyle = background;
|
|
@@ -3438,6 +3540,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3438
3540
|
);
|
|
3439
3541
|
this.drawZones();
|
|
3440
3542
|
const prior = [];
|
|
3543
|
+
const labels = [];
|
|
3441
3544
|
const hoveredNode = this.hoverPort?.node ?? this.hoverNode;
|
|
3442
3545
|
for (const l of this.links) {
|
|
3443
3546
|
const a = this.endpoint(l, "from");
|
|
@@ -3451,6 +3554,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3451
3554
|
const pts = this.routeLink(a, b, /* @__PURE__ */ new Set([l.from, l.to]));
|
|
3452
3555
|
this.strokeRoute(pts, color, width, prior, l.style);
|
|
3453
3556
|
this.drawArrow(pts, color);
|
|
3557
|
+
if (l.label.length > 0) labels.push({ pts, text: l.label, color });
|
|
3454
3558
|
for (let k = 1; k < pts.length; k += 1) {
|
|
3455
3559
|
const [x1, y1] = pts[k - 1];
|
|
3456
3560
|
const [x2, y2] = pts[k];
|
|
@@ -3458,6 +3562,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3458
3562
|
else if (y1 === y2 && x1 !== x2) prior.push({ h: true, c: y1, a: Math.min(x1, x2), b: Math.max(x1, x2) });
|
|
3459
3563
|
}
|
|
3460
3564
|
}
|
|
3565
|
+
for (const label of labels) this.drawLinkLabel(label.pts, label.text, label.color, background);
|
|
3461
3566
|
if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
|
|
3462
3567
|
this.portFeed = this.feedTypes();
|
|
3463
3568
|
for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
|
|
@@ -3612,8 +3717,9 @@ ${runtime.error}`;
|
|
|
3612
3717
|
drawZones() {
|
|
3613
3718
|
if (this.zones.length === 0) return;
|
|
3614
3719
|
const ctx = this.ctx;
|
|
3720
|
+
const defaultTint = this.resolveColor(this.opts.zoneColor ?? DEFAULT_ZONE_TINT, DEFAULT_ZONE_TINT);
|
|
3615
3721
|
for (const zone of this.zones) {
|
|
3616
|
-
const tint = zone.color.length > 0 ? zone.color :
|
|
3722
|
+
const tint = zone.color.length > 0 ? this.resolveColor(zone.color, defaultTint) : defaultTint;
|
|
3617
3723
|
ctx.save();
|
|
3618
3724
|
roundRect(ctx, zone.x, zone.y, zone.width, zone.height, 10);
|
|
3619
3725
|
ctx.globalAlpha = 0.12;
|
|
@@ -3629,10 +3735,10 @@ ${runtime.error}`;
|
|
|
3629
3735
|
ctx.save();
|
|
3630
3736
|
ctx.globalAlpha = 0.75;
|
|
3631
3737
|
ctx.fillStyle = tint;
|
|
3632
|
-
ctx.font =
|
|
3738
|
+
ctx.font = TITLE_FONT;
|
|
3633
3739
|
ctx.textAlign = "left";
|
|
3634
3740
|
ctx.textBaseline = "top";
|
|
3635
|
-
ctx.fillText(zone.name, zone.x + 12, zone.y +
|
|
3741
|
+
ctx.fillText(zone.name, zone.x + 12, zone.y + ZONE_CAPTION_TOP, Math.max(0, zone.width - 24));
|
|
3636
3742
|
ctx.restore();
|
|
3637
3743
|
}
|
|
3638
3744
|
}
|
|
@@ -3677,11 +3783,12 @@ ${runtime.error}`;
|
|
|
3677
3783
|
const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
|
|
3678
3784
|
const runtime = options.runtime ? getJsonKey(this.runtimeState.nodes, n2.id) : void 0;
|
|
3679
3785
|
const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
|
|
3786
|
+
const fill = this.resolveColor(n2.color, DEFAULT_NODE_COLOR);
|
|
3680
3787
|
roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
|
|
3681
|
-
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" :
|
|
3788
|
+
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : fill;
|
|
3682
3789
|
ctx.fill();
|
|
3683
3790
|
ctx.lineWidth = selected ? 2 : 1.5;
|
|
3684
|
-
ctx.strokeStyle = selected ? this.selectionColor() : n2.border;
|
|
3791
|
+
ctx.strokeStyle = selected ? this.selectionColor() : this.resolveColor(n2.border, DEFAULT_NODE_BORDER);
|
|
3685
3792
|
ctx.stroke();
|
|
3686
3793
|
if (hasLoadError || hasRuntimeError) {
|
|
3687
3794
|
let alpha = 1;
|
|
@@ -3714,11 +3821,22 @@ ${runtime.error}`;
|
|
|
3714
3821
|
}
|
|
3715
3822
|
}
|
|
3716
3823
|
const titleShift = n2.icon ? iconW + 4 : 0;
|
|
3717
|
-
|
|
3718
|
-
|
|
3824
|
+
const textX = n2.x + titleShift + (n2.w - titleShift) / 2;
|
|
3825
|
+
const textWidth = n2.w - titleShift - 14;
|
|
3826
|
+
ctx.fillStyle = hasLoadError ? "#ffffff" : readableTextOn(active ? "#ffd1dc" : fill);
|
|
3827
|
+
ctx.font = TITLE_FONT;
|
|
3719
3828
|
ctx.textAlign = "center";
|
|
3720
3829
|
ctx.textBaseline = "middle";
|
|
3721
|
-
|
|
3830
|
+
if (n2.subtitle.length === 0) {
|
|
3831
|
+
ctx.fillText(n2.name, textX, n2.y + n2.h / 2, textWidth);
|
|
3832
|
+
} else {
|
|
3833
|
+
ctx.fillText(n2.name, textX, n2.y + n2.h / 2 - 8, textWidth);
|
|
3834
|
+
ctx.save();
|
|
3835
|
+
ctx.globalAlpha *= 0.72;
|
|
3836
|
+
ctx.font = SUBTITLE_FONT;
|
|
3837
|
+
ctx.fillText(n2.subtitle, textX, n2.y + n2.h / 2 + 9, textWidth);
|
|
3838
|
+
ctx.restore();
|
|
3839
|
+
}
|
|
3722
3840
|
for (const p of n2.inPorts) this.drawPort(n2, p, options);
|
|
3723
3841
|
for (const p of n2.outPorts) this.drawPort(n2, p, options);
|
|
3724
3842
|
}
|
|
@@ -3835,6 +3953,45 @@ ${runtime.error}`;
|
|
|
3835
3953
|
ctx.fill();
|
|
3836
3954
|
ctx.restore();
|
|
3837
3955
|
}
|
|
3956
|
+
// A plate of the canvas colour under the text, edged in the wire's colour: the label reads the same
|
|
3957
|
+
// whatever the wire crosses, and still says which wire it belongs to.
|
|
3958
|
+
drawLinkLabel(pts, text, color, background) {
|
|
3959
|
+
const [x, y] = pathMidpoint(pts);
|
|
3960
|
+
const ctx = this.ctx;
|
|
3961
|
+
ctx.save();
|
|
3962
|
+
ctx.font = LINK_LABEL_FONT;
|
|
3963
|
+
const w = Math.ceil(ctx.measureText(text).width) + 12;
|
|
3964
|
+
const h = 16;
|
|
3965
|
+
roundRect(ctx, x - w / 2, y - h / 2, w, h, h / 2);
|
|
3966
|
+
ctx.fillStyle = background;
|
|
3967
|
+
ctx.fill();
|
|
3968
|
+
ctx.lineWidth = 1;
|
|
3969
|
+
ctx.strokeStyle = color;
|
|
3970
|
+
ctx.stroke();
|
|
3971
|
+
ctx.fillStyle = readableTextOn(background);
|
|
3972
|
+
ctx.textAlign = "center";
|
|
3973
|
+
ctx.textBaseline = "middle";
|
|
3974
|
+
ctx.fillText(text, x, y);
|
|
3975
|
+
ctx.restore();
|
|
3976
|
+
}
|
|
3977
|
+
// Scheme colours may name a custom property of the page, var(--token), which a canvas cannot
|
|
3978
|
+
// paint. They resolve against the host element; one that resolves to nothing and has no
|
|
3979
|
+
// fallback of its own gets the renderer's default for that slot.
|
|
3980
|
+
resolveColor(value, fallback) {
|
|
3981
|
+
if (!value.includes("var(")) return value;
|
|
3982
|
+
return resolveCssVariables(value, (name) => this.hostProperty(name)) ?? fallback;
|
|
3983
|
+
}
|
|
3984
|
+
hostProperty(name) {
|
|
3985
|
+
if (this.hostStyle === null) {
|
|
3986
|
+
if (typeof getComputedStyle !== "function") return "";
|
|
3987
|
+
try {
|
|
3988
|
+
this.hostStyle = getComputedStyle(this.host);
|
|
3989
|
+
} catch {
|
|
3990
|
+
return "";
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
return this.hostStyle.getPropertyValue(name);
|
|
3994
|
+
}
|
|
3838
3995
|
drawSelectedLinkEndpoints() {
|
|
3839
3996
|
if (this.selectedLink === null) return;
|
|
3840
3997
|
const ctx = this.ctx;
|
|
@@ -3950,9 +4107,9 @@ ${runtime.error}`;
|
|
|
3950
4107
|
ctx.stroke();
|
|
3951
4108
|
ctx.clip();
|
|
3952
4109
|
for (const n2 of this.nodes) {
|
|
3953
|
-
ctx.fillStyle = n2.color;
|
|
4110
|
+
ctx.fillStyle = this.resolveColor(n2.color, DEFAULT_NODE_COLOR);
|
|
3954
4111
|
ctx.fillRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3955
|
-
ctx.strokeStyle = n2.border;
|
|
4112
|
+
ctx.strokeStyle = this.resolveColor(n2.border, DEFAULT_NODE_BORDER);
|
|
3956
4113
|
ctx.lineWidth = 1;
|
|
3957
4114
|
ctx.strokeRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3958
4115
|
}
|
|
@@ -3993,6 +4150,23 @@ ${runtime.error}`;
|
|
|
3993
4150
|
ctx.arcTo(x, y, x + w, y, rr);
|
|
3994
4151
|
ctx.closePath();
|
|
3995
4152
|
}
|
|
4153
|
+
function pathMidpoint(pts) {
|
|
4154
|
+
let total = 0;
|
|
4155
|
+
for (let i = 1; i < pts.length; i += 1) total += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
4156
|
+
let remaining = total / 2;
|
|
4157
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
4158
|
+
const [x1, y1] = pts[i - 1];
|
|
4159
|
+
const [x2, y2] = pts[i];
|
|
4160
|
+
const length = Math.hypot(x2 - x1, y2 - y1);
|
|
4161
|
+
if (length > 0 && remaining <= length) {
|
|
4162
|
+
const t = remaining / length;
|
|
4163
|
+
return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];
|
|
4164
|
+
}
|
|
4165
|
+
remaining -= length;
|
|
4166
|
+
}
|
|
4167
|
+
const last = pts[pts.length - 1];
|
|
4168
|
+
return [last[0], last[1]];
|
|
4169
|
+
}
|
|
3996
4170
|
function route(a, b) {
|
|
3997
4171
|
const [ax, ay] = a;
|
|
3998
4172
|
const [bx, by] = b;
|