@stocksharp/diagram 1.3.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 +178 -31
- package/dist/esm/canvas-renderer.js.map +1 -1
- package/dist/esm/color.js +110 -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 +247 -26
- package/dist/ssdiagram-headless.js.map +2 -2
- package/dist/ssdiagram.js +352 -48
- package/dist/ssdiagram.js.map +3 -3
- package/dist/types/canvas-renderer.d.ts +26 -3
- package/dist/types/canvas-renderer.d.ts.map +1 -1
- package/dist/types/color.d.ts +9 -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 +182 -30
- package/src/color.ts +130 -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
|
@@ -418,6 +418,82 @@ var SSDiagramHeadless = (() => {
|
|
|
418
418
|
return DARK_OUTLINE;
|
|
419
419
|
return contrast(background, relativeLuminance(LIGHT_OUTLINE)) > contrast(background, relativeLuminance(DARK_OUTLINE)) ? LIGHT_OUTLINE : DARK_OUTLINE;
|
|
420
420
|
}
|
|
421
|
+
function toHsl(color) {
|
|
422
|
+
const hsl = color.trim().match(/^hsl\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*\)$/i);
|
|
423
|
+
if (hsl !== null)
|
|
424
|
+
return [+hsl[1], +hsl[2] / 100, +hsl[3] / 100];
|
|
425
|
+
const rgb = parseRgb(color);
|
|
426
|
+
if (rgb === null)
|
|
427
|
+
return null;
|
|
428
|
+
const [r, g, b] = rgb.map((channel) => channel / 255);
|
|
429
|
+
const max = Math.max(r, g, b);
|
|
430
|
+
const min = Math.min(r, g, b);
|
|
431
|
+
const lightness = (max + min) / 2;
|
|
432
|
+
if (max === min)
|
|
433
|
+
return [0, 0, lightness];
|
|
434
|
+
const span = max - min;
|
|
435
|
+
const saturation = lightness > 0.5 ? span / (2 - max - min) : span / (max + min);
|
|
436
|
+
const hue = max === r ? (g - b) / span + (g < b ? 6 : 0) : max === g ? (b - r) / span + 2 : (r - g) / span + 4;
|
|
437
|
+
return [hue * 60, saturation, lightness];
|
|
438
|
+
}
|
|
439
|
+
var DARK_CANVAS_FLOOR = 0.42;
|
|
440
|
+
var LIGHT_CANVAS_CEILING = 0.45;
|
|
441
|
+
function legibleOn(color, background) {
|
|
442
|
+
const hsl = toHsl(color);
|
|
443
|
+
if (hsl === null)
|
|
444
|
+
return color;
|
|
445
|
+
const canvas = relativeLuminance(background);
|
|
446
|
+
const [hue, saturation, lightness] = hsl;
|
|
447
|
+
const moved = canvas < 0 ? lightness : canvas > 0.18 ? Math.min(lightness, LIGHT_CANVAS_CEILING) : Math.max(lightness, DARK_CANVAS_FLOOR);
|
|
448
|
+
if (moved === lightness && /^hsl/i.test(color.trim()))
|
|
449
|
+
return color;
|
|
450
|
+
return `hsl(${Math.round(hue * 10) / 10}, ${Math.round(saturation * 1e3) / 10}%, ${Math.round(moved * 1e3) / 10}%)`;
|
|
451
|
+
}
|
|
452
|
+
function cappedLightness(color, maxLightness) {
|
|
453
|
+
const hsl = toHsl(color);
|
|
454
|
+
if (hsl === null)
|
|
455
|
+
return color;
|
|
456
|
+
const [hue, saturation, lightness] = hsl;
|
|
457
|
+
if (lightness <= maxLightness)
|
|
458
|
+
return color;
|
|
459
|
+
return `hsl(${Math.round(hue * 10) / 10}, ${Math.round(saturation * 1e3) / 10}%, ${Math.round(maxLightness * 1e3) / 10}%)`;
|
|
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
|
+
}
|
|
421
497
|
|
|
422
498
|
// src/core/json.ts
|
|
423
499
|
function setJsonKey(target, key, value) {
|
|
@@ -515,10 +591,12 @@ var SSDiagramHeadless = (() => {
|
|
|
515
591
|
function normalizeNode(value, path) {
|
|
516
592
|
const input = requireStructure(value, path);
|
|
517
593
|
const id = requireIdentifier(input.id, `${path}.id`);
|
|
594
|
+
const subtitle = requireOptionalText(input.subtitle, `${path}.subtitle`);
|
|
518
595
|
return {
|
|
519
596
|
id,
|
|
520
597
|
typeId: requireIdentifier(input.typeId ?? id, `${path}.typeId`),
|
|
521
598
|
name: requireString(input.name, `${path}.name`),
|
|
599
|
+
...subtitle === "" ? {} : { subtitle },
|
|
522
600
|
description: requireString(input.description ?? "", `${path}.description`),
|
|
523
601
|
groupName: requireString(input.groupName ?? "Common", `${path}.groupName`),
|
|
524
602
|
x: requireFiniteNumber(input.x ?? 0, `${path}.x`),
|
|
@@ -569,14 +647,19 @@ var SSDiagramHeadless = (() => {
|
|
|
569
647
|
}
|
|
570
648
|
function normalizeLink(value, path) {
|
|
571
649
|
const input = requireStructure(value, path);
|
|
650
|
+
const label = requireOptionalText(input.label, `${path}.label`);
|
|
572
651
|
return {
|
|
573
652
|
id: requireIdentifier(input.id, `${path}.id`),
|
|
574
653
|
from: normalizeEndpoint(input.from, `${path}.from`),
|
|
575
654
|
to: normalizeEndpoint(input.to, `${path}.to`),
|
|
576
655
|
style: requireLinkStyle(input.style, `${path}.style`),
|
|
656
|
+
...label === "" ? {} : { label },
|
|
577
657
|
metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`)
|
|
578
658
|
};
|
|
579
659
|
}
|
|
660
|
+
function requireOptionalText(value, path) {
|
|
661
|
+
return value === void 0 || value === null ? "" : requireString(value, path);
|
|
662
|
+
}
|
|
580
663
|
function requireLinkStyle(value, path) {
|
|
581
664
|
if (value === void 0 || value === null) return "solid";
|
|
582
665
|
if (value !== "solid" && value !== "dashed") {
|
|
@@ -616,6 +699,7 @@ var SSDiagramHeadless = (() => {
|
|
|
616
699
|
id: requireString(node.id, `${path}.id`),
|
|
617
700
|
typeId: requireString(node.typeId, `${path}.typeId`),
|
|
618
701
|
name: requireString(node.name, `${path}.name`),
|
|
702
|
+
subtitle: node.subtitle,
|
|
619
703
|
description: requireString(node.description, `${path}.description`),
|
|
620
704
|
groupName: requireString(node.groupName, `${path}.groupName`),
|
|
621
705
|
x: requireNumber(node.x, `${path}.x`),
|
|
@@ -671,6 +755,7 @@ var SSDiagramHeadless = (() => {
|
|
|
671
755
|
from: parseEndpoint(link.from, `${path}.from`),
|
|
672
756
|
to: parseEndpoint(link.to, `${path}.to`),
|
|
673
757
|
style: link.style === void 0 ? void 0 : requireLinkStyle(link.style, `${path}.style`),
|
|
758
|
+
label: link.label,
|
|
674
759
|
metadata: cloneJsonObject(link.metadata, `${path}.metadata`)
|
|
675
760
|
}, path);
|
|
676
761
|
}
|
|
@@ -1101,6 +1186,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1101
1186
|
this.id = id;
|
|
1102
1187
|
this.typeId = init.typeId ?? id;
|
|
1103
1188
|
this.name = init.name;
|
|
1189
|
+
this.subtitle = init.subtitle ?? "";
|
|
1104
1190
|
this.description = init.description ?? "";
|
|
1105
1191
|
this.groupName = init.groupName ?? "Common";
|
|
1106
1192
|
this.color = init.color ?? "#d7d7d7";
|
|
@@ -1126,6 +1212,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1126
1212
|
id: this.id,
|
|
1127
1213
|
typeId: this.typeId,
|
|
1128
1214
|
name: this.name,
|
|
1215
|
+
...this.subtitle === "" ? {} : { subtitle: this.subtitle },
|
|
1129
1216
|
description: this.description,
|
|
1130
1217
|
groupName: this.groupName,
|
|
1131
1218
|
color: this.color,
|
|
@@ -1147,12 +1234,13 @@ var SSDiagramHeadless = (() => {
|
|
|
1147
1234
|
}
|
|
1148
1235
|
};
|
|
1149
1236
|
var LinkModel = class {
|
|
1150
|
-
constructor(from, fromPort, to, toPort, id = "", metadata, style = "solid") {
|
|
1237
|
+
constructor(from, fromPort, to, toPort, id = "", metadata, style = "solid", label = "") {
|
|
1151
1238
|
this.from = from;
|
|
1152
1239
|
this.fromPort = fromPort;
|
|
1153
1240
|
this.to = to;
|
|
1154
1241
|
this.toPort = toPort;
|
|
1155
1242
|
this.style = style;
|
|
1243
|
+
this.label = label;
|
|
1156
1244
|
this.id = id;
|
|
1157
1245
|
this.metadata = copyJsonObject(metadata);
|
|
1158
1246
|
}
|
|
@@ -1167,6 +1255,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1167
1255
|
to: this.to,
|
|
1168
1256
|
toPort: this.toPort,
|
|
1169
1257
|
style: this.style,
|
|
1258
|
+
...this.label === "" ? {} : { label: this.label },
|
|
1170
1259
|
metadata: copyJsonObject(this.metadata)
|
|
1171
1260
|
};
|
|
1172
1261
|
}
|
|
@@ -1187,6 +1276,15 @@ var SSDiagramHeadless = (() => {
|
|
|
1187
1276
|
var INTRO_RISE = 70;
|
|
1188
1277
|
var ERROR_FLASH_MS = 1100;
|
|
1189
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;
|
|
1190
1288
|
var ERROR_BACKGROUND = "#7d2632";
|
|
1191
1289
|
var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
1192
1290
|
function hashHue(s) {
|
|
@@ -1195,9 +1293,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1195
1293
|
return (h % 360 + 360) % 360;
|
|
1196
1294
|
}
|
|
1197
1295
|
function clampLightness(color, maxL) {
|
|
1198
|
-
|
|
1199
|
-
if (m === null) return color;
|
|
1200
|
-
return `hsl(${m[1]}, ${m[2]}%, ${Math.min(+m[3], maxL * 100)}%)`;
|
|
1296
|
+
return cappedLightness(color, maxL);
|
|
1201
1297
|
}
|
|
1202
1298
|
function colorLuminance(color) {
|
|
1203
1299
|
if (color === void 0) return 0;
|
|
@@ -1286,10 +1382,14 @@ var SSDiagramHeadless = (() => {
|
|
|
1286
1382
|
this.ovGeo = null;
|
|
1287
1383
|
this.validator = null;
|
|
1288
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;
|
|
1289
1388
|
this.opts = opts;
|
|
1290
1389
|
if (opts.typeColors !== void 0) this.setTypeColorsInternal(opts.typeColors);
|
|
1291
1390
|
this.host = opts.host;
|
|
1292
1391
|
this.gridSnapEnabled = opts.gridSnap ?? false;
|
|
1392
|
+
this.pageScroll = opts.pageScroll ?? false;
|
|
1293
1393
|
this.gridSize = this.normalizeGridSize(opts.gridSize ?? DEFAULT_GRID_SIZE);
|
|
1294
1394
|
this.history = new DiagramCommandHistory(() => {
|
|
1295
1395
|
this.emit("undoStackChanged", { canUndo: this.canUndo(), canRedo: this.canRedo() });
|
|
@@ -1298,7 +1398,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1298
1398
|
this.canvas.style.display = "block";
|
|
1299
1399
|
this.canvas.tabIndex = 0;
|
|
1300
1400
|
this.canvas.style.outline = "none";
|
|
1301
|
-
this.
|
|
1401
|
+
this.applyTouchAction();
|
|
1302
1402
|
this.canvas.style.userSelect = "none";
|
|
1303
1403
|
this.canvas.style.webkitUserSelect = "none";
|
|
1304
1404
|
this.canvas.style.webkitTouchCallout = "none";
|
|
@@ -1352,6 +1452,19 @@ var SSDiagramHeadless = (() => {
|
|
|
1352
1452
|
this.gridSize = nextSize;
|
|
1353
1453
|
this.scheduleDraw();
|
|
1354
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
|
+
}
|
|
1355
1468
|
getGridSnap() {
|
|
1356
1469
|
return { enabled: this.gridSnapEnabled, size: this.gridSize };
|
|
1357
1470
|
}
|
|
@@ -1567,7 +1680,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1567
1680
|
this.emit("linkValidation", { fromNode: fn, from: fp, toNode: tn, to: tp, ...validation });
|
|
1568
1681
|
if (!validation.allowed) return false;
|
|
1569
1682
|
}
|
|
1570
|
-
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 ?? "");
|
|
1571
1684
|
if (this.links.some((l) => l.id === link.id)) return false;
|
|
1572
1685
|
this.links.splice(clamp(Math.trunc(index), 0, this.links.length), 0, link);
|
|
1573
1686
|
this.emit("linkAdded", { link });
|
|
@@ -1839,6 +1952,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1839
1952
|
updateNode(nodeId, patch) {
|
|
1840
1953
|
return this.updateNodeState(nodeId, "update node", (node) => {
|
|
1841
1954
|
if (patch.name !== void 0) node.name = patch.name;
|
|
1955
|
+
if (patch.subtitle !== void 0) node.subtitle = patch.subtitle;
|
|
1842
1956
|
if (patch.description !== void 0) node.description = patch.description;
|
|
1843
1957
|
if (patch.color !== void 0) node.color = patch.color;
|
|
1844
1958
|
if (patch.border !== void 0) node.border = patch.border;
|
|
@@ -1884,6 +1998,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1884
1998
|
const restored = new NodeModel(snapshot, nodeId);
|
|
1885
1999
|
node.typeId = restored.typeId;
|
|
1886
2000
|
node.name = restored.name;
|
|
2001
|
+
node.subtitle = restored.subtitle;
|
|
1887
2002
|
node.description = restored.description;
|
|
1888
2003
|
node.groupName = restored.groupName;
|
|
1889
2004
|
node.color = restored.color;
|
|
@@ -1914,6 +2029,10 @@ var SSDiagramHeadless = (() => {
|
|
|
1914
2029
|
return true;
|
|
1915
2030
|
}
|
|
1916
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) {
|
|
1917
2036
|
this.clear();
|
|
1918
2037
|
this.idSeq = 1;
|
|
1919
2038
|
this.linkSeq = 1;
|
|
@@ -1940,9 +2059,10 @@ var SSDiagramHeadless = (() => {
|
|
|
1940
2059
|
if (this.links.some((existing) => existing.id === id)) {
|
|
1941
2060
|
throw new Error(`ssdiagram: duplicate link id "${id}"`);
|
|
1942
2061
|
}
|
|
1943
|
-
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 ?? "");
|
|
1944
2063
|
if (!this.links.some((existing) => existing.key() === model.key())) this.links.push(model);
|
|
1945
2064
|
}
|
|
2065
|
+
this.zones = zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
|
|
1946
2066
|
for (const node of this.nodes) {
|
|
1947
2067
|
if (node.loadError.length === 0) continue;
|
|
1948
2068
|
const state = createDiagramNodeRuntimeState();
|
|
@@ -1965,13 +2085,14 @@ var SSDiagramHeadless = (() => {
|
|
|
1965
2085
|
to: l.to,
|
|
1966
2086
|
toPort: l.toPort,
|
|
1967
2087
|
style: l.style,
|
|
2088
|
+
...l.label === "" ? {} : { label: l.label },
|
|
1968
2089
|
metadata: copyJsonObject(l.metadata)
|
|
1969
2090
|
}))
|
|
1970
2091
|
};
|
|
1971
2092
|
}
|
|
1972
2093
|
loadDocument(source) {
|
|
1973
2094
|
const document2 = parseDiagramDocument(source);
|
|
1974
|
-
this.
|
|
2095
|
+
this.loadModel(
|
|
1975
2096
|
document2.nodes,
|
|
1976
2097
|
document2.links.map((link) => ({
|
|
1977
2098
|
id: link.id,
|
|
@@ -1980,10 +2101,11 @@ var SSDiagramHeadless = (() => {
|
|
|
1980
2101
|
to: link.to.nodeId,
|
|
1981
2102
|
toPort: link.to.portId,
|
|
1982
2103
|
style: link.style,
|
|
2104
|
+
label: link.label,
|
|
1983
2105
|
metadata: link.metadata
|
|
1984
|
-
}))
|
|
2106
|
+
})),
|
|
2107
|
+
document2.zones
|
|
1985
2108
|
);
|
|
1986
|
-
this.zones = document2.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) }));
|
|
1987
2109
|
this.documentMetadata = copyJsonObject(document2.metadata);
|
|
1988
2110
|
}
|
|
1989
2111
|
saveDocument() {
|
|
@@ -1994,6 +2116,7 @@ var SSDiagramHeadless = (() => {
|
|
|
1994
2116
|
from: { nodeId: link.from, portId: link.fromPort },
|
|
1995
2117
|
to: { nodeId: link.to, portId: link.toPort },
|
|
1996
2118
|
style: link.style,
|
|
2119
|
+
label: link.label,
|
|
1997
2120
|
metadata: link.metadata
|
|
1998
2121
|
})),
|
|
1999
2122
|
zones: this.zones.map((zone) => ({ ...zone, metadata: copyJsonObject(zone.metadata) })),
|
|
@@ -2420,8 +2543,10 @@ var SSDiagramHeadless = (() => {
|
|
|
2420
2543
|
if (t.overviewViewportFill !== void 0) this.opts.overviewViewportFill = t.overviewViewportFill;
|
|
2421
2544
|
this.scheduleDraw();
|
|
2422
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.
|
|
2423
2548
|
graphBounds() {
|
|
2424
|
-
if (this.nodes.length === 0) return null;
|
|
2549
|
+
if (this.nodes.length === 0 && this.zones.length === 0) return null;
|
|
2425
2550
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2426
2551
|
for (const n2 of this.nodes) {
|
|
2427
2552
|
minX = Math.min(minX, n2.x);
|
|
@@ -2429,6 +2554,13 @@ var SSDiagramHeadless = (() => {
|
|
|
2429
2554
|
maxX = Math.max(maxX, n2.x + n2.w);
|
|
2430
2555
|
maxY = Math.max(maxY, n2.y + n2.h);
|
|
2431
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
|
+
}
|
|
2432
2564
|
return { minX, minY, maxX, maxY };
|
|
2433
2565
|
}
|
|
2434
2566
|
resize(w, h) {
|
|
@@ -2480,11 +2612,16 @@ var SSDiagramHeadless = (() => {
|
|
|
2480
2612
|
// ---- geometry ---------------------------------------------------
|
|
2481
2613
|
layoutNode(n2) {
|
|
2482
2614
|
const ctx = this.ctx;
|
|
2483
|
-
ctx.font =
|
|
2615
|
+
ctx.font = TITLE_FONT;
|
|
2484
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
|
+
}
|
|
2485
2622
|
const rows = Math.max(n2.inPorts.length, n2.outPorts.length, 1);
|
|
2486
|
-
n2.w = Math.max(170, Math.ceil(titleW) + 28);
|
|
2487
|
-
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);
|
|
2488
2625
|
const off = PORT_SQ / 2;
|
|
2489
2626
|
const place = (ports, x) => {
|
|
2490
2627
|
const stack = ports.some(isGrowAnchor) ? [...ports.filter((p) => !isGrowAnchor(p)), ...ports.filter(isGrowAnchor)] : ports;
|
|
@@ -2526,6 +2663,15 @@ var SSDiagramHeadless = (() => {
|
|
|
2526
2663
|
if (p.direction !== "in" || p.type === "" || !isWildcardPortType(p.type)) return p.type;
|
|
2527
2664
|
return this.portFeed.get(p) ?? p.type;
|
|
2528
2665
|
}
|
|
2666
|
+
/// A type's colour as a wire.
|
|
2667
|
+
///
|
|
2668
|
+
/// The same colour a socket of that type is filled with, moved into the band the canvas
|
|
2669
|
+
/// leaves room for. A socket can afford the host's exact colour because it is a filled
|
|
2670
|
+
/// square with an outline; a wire is a thin line with nothing behind it, and one drawn in
|
|
2671
|
+
/// #000000 on a dark canvas is not there at all.
|
|
2672
|
+
linkColor(type) {
|
|
2673
|
+
return legibleOn(this.portColor(type), this.opts.background ?? "#1b1b1f");
|
|
2674
|
+
}
|
|
2529
2675
|
portColor(type) {
|
|
2530
2676
|
const maxL = this.opts.linkMaxLightness;
|
|
2531
2677
|
const light = maxL !== void 0 && maxL < 0.5;
|
|
@@ -2834,6 +2980,7 @@ var SSDiagramHeadless = (() => {
|
|
|
2834
2980
|
// The clipboard is built from saveDocument(), so it carries the style;
|
|
2835
2981
|
// rebuilding without it silently turned every pasted dashed link solid.
|
|
2836
2982
|
style: l.style,
|
|
2983
|
+
label: l.label,
|
|
2837
2984
|
metadata: l.metadata
|
|
2838
2985
|
});
|
|
2839
2986
|
}
|
|
@@ -3219,6 +3366,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3219
3366
|
this.listen(window, "pointerup", finish);
|
|
3220
3367
|
this.listen(window, "pointercancel", finish);
|
|
3221
3368
|
this.listen(this.canvas, "wheel", (e) => {
|
|
3369
|
+
if (this.pageScroll && !e.ctrlKey && !e.metaKey) return;
|
|
3222
3370
|
e.preventDefault();
|
|
3223
3371
|
const [sx, sy] = localXY(e);
|
|
3224
3372
|
const [wx, wy] = this.toWorld(sx, sy);
|
|
@@ -3364,6 +3512,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3364
3512
|
}
|
|
3365
3513
|
draw(options = SCREEN_RENDER_OPTIONS) {
|
|
3366
3514
|
const ctx = this.ctx;
|
|
3515
|
+
this.hostStyle = null;
|
|
3367
3516
|
ctx.clearRect(0, 0, this.width, this.height);
|
|
3368
3517
|
const background = this.opts.background ?? "#1b1b1f";
|
|
3369
3518
|
ctx.fillStyle = background;
|
|
@@ -3391,19 +3540,21 @@ var SSDiagramHeadless = (() => {
|
|
|
3391
3540
|
);
|
|
3392
3541
|
this.drawZones();
|
|
3393
3542
|
const prior = [];
|
|
3543
|
+
const labels = [];
|
|
3394
3544
|
const hoveredNode = this.hoverPort?.node ?? this.hoverNode;
|
|
3395
3545
|
for (const l of this.links) {
|
|
3396
3546
|
const a = this.endpoint(l, "from");
|
|
3397
3547
|
const b = this.endpoint(l, "to");
|
|
3398
3548
|
if (a === null || b === null) continue;
|
|
3399
3549
|
const fp = this.nodes.find((n2) => n2.id === l.from)?.outPorts.find((p) => p.id === l.fromPort);
|
|
3400
|
-
const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.
|
|
3550
|
+
const baseColor = (this.opts.typedLinkColors ?? false) && fp !== void 0 ? this.linkColor(fp.type) : this.portColor("");
|
|
3401
3551
|
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";
|
|
3402
3552
|
const color = state === "sel" ? this.selectionColor() : state === "hov" ? this.linkHoverColor() : baseColor;
|
|
3403
3553
|
const width = state === "sel" ? 3 : state === "hov" ? 2.6 : 2;
|
|
3404
3554
|
const pts = this.routeLink(a, b, /* @__PURE__ */ new Set([l.from, l.to]));
|
|
3405
3555
|
this.strokeRoute(pts, color, width, prior, l.style);
|
|
3406
3556
|
this.drawArrow(pts, color);
|
|
3557
|
+
if (l.label.length > 0) labels.push({ pts, text: l.label, color });
|
|
3407
3558
|
for (let k = 1; k < pts.length; k += 1) {
|
|
3408
3559
|
const [x1, y1] = pts[k - 1];
|
|
3409
3560
|
const [x2, y2] = pts[k];
|
|
@@ -3411,6 +3562,7 @@ var SSDiagramHeadless = (() => {
|
|
|
3411
3562
|
else if (y1 === y2 && x1 !== x2) prior.push({ h: true, c: y1, a: Math.min(x1, x2), b: Math.max(x1, x2) });
|
|
3412
3563
|
}
|
|
3413
3564
|
}
|
|
3565
|
+
for (const label of labels) this.drawLinkLabel(label.pts, label.text, label.color, background);
|
|
3414
3566
|
if (options.transient && (this.linking !== null || this.relinking !== null)) this.drawPendingLink();
|
|
3415
3567
|
this.portFeed = this.feedTypes();
|
|
3416
3568
|
for (const n2 of this.nodes) this.drawNode(n2, options.selection && this.selectedNodes.has(n2), options);
|
|
@@ -3565,8 +3717,9 @@ ${runtime.error}`;
|
|
|
3565
3717
|
drawZones() {
|
|
3566
3718
|
if (this.zones.length === 0) return;
|
|
3567
3719
|
const ctx = this.ctx;
|
|
3720
|
+
const defaultTint = this.resolveColor(this.opts.zoneColor ?? DEFAULT_ZONE_TINT, DEFAULT_ZONE_TINT);
|
|
3568
3721
|
for (const zone of this.zones) {
|
|
3569
|
-
const tint = zone.color.length > 0 ? zone.color :
|
|
3722
|
+
const tint = zone.color.length > 0 ? this.resolveColor(zone.color, defaultTint) : defaultTint;
|
|
3570
3723
|
ctx.save();
|
|
3571
3724
|
roundRect(ctx, zone.x, zone.y, zone.width, zone.height, 10);
|
|
3572
3725
|
ctx.globalAlpha = 0.12;
|
|
@@ -3582,10 +3735,10 @@ ${runtime.error}`;
|
|
|
3582
3735
|
ctx.save();
|
|
3583
3736
|
ctx.globalAlpha = 0.75;
|
|
3584
3737
|
ctx.fillStyle = tint;
|
|
3585
|
-
ctx.font =
|
|
3738
|
+
ctx.font = TITLE_FONT;
|
|
3586
3739
|
ctx.textAlign = "left";
|
|
3587
3740
|
ctx.textBaseline = "top";
|
|
3588
|
-
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));
|
|
3589
3742
|
ctx.restore();
|
|
3590
3743
|
}
|
|
3591
3744
|
}
|
|
@@ -3630,11 +3783,12 @@ ${runtime.error}`;
|
|
|
3630
3783
|
const hasRuntimeError = options.runtime && n2.runtimeError.length > 0;
|
|
3631
3784
|
const runtime = options.runtime ? getJsonKey(this.runtimeState.nodes, n2.id) : void 0;
|
|
3632
3785
|
const active = options.runtime && (this.runtimeState.activeNodeId === n2.id || runtime?.active === true);
|
|
3786
|
+
const fill = this.resolveColor(n2.color, DEFAULT_NODE_COLOR);
|
|
3633
3787
|
roundRect(ctx, n2.x, n2.y, n2.w, n2.h, 6);
|
|
3634
|
-
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" :
|
|
3788
|
+
ctx.fillStyle = hasLoadError ? ERROR_BACKGROUND : active ? "#ffd1dc" : fill;
|
|
3635
3789
|
ctx.fill();
|
|
3636
3790
|
ctx.lineWidth = selected ? 2 : 1.5;
|
|
3637
|
-
ctx.strokeStyle = selected ? this.selectionColor() : n2.border;
|
|
3791
|
+
ctx.strokeStyle = selected ? this.selectionColor() : this.resolveColor(n2.border, DEFAULT_NODE_BORDER);
|
|
3638
3792
|
ctx.stroke();
|
|
3639
3793
|
if (hasLoadError || hasRuntimeError) {
|
|
3640
3794
|
let alpha = 1;
|
|
@@ -3667,11 +3821,22 @@ ${runtime.error}`;
|
|
|
3667
3821
|
}
|
|
3668
3822
|
}
|
|
3669
3823
|
const titleShift = n2.icon ? iconW + 4 : 0;
|
|
3670
|
-
|
|
3671
|
-
|
|
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;
|
|
3672
3828
|
ctx.textAlign = "center";
|
|
3673
3829
|
ctx.textBaseline = "middle";
|
|
3674
|
-
|
|
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
|
+
}
|
|
3675
3840
|
for (const p of n2.inPorts) this.drawPort(n2, p, options);
|
|
3676
3841
|
for (const p of n2.outPorts) this.drawPort(n2, p, options);
|
|
3677
3842
|
}
|
|
@@ -3788,6 +3953,45 @@ ${runtime.error}`;
|
|
|
3788
3953
|
ctx.fill();
|
|
3789
3954
|
ctx.restore();
|
|
3790
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
|
+
}
|
|
3791
3995
|
drawSelectedLinkEndpoints() {
|
|
3792
3996
|
if (this.selectedLink === null) return;
|
|
3793
3997
|
const ctx = this.ctx;
|
|
@@ -3845,7 +4049,7 @@ ${runtime.error}`;
|
|
|
3845
4049
|
}
|
|
3846
4050
|
if (snapped !== null) excludedNodes.add(snapped.node.id);
|
|
3847
4051
|
const points = this.routeLink(a, b, excludedNodes);
|
|
3848
|
-
ctx.strokeStyle = this.
|
|
4052
|
+
ctx.strokeStyle = this.linkColor(colorType);
|
|
3849
4053
|
if (snapped !== null) {
|
|
3850
4054
|
ctx.setLineDash([]);
|
|
3851
4055
|
ctx.lineWidth = 2.6;
|
|
@@ -3903,9 +4107,9 @@ ${runtime.error}`;
|
|
|
3903
4107
|
ctx.stroke();
|
|
3904
4108
|
ctx.clip();
|
|
3905
4109
|
for (const n2 of this.nodes) {
|
|
3906
|
-
ctx.fillStyle = n2.color;
|
|
4110
|
+
ctx.fillStyle = this.resolveColor(n2.color, DEFAULT_NODE_COLOR);
|
|
3907
4111
|
ctx.fillRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3908
|
-
ctx.strokeStyle = n2.border;
|
|
4112
|
+
ctx.strokeStyle = this.resolveColor(n2.border, DEFAULT_NODE_BORDER);
|
|
3909
4113
|
ctx.lineWidth = 1;
|
|
3910
4114
|
ctx.strokeRect(mx(n2.x), my(n2.y), Math.max(2, n2.w * s), Math.max(2, n2.h * s));
|
|
3911
4115
|
}
|
|
@@ -3946,6 +4150,23 @@ ${runtime.error}`;
|
|
|
3946
4150
|
ctx.arcTo(x, y, x + w, y, rr);
|
|
3947
4151
|
ctx.closePath();
|
|
3948
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
|
+
}
|
|
3949
4170
|
function route(a, b) {
|
|
3950
4171
|
const [ax, ay] = a;
|
|
3951
4172
|
const [bx, by] = b;
|