@diagrammo/dgmo 0.33.0 → 0.34.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/dist/advanced.cjs +329 -149
- package/dist/advanced.d.cts +6 -0
- package/dist/advanced.d.ts +6 -0
- package/dist/advanced.js +329 -149
- package/dist/auto.cjs +331 -151
- package/dist/auto.js +93 -93
- package/dist/auto.mjs +331 -151
- package/dist/cli.cjs +127 -127
- package/dist/index.cjs +330 -150
- package/dist/index.js +330 -150
- package/package.json +2 -2
- package/src/boxes-and-lines/label-placement.ts +339 -0
- package/src/boxes-and-lines/layout-search.ts +16 -2
- package/src/boxes-and-lines/layout.ts +32 -7
- package/src/boxes-and-lines/renderer.ts +67 -139
|
@@ -78,7 +78,6 @@ const GROUP_LABEL_ZONE = 32;
|
|
|
78
78
|
const RAMP_FLOOR = 15;
|
|
79
79
|
const VALUE_FONT_SIZE = 11;
|
|
80
80
|
|
|
81
|
-
type D3G = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
82
81
|
type D3Svg = d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
|
|
83
82
|
|
|
84
83
|
// ── Edge path generators ───────────────────────────────────
|
|
@@ -352,78 +351,6 @@ function ensureArrowMarkers(
|
|
|
352
351
|
}
|
|
353
352
|
}
|
|
354
353
|
|
|
355
|
-
// ── Edge label placement ───────────────────────────────────
|
|
356
|
-
|
|
357
|
-
/** Point at the half-way arc length along an edge polyline — the geometric
|
|
358
|
-
* centre of the connector, so a label sits in the gap BETWEEN the two nodes
|
|
359
|
-
* (ELK's own label anchor drifts toward the target and ends up clipped under
|
|
360
|
-
* it). Falls back gracefully for degenerate point lists. */
|
|
361
|
-
function edgePolylineMidpoint(
|
|
362
|
-
points: ReadonlyArray<{ readonly x: number; readonly y: number }>
|
|
363
|
-
): { x: number; y: number } {
|
|
364
|
-
if (points.length === 0) return { x: 0, y: 0 };
|
|
365
|
-
if (points.length === 1) return { x: points[0]!.x, y: points[0]!.y };
|
|
366
|
-
let total = 0;
|
|
367
|
-
const segLen: number[] = [];
|
|
368
|
-
for (let k = 1; k < points.length; k++) {
|
|
369
|
-
const len = Math.hypot(
|
|
370
|
-
points[k]!.x - points[k - 1]!.x,
|
|
371
|
-
points[k]!.y - points[k - 1]!.y
|
|
372
|
-
);
|
|
373
|
-
segLen.push(len);
|
|
374
|
-
total += len;
|
|
375
|
-
}
|
|
376
|
-
let half = total / 2;
|
|
377
|
-
for (let k = 1; k < points.length; k++) {
|
|
378
|
-
const len = segLen[k - 1]!;
|
|
379
|
-
if (half <= len || k === points.length - 1) {
|
|
380
|
-
const t = len === 0 ? 0 : Math.min(1, half / len);
|
|
381
|
-
return {
|
|
382
|
-
x: points[k - 1]!.x + (points[k]!.x - points[k - 1]!.x) * t,
|
|
383
|
-
y: points[k - 1]!.y + (points[k]!.y - points[k - 1]!.y) * t,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
half -= len;
|
|
387
|
-
}
|
|
388
|
-
const last = points[points.length - 1]!;
|
|
389
|
-
return { x: last.x, y: last.y };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ── Edge label overlap resolution ──────────────────────────
|
|
393
|
-
|
|
394
|
-
function resolveEdgeLabelOverlaps(
|
|
395
|
-
labels: { x: number; y: number; width: number; height: number }[]
|
|
396
|
-
): void {
|
|
397
|
-
const MAX_PASSES = 8;
|
|
398
|
-
const PAD = 4;
|
|
399
|
-
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
400
|
-
let moved = false;
|
|
401
|
-
for (let i = 0; i < labels.length; i++) {
|
|
402
|
-
for (let j = i + 1; j < labels.length; j++) {
|
|
403
|
-
// In-bounds by loop guard.
|
|
404
|
-
const a = labels[i]!;
|
|
405
|
-
const b = labels[j]!;
|
|
406
|
-
const dx = Math.abs(a.x - b.x);
|
|
407
|
-
const dy = Math.abs(a.y - b.y);
|
|
408
|
-
const overlapX = (a.width + b.width) / 2 + PAD - dx;
|
|
409
|
-
const overlapY = (a.height + b.height) / 2 + PAD - dy;
|
|
410
|
-
if (overlapX > 0 && overlapY > 0) {
|
|
411
|
-
const shift = overlapY / 2 + 1;
|
|
412
|
-
if (a.y < b.y) {
|
|
413
|
-
a.y -= shift;
|
|
414
|
-
b.y += shift;
|
|
415
|
-
} else {
|
|
416
|
-
a.y += shift;
|
|
417
|
-
b.y -= shift;
|
|
418
|
-
}
|
|
419
|
-
moved = true;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (!moved) break;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
354
|
// ── Main render function ───────────────────────────────────
|
|
428
355
|
|
|
429
356
|
interface BLRenderOptions {
|
|
@@ -847,18 +774,8 @@ export function renderBoxesAndLines(
|
|
|
847
774
|
}
|
|
848
775
|
|
|
849
776
|
// ── Render edges ───────────────────────────────────────
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
x: number;
|
|
853
|
-
y: number;
|
|
854
|
-
width: number;
|
|
855
|
-
height: number;
|
|
856
|
-
idx: number;
|
|
857
|
-
}[] = [];
|
|
858
|
-
|
|
859
|
-
// Store edge group elements for label pass
|
|
860
|
-
const edgeGroups = new Map<number, D3G>();
|
|
861
|
-
|
|
777
|
+
// Edge labels are placed in layout (label-placement.ts) and drawn AFTER the
|
|
778
|
+
// node loop (see the bl-edge-labels layer) so boxes never clip them.
|
|
862
779
|
for (let i = 0; i < layout.edges.length; i++) {
|
|
863
780
|
// In-bounds by loop guard.
|
|
864
781
|
const le = layout.edges[i]!;
|
|
@@ -885,7 +802,6 @@ export function renderBoxesAndLines(
|
|
|
885
802
|
.append('g')
|
|
886
803
|
.attr('class', 'bl-edge-group')
|
|
887
804
|
.attr('data-line-number', String(le.lineNumber));
|
|
888
|
-
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
889
805
|
|
|
890
806
|
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
891
807
|
const cx = nodeLayout.x;
|
|
@@ -950,7 +866,6 @@ export function renderBoxesAndLines(
|
|
|
950
866
|
.append('g')
|
|
951
867
|
.attr('class', 'bl-edge-group')
|
|
952
868
|
.attr('data-line-number', String(le.lineNumber));
|
|
953
|
-
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
954
869
|
|
|
955
870
|
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
956
871
|
const gen = le.straight
|
|
@@ -971,58 +886,6 @@ export function renderBoxesAndLines(
|
|
|
971
886
|
const revId = `bl-arrow-rev-${color.replace('#', '')}`;
|
|
972
887
|
path.attr('marker-start', `url(#${revId})`);
|
|
973
888
|
}
|
|
974
|
-
|
|
975
|
-
// Edge label — centred on the connector's polyline midpoint (the gap
|
|
976
|
-
// between the two nodes), NOT ELK's target-biased anchor. For parallel
|
|
977
|
-
// edges, nudge above/below so each line's label clears the line.
|
|
978
|
-
if (le.label && le.points.length > 0) {
|
|
979
|
-
const lw = measureText(le.label, sEdgeLabelFontSize);
|
|
980
|
-
const labelH = sEdgeLabelFontSize + 6;
|
|
981
|
-
const mid = edgePolylineMidpoint(le.points);
|
|
982
|
-
let ly = mid.y;
|
|
983
|
-
if (le.parallelCount > 1 && le.yOffset !== 0) {
|
|
984
|
-
ly += (le.yOffset < 0 ? -labelH : labelH) * 0.5;
|
|
985
|
-
}
|
|
986
|
-
labelPositions.push({
|
|
987
|
-
x: mid.x,
|
|
988
|
-
y: ly,
|
|
989
|
-
width: lw + 8,
|
|
990
|
-
height: labelH,
|
|
991
|
-
idx: i,
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Resolve overlaps
|
|
997
|
-
resolveEdgeLabelOverlaps(labelPositions);
|
|
998
|
-
|
|
999
|
-
// Render edge labels into their edge groups
|
|
1000
|
-
for (const lp of labelPositions) {
|
|
1001
|
-
// In-bounds: lp.idx was set from a valid index into layout.edges above.
|
|
1002
|
-
const le = layout.edges[lp.idx]!;
|
|
1003
|
-
if (!le.label) continue;
|
|
1004
|
-
|
|
1005
|
-
const edgeG = edgeGroups.get(lp.idx);
|
|
1006
|
-
const target = edgeG ?? diagramG;
|
|
1007
|
-
|
|
1008
|
-
target
|
|
1009
|
-
.append('rect')
|
|
1010
|
-
.attr('x', lp.x - lp.width / 2)
|
|
1011
|
-
.attr('y', lp.y - lp.height / 2)
|
|
1012
|
-
.attr('width', lp.width)
|
|
1013
|
-
.attr('height', lp.height)
|
|
1014
|
-
.attr('rx', 3)
|
|
1015
|
-
.attr('fill', palette.bg)
|
|
1016
|
-
.attr('opacity', 0.85);
|
|
1017
|
-
|
|
1018
|
-
target
|
|
1019
|
-
.append('text')
|
|
1020
|
-
.attr('x', lp.x)
|
|
1021
|
-
.attr('y', lp.y + sEdgeLabelFontSize / 3)
|
|
1022
|
-
.attr('text-anchor', 'middle')
|
|
1023
|
-
.attr('font-size', sEdgeLabelFontSize)
|
|
1024
|
-
.attr('fill', palette.textMuted)
|
|
1025
|
-
.text(le.label);
|
|
1026
889
|
}
|
|
1027
890
|
|
|
1028
891
|
// ── Render nodes ───────────────────────────────────────
|
|
@@ -1396,6 +1259,71 @@ export function renderBoxesAndLines(
|
|
|
1396
1259
|
}
|
|
1397
1260
|
}
|
|
1398
1261
|
|
|
1262
|
+
// ── Render edge labels ─────────────────────────────────
|
|
1263
|
+
// Drawn AFTER nodes (and groups) so the halo + text always paint on top and
|
|
1264
|
+
// are never clipped by a box. Positions/wrapping come from label-placement
|
|
1265
|
+
// (layout); this is a pure consumer. Labels stay in their own layer rather
|
|
1266
|
+
// than inside each edge group, but carry data-line-number so line-hover/click
|
|
1267
|
+
// still highlights the matching label.
|
|
1268
|
+
const labelLayer = diagramG.append('g').attr('class', 'bl-edge-labels');
|
|
1269
|
+
const labelLineHeight = sEdgeLabelFontSize * 1.3;
|
|
1270
|
+
for (const le of layout.edges) {
|
|
1271
|
+
if (!le.labelLines || le.labelLines.length === 0) continue;
|
|
1272
|
+
if (le.labelX === undefined || le.labelY === undefined) continue;
|
|
1273
|
+
const lx = le.labelX;
|
|
1274
|
+
const ly = le.labelY;
|
|
1275
|
+
|
|
1276
|
+
// Honour the same tag-value hiding as the edges themselves.
|
|
1277
|
+
if (hidden.size > 0) {
|
|
1278
|
+
let isHidden = false;
|
|
1279
|
+
for (const [groupKey, hiddenVals] of hidden) {
|
|
1280
|
+
const val = le.metadata[groupKey];
|
|
1281
|
+
if (val && hiddenVals.has(val.toLowerCase())) {
|
|
1282
|
+
isHidden = true;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (isHidden) continue;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const lw = le.labelWidth ?? 0;
|
|
1290
|
+
const lh = le.labelHeight ?? sEdgeLabelFontSize + 6;
|
|
1291
|
+
const lines = le.labelLines;
|
|
1292
|
+
const labelG = labelLayer
|
|
1293
|
+
.append('g')
|
|
1294
|
+
.attr('class', 'bl-edge-label')
|
|
1295
|
+
.attr('data-line-number', String(le.lineNumber));
|
|
1296
|
+
|
|
1297
|
+
labelG
|
|
1298
|
+
.append('rect')
|
|
1299
|
+
.attr('x', lx - lw / 2)
|
|
1300
|
+
.attr('y', ly - lh / 2)
|
|
1301
|
+
.attr('width', lw)
|
|
1302
|
+
.attr('height', lh)
|
|
1303
|
+
.attr('rx', 3)
|
|
1304
|
+
.attr('fill', palette.bg)
|
|
1305
|
+
// No border; semi-transparent so the line stays faintly visible behind the
|
|
1306
|
+
// label while the (full-opacity) text on top stays crisp.
|
|
1307
|
+
.attr('opacity', 0.72);
|
|
1308
|
+
|
|
1309
|
+
const text = labelG
|
|
1310
|
+
.append('text')
|
|
1311
|
+
.attr('x', lx)
|
|
1312
|
+
.attr('text-anchor', 'middle')
|
|
1313
|
+
.attr('font-family', FONT_FAMILY)
|
|
1314
|
+
.attr('font-size', sEdgeLabelFontSize)
|
|
1315
|
+
.attr('fill', palette.textMuted);
|
|
1316
|
+
const firstY =
|
|
1317
|
+
ly - ((lines.length - 1) / 2) * labelLineHeight + sEdgeLabelFontSize / 3;
|
|
1318
|
+
lines.forEach((line, k) => {
|
|
1319
|
+
text
|
|
1320
|
+
.append('tspan')
|
|
1321
|
+
.attr('x', lx)
|
|
1322
|
+
.attr('y', firstY + k * labelLineHeight)
|
|
1323
|
+
.text(line);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1399
1327
|
// ── Render legend ──────────────────────────────────────
|
|
1400
1328
|
const hasDescriptions = parsed.nodes.some(
|
|
1401
1329
|
(n) => n.description && n.description.length > 0
|