@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.
@@ -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
- // Collect label positions for overlap resolution
851
- const labelPositions: {
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