compasso 0.4.0 → 0.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.
Files changed (100) hide show
  1. package/README.md +115 -5
  2. package/dist/{chunk-ZBDABVIO.js → chunk-2NDET6O5.js} +3 -3
  3. package/dist/{chunk-ZBDABVIO.js.map → chunk-2NDET6O5.js.map} +1 -1
  4. package/dist/{chunk-Q6DVTCXD.js → chunk-3RGYLVTN.js} +18 -6
  5. package/dist/chunk-3RGYLVTN.js.map +1 -0
  6. package/dist/chunk-BM7UJBK5.js +680 -0
  7. package/dist/chunk-BM7UJBK5.js.map +1 -0
  8. package/dist/chunk-DVLWT565.js +372 -0
  9. package/dist/chunk-DVLWT565.js.map +1 -0
  10. package/dist/{chunk-F47C6ZEB.js → chunk-JBDA7E2O.js} +3 -3
  11. package/dist/{chunk-F47C6ZEB.js.map → chunk-JBDA7E2O.js.map} +1 -1
  12. package/dist/chunk-MIJTBYX2.js +982 -0
  13. package/dist/chunk-MIJTBYX2.js.map +1 -0
  14. package/dist/{chunk-JP4N42AY.js → chunk-PJHLWSGD.js} +3 -3
  15. package/dist/{chunk-JP4N42AY.js.map → chunk-PJHLWSGD.js.map} +1 -1
  16. package/dist/{chunk-LRHHUJFZ.js → chunk-RDH4XHA2.js} +3 -3
  17. package/dist/{chunk-LRHHUJFZ.js.map → chunk-RDH4XHA2.js.map} +1 -1
  18. package/dist/{chunk-UJVU7B44.js → chunk-WEHUSHVI.js} +31 -51
  19. package/dist/chunk-WEHUSHVI.js.map +1 -0
  20. package/dist/{chunk-RWPGGWO5.js → chunk-Z66YUOUM.js} +34 -10
  21. package/dist/chunk-Z66YUOUM.js.map +1 -0
  22. package/dist/core/index.cjs +247 -0
  23. package/dist/core/index.cjs.map +1 -1
  24. package/dist/core/index.d.cts +94 -2
  25. package/dist/core/index.d.ts +94 -2
  26. package/dist/core/index.js +1 -1
  27. package/dist/ecomap/index.cjs +34 -11
  28. package/dist/ecomap/index.cjs.map +1 -1
  29. package/dist/ecomap/index.d.cts +12 -0
  30. package/dist/ecomap/index.d.ts +12 -0
  31. package/dist/ecomap/index.js +2 -2
  32. package/dist/fault-tree/index.d.cts +2 -2
  33. package/dist/fault-tree/index.d.ts +2 -2
  34. package/dist/fault-tree/index.js +2 -2
  35. package/dist/fishbone/index.js +2 -2
  36. package/dist/genogram/index.cjs +57 -7
  37. package/dist/genogram/index.cjs.map +1 -1
  38. package/dist/genogram/index.d.cts +22 -6
  39. package/dist/genogram/index.d.ts +22 -6
  40. package/dist/genogram/index.js +2 -2
  41. package/dist/geometry-P-XGqGe7.d.cts +8 -0
  42. package/dist/geometry-P-XGqGe7.d.ts +8 -0
  43. package/dist/grid-BMgUSly1.d.cts +79 -0
  44. package/dist/grid-BMgUSly1.d.ts +79 -0
  45. package/dist/index.cjs +2360 -395
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +14 -7
  48. package/dist/index.d.ts +14 -7
  49. package/dist/index.js +10 -8
  50. package/dist/{kinship-DqEklrDN.d.ts → kinship-BF90HyyS.d.ts} +1 -1
  51. package/dist/{kinship-Dy_ijjJV.d.cts → kinship-BOUss5cT.d.cts} +1 -1
  52. package/dist/{labels-RtFw9tX1.d.cts → labels-B0aOMbHy.d.cts} +12 -0
  53. package/dist/{labels-RtFw9tX1.d.ts → labels-B0aOMbHy.d.ts} +12 -0
  54. package/dist/labels-Br8yjc3C.d.cts +29 -0
  55. package/dist/labels-Br8yjc3C.d.ts +29 -0
  56. package/dist/{labels-DNqRkWuI.d.ts → labels-CuLbFyrz.d.ts} +1 -1
  57. package/dist/labels-D1v1RWZd.d.cts +97 -0
  58. package/dist/labels-D1v1RWZd.d.ts +97 -0
  59. package/dist/{labels-CBQ_3Ec9.d.cts → labels-DhQe7I8m.d.cts} +1 -1
  60. package/dist/layered-DmZluAqe.d.cts +72 -0
  61. package/dist/layered-DmZluAqe.d.ts +72 -0
  62. package/dist/locales/pt-br.cjs +53 -0
  63. package/dist/locales/pt-br.cjs.map +1 -1
  64. package/dist/locales/pt-br.d.cts +11 -5
  65. package/dist/locales/pt-br.d.ts +11 -5
  66. package/dist/locales/pt-br.js +50 -1
  67. package/dist/locales/pt-br.js.map +1 -1
  68. package/dist/org-chart/index.cjs +138 -94
  69. package/dist/org-chart/index.cjs.map +1 -1
  70. package/dist/org-chart/index.d.cts +24 -33
  71. package/dist/org-chart/index.d.ts +24 -33
  72. package/dist/org-chart/index.js +2 -2
  73. package/dist/pedigree/index.d.cts +6 -6
  74. package/dist/pedigree/index.d.ts +6 -6
  75. package/dist/pedigree/index.js +2 -2
  76. package/dist/phylo/index.d.cts +2 -2
  77. package/dist/phylo/index.d.ts +2 -2
  78. package/dist/phylo/index.js +2 -2
  79. package/dist/prisma/index.cjs +882 -0
  80. package/dist/prisma/index.cjs.map +1 -0
  81. package/dist/prisma/index.d.cts +174 -0
  82. package/dist/prisma/index.d.ts +174 -0
  83. package/dist/prisma/index.js +4 -0
  84. package/dist/prisma/index.js.map +1 -0
  85. package/dist/{text-DuO_PwYw.d.cts → text-DDVzpwPZ.d.cts} +1 -8
  86. package/dist/{text-DuO_PwYw.d.ts → text-DDVzpwPZ.d.ts} +1 -8
  87. package/dist/{types-BnMG7TCd.d.cts → types-jE2fdM1t.d.cts} +8 -0
  88. package/dist/{types-BnMG7TCd.d.ts → types-jE2fdM1t.d.ts} +8 -0
  89. package/dist/uml/index.cjs +1214 -0
  90. package/dist/uml/index.cjs.map +1 -0
  91. package/dist/uml/index.d.cts +189 -0
  92. package/dist/uml/index.d.ts +189 -0
  93. package/dist/uml/index.js +4 -0
  94. package/dist/uml/index.js.map +1 -0
  95. package/package.json +28 -2
  96. package/dist/chunk-O3BT2O42.js +0 -145
  97. package/dist/chunk-O3BT2O42.js.map +0 -1
  98. package/dist/chunk-Q6DVTCXD.js.map +0 -1
  99. package/dist/chunk-RWPGGWO5.js.map +0 -1
  100. package/dist/chunk-UJVU7B44.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -142,6 +142,233 @@ function qualityLineStyle(quality, lexicon = QUALITY_LEXICON_EN) {
142
142
  return matched.length === 1 ? matched[0] : "plain";
143
143
  }
144
144
 
145
+ // src/core/annotation.ts
146
+ var round = (n) => Math.round(n * 100) / 100;
147
+ var ANNOTATION_INK = "#52525b";
148
+ var DOT_R = 3;
149
+ var TICK_HALF = 3;
150
+ function annotationDot(cx, cy) {
151
+ return `<circle cx="${round(cx)}" cy="${round(cy)}" r="${DOT_R}" fill="${ANNOTATION_INK}" fill-opacity="0.6"/>`;
152
+ }
153
+ function annotationTick(points) {
154
+ if (points.length < 2) return "";
155
+ const i = Math.floor((points.length - 1) / 2);
156
+ const a = points[i];
157
+ const b = points[i + 1] ?? points[i];
158
+ const mx = (a.x + b.x) / 2;
159
+ const my = (a.y + b.y) / 2;
160
+ const horizontal = Math.abs(a.y - b.y) <= Math.abs(a.x - b.x);
161
+ const x1 = horizontal ? mx : mx - TICK_HALF;
162
+ const x2 = horizontal ? mx : mx + TICK_HALF;
163
+ const y1 = horizontal ? my - TICK_HALF : my;
164
+ const y2 = horizontal ? my + TICK_HALF : my;
165
+ return `<line x1="${round(x1)}" y1="${round(y1)}" x2="${round(x2)}" y2="${round(y2)}" stroke="${ANNOTATION_INK}" stroke-width="1.5"/>`;
166
+ }
167
+ function annotationSwatch(x, yCenter) {
168
+ return annotationDot(x + LEGEND_SWATCH_W / 2, yCenter);
169
+ }
170
+
171
+ // src/core/layered.ts
172
+ function packSubtree(node, gaps) {
173
+ const { ownHalfL, ownHalfR, children } = node;
174
+ if (children.length === 0) {
175
+ return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
176
+ }
177
+ const xs = [0];
178
+ for (let i = 1; i < children.length; i++) {
179
+ xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
180
+ }
181
+ const first = children[0];
182
+ const last = children[children.length - 1];
183
+ const axis = (xs[0] + xs[xs.length - 1]) / 2;
184
+ const offsets = xs.map((x) => x - axis);
185
+ const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
186
+ const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
187
+ return { halfL, halfR, offsets };
188
+ }
189
+ function allocateLanes(items) {
190
+ const lanes = [];
191
+ for (const it of items) {
192
+ let chosen = -1;
193
+ for (let l = 0; l < lanes.length; l++) {
194
+ if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
195
+ chosen = l;
196
+ break;
197
+ }
198
+ }
199
+ if (chosen === -1) {
200
+ chosen = lanes.length;
201
+ lanes.push([]);
202
+ }
203
+ lanes[chosen].push({ lo: it.lo, hi: it.hi });
204
+ it.set(chosen);
205
+ }
206
+ return lanes.length;
207
+ }
208
+ function bandStack(rows, opts) {
209
+ const { padding, corridor } = opts;
210
+ if (rows.length === 0) {
211
+ return { rowTop: [], busY: () => 0, height: padding * 2 };
212
+ }
213
+ const rowTop = [padding];
214
+ for (let d = 0; d < rows.length - 1; d++) {
215
+ rowTop.push(rowTop[d] + rows[d].rowH + rows[d].zoneH + corridor);
216
+ }
217
+ const last = rows.length - 1;
218
+ const busY = (d) => rowTop[d + 1] - corridor / 2;
219
+ const height = rowTop[last] + rows[last].rowH + rows[last].zoneH + padding;
220
+ return { rowTop, busY, height };
221
+ }
222
+
223
+ // src/core/glyph.ts
224
+ function dirVec(dir) {
225
+ switch (dir) {
226
+ case "right":
227
+ return [1, 0];
228
+ case "left":
229
+ return [-1, 0];
230
+ case "down":
231
+ return [0, 1];
232
+ case "up":
233
+ return [0, -1];
234
+ }
235
+ }
236
+ function r(n) {
237
+ return Math.round(n * 100) / 100;
238
+ }
239
+ function pt(x, y) {
240
+ return `${r(x)},${r(y)}`;
241
+ }
242
+ var GLYPH_ARROW_LEN = 10;
243
+ var GLYPH_ARROW_HALF = 5;
244
+ var GLYPH_TRI_LEN = 14;
245
+ var GLYPH_TRI_HALF = 8;
246
+ var GLYPH_DIAMOND_LEN = 16;
247
+ var GLYPH_DIAMOND_HALF = 6;
248
+ function arrowOpenPoints(tip, dir, len = GLYPH_ARROW_LEN, half = GLYPH_ARROW_HALF) {
249
+ return _arrowPoints(tip, dir, len, half);
250
+ }
251
+ function arrowFilledPoints(tip, dir, len = GLYPH_ARROW_LEN, half = GLYPH_ARROW_HALF) {
252
+ return _arrowPoints(tip, dir, len, half);
253
+ }
254
+ function _arrowPoints(tip, dir, len, half) {
255
+ const [ux, uy] = dirVec(dir);
256
+ const tx = tip.x;
257
+ const ty = tip.y;
258
+ const bx = tx - ux * len;
259
+ const by = ty - uy * len;
260
+ const px = -uy;
261
+ const py = ux;
262
+ return [
263
+ pt(tx, ty),
264
+ pt(bx + px * half, by + py * half),
265
+ pt(bx - px * half, by - py * half)
266
+ ].join(" ");
267
+ }
268
+ function trianglePoints(tip, dir, len = GLYPH_TRI_LEN, half = GLYPH_TRI_HALF) {
269
+ return _arrowPoints(tip, dir, len, half);
270
+ }
271
+ function diamondPoints(tip, dir, len = GLYPH_DIAMOND_LEN, half = GLYPH_DIAMOND_HALF) {
272
+ const [ux, uy] = dirVec(dir);
273
+ const tx = tip.x;
274
+ const ty = tip.y;
275
+ const mx = tx + ux * (len / 2);
276
+ const my = ty + uy * (len / 2);
277
+ const basex = tx + ux * len;
278
+ const basey = ty + uy * len;
279
+ const px = -uy;
280
+ const py = ux;
281
+ return [
282
+ pt(tx, ty),
283
+ pt(mx - px * half, my - py * half),
284
+ pt(basex, basey),
285
+ pt(mx + px * half, my + py * half)
286
+ ].join(" ");
287
+ }
288
+ function glyphInset(_dir, kind) {
289
+ switch (kind) {
290
+ case "arrow":
291
+ return GLYPH_ARROW_LEN;
292
+ case "triangle":
293
+ return GLYPH_TRI_LEN;
294
+ case "diamond":
295
+ return GLYPH_DIAMOND_LEN;
296
+ }
297
+ }
298
+
299
+ // src/core/compartment.ts
300
+ function round2(n) {
301
+ return Math.round(n * 100) / 100;
302
+ }
303
+ function measureCompartmentBox(compartments, opts) {
304
+ const { padX, padY, lineH, minW } = opts;
305
+ let maxLineW = 0;
306
+ for (const c of compartments) {
307
+ for (const line of c.lines) {
308
+ const w = estimateTextWidth(line, c.font);
309
+ if (w > maxLineW) maxLineW = w;
310
+ }
311
+ }
312
+ const boxW = round2(Math.max(minW, maxLineW + 2 * padX));
313
+ const rows = [];
314
+ const dividerYs = [];
315
+ let y = 0;
316
+ for (let i = 0; i < compartments.length; i++) {
317
+ const c = compartments[i];
318
+ const lineCount = c.lines.length > 0 ? c.lines.length : 1;
319
+ const compartmentH = padY + lineCount * lineH + padY;
320
+ rows.push({
321
+ lines: c.lines,
322
+ top: round2(y + padY),
323
+ font: c.font
324
+ });
325
+ y += compartmentH;
326
+ if (i < compartments.length - 1) {
327
+ dividerYs.push(round2(y));
328
+ }
329
+ }
330
+ const boxH = round2(y);
331
+ return { boxW, boxH, rows, dividerYs };
332
+ }
333
+
334
+ // src/core/grid.ts
335
+ function round3(n) {
336
+ return Math.round(n * 100) / 100;
337
+ }
338
+ function packGrid(cells, gaps) {
339
+ if (cells.length === 0) {
340
+ return { cellX: [], cellY: [], colW: [], rowH: [] };
341
+ }
342
+ const { colGap, rowGap } = gaps;
343
+ let maxCol = 0;
344
+ let maxRow = 0;
345
+ for (const c of cells) {
346
+ if (c.col > maxCol) maxCol = c.col;
347
+ if (c.row > maxRow) maxRow = c.row;
348
+ }
349
+ const colW = Array.from({ length: maxCol + 1 }, () => 0);
350
+ const rowH = Array.from({ length: maxRow + 1 }, () => 0);
351
+ for (const c of cells) {
352
+ if (c.w > colW[c.col]) colW[c.col] = c.w;
353
+ if (c.h > rowH[c.row]) rowH[c.row] = c.h;
354
+ }
355
+ const colX = Array.from({ length: maxCol + 1 }, () => 0);
356
+ for (let c = 1; c <= maxCol; c++) {
357
+ colX[c] = round3(colX[c - 1] + colW[c - 1] + colGap);
358
+ }
359
+ const rowY = Array.from({ length: maxRow + 1 }, () => 0);
360
+ for (let r2 = 1; r2 <= maxRow; r2++) {
361
+ rowY[r2] = round3(rowY[r2 - 1] + rowH[r2 - 1] + rowGap);
362
+ }
363
+ const cellX = cells.map(
364
+ (c) => round3(colX[c.col] + colW[c.col] / 2)
365
+ );
366
+ const cellY = cells.map(
367
+ (c) => round3(rowY[c.row] + rowH[c.row] / 2)
368
+ );
369
+ return { cellX, cellY, colW, rowH };
370
+ }
371
+
145
372
  // src/genogram/types.ts
146
373
  var UNION_STATUSES = [
147
374
  "married",
@@ -276,7 +503,7 @@ function arrivalOffset(slot, total) {
276
503
  const sign = slot % 2 === 1 ? -1 : 1;
277
504
  return sign * Math.ceil(slot / 2) * step;
278
505
  }
279
- function allocateLanes(items) {
506
+ function allocateLanes2(items) {
280
507
  const lanes = [];
281
508
  for (const it of items) {
282
509
  let chosen = -1;
@@ -307,36 +534,37 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
307
534
  const validUnions = unions.filter((u) => ids.has(u.personAId) && ids.has(u.personBId) && u.personAId !== u.personBId).sort((a, b) => a.id - b.id);
308
535
  const coupleUnions = validUnions.filter((u) => u.status !== "coparental");
309
536
  const validLinks = parentLinks.filter((l) => ids.has(l.parentId) && ids.has(l.childId) && l.parentId !== l.childId).sort((a, b) => a.id - b.id);
310
- const validRels = relationships.filter((r) => ids.has(r.fromPersonId) && ids.has(r.toPersonId) && r.fromPersonId !== r.toPersonId).sort((a, b) => a.id - b.id);
537
+ const validRels = relationships.filter((r2) => ids.has(r2.fromPersonId) && ids.has(r2.toPersonId) && r2.fromPersonId !== r2.toPersonId).sort((a, b) => a.id - b.id);
311
538
  const relClass = new Map(
312
- validRels.map((r) => [r.id, classifyRelationshipType(r.type, kinship)])
539
+ validRels.map((r2) => [r2.id, classifyRelationshipType(r2.type, kinship)])
313
540
  );
314
- const bondRels = validRels.filter((r) => relClass.get(r.id) === "bond");
315
- const parentageRels = validRels.filter((r) => relClass.get(r.id) === "parentage");
541
+ const bondRels = validRels.filter((r2) => relClass.get(r2.id) === "bond");
542
+ const parentageRels = validRels.filter((r2) => relClass.get(r2.id) === "parentage");
316
543
  const realLinks = validLinks.map((l) => ({
317
544
  parentId: l.parentId,
318
545
  childId: l.childId,
319
546
  quality: l.quality,
320
- edgeId: PARENT_REL_ID_BASE + l.id
547
+ edgeId: PARENT_REL_ID_BASE + l.id,
548
+ annotated: l.annotated ?? false
321
549
  }));
322
550
  const declaredPairs = new Set(validLinks.map((l) => pairKey(l.parentId, l.childId)));
323
551
  const promotedByPair = /* @__PURE__ */ new Map();
324
- for (const r of parentageRels) {
325
- const key = pairKey(r.fromPersonId, r.toPersonId);
552
+ for (const r2 of parentageRels) {
553
+ const key = pairKey(r2.fromPersonId, r2.toPersonId);
326
554
  if (declaredPairs.has(key)) continue;
327
555
  if (promotedByPair.has(key)) continue;
328
- const ga = genById.get(r.fromPersonId) ?? null;
329
- const gb = genById.get(r.toPersonId) ?? null;
556
+ const ga = genById.get(r2.fromPersonId) ?? null;
557
+ const gb = genById.get(r2.toPersonId) ?? null;
330
558
  let parentId;
331
559
  let childId;
332
560
  if (ga !== null && gb !== null && ga !== gb) {
333
- [parentId, childId] = ga < gb ? [r.fromPersonId, r.toPersonId] : [r.toPersonId, r.fromPersonId];
561
+ [parentId, childId] = ga < gb ? [r2.fromPersonId, r2.toPersonId] : [r2.toPersonId, r2.fromPersonId];
334
562
  } else {
335
- const fromIsChild = relationshipTypeTokens(r.type).some((t) => kinship.childWords.has(t));
336
- [parentId, childId] = fromIsChild ? [r.toPersonId, r.fromPersonId] : [r.fromPersonId, r.toPersonId];
563
+ const fromIsChild = relationshipTypeTokens(r2.type).some((t) => kinship.childWords.has(t));
564
+ [parentId, childId] = fromIsChild ? [r2.toPersonId, r2.fromPersonId] : [r2.fromPersonId, r2.toPersonId];
337
565
  }
338
566
  if (parentId === childId) continue;
339
- promotedByPair.set(key, { parentId, childId, quality: r.quality, edgeId: PROMOTED_REL_ID_BASE + r.id });
567
+ promotedByPair.set(key, { parentId, childId, quality: r2.quality, edgeId: PROMOTED_REL_ID_BASE + r2.id, annotated: r2.annotated ?? false });
340
568
  }
341
569
  const allLinks = [...realLinks, ...promotedByPair.values()].sort((a, b) => a.edgeId - b.edgeId);
342
570
  const coupleByPair = /* @__PURE__ */ new Map();
@@ -358,9 +586,9 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
358
586
  hasTie.add(l.parentId);
359
587
  hasTie.add(l.childId);
360
588
  }
361
- for (const r of bondRels) {
362
- hasTie.add(r.fromPersonId);
363
- hasTie.add(r.toPersonId);
589
+ for (const r2 of bondRels) {
590
+ hasTie.add(r2.fromPersonId);
591
+ hasTie.add(r2.toPersonId);
364
592
  }
365
593
  const isIsolated = (id) => !hasTie.has(id);
366
594
  const byGen = /* @__PURE__ */ new Map();
@@ -376,13 +604,13 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
376
604
  });
377
605
  const rowCount = genKeys.length;
378
606
  const rowOfPerson = /* @__PURE__ */ new Map();
379
- genKeys.forEach((g, r) => byGen.get(g).forEach((p) => rowOfPerson.set(p.id, r)));
380
- const rowBlocks = genKeys.map((g, r) => {
607
+ genKeys.forEach((g, r2) => byGen.get(g).forEach((p) => rowOfPerson.set(p.id, r2)));
608
+ const rowBlocks = genKeys.map((g, r2) => {
381
609
  const members = byGen.get(g).map((p) => p.id);
382
610
  const adj = /* @__PURE__ */ new Map();
383
611
  for (const id of members) adj.set(id, []);
384
612
  for (const u of coupleUnions) {
385
- if (rowOfPerson.get(u.personAId) === r && rowOfPerson.get(u.personBId) === r) {
613
+ if (rowOfPerson.get(u.personAId) === r2 && rowOfPerson.get(u.personBId) === r2) {
386
614
  adj.get(u.personAId).push(u.personBId);
387
615
  adj.get(u.personBId).push(u.personAId);
388
616
  }
@@ -438,9 +666,9 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
438
666
  for (const rl of bondRels) if (rl.fromPersonId === id && rl.toPersonId === other || rl.toPersonId === id && rl.fromPersonId === other) w = Math.max(w, 0.5);
439
667
  return w;
440
668
  };
441
- const sweep = (r, ref) => {
669
+ const sweep = (r2, ref) => {
442
670
  const pos = flattened();
443
- const blocks = rowBlocks[r];
671
+ const blocks = rowBlocks[r2];
444
672
  const bcOf = (b) => {
445
673
  let sum = 0;
446
674
  let wsum = 0;
@@ -456,23 +684,23 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
456
684
  };
457
685
  const keyed = blocks.map((b) => ({ b, bc: bcOf(b), minId: Math.min(...b) }));
458
686
  keyed.sort((x, y) => x.bc !== y.bc ? x.bc - y.bc : x.minId - y.minId);
459
- rowBlocks[r] = keyed.map((k) => k.b);
687
+ rowBlocks[r2] = keyed.map((k) => k.b);
460
688
  };
461
689
  if (rowCount >= 2) {
462
- for (let r = 1; r < rowCount; r++) sweep(r, r - 1);
463
- for (let r = rowCount - 2; r >= 0; r--) sweep(r, r + 1);
690
+ for (let r2 = 1; r2 < rowCount; r2++) sweep(r2, r2 - 1);
691
+ for (let r2 = rowCount - 2; r2 >= 0; r2--) sweep(r2, r2 + 1);
464
692
  }
465
- for (let r = 0; r < rowCount; r++) {
466
- const blocks = rowBlocks[r];
693
+ for (let r2 = 0; r2 < rowCount; r2++) {
694
+ const blocks = rowBlocks[r2];
467
695
  const connected = blocks.filter((b) => !(b.length === 1 && isIsolated(b[0])));
468
696
  const isolated = blocks.filter((b) => b.length === 1 && isIsolated(b[0])).sort((a, b) => a[0] - b[0]);
469
- rowBlocks[r] = [...connected, ...isolated];
697
+ rowBlocks[r2] = [...connected, ...isolated];
470
698
  }
471
699
  const colOf = /* @__PURE__ */ new Map();
472
700
  let colCount = 0;
473
- for (let r = 0; r < rowCount; r++) {
701
+ for (let r2 = 0; r2 < rowCount; r2++) {
474
702
  let c = 0;
475
- for (const b of rowBlocks[r]) for (const id of b) colOf.set(id, c++);
703
+ for (const b of rowBlocks[r2]) for (const id of b) colOf.set(id, c++);
476
704
  colCount = Math.max(colCount, c);
477
705
  }
478
706
  const colOrThrow = (id) => colOf.get(id);
@@ -499,8 +727,8 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
499
727
  }
500
728
  const rowMaxLines = Array.from({ length: rowCount }, () => 1);
501
729
  for (const p of people) {
502
- const r = rowOfPerson.get(p.id);
503
- rowMaxLines[r] = Math.max(rowMaxLines[r], measured.get(p.id).lines.length);
730
+ const r2 = rowOfPerson.get(p.id);
731
+ rowMaxLines[r2] = Math.max(rowMaxLines[r2], measured.get(p.id).lines.length);
504
732
  }
505
733
  const planned = [];
506
734
  const gutterReqs = [];
@@ -619,10 +847,10 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
619
847
  {
620
848
  const byRow = /* @__PURE__ */ new Map();
621
849
  for (const u of barUnions) {
622
- const r = rowOfPerson.get(u.personAId);
623
- (byRow.get(r) ?? byRow.set(r, []).get(r)).push(u);
850
+ const r2 = rowOfPerson.get(u.personAId);
851
+ (byRow.get(r2) ?? byRow.set(r2, []).get(r2)).push(u);
624
852
  }
625
- for (const [r, rowUnions] of byRow) {
853
+ for (const [r2, rowUnions] of byRow) {
626
854
  const unionsOfPerson = /* @__PURE__ */ new Map();
627
855
  for (const u of rowUnions) {
628
856
  (unionsOfPerson.get(u.personAId) ?? unionsOfPerson.set(u.personAId, []).get(u.personAId)).push(u);
@@ -648,7 +876,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
648
876
  }
649
877
  comp.sort((a, b) => a.id - b.id);
650
878
  comp.forEach((u, i) => unionDipLevel.set(u.id, i));
651
- rowDipLevels[r] = Math.max(rowDipLevels[r], comp.length - 1);
879
+ rowDipLevels[r2] = Math.max(rowDipLevels[r2], comp.length - 1);
652
880
  }
653
881
  }
654
882
  }
@@ -669,6 +897,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
669
897
  toPersonId: u.personBId,
670
898
  titles: [unionTitle(u)],
671
899
  lineStyle: "plain",
900
+ annotated: u.annotated ?? false,
672
901
  build: () => {
673
902
  const cy = geo.cy(rowOfPerson.get(leftId));
674
903
  if (dipLevel === 0) {
@@ -703,6 +932,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
703
932
  toPersonId: u.personBId,
704
933
  titles: [unionTitle(u)],
705
934
  lineStyle: "plain",
935
+ annotated: u.annotated ?? false,
706
936
  build
707
937
  });
708
938
  }
@@ -759,6 +989,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
759
989
  };
760
990
  gutterReqs.push(spineReq);
761
991
  const aId = (c) => linkOf.get(`${u.personAId}>${c}`).edgeId;
992
+ const aLinkAnnotated = (c) => linkOf.get(`${u.personAId}>${c}`).annotated ?? false;
762
993
  const childDrop = (c, parentId) => {
763
994
  const link = linkOf.get(`${parentId}>${c}`);
764
995
  const edgeId = link.edgeId;
@@ -771,6 +1002,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
771
1002
  toPersonId: c,
772
1003
  titles: [linkTitle(link)],
773
1004
  lineStyle: "plain",
1005
+ annotated: link.annotated,
774
1006
  build: () => {
775
1007
  const y = geo.corridorLaneY(sibReq);
776
1008
  const x = geo.cx(colOrThrow(c)) + arrivalOffset(arr.slot, arrivalCount.get(c) ?? 1);
@@ -791,6 +1023,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
791
1023
  toPersonId: null,
792
1024
  titles: [linkTitle(linkOf.get(`${u.personAId}>${c}`))],
793
1025
  lineStyle: "plain",
1026
+ annotated: aLinkAnnotated(c),
794
1027
  build: () => {
795
1028
  const sy = geo.corridorLaneY(sibReq);
796
1029
  const sx = geo.gutterCenterX(interGutter);
@@ -816,6 +1049,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
816
1049
  // declared quality word is dropped from the drawn element (FIX C-2 / SPEC inv #4).
817
1050
  titles: [aLinkTitle(groupKids[0]), ...groupKids.slice(2).map(aLinkTitle)],
818
1051
  lineStyle: "plain",
1052
+ annotated: [groupKids[0], ...groupKids.slice(2)].some(aLinkAnnotated),
819
1053
  build: () => {
820
1054
  const y = geo.corridorLaneY(sibReq);
821
1055
  const xs = sibSpan();
@@ -834,6 +1068,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
834
1068
  titles: [aLinkTitle(groupKids[1])],
835
1069
  // the A-side link this spine carries, verbatim
836
1070
  lineStyle: "plain",
1071
+ annotated: aLinkAnnotated(groupKids[1]),
837
1072
  build: () => {
838
1073
  const sy = geo.corridorLaneY(sibReq);
839
1074
  const x = geo.gutterCenterX(interGutter);
@@ -861,22 +1096,24 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
861
1096
  titles: [linkTitle(l)],
862
1097
  lineStyle: "plain",
863
1098
  dotted: true,
1099
+ annotated: l.annotated,
864
1100
  build
865
1101
  });
866
1102
  }
867
1103
  const bondGroups = /* @__PURE__ */ new Map();
868
- for (const r of bondRels) {
869
- const style = qualityLineStyle(r.quality, opts.qualityLexicon);
870
- const lo = Math.min(r.fromPersonId, r.toPersonId);
871
- const hi = Math.max(r.fromPersonId, r.toPersonId);
1104
+ for (const r2 of bondRels) {
1105
+ const style = qualityLineStyle(r2.quality, opts.qualityLexicon);
1106
+ const lo = Math.min(r2.fromPersonId, r2.toPersonId);
1107
+ const hi = Math.max(r2.fromPersonId, r2.toPersonId);
872
1108
  const key = `${lo}|${hi}|${style}`;
873
- const title = r.quality !== null ? `${r.type} \xB7 ${r.quality}` : r.type;
1109
+ const title = r2.quality !== null ? `${r2.type} \xB7 ${r2.quality}` : r2.type;
874
1110
  const g = bondGroups.get(key);
875
1111
  if (g === void 0) {
876
- bondGroups.set(key, { relIds: [r.id], titles: [title], style, aId: r.fromPersonId, bId: r.toPersonId });
1112
+ bondGroups.set(key, { relIds: [r2.id], titles: [title], style, aId: r2.fromPersonId, bId: r2.toPersonId, annotated: r2.annotated ?? false });
877
1113
  } else {
878
- g.relIds.push(r.id);
1114
+ g.relIds.push(r2.id);
879
1115
  g.titles.push(title);
1116
+ if (r2.annotated) g.annotated = true;
880
1117
  }
881
1118
  }
882
1119
  const bondList = [...bondGroups.values()].sort((a, b) => Math.max(...a.relIds) - Math.max(...b.relIds));
@@ -895,6 +1132,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
895
1132
  toPersonId: g.bId,
896
1133
  titles: g.titles,
897
1134
  lineStyle: g.style,
1135
+ annotated: g.annotated,
898
1136
  build
899
1137
  });
900
1138
  }
@@ -910,7 +1148,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
910
1148
  const laneCount = (gutter, side) => gutterLaneCount.get(`${gutter}:${side}`) ?? 0;
911
1149
  const geo = {
912
1150
  cx: (c) => colCenterX[c],
913
- cy: (r) => rowCenterY[r],
1151
+ cy: (r2) => rowCenterY[r2],
914
1152
  glyphRight: (id) => colCenterX[colOrThrow(id)] + NODE_SIZE / 2,
915
1153
  glyphLeft: (id) => colCenterX[colOrThrow(id)] - NODE_SIZE / 2,
916
1154
  gutterCenterX: (g) => {
@@ -928,7 +1166,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
928
1166
  },
929
1167
  corridorLaneY: (req) => corridorTopY[req.corridor] + CORRIDOR_BASE / 2 + req.lane * LANE_H,
930
1168
  /** Y of a dipped serial-union bar (level ≥ 1): below the row's glyph bottom edge. */
931
- unionDipY: (r, level) => rowCenterY[r] + NODE_SIZE / 2 + level * UNION_STAGGER
1169
+ unionDipY: (r2, level) => rowCenterY[r2] + NODE_SIZE / 2 + level * UNION_STAGGER
932
1170
  };
933
1171
  const gutterGroups = /* @__PURE__ */ new Map();
934
1172
  for (const req of gutterReqs) {
@@ -938,7 +1176,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
938
1176
  for (const [k, reqs] of gutterGroups) {
939
1177
  gutterLaneCount.set(
940
1178
  k,
941
- allocateLanes(reqs.map((req) => ({ lo: req.rowLo, hi: req.rowHi, set: (lane) => req.lane = lane })))
1179
+ allocateLanes2(reqs.map((req) => ({ lo: req.rowLo, hi: req.rowHi, set: (lane) => req.lane = lane })))
942
1180
  );
943
1181
  }
944
1182
  const gutterWidth = (g) => {
@@ -959,7 +1197,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
959
1197
  const corridorGroups = /* @__PURE__ */ new Map();
960
1198
  for (const req of corridorReqs) (corridorGroups.get(req.corridor) ?? corridorGroups.set(req.corridor, []).get(req.corridor)).push(req);
961
1199
  for (const [k, reqs] of corridorGroups) {
962
- corridorLaneCount[k] = allocateLanes(
1200
+ corridorLaneCount[k] = allocateLanes2(
963
1201
  reqs.map((req) => {
964
1202
  const [lo, hi] = req.xRange();
965
1203
  return { lo, hi, set: (lane) => req.lane = lane };
@@ -971,31 +1209,32 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
971
1209
  if (lanes > 0) return CORRIDOR_BASE + lanes * LANE_H;
972
1210
  return k < rowCount - 1 ? CORRIDOR_BASE : 0;
973
1211
  };
974
- const rowDipReserve = (r) => rowDipLevels[r] > 0 ? rowDipLevels[r] * UNION_STAGGER + 6 : 0;
1212
+ const rowDipReserve = (r2) => rowDipLevels[r2] > 0 ? rowDipLevels[r2] * UNION_STAGGER + 6 : 0;
975
1213
  let cursorY = PADDING;
976
- for (let r = 0; r < rowCount; r++) {
977
- rowCenterY[r] = cursorY + NODE_SIZE / 2;
978
- rowLabelTopY[r] = cursorY + NODE_SIZE + rowDipReserve(r) + LABEL_GAP_TOP;
979
- cursorY = rowLabelTopY[r] + rowMaxLines[r] * LABEL_LINE_H;
980
- corridorTopY[r] = cursorY;
981
- cursorY += corridorHeight(r);
1214
+ for (let r2 = 0; r2 < rowCount; r2++) {
1215
+ rowCenterY[r2] = cursorY + NODE_SIZE / 2;
1216
+ rowLabelTopY[r2] = cursorY + NODE_SIZE + rowDipReserve(r2) + LABEL_GAP_TOP;
1217
+ cursorY = rowLabelTopY[r2] + rowMaxLines[r2] * LABEL_LINE_H;
1218
+ corridorTopY[r2] = cursorY;
1219
+ cursorY += corridorHeight(r2);
982
1220
  }
983
1221
  const width = dims.width;
984
1222
  const height = cursorY + PADDING;
985
1223
  const nodes = [...people].sort((a, b) => a.id - b.id).map((p) => {
986
1224
  const m = measured.get(p.id);
987
- const r = rowOfPerson.get(p.id);
1225
+ const r2 = rowOfPerson.get(p.id);
988
1226
  const c = colOrThrow(p.id);
989
1227
  return {
990
1228
  personId: p.id,
991
1229
  label: p.label,
992
1230
  shape: shapeForSex(p.sex),
993
1231
  deceased: p.deceased,
1232
+ annotated: p.annotated ?? false,
994
1233
  cx: colCenterX[c],
995
- cy: rowCenterY[r],
1234
+ cy: rowCenterY[r2],
996
1235
  size: NODE_SIZE,
997
1236
  labelLines: m.lines,
998
- labelTop: rowLabelTopY[r]
1237
+ labelTop: rowLabelTopY[r2]
999
1238
  };
1000
1239
  });
1001
1240
  const elements = planned.map((pl) => ({
@@ -1007,6 +1246,7 @@ function computeGenogramLayout(people, unions, parentLinks, relationships, opts
1007
1246
  points: pl.build(),
1008
1247
  titles: pl.titles,
1009
1248
  lineStyle: pl.lineStyle,
1249
+ annotated: pl.annotated,
1010
1250
  ...pl.unionStyle !== void 0 ? { unionStyle: pl.unionStyle } : {},
1011
1251
  ...pl.dotted ? { dotted: true } : {}
1012
1252
  }));
@@ -1111,6 +1351,7 @@ function elementSvg(el, override) {
1111
1351
  }
1112
1352
  return `<path d="${pathData2(pts)}" fill="none" stroke="${EDGE_INK}" stroke-width="${width}" stroke-opacity="${opacity}"${dashAttr}/>`;
1113
1353
  };
1354
+ const tick = el.annotated ? annotationTick(el.points) : "";
1114
1355
  if (el.kind === "union-bar" || el.kind === "union-elbow") {
1115
1356
  const style = resolveUnionStyle(el, override);
1116
1357
  const body = [drawLine(style.dash ?? null, STRUCT_WIDTH, STRUCT_OPACITY)];
@@ -1119,15 +1360,15 @@ function elementSvg(el, override) {
1119
1360
  const [a, b] = pts.length === 2 ? [pts[0], pts[1]] : longestHSegment(pts);
1120
1361
  body.push(slashMarks(a, b, slashes, STRUCT_WIDTH));
1121
1362
  }
1122
- return `<g data-edge-id="${el.edgeId}">${title}${body.join("")}</g>`;
1363
+ return `<g data-edge-id="${el.edgeId}">${title}${body.join("")}${tick}</g>`;
1123
1364
  }
1124
1365
  if (el.kind === "descent" || el.kind === "sibling-bar") {
1125
1366
  const dash = el.dotted ? DOTTED_DASH : null;
1126
1367
  const opacity = el.dotted ? DOTTED_OPACITY : STRUCT_OPACITY;
1127
- return `<g data-edge-id="${el.edgeId}">${title}${drawLine(dash, STRUCT_WIDTH, opacity)}</g>`;
1368
+ return `<g data-edge-id="${el.edgeId}">${title}${drawLine(dash, STRUCT_WIDTH, opacity)}${tick}</g>`;
1128
1369
  }
1129
1370
  const ink = EDGE_STROKE[el.lineStyle];
1130
- return `<g data-edge-id="${el.edgeId}">${title}${drawLine(ink.dash, ink.width, ink.opacity)}</g>`;
1371
+ return `<g data-edge-id="${el.edgeId}">${title}${drawLine(ink.dash, ink.width, ink.opacity)}${tick}</g>`;
1131
1372
  }
1132
1373
  function genogramLayoutSvg(layout, opts = {}) {
1133
1374
  const override = opts.unionStyleByRelId ?? /* @__PURE__ */ new Map();
@@ -1145,6 +1386,9 @@ function genogramLayoutSvg(layout, opts = {}) {
1145
1386
  `<line x1="${node.cx - half}" y1="${node.cy - half}" x2="${node.cx + half}" y2="${node.cy + half}" stroke="${GLYPH_STROKE}" stroke-width="2"/>`
1146
1387
  );
1147
1388
  }
1389
+ if (node.annotated) {
1390
+ pieces.push(annotationDot(node.cx + 0.7 * half, node.cy - 0.7 * half));
1391
+ }
1148
1392
  const tspans = node.labelLines.map(
1149
1393
  (line, i) => `<tspan x="${node.cx}" y="${node.labelTop + 10 + i * LABEL_LINE_H}">${xmlEscape(line)}</tspan>`
1150
1394
  ).join("");
@@ -1187,6 +1431,12 @@ function genogramLayoutSvg(layout, opts = {}) {
1187
1431
  label: labels.isolated
1188
1432
  });
1189
1433
  }
1434
+ if (opts.annotationLabel !== void 0 && (layout.nodes.some((n) => n.annotated) || layout.elements.some((e) => e.annotated))) {
1435
+ entries.push({
1436
+ swatch: (x, y) => annotationSwatch(x, y),
1437
+ label: opts.annotationLabel
1438
+ });
1439
+ }
1190
1440
  const block = legendBlock(entries, layout.height);
1191
1441
  if (block.svg !== "") {
1192
1442
  parts.push(block.svg);
@@ -1238,7 +1488,8 @@ function genogramSvg(input, opts = {}) {
1238
1488
  const svg = genogramLayoutSvg(layout, {
1239
1489
  unionStyleByRelId,
1240
1490
  ...opts.legend === false ? { legend: false } : {},
1241
- ...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
1491
+ ...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {},
1492
+ ...opts.annotationLabel !== void 0 ? { annotationLabel: opts.annotationLabel } : {}
1242
1493
  });
1243
1494
  return { svg, layout };
1244
1495
  }
@@ -1268,7 +1519,7 @@ var SINGLE_RING_MAX = 8;
1268
1519
  var NODE_STROKE = "#52525b";
1269
1520
  var LABEL_FILL = "#3f3f46";
1270
1521
  var EDGE_INK2 = "#71717a";
1271
- var round = (n) => Math.round(n * 100) / 100;
1522
+ var round4 = (n) => Math.round(n * 100) / 100;
1272
1523
  function arrowHead(tipX, tipY, ux, uy, opacity) {
1273
1524
  const LEN = 9;
1274
1525
  const HALF_W = 4.5;
@@ -1277,9 +1528,9 @@ function arrowHead(tipX, tipY, ux, uy, opacity) {
1277
1528
  const px = -uy;
1278
1529
  const py = ux;
1279
1530
  const points = [
1280
- `${round(tipX)},${round(tipY)}`,
1281
- `${round(bx + px * HALF_W)},${round(by + py * HALF_W)}`,
1282
- `${round(bx - px * HALF_W)},${round(by - py * HALF_W)}`
1531
+ `${round4(tipX)},${round4(tipY)}`,
1532
+ `${round4(bx + px * HALF_W)},${round4(by + py * HALF_W)}`,
1533
+ `${round4(bx - px * HALF_W)},${round4(by - py * HALF_W)}`
1283
1534
  ].join(" ");
1284
1535
  return `<polygon points="${points}" fill="${EDGE_INK2}" fill-opacity="${opacity}"/>`;
1285
1536
  }
@@ -1325,16 +1576,16 @@ function ecomapSvg(input, opts = {}) {
1325
1576
  sameRingRadius(twoRings ? 2 : 1)
1326
1577
  );
1327
1578
  if (twoRings) {
1328
- const crossDist = (r) => Math.sqrt(r * r + (r + ringStep) ** 2 - 2 * r * (r + ringStep) * Math.cos(stepAngle));
1579
+ const crossDist = (r2) => Math.sqrt(r2 * r2 + (r2 + ringStep) ** 2 - 2 * r2 * (r2 + ringStep) * Math.cos(stepAngle));
1329
1580
  while (crossDist(innerR) < safeDist) innerR += 8;
1330
1581
  }
1331
1582
  const outerR = innerR + ringStep;
1332
1583
  for (let i = 0; i < n; i++) {
1333
1584
  const s = sats[i];
1334
1585
  s.angle = -Math.PI / 2 + i * Math.PI * 2 / n;
1335
- const r = twoRings && i % 2 === 1 ? outerR : innerR;
1336
- s.x = r * Math.cos(s.angle);
1337
- s.y = r * Math.sin(s.angle);
1586
+ const r2 = twoRings && i % 2 === 1 ? outerR : innerR;
1587
+ s.x = r2 * Math.cos(s.angle);
1588
+ s.y = r2 * Math.sin(s.angle);
1338
1589
  }
1339
1590
  let minX = -centerR;
1340
1591
  let minY = -centerR;
@@ -1369,7 +1620,7 @@ function ecomapSvg(input, opts = {}) {
1369
1620
  const dashAttr = ink.dash === null ? "" : ` stroke-dasharray="${ink.dash[0]} ${ink.dash[1]}"`;
1370
1621
  const title = s.tie.title ?? (s.tie.quality !== null ? `${s.tie.label} \xB7 ${s.tie.quality}` : s.tie.label);
1371
1622
  const body = [
1372
- `<line x1="${round(x1)}" y1="${round(y1)}" x2="${round(x2)}" y2="${round(y2)}" stroke="${EDGE_INK2}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`
1623
+ `<line x1="${round4(x1)}" y1="${round4(y1)}" x2="${round4(x2)}" y2="${round4(y2)}" stroke="${EDGE_INK2}" stroke-width="${ink.width}" stroke-opacity="${ink.opacity}"${dashAttr}/>`
1373
1624
  ];
1374
1625
  if (s.tie.direction === "in" || s.tie.direction === "both") {
1375
1626
  body.push(arrowHead(x2, y2, ux, uy, ink.opacity));
@@ -1381,19 +1632,25 @@ function ecomapSvg(input, opts = {}) {
1381
1632
  }
1382
1633
  {
1383
1634
  const tspans = centerLines.map(
1384
- (line, i) => `<tspan x="${round(cx)}" y="${round(cy - (centerLines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
1635
+ (line, i) => `<tspan x="${round4(cx)}" y="${round4(cy - (centerLines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
1385
1636
  ).join("");
1386
1637
  parts.push(
1387
- `<g data-individual-id="center"><title>${xmlEscape(input.centerLabel)}</title><circle cx="${round(cx)}" cy="${round(cy)}" r="${round(centerR)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="2"/><text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text></g>`
1638
+ `<g data-individual-id="center"><title>${xmlEscape(input.centerLabel)}</title><circle cx="${round4(cx)}" cy="${round4(cy)}" r="${round4(centerR)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="2"/><text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text></g>`
1388
1639
  );
1389
1640
  }
1390
1641
  for (const s of sats) {
1391
1642
  const tspans = s.lines.map(
1392
- (line, i) => `<tspan x="${round(s.x)}" y="${round(s.y - (s.lines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
1643
+ (line, i) => `<tspan x="${round4(s.x)}" y="${round4(s.y - (s.lines.length - 1) * LINE_H / 2 + i * LINE_H + fontSize * 0.32)}">${xmlEscape(line)}</tspan>`
1393
1644
  ).join("");
1394
- parts.push(
1395
- `<g data-individual-id="e${s.tie.id}"><title>${xmlEscape(s.tie.label)}</title><ellipse cx="${round(s.x)}" cy="${round(s.y)}" rx="${round(s.rx)}" ry="${round(s.ry)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="1.5"/><text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text></g>`
1396
- );
1645
+ const pieces = [
1646
+ `<title>${xmlEscape(s.tie.label)}</title>`,
1647
+ `<ellipse cx="${round4(s.x)}" cy="${round4(s.y)}" rx="${round4(s.rx)}" ry="${round4(s.ry)}" fill="transparent" stroke="${NODE_STROKE}" stroke-width="1.5"/>`,
1648
+ `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL}">${tspans}</text>`
1649
+ ];
1650
+ if (s.tie.annotated) {
1651
+ pieces.push(annotationDot(s.x + 0.7 * s.rx, s.y - 0.7 * s.ry));
1652
+ }
1653
+ parts.push(`<g data-individual-id="e${s.tie.id}">${pieces.join("")}</g>`);
1397
1654
  }
1398
1655
  if (opts.legend !== false && sats.length > 0) {
1399
1656
  const entries = [];
@@ -1419,6 +1676,12 @@ function ecomapSvg(input, opts = {}) {
1419
1676
  label: labels.direction
1420
1677
  });
1421
1678
  }
1679
+ if (opts.annotationLabel !== void 0 && sats.some((s) => s.tie.annotated)) {
1680
+ entries.push({
1681
+ swatch: (x, y) => annotationSwatch(x, y),
1682
+ label: opts.annotationLabel
1683
+ });
1684
+ }
1422
1685
  if (entries.length > 0) {
1423
1686
  const block = legendBlock(entries, height);
1424
1687
  parts.push(block.svg);
@@ -1592,7 +1855,7 @@ function faultTreeIssues(input) {
1592
1855
  );
1593
1856
  }
1594
1857
  }
1595
- const GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
1858
+ const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
1596
1859
  "duplicate-id",
1597
1860
  "unknown-input",
1598
1861
  "gate-on-non-intermediate",
@@ -1601,7 +1864,7 @@ function faultTreeIssues(input) {
1601
1864
  "inhibit-condition",
1602
1865
  "top-not-intermediate"
1603
1866
  ]);
1604
- if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
1867
+ if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
1605
1868
  const gateOf = (eventId) => gates.find((g) => g.eventId === eventId);
1606
1869
  const reachable = /* @__PURE__ */ new Set();
1607
1870
  const queue = [input.topId];
@@ -1681,7 +1944,7 @@ var FT_DROP_ID_BASE = 2e6;
1681
1944
  var FT_BUS_ID_BASE = 3e6;
1682
1945
  var FT_RISER_ID_BASE = 4e6;
1683
1946
  var FT_CONDITION_ID_BASE = 5e6;
1684
- var round2 = (n) => Math.round(n * 100) / 100;
1947
+ var round5 = (n) => Math.round(n * 100) / 100;
1685
1948
  function wrapLeafLabel(displayLabel) {
1686
1949
  const perLine = Math.min(20, Math.max(12, Math.ceil(displayLabel.length / 2) + 2));
1687
1950
  return wrapLabel(displayLabel, perLine);
@@ -1868,14 +2131,14 @@ function computeFaultTreeLayout(input, opts = {}) {
1868
2131
  eventId: inst.event.id,
1869
2132
  kind: inst.event.kind,
1870
2133
  instance: instanceOf(inst),
1871
- cx: round2(inst.cx),
1872
- top: round2(top),
1873
- nodeW: round2(inst.m.nodeW),
1874
- nodeH: round2(inst.m.nodeH),
1875
- glyphW: round2(inst.m.glyphW),
1876
- glyphH: round2(inst.m.glyphH),
2134
+ cx: round5(inst.cx),
2135
+ top: round5(top),
2136
+ nodeW: round5(inst.m.nodeW),
2137
+ nodeH: round5(inst.m.nodeH),
2138
+ glyphW: round5(inst.m.glyphW),
2139
+ glyphH: round5(inst.m.glyphH),
1877
2140
  labelLines: inst.m.labelLines,
1878
- labelTop: labelTop === null ? null : round2(labelTop),
2141
+ labelTop: labelTop === null ? null : round5(labelTop),
1879
2142
  code: inst.m.code,
1880
2143
  depth: inst.depth,
1881
2144
  title: inst.title
@@ -1893,8 +2156,8 @@ function computeFaultTreeLayout(input, opts = {}) {
1893
2156
  gates.push({
1894
2157
  gateId: g.id,
1895
2158
  type: g.type,
1896
- cx: round2(inst.cx),
1897
- top: round2(gateTop),
2159
+ cx: round5(inst.cx),
2160
+ top: round5(gateTop),
1898
2161
  glyphW,
1899
2162
  glyphH,
1900
2163
  title,
@@ -1911,8 +2174,8 @@ function computeFaultTreeLayout(input, opts = {}) {
1911
2174
  edgeId: FT_CONDITION_ID_BASE + g.id,
1912
2175
  kind: "condition",
1913
2176
  points: [
1914
- { x: round2(inst.cx + INHIBIT_W / 2), y: round2(gateTop + INHIBIT_CY) },
1915
- { x: round2(ovalLeft), y: round2(gateTop + INHIBIT_CY) }
2177
+ { x: round5(inst.cx + INHIBIT_W / 2), y: round5(gateTop + INHIBIT_CY) },
2178
+ { x: round5(ovalLeft), y: round5(gateTop + INHIBIT_CY) }
1916
2179
  ],
1917
2180
  instance: null,
1918
2181
  title: titleLabels.condition
@@ -1922,8 +2185,8 @@ function computeFaultTreeLayout(input, opts = {}) {
1922
2185
  edgeId: FT_STEM_ID_BASE + g.id,
1923
2186
  kind: "stem",
1924
2187
  points: [
1925
- { x: round2(inst.cx), y: round2(rowTop[d] + inst.m.nodeH) },
1926
- { x: round2(inst.cx), y: round2(gateTop) }
2188
+ { x: round5(inst.cx), y: round5(rowTop[d] + inst.m.nodeH) },
2189
+ { x: round5(inst.cx), y: round5(gateTop) }
1927
2190
  ],
1928
2191
  instance: null,
1929
2192
  title
@@ -1933,8 +2196,8 @@ function computeFaultTreeLayout(input, opts = {}) {
1933
2196
  edgeId: FT_DROP_ID_BASE + g.id,
1934
2197
  kind: "drop",
1935
2198
  points: [
1936
- { x: round2(inst.cx), y: round2(gateTop + glyphH) },
1937
- { x: round2(inst.cx), y: round2(by) }
2199
+ { x: round5(inst.cx), y: round5(gateTop + glyphH) },
2200
+ { x: round5(inst.cx), y: round5(by) }
1938
2201
  ],
1939
2202
  instance: null,
1940
2203
  title
@@ -1945,8 +2208,8 @@ function computeFaultTreeLayout(input, opts = {}) {
1945
2208
  edgeId: FT_BUS_ID_BASE + g.id,
1946
2209
  kind: "bus",
1947
2210
  points: [
1948
- { x: round2(Math.min(...xs)), y: round2(by) },
1949
- { x: round2(Math.max(...xs)), y: round2(by) }
2211
+ { x: round5(Math.min(...xs)), y: round5(by) },
2212
+ { x: round5(Math.max(...xs)), y: round5(by) }
1950
2213
  ],
1951
2214
  instance: null,
1952
2215
  title
@@ -1957,8 +2220,8 @@ function computeFaultTreeLayout(input, opts = {}) {
1957
2220
  edgeId: FT_RISER_ID_BASE + c.event.id,
1958
2221
  kind: "riser",
1959
2222
  points: [
1960
- { x: round2(c.cx), y: round2(by) },
1961
- { x: round2(c.cx), y: round2(rowTop[d + 1]) }
2223
+ { x: round5(c.cx), y: round5(by) },
2224
+ { x: round5(c.cx), y: round5(rowTop[d + 1]) }
1962
2225
  ],
1963
2226
  instance: instanceOf(c),
1964
2227
  title
@@ -1975,42 +2238,42 @@ var GLYPH_STROKE2 = "#52525b";
1975
2238
  var LABEL_FILL2 = "#3f3f46";
1976
2239
  var EDGE_INK3 = "#71717a";
1977
2240
  var GLYPH_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="2"`;
1978
- var round3 = (n) => Math.round(n * 100) / 100;
2241
+ var round6 = (n) => Math.round(n * 100) / 100;
1979
2242
  function eventGlyph(n) {
1980
2243
  const cx = n.cx;
1981
2244
  const top = n.top;
1982
2245
  if (n.kind === "intermediate") {
1983
- return `<rect x="${round3(cx - n.nodeW / 2)}" y="${top}" width="${n.nodeW}" height="${n.nodeH}" rx="2" ${GLYPH_ATTRS}/>`;
2246
+ return `<rect x="${round6(cx - n.nodeW / 2)}" y="${top}" width="${n.nodeW}" height="${n.nodeH}" rx="2" ${GLYPH_ATTRS}/>`;
1984
2247
  }
1985
2248
  if (n.kind === "basic") {
1986
- return `<circle cx="${cx}" cy="${round3(top + 22)}" r="22" ${GLYPH_ATTRS}/>`;
2249
+ return `<circle cx="${cx}" cy="${round6(top + 22)}" r="22" ${GLYPH_ATTRS}/>`;
1987
2250
  }
1988
2251
  if (n.kind === "undeveloped") {
1989
- const pts2 = `${cx},${top} ${round3(cx + 24)},${round3(top + 24)} ${cx},${round3(top + 48)} ${round3(cx - 24)},${round3(top + 24)}`;
2252
+ const pts2 = `${cx},${top} ${round6(cx + 24)},${round6(top + 24)} ${cx},${round6(top + 48)} ${round6(cx - 24)},${round6(top + 24)}`;
1990
2253
  return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
1991
2254
  }
1992
2255
  if (n.kind === "house") {
1993
- const yB = round3(top + 40);
1994
- const eave = round3(top + 16);
1995
- const pts2 = `${round3(cx - 22)},${yB} ${round3(cx - 22)},${eave} ${cx},${top} ${round3(cx + 22)},${eave} ${round3(cx + 22)},${yB}`;
2256
+ const yB = round6(top + 40);
2257
+ const eave = round6(top + 16);
2258
+ const pts2 = `${round6(cx - 22)},${yB} ${round6(cx - 22)},${eave} ${cx},${top} ${round6(cx + 22)},${eave} ${round6(cx + 22)},${yB}`;
1996
2259
  return `<polygon points="${pts2}" ${GLYPH_ATTRS}/>`;
1997
2260
  }
1998
2261
  if (n.kind === "conditioning") {
1999
- return `<ellipse cx="${cx}" cy="${round3(top + 16)}" rx="${round3(n.glyphW / 2)}" ry="16" ${GLYPH_ATTRS}/>`;
2262
+ return `<ellipse cx="${cx}" cy="${round6(top + 16)}" rx="${round6(n.glyphW / 2)}" ry="16" ${GLYPH_ATTRS}/>`;
2000
2263
  }
2001
- const pts = `${cx},${top} ${round3(cx + 22)},${round3(top + 35)} ${round3(cx - 22)},${round3(top + 35)}`;
2264
+ const pts = `${cx},${top} ${round6(cx + 22)},${round6(top + 35)} ${round6(cx - 22)},${round6(top + 35)}`;
2002
2265
  return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
2003
2266
  }
2004
2267
  function nodeSvg(n) {
2005
2268
  const pieces = [`<title>${xmlEscape(n.title)}</title>`, eventGlyph(n)];
2006
2269
  if (n.code !== null && n.kind !== "intermediate") {
2007
2270
  pieces.push(
2008
- `<text x="${n.cx}" y="${round3(n.top + n.glyphH / 2 + CODE_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${CODE_FONT}" fill="${LABEL_FILL2}">${xmlEscape(n.code)}</text>`
2271
+ `<text x="${n.cx}" y="${round6(n.top + n.glyphH / 2 + CODE_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${CODE_FONT}" fill="${LABEL_FILL2}">${xmlEscape(n.code)}</text>`
2009
2272
  );
2010
2273
  }
2011
2274
  if (n.labelLines.length > 0) {
2012
- const firstBaseline = n.labelTop === null ? round3(n.top + 19) : round3(n.labelTop + 10);
2013
- const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round3(firstBaseline + i * FT_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
2275
+ const firstBaseline = n.labelTop === null ? round6(n.top + 19) : round6(n.labelTop + 10);
2276
+ const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round6(firstBaseline + i * FT_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
2014
2277
  pieces.push(
2015
2278
  `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${FT_LABEL_FONT}" fill="${LABEL_FILL2}">${tspans}</text>`
2016
2279
  );
@@ -2019,28 +2282,28 @@ function nodeSvg(n) {
2019
2282
  return `<g data-node-id="e${n.eventId}"${instance}>${pieces.join("")}</g>`;
2020
2283
  }
2021
2284
  function orBodyPath(cx, top, yB) {
2022
- return `M ${round3(cx - 22)} ${yB} Q ${cx} ${round3(yB - 14)} ${round3(cx + 22)} ${yB} Q ${round3(cx + 22)} ${round3(top + 14)} ${cx} ${top} Q ${round3(cx - 22)} ${round3(top + 14)} ${round3(cx - 22)} ${yB} Z`;
2285
+ return `M ${round6(cx - 22)} ${yB} Q ${cx} ${round6(yB - 14)} ${round6(cx + 22)} ${yB} Q ${round6(cx + 22)} ${round6(top + 14)} ${cx} ${top} Q ${round6(cx - 22)} ${round6(top + 14)} ${round6(cx - 22)} ${yB} Z`;
2023
2286
  }
2024
2287
  function gateGlyph(g) {
2025
2288
  const cx = g.cx;
2026
2289
  const top = g.top;
2027
- const yB = round3(top + 36);
2290
+ const yB = round6(top + 36);
2028
2291
  if (g.type === "and") {
2029
- const d = `M ${round3(cx - 22)} ${yB} L ${round3(cx - 22)} ${round3(yB - 14)} A 22 22 0 0 1 ${round3(cx + 22)} ${round3(yB - 14)} L ${round3(cx + 22)} ${yB} Z`;
2292
+ const d = `M ${round6(cx - 22)} ${yB} L ${round6(cx - 22)} ${round6(yB - 14)} A 22 22 0 0 1 ${round6(cx + 22)} ${round6(yB - 14)} L ${round6(cx + 22)} ${yB} Z`;
2030
2293
  return `<path d="${d}" ${GLYPH_ATTRS}/>`;
2031
2294
  }
2032
2295
  if (g.type === "or") {
2033
2296
  return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/>`;
2034
2297
  }
2035
2298
  if (g.type === "xor") {
2036
- const arc = `M ${round3(cx - 22)} ${round3(yB + 5)} Q ${cx} ${round3(yB - 9)} ${round3(cx + 22)} ${round3(yB + 5)}`;
2299
+ const arc = `M ${round6(cx - 22)} ${round6(yB + 5)} Q ${cx} ${round6(yB - 9)} ${round6(cx + 22)} ${round6(yB + 5)}`;
2037
2300
  return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><path d="${arc}" ${GLYPH_ATTRS}/>`;
2038
2301
  }
2039
2302
  if (g.type === "inhibit") {
2040
- const pts = `${cx},${top} ${round3(cx + 18)},${round3(top + 12)} ${round3(cx + 18)},${round3(top + 34)} ${cx},${round3(top + 46)} ${round3(cx - 18)},${round3(top + 34)} ${round3(cx - 18)},${round3(top + 12)}`;
2303
+ const pts = `${cx},${top} ${round6(cx + 18)},${round6(top + 12)} ${round6(cx + 18)},${round6(top + 34)} ${cx},${round6(top + 46)} ${round6(cx - 18)},${round6(top + 34)} ${round6(cx - 18)},${round6(top + 12)}`;
2041
2304
  return `<polygon points="${pts}" ${GLYPH_ATTRS}/>`;
2042
2305
  }
2043
- return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><text x="${cx}" y="${round3(yB - 10)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL2}">${xmlEscape(g.voteText ?? "")}</text>`;
2306
+ return `<path d="${orBodyPath(cx, top, yB)}" ${GLYPH_ATTRS}/><text x="${cx}" y="${round6(yB - 10)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL2}">${xmlEscape(g.voteText ?? "")}</text>`;
2044
2307
  }
2045
2308
  function gateSvg(g) {
2046
2309
  return `<g data-node-id="g${g.gateId}"><title>${xmlEscape(g.title)}</title>${gateGlyph(g)}</g>`;
@@ -2053,34 +2316,34 @@ function elementSvg2(el) {
2053
2316
  }
2054
2317
  var MINI_ATTRS = `fill="transparent" stroke="${GLYPH_STROKE2}" stroke-width="1.5"`;
2055
2318
  function miniEventSwatch(kind, x, y) {
2056
- const cx = round3(x + LEGEND_SWATCH_W / 2);
2319
+ const cx = round6(x + LEGEND_SWATCH_W / 2);
2057
2320
  if (kind === "intermediate") {
2058
- return `<rect x="${round3(cx - 7)}" y="${round3(y - 4.5)}" width="14" height="9" rx="1" ${MINI_ATTRS}/>`;
2321
+ return `<rect x="${round6(cx - 7)}" y="${round6(y - 4.5)}" width="14" height="9" rx="1" ${MINI_ATTRS}/>`;
2059
2322
  }
2060
2323
  if (kind === "basic") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS}/>`;
2061
2324
  if (kind === "undeveloped") {
2062
- return `<polygon points="${cx},${round3(y - 7)} ${round3(cx + 7)},${y} ${cx},${round3(y + 7)} ${round3(cx - 7)},${y}" ${MINI_ATTRS}/>`;
2325
+ return `<polygon points="${cx},${round6(y - 7)} ${round6(cx + 7)},${y} ${cx},${round6(y + 7)} ${round6(cx - 7)},${y}" ${MINI_ATTRS}/>`;
2063
2326
  }
2064
2327
  if (kind === "house") {
2065
- return `<polygon points="${round3(cx - 6)},${round3(y + 5.5)} ${round3(cx - 6)},${round3(y - 1)} ${cx},${round3(y - 5.5)} ${round3(cx + 6)},${round3(y - 1)} ${round3(cx + 6)},${round3(y + 5.5)}" ${MINI_ATTRS}/>`;
2328
+ return `<polygon points="${round6(cx - 6)},${round6(y + 5.5)} ${round6(cx - 6)},${round6(y - 1)} ${cx},${round6(y - 5.5)} ${round6(cx + 6)},${round6(y - 1)} ${round6(cx + 6)},${round6(y + 5.5)}" ${MINI_ATTRS}/>`;
2066
2329
  }
2067
2330
  if (kind === "conditioning") return `<ellipse cx="${cx}" cy="${y}" rx="9" ry="5.5" ${MINI_ATTRS}/>`;
2068
- return `<polygon points="${cx},${round3(y - 5)} ${round3(cx + 6)},${round3(y + 5)} ${round3(cx - 6)},${round3(y + 5)}" ${MINI_ATTRS}/>`;
2331
+ return `<polygon points="${cx},${round6(y - 5)} ${round6(cx + 6)},${round6(y + 5)} ${round6(cx - 6)},${round6(y + 5)}" ${MINI_ATTRS}/>`;
2069
2332
  }
2070
2333
  function miniOrPath(cx, y) {
2071
- return `M ${round3(cx - 7)} ${round3(y + 5.5)} Q ${cx} ${round3(y + 1)} ${round3(cx + 7)} ${round3(y + 5.5)} Q ${round3(cx + 7)} ${round3(y - 1.5)} ${cx} ${round3(y - 5.5)} Q ${round3(cx - 7)} ${round3(y - 1.5)} ${round3(cx - 7)} ${round3(y + 5.5)} Z`;
2334
+ return `M ${round6(cx - 7)} ${round6(y + 5.5)} Q ${cx} ${round6(y + 1)} ${round6(cx + 7)} ${round6(y + 5.5)} Q ${round6(cx + 7)} ${round6(y - 1.5)} ${cx} ${round6(y - 5.5)} Q ${round6(cx - 7)} ${round6(y - 1.5)} ${round6(cx - 7)} ${round6(y + 5.5)} Z`;
2072
2335
  }
2073
2336
  function miniGateSwatch(type, x, y) {
2074
- const cx = round3(x + LEGEND_SWATCH_W / 2);
2337
+ const cx = round6(x + LEGEND_SWATCH_W / 2);
2075
2338
  if (type === "and") {
2076
- const d = `M ${round3(cx - 7)} ${round3(y + 5.5)} L ${round3(cx - 7)} ${round3(y + 1.5)} A 7 7 0 0 1 ${round3(cx + 7)} ${round3(y + 1.5)} L ${round3(cx + 7)} ${round3(y + 5.5)} Z`;
2339
+ const d = `M ${round6(cx - 7)} ${round6(y + 5.5)} L ${round6(cx - 7)} ${round6(y + 1.5)} A 7 7 0 0 1 ${round6(cx + 7)} ${round6(y + 1.5)} L ${round6(cx + 7)} ${round6(y + 5.5)} Z`;
2077
2340
  return `<path d="${d}" ${MINI_ATTRS}/>`;
2078
2341
  }
2079
2342
  if (type === "xor") {
2080
- return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/><path d="M ${round3(cx - 7)} ${round3(y + 7.5)} Q ${cx} ${round3(y + 3)} ${round3(cx + 7)} ${round3(y + 7.5)}" ${MINI_ATTRS}/>`;
2343
+ return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/><path d="M ${round6(cx - 7)} ${round6(y + 7.5)} Q ${cx} ${round6(y + 3)} ${round6(cx + 7)} ${round6(y + 7.5)}" ${MINI_ATTRS}/>`;
2081
2344
  }
2082
2345
  if (type === "inhibit") {
2083
- return `<polygon points="${cx},${round3(y - 5.5)} ${round3(cx + 4.5)},${round3(y - 2.5)} ${round3(cx + 4.5)},${round3(y + 2.5)} ${cx},${round3(y + 5.5)} ${round3(cx - 4.5)},${round3(y + 2.5)} ${round3(cx - 4.5)},${round3(y - 2.5)}" ${MINI_ATTRS}/>`;
2346
+ return `<polygon points="${cx},${round6(y - 5.5)} ${round6(cx + 4.5)},${round6(y - 2.5)} ${round6(cx + 4.5)},${round6(y + 2.5)} ${cx},${round6(y + 5.5)} ${round6(cx - 4.5)},${round6(y + 2.5)} ${round6(cx - 4.5)},${round6(y - 2.5)}" ${MINI_ATTRS}/>`;
2084
2347
  }
2085
2348
  return `<path d="${miniOrPath(cx, y)}" ${MINI_ATTRS}/>`;
2086
2349
  }
@@ -2209,7 +2472,7 @@ var TWIG_W = 1.5;
2209
2472
  var TWIG_OP = 0.75;
2210
2473
  var SUB_W = 1.2;
2211
2474
  var SUB_OP = 0.7;
2212
- var round4 = (n) => Math.round(n * 100) / 100;
2475
+ var round7 = (n) => Math.round(n * 100) / 100;
2213
2476
  function arrowHead2(tipX, tipY, ux, uy, opacity) {
2214
2477
  const LEN = 9;
2215
2478
  const HALF_W = 4.5;
@@ -2218,14 +2481,14 @@ function arrowHead2(tipX, tipY, ux, uy, opacity) {
2218
2481
  const px = -uy;
2219
2482
  const py = ux;
2220
2483
  const points = [
2221
- `${round4(tipX)},${round4(tipY)}`,
2222
- `${round4(bx + px * HALF_W)},${round4(by + py * HALF_W)}`,
2223
- `${round4(bx - px * HALF_W)},${round4(by - py * HALF_W)}`
2484
+ `${round7(tipX)},${round7(tipY)}`,
2485
+ `${round7(bx + px * HALF_W)},${round7(by + py * HALF_W)}`,
2486
+ `${round7(bx - px * HALF_W)},${round7(by - py * HALF_W)}`
2224
2487
  ].join(" ");
2225
2488
  return `<polygon points="${points}" fill="${EDGE_INK4}" fill-opacity="${opacity}"/>`;
2226
2489
  }
2227
2490
  function lineEl(x1, y1, x2, y2, w, op) {
2228
- return `<line x1="${round4(x1)}" y1="${round4(y1)}" x2="${round4(x2)}" y2="${round4(y2)}" stroke="${EDGE_INK4}" stroke-width="${w}" stroke-opacity="${op}"/>`;
2491
+ return `<line x1="${round7(x1)}" y1="${round7(y1)}" x2="${round7(x2)}" y2="${round7(y2)}" stroke="${EDGE_INK4}" stroke-width="${w}" stroke-opacity="${op}"/>`;
2229
2492
  }
2230
2493
  function fishboneSvg(input, opts = {}) {
2231
2494
  validateIds(input);
@@ -2306,7 +2569,7 @@ function fishboneSvg(input, opts = {}) {
2306
2569
  { length: n },
2307
2570
  (_, k) => up ? spineY - (e + twigGap + (n - 1 - k) * lineH) : spineY + e + twigGap + ascent + k * lineH
2308
2571
  );
2309
- const textBlock = (anchor, x, ys, lines) => `<text text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL3}">` + lines.map((line, i) => `<tspan x="${round4(x)}" y="${round4(ys[i])}">${xmlEscape(line)}</tspan>`).join("") + `</text>`;
2572
+ const textBlock = (anchor, x, ys, lines) => `<text text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" fill="${LABEL_FILL3}">` + lines.map((line, i) => `<tspan x="${round7(x)}" y="${round7(ys[i])}">${xmlEscape(line)}</tspan>`).join("") + `</text>`;
2310
2573
  const parts = [];
2311
2574
  {
2312
2575
  const body = [lineEl(tailX + dx, spineY, headLeft + dx, spineY, SPINE_W, SPINE_OP)];
@@ -2321,7 +2584,7 @@ function fishboneSvg(input, opts = {}) {
2321
2584
  if (arrows) body.push(arrowHead2(ax, spineY, COS60, -sgn * SIN60, BONE_OP));
2322
2585
  const boxTop = bone.up ? spineY - bone.B - CAT_GAP - bone.boxH : spineY + bone.B + CAT_GAP;
2323
2586
  body.push(
2324
- `<rect x="${round4(tipX - bone.boxW / 2)}" y="${round4(boxTop)}" width="${round4(bone.boxW)}" height="${round4(bone.boxH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="1.5"/>`
2587
+ `<rect x="${round7(tipX - bone.boxW / 2)}" y="${round7(boxTop)}" width="${round7(bone.boxW)}" height="${round7(bone.boxH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="1.5"/>`
2325
2588
  );
2326
2589
  body.push(textBlock("middle", tipX, centeredYs(boxTop + bone.boxH / 2, bone.catLines.length), bone.catLines));
2327
2590
  parts.push(
@@ -2351,7 +2614,7 @@ function fishboneSvg(input, opts = {}) {
2351
2614
  {
2352
2615
  const x = headLeft + dx;
2353
2616
  parts.push(
2354
- `<g data-node-id="head"><title>${xmlEscape(input.effectLabel)}</title><rect x="${round4(x)}" y="${round4(spineY - headH / 2)}" width="${round4(headW)}" height="${round4(headH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="2"/>` + textBlock("middle", x + headW / 2, centeredYs(spineY, effLines.length), effLines) + `</g>`
2617
+ `<g data-node-id="head"><title>${xmlEscape(input.effectLabel)}</title><rect x="${round7(x)}" y="${round7(spineY - headH / 2)}" width="${round7(headW)}" height="${round7(headH)}" rx="2" fill="transparent" stroke="${BOX_STROKE}" stroke-width="2"/>` + textBlock("middle", x + headW / 2, centeredYs(spineY, effLines.length), effLines) + `</g>`
2355
2618
  );
2356
2619
  }
2357
2620
  const anySubs = input.categories.some((c) => c.causes.some((k) => k.subCauses.length > 0));
@@ -2565,13 +2828,13 @@ function pedigreeIssues(input) {
2565
2828
  );
2566
2829
  }
2567
2830
  }
2568
- const GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
2831
+ const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
2569
2832
  "duplicate-id",
2570
2833
  "unknown-partner",
2571
2834
  "unknown-sibship-mating",
2572
2835
  "unknown-child"
2573
2836
  ]);
2574
- if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
2837
+ if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
2575
2838
  for (const s of sibships) {
2576
2839
  const mating = matingById.get(s.matingId);
2577
2840
  if (mating === void 0) continue;
@@ -2619,7 +2882,7 @@ var PED_DESCENT_ID_BASE = 2e6;
2619
2882
  var PED_SIBBAR_ID_BASE = 3e6;
2620
2883
  var PED_RISER_ID_BASE = 4e6;
2621
2884
  var PED_TWINBAR_ID_BASE = 5e6;
2622
- var round5 = (n) => Math.round(n * 100) / 100;
2885
+ var round8 = (n) => Math.round(n * 100) / 100;
2623
2886
  function shapeForSex2(sex) {
2624
2887
  if (sex === "male") return "square";
2625
2888
  if (sex === "female") return "circle";
@@ -2858,8 +3121,8 @@ function computePedigreeLayout(input, opts = {}) {
2858
3121
  rowHeight[row] = Math.max(rowHeight[row], h);
2859
3122
  }
2860
3123
  const rowTop = [PADDING5];
2861
- for (let r = 0; r < rowCount - 1; r++) {
2862
- rowTop.push(rowTop[r] + rowHeight[r] + CORRIDOR2);
3124
+ for (let r2 = 0; r2 < rowCount - 1; r2++) {
3125
+ rowTop.push(rowTop[r2] + rowHeight[r2] + CORRIDOR2);
2863
3126
  }
2864
3127
  const glyphCyOfRow = (row) => rowTop[row] + PED_GLYPH / 2;
2865
3128
  const addressByIndividual = /* @__PURE__ */ new Map();
@@ -2885,8 +3148,8 @@ function computePedigreeLayout(input, opts = {}) {
2885
3148
  nodes.push({
2886
3149
  individualId: ind.id,
2887
3150
  shape: shapeForSex2(ind.sex),
2888
- cx: round5(placed.cx),
2889
- cy: round5(cy),
3151
+ cx: round8(placed.cx),
3152
+ cy: round8(cy),
2890
3153
  size: PED_GLYPH,
2891
3154
  deceased: ind.deceased,
2892
3155
  carrier: ind.carrier,
@@ -2896,7 +3159,7 @@ function computePedigreeLayout(input, opts = {}) {
2896
3159
  labelLines: lines,
2897
3160
  // Labels sit BELOW the row's serial-union dip band (rowDipBand) so a hub's bridge dips
2898
3161
  // never cross a label box.
2899
- labelTop: round5(cy + PED_GLYPH / 2 + PED_LABEL_GAP + rowDipBand[row]),
3162
+ labelTop: round8(cy + PED_GLYPH / 2 + PED_LABEL_GAP + rowDipBand[row]),
2900
3163
  addressLabel: address,
2901
3164
  title: individualTitle(ind, address, conditionLabelById, titleLabels)
2902
3165
  });
@@ -2939,23 +3202,23 @@ function computePedigreeLayout(input, opts = {}) {
2939
3202
  edgeId: PED_MATING_ID_BASE + m.id,
2940
3203
  kind: "mating-elbow",
2941
3204
  points: [
2942
- { x: round5(hubStubX), y: round5(glyphBottom) },
2943
- { x: round5(hubStubX), y: round5(dipY) },
2944
- { x: round5(spouseStubX), y: round5(dipY) },
2945
- { x: round5(spouseStubX), y: round5(glyphBottom) }
3205
+ { x: round8(hubStubX), y: round8(glyphBottom) },
3206
+ { x: round8(hubStubX), y: round8(dipY) },
3207
+ { x: round8(spouseStubX), y: round8(dipY) },
3208
+ { x: round8(spouseStubX), y: round8(glyphBottom) }
2946
3209
  ],
2947
3210
  consanguineous: m.consanguineous,
2948
3211
  title
2949
3212
  });
2950
- matingMidpoint.set(m.id, { x: round5((hubStubX + spouseStubX) / 2), y: round5(dipY) });
3213
+ matingMidpoint.set(m.id, { x: round8((hubStubX + spouseStubX) / 2), y: round8(dipY) });
2951
3214
  } else if (ay === by) {
2952
3215
  const y = ay;
2953
3216
  elements.push({
2954
3217
  edgeId: PED_MATING_ID_BASE + m.id,
2955
3218
  kind: "mating",
2956
3219
  points: [
2957
- { x: round5(lx), y: round5(y) },
2958
- { x: round5(rx), y: round5(y) }
3220
+ { x: round8(lx), y: round8(y) },
3221
+ { x: round8(rx), y: round8(y) }
2959
3222
  ],
2960
3223
  consanguineous: m.consanguineous,
2961
3224
  title
@@ -2965,7 +3228,7 @@ function computePedigreeLayout(input, opts = {}) {
2965
3228
  const channelRight = cxOf(rightId) - nodeHalfWidth(individualById.get(rightId));
2966
3229
  const barCenter = matingBarCenter.get(m.id);
2967
3230
  const originX = barCenter !== void 0 && barCenter >= channelLeft && barCenter <= channelRight ? barCenter : midX;
2968
- matingMidpoint.set(m.id, { x: round5(originX), y: round5(y) });
3231
+ matingMidpoint.set(m.id, { x: round8(originX), y: round8(y) });
2969
3232
  } else {
2970
3233
  const upId = ay <= by ? aId : bId;
2971
3234
  const downId = ay <= by ? bId : aId;
@@ -2984,32 +3247,32 @@ function computePedigreeLayout(input, opts = {}) {
2984
3247
  edgeId: PED_MATING_ID_BASE + m.id,
2985
3248
  kind: "mating-elbow",
2986
3249
  points: [
2987
- { x: round5(upX + (downRight ? -GLYPH_HALF : GLYPH_HALF)), y: round5(upY) },
2988
- { x: round5(upExitX), y: round5(upY) },
2989
- { x: round5(upExitX), y: round5(laneY) },
2990
- { x: round5(downFarX), y: round5(laneY) },
2991
- { x: round5(downFarX), y: round5(downY) },
2992
- { x: round5(downSideX), y: round5(downY) }
3250
+ { x: round8(upX + (downRight ? -GLYPH_HALF : GLYPH_HALF)), y: round8(upY) },
3251
+ { x: round8(upExitX), y: round8(upY) },
3252
+ { x: round8(upExitX), y: round8(laneY) },
3253
+ { x: round8(downFarX), y: round8(laneY) },
3254
+ { x: round8(downFarX), y: round8(downY) },
3255
+ { x: round8(downSideX), y: round8(downY) }
2993
3256
  ],
2994
3257
  consanguineous: m.consanguineous,
2995
3258
  title
2996
3259
  });
2997
- matingMidpoint.set(m.id, { x: round5(downFarX), y: round5(downY) });
3260
+ matingMidpoint.set(m.id, { x: round8(downFarX), y: round8(downY) });
2998
3261
  } else {
2999
3262
  const elbowX = (upX + downX) / 2;
3000
3263
  elements.push({
3001
3264
  edgeId: PED_MATING_ID_BASE + m.id,
3002
3265
  kind: "mating-elbow",
3003
3266
  points: [
3004
- { x: round5(upX + (downX >= upX ? GLYPH_HALF : -GLYPH_HALF)), y: round5(upY) },
3005
- { x: round5(elbowX), y: round5(upY) },
3006
- { x: round5(elbowX), y: round5(downY) },
3007
- { x: round5(downX + (upX >= downX ? GLYPH_HALF : -GLYPH_HALF)), y: round5(downY) }
3267
+ { x: round8(upX + (downX >= upX ? GLYPH_HALF : -GLYPH_HALF)), y: round8(upY) },
3268
+ { x: round8(elbowX), y: round8(upY) },
3269
+ { x: round8(elbowX), y: round8(downY) },
3270
+ { x: round8(downX + (upX >= downX ? GLYPH_HALF : -GLYPH_HALF)), y: round8(downY) }
3008
3271
  ],
3009
3272
  consanguineous: m.consanguineous,
3010
3273
  title
3011
3274
  });
3012
- matingMidpoint.set(m.id, { x: round5(elbowX), y: round5(downY) });
3275
+ matingMidpoint.set(m.id, { x: round8(elbowX), y: round8(downY) });
3013
3276
  }
3014
3277
  }
3015
3278
  }
@@ -3031,13 +3294,13 @@ function computePedigreeLayout(input, opts = {}) {
3031
3294
  const barCenterX = (barLeft + barRight) / 2;
3032
3295
  const bendY = barY - 2 - lane % 3 * 3;
3033
3296
  const descentPoints = Math.abs(mid.x - barCenterX) < 0.01 ? [
3034
- { x: round5(mid.x), y: round5(mid.y) },
3035
- { x: round5(mid.x), y: round5(barY) }
3297
+ { x: round8(mid.x), y: round8(mid.y) },
3298
+ { x: round8(mid.x), y: round8(barY) }
3036
3299
  ] : [
3037
- { x: round5(mid.x), y: round5(mid.y) },
3038
- { x: round5(mid.x), y: round5(bendY) },
3039
- { x: round5(barCenterX), y: round5(bendY) },
3040
- { x: round5(barCenterX), y: round5(barY) }
3300
+ { x: round8(mid.x), y: round8(mid.y) },
3301
+ { x: round8(mid.x), y: round8(bendY) },
3302
+ { x: round8(barCenterX), y: round8(bendY) },
3303
+ { x: round8(barCenterX), y: round8(barY) }
3041
3304
  ];
3042
3305
  elements.push({
3043
3306
  edgeId: PED_DESCENT_ID_BASE + s.id,
@@ -3052,8 +3315,8 @@ function computePedigreeLayout(input, opts = {}) {
3052
3315
  edgeId: PED_SIBBAR_ID_BASE + s.id,
3053
3316
  kind: "sibship-bar",
3054
3317
  points: [
3055
- { x: round5(barLeft), y: round5(barY) },
3056
- { x: round5(barRight), y: round5(barY) }
3318
+ { x: round8(barLeft), y: round8(barY) },
3319
+ { x: round8(barRight), y: round8(barY) }
3057
3320
  ],
3058
3321
  consanguineous: false,
3059
3322
  title: titleLabels.sibship
@@ -3074,8 +3337,8 @@ function computePedigreeLayout(input, opts = {}) {
3074
3337
  edgeId: PED_RISER_ID_BASE + childId,
3075
3338
  kind: "riser",
3076
3339
  points: [
3077
- { x: round5(cx), y: round5(barY) },
3078
- { x: round5(cx), y: round5(childTop) }
3340
+ { x: round8(cx), y: round8(barY) },
3341
+ { x: round8(cx), y: round8(childTop) }
3079
3342
  ],
3080
3343
  consanguineous: false,
3081
3344
  title: titleLabels.sibship
@@ -3089,15 +3352,15 @@ function computePedigreeLayout(input, opts = {}) {
3089
3352
  if (!emittedTwinOrdinals.has(tg.ordinal)) {
3090
3353
  emittedTwinOrdinals.add(tg.ordinal);
3091
3354
  if (tg.zygosity === "unknown") {
3092
- unknownTwinJunctions.push({ x: round5(junctionX), y: round5(junctionY) });
3355
+ unknownTwinJunctions.push({ x: round8(junctionX), y: round8(junctionY) });
3093
3356
  }
3094
3357
  elements.push({
3095
3358
  edgeId: PED_RISER_ID_BASE + childId,
3096
3359
  // anchored on the first member for a stable id
3097
3360
  kind: "riser",
3098
3361
  points: [
3099
- { x: round5(junctionX), y: round5(barY) },
3100
- { x: round5(junctionX), y: round5(junctionY) }
3362
+ { x: round8(junctionX), y: round8(barY) },
3363
+ { x: round8(junctionX), y: round8(junctionY) }
3101
3364
  ],
3102
3365
  consanguineous: false,
3103
3366
  title: titleLabels.twins[tg.zygosity]
@@ -3108,8 +3371,8 @@ function computePedigreeLayout(input, opts = {}) {
3108
3371
  edgeId: PED_TWINBAR_ID_BASE + s.id * 100 + tg.ordinal,
3109
3372
  kind: "twin-bar",
3110
3373
  points: [
3111
- { x: round5(Math.min(...memberXs)), y: round5(tieY) },
3112
- { x: round5(Math.max(...memberXs)), y: round5(tieY) }
3374
+ { x: round8(Math.min(...memberXs)), y: round8(tieY) },
3375
+ { x: round8(Math.max(...memberXs)), y: round8(tieY) }
3113
3376
  ],
3114
3377
  consanguineous: false,
3115
3378
  title: titleLabels.twins.mz
@@ -3117,12 +3380,12 @@ function computePedigreeLayout(input, opts = {}) {
3117
3380
  }
3118
3381
  }
3119
3382
  const horizontalThenDown = Math.abs(cx - junctionX) < 0.01 ? [
3120
- { x: round5(cx), y: round5(junctionY) },
3121
- { x: round5(cx), y: round5(childTop) }
3383
+ { x: round8(cx), y: round8(junctionY) },
3384
+ { x: round8(cx), y: round8(childTop) }
3122
3385
  ] : [
3123
- { x: round5(junctionX), y: round5(junctionY) },
3124
- { x: round5(cx), y: round5(junctionY) },
3125
- { x: round5(cx), y: round5(childTop) }
3386
+ { x: round8(junctionX), y: round8(junctionY) },
3387
+ { x: round8(cx), y: round8(junctionY) },
3388
+ { x: round8(cx), y: round8(childTop) }
3126
3389
  ];
3127
3390
  elements.push({
3128
3391
  edgeId: PED_RISER_ID_BASE + childId,
@@ -3150,7 +3413,7 @@ function computePedigreeLayout(input, opts = {}) {
3150
3413
  }
3151
3414
  const generations = [];
3152
3415
  for (let row = 0; row < rowCount; row++) {
3153
- generations.push({ roman: romanNumeral(row + 1), y: round5(glyphCyOfRow(row)) });
3416
+ generations.push({ roman: romanNumeral(row + 1), y: round8(glyphCyOfRow(row)) });
3154
3417
  }
3155
3418
  const ZYGOSITY_ORDER = ["mz", "dz", "unknown"];
3156
3419
  const twinZygositiesUsed = ZYGOSITY_ORDER.filter((z) => twinZygositiesUsedSet.has(z));
@@ -3174,15 +3437,15 @@ var EDGE_INK5 = "#71717a";
3174
3437
  var GLYPH_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="2"`;
3175
3438
  var CONSANG_GAP = 3;
3176
3439
  var ZYGOSITIES = ["mz", "dz", "unknown"];
3177
- var round6 = (n) => Math.round(n * 100) / 100;
3440
+ var round9 = (n) => Math.round(n * 100) / 100;
3178
3441
  function glyphOutline(shape, cx, cy, half) {
3179
3442
  if (shape === "square") {
3180
- return `<rect x="${round6(cx - half)}" y="${round6(cy - half)}" width="${half * 2}" height="${half * 2}" ${GLYPH_ATTRS2}/>`;
3443
+ return `<rect x="${round9(cx - half)}" y="${round9(cy - half)}" width="${half * 2}" height="${half * 2}" ${GLYPH_ATTRS2}/>`;
3181
3444
  }
3182
3445
  if (shape === "circle") {
3183
3446
  return `<circle cx="${cx}" cy="${cy}" r="${half}" ${GLYPH_ATTRS2}/>`;
3184
3447
  }
3185
- return `<polygon points="${cx},${round6(cy - half)} ${round6(cx + half)},${cy} ${cx},${round6(cy + half)} ${round6(cx - half)},${cy}" ${GLYPH_ATTRS2}/>`;
3448
+ return `<polygon points="${cx},${round9(cy - half)} ${round9(cx + half)},${cy} ${cx},${round9(cy + half)} ${round9(cx - half)},${cy}" ${GLYPH_ATTRS2}/>`;
3186
3449
  }
3187
3450
  function glyphVerticalExtentAt(shape, cy, half, dx) {
3188
3451
  const ax = Math.min(Math.abs(dx), half);
@@ -3208,8 +3471,8 @@ function fillPartitions(n, half, inkByCondition) {
3208
3471
  for (let s = 0; s <= SAMPLES; s++) {
3209
3472
  const x = left + sliceW * s / SAMPLES;
3210
3473
  const [yt, yb] = glyphVerticalExtentAt(n.shape, cy, half, x - cx);
3211
- top.push(`${round6(x)},${round6(yt)}`);
3212
- bottom.push(`${round6(x)},${round6(yb)}`);
3474
+ top.push(`${round9(x)},${round9(yt)}`);
3475
+ bottom.push(`${round9(x)},${round9(yb)}`);
3213
3476
  }
3214
3477
  const pts = [...top, ...bottom.reverse()].join(" ");
3215
3478
  const ink = inkByCondition.get(id) ?? GLYPH_STROKE3;
@@ -3223,24 +3486,24 @@ function carrierDot(n) {
3223
3486
  function deceasedSlash(n, half) {
3224
3487
  if (!n.deceased) return "";
3225
3488
  const ext = half + 4;
3226
- return `<line x1="${round6(n.cx - ext)}" y1="${round6(n.cy - ext)}" x2="${round6(n.cx + ext)}" y2="${round6(n.cy + ext)}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
3489
+ return `<line x1="${round9(n.cx - ext)}" y1="${round9(n.cy - ext)}" x2="${round9(n.cx + ext)}" y2="${round9(n.cy + ext)}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
3227
3490
  }
3228
3491
  function probandArrow(n, half) {
3229
3492
  if (n.role === null) return "";
3230
- const tipX = round6(n.cx - half);
3231
- const tipY = round6(n.cy + half);
3232
- const tailX = round6(tipX - 16);
3233
- const tailY = round6(tipY + 16);
3493
+ const tipX = round9(n.cx - half);
3494
+ const tipY = round9(n.cy + half);
3495
+ const tailX = round9(tipX - 16);
3496
+ const tailY = round9(tipY + 16);
3234
3497
  const filled = n.role === "proband";
3235
3498
  const fill = filled ? GLYPH_STROKE3 : "transparent";
3236
3499
  const shaft = `<line x1="${tailX}" y1="${tailY}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="2"/>`;
3237
- const head = `<polygon points="${tipX},${tipY} ${round6(tipX - 8)},${round6(tipY + 2)} ${round6(tipX - 2)},${round6(tipY + 8)}" fill="${fill}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
3500
+ const head = `<polygon points="${tipX},${tipY} ${round9(tipX - 8)},${round9(tipY + 2)} ${round9(tipX - 2)},${round9(tipY + 8)}" fill="${fill}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
3238
3501
  return shaft + head;
3239
3502
  }
3240
3503
  function stillbirthMark(n, half) {
3241
3504
  if (!n.stillbirth) return "";
3242
- const y = round6(n.cy + half + PED_ADDRESS_FONT);
3243
- return `<text x="${round6(n.cx - half - 2)}" y="${y}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" font-weight="bold" fill="${LABEL_FILL4}">SB</text>`;
3505
+ const y = round9(n.cy + half + PED_ADDRESS_FONT);
3506
+ return `<text x="${round9(n.cx - half - 2)}" y="${y}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" font-weight="bold" fill="${LABEL_FILL4}">SB</text>`;
3244
3507
  }
3245
3508
  function nodeSvg2(n, inkByCondition) {
3246
3509
  const half = n.size / 2;
@@ -3254,13 +3517,13 @@ function nodeSvg2(n, inkByCondition) {
3254
3517
  probandArrow(n, half),
3255
3518
  stillbirthMark(n, half)
3256
3519
  ];
3257
- const addressY = round6(n.labelTop + PED_ADDRESS_FONT);
3520
+ const addressY = round9(n.labelTop + PED_ADDRESS_FONT);
3258
3521
  pieces.push(
3259
3522
  `<text x="${n.cx}" y="${addressY}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_ADDRESS_FONT}" fill="${LABEL_FILL4}">${xmlEscape(n.addressLabel)}</text>`
3260
3523
  );
3261
3524
  if (n.labelLines.length > 0) {
3262
- const firstBaseline = round6(n.labelTop + PED_LABEL_LINE_H + 10);
3263
- const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round6(firstBaseline + i * PED_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
3525
+ const firstBaseline = round9(n.labelTop + PED_LABEL_LINE_H + 10);
3526
+ const tspans = n.labelLines.map((line, i) => `<tspan x="${n.cx}" y="${round9(firstBaseline + i * PED_LABEL_LINE_H)}">${xmlEscape(line)}</tspan>`).join("");
3264
3527
  pieces.push(
3265
3528
  `<text text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" fill="${LABEL_FILL4}">${tspans}</text>`
3266
3529
  );
@@ -3275,7 +3538,7 @@ function elementSvg3(el) {
3275
3538
  const title = `<title>${xmlEscape(el.title)}</title>`;
3276
3539
  const stroke = `stroke="${EDGE_INK5}" stroke-width="1.5" stroke-opacity="0.75"`;
3277
3540
  const draw = (offsetY) => {
3278
- const shifted = pts.map((p) => ({ x: p.x, y: round6(p.y + offsetY) }));
3541
+ const shifted = pts.map((p) => ({ x: p.x, y: round9(p.y + offsetY) }));
3279
3542
  if (shifted.length === 2) {
3280
3543
  return `<line x1="${shifted[0].x}" y1="${shifted[0].y}" x2="${shifted[1].x}" y2="${shifted[1].y}" ${stroke}/>`;
3281
3544
  }
@@ -3286,51 +3549,51 @@ function elementSvg3(el) {
3286
3549
  }
3287
3550
  function unknownTwinMarks(layout) {
3288
3551
  return layout.unknownTwinJunctions.map(
3289
- (p) => `<text x="${round6(p.x + 6)}" y="${round6(p.y + 4)}" font-family="${FONT_FAMILY}" font-size="12" font-weight="bold" fill="${LABEL_FILL4}">?</text>`
3552
+ (p) => `<text x="${round9(p.x + 6)}" y="${round9(p.y + 4)}" font-family="${FONT_FAMILY}" font-size="12" font-weight="bold" fill="${LABEL_FILL4}">?</text>`
3290
3553
  ).join("");
3291
3554
  }
3292
3555
  var MINI_ATTRS2 = `fill="transparent" stroke="${GLYPH_STROKE3}" stroke-width="1.5"`;
3293
3556
  function miniShapeSwatch(shape, x, y) {
3294
- const cx = round6(x + LEGEND_SWATCH_W / 2);
3295
- if (shape === "square") return `<rect x="${round6(cx - 6)}" y="${round6(y - 6)}" width="12" height="12" ${MINI_ATTRS2}/>`;
3557
+ const cx = round9(x + LEGEND_SWATCH_W / 2);
3558
+ if (shape === "square") return `<rect x="${round9(cx - 6)}" y="${round9(y - 6)}" width="12" height="12" ${MINI_ATTRS2}/>`;
3296
3559
  if (shape === "circle") return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/>`;
3297
- return `<polygon points="${cx},${round6(y - 7)} ${round6(cx + 7)},${y} ${cx},${round6(y + 7)} ${round6(cx - 7)},${y}" ${MINI_ATTRS2}/>`;
3560
+ return `<polygon points="${cx},${round9(y - 7)} ${round9(cx + 7)},${y} ${cx},${round9(y + 7)} ${round9(cx - 7)},${y}" ${MINI_ATTRS2}/>`;
3298
3561
  }
3299
3562
  function miniSwatchCircle(filled, ink, x, y) {
3300
- const cx = round6(x + LEGEND_SWATCH_W / 2);
3563
+ const cx = round9(x + LEGEND_SWATCH_W / 2);
3301
3564
  return `<circle cx="${cx}" cy="${y}" r="6" fill="${filled ? ink : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
3302
3565
  }
3303
3566
  function miniCarrierSwatch(x, y) {
3304
- const cx = round6(x + LEGEND_SWATCH_W / 2);
3567
+ const cx = round9(x + LEGEND_SWATCH_W / 2);
3305
3568
  return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><circle cx="${cx}" cy="${y}" r="2" fill="${GLYPH_STROKE3}" stroke="none"/>`;
3306
3569
  }
3307
3570
  function miniDeceasedSwatch(x, y) {
3308
- const cx = round6(x + LEGEND_SWATCH_W / 2);
3309
- return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><line x1="${round6(cx - 7)}" y1="${round6(y - 7)}" x2="${round6(cx + 7)}" y2="${round6(y + 7)}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
3571
+ const cx = round9(x + LEGEND_SWATCH_W / 2);
3572
+ return `<circle cx="${cx}" cy="${y}" r="6" ${MINI_ATTRS2}/><line x1="${round9(cx - 7)}" y1="${round9(y - 7)}" x2="${round9(cx + 7)}" y2="${round9(y + 7)}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/>`;
3310
3573
  }
3311
3574
  function miniArrowSwatch(filled, x, y) {
3312
- const cx = round6(x + LEGEND_SWATCH_W / 2);
3313
- const tipX = round6(cx - 2);
3314
- const tipY = round6(y + 2);
3315
- return `<line x1="${round6(tipX - 8)}" y1="${round6(tipY + 8)}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/><polygon points="${tipX},${tipY} ${round6(tipX - 5)},${round6(tipY + 1)} ${round6(tipX - 1)},${round6(tipY + 5)}" fill="${filled ? GLYPH_STROKE3 : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1"/>`;
3575
+ const cx = round9(x + LEGEND_SWATCH_W / 2);
3576
+ const tipX = round9(cx - 2);
3577
+ const tipY = round9(y + 2);
3578
+ return `<line x1="${round9(tipX - 8)}" y1="${round9(tipY + 8)}" x2="${tipX}" y2="${tipY}" stroke="${GLYPH_STROKE3}" stroke-width="1.5"/><polygon points="${tipX},${tipY} ${round9(tipX - 5)},${round9(tipY + 1)} ${round9(tipX - 1)},${round9(tipY + 5)}" fill="${filled ? GLYPH_STROKE3 : "transparent"}" stroke="${GLYPH_STROKE3}" stroke-width="1"/>`;
3316
3579
  }
3317
3580
  function miniConsanguineousSwatch(x, y) {
3318
- const x1 = round6(x + 2);
3319
- const x2 = round6(x + LEGEND_SWATCH_W - 2);
3320
- return `<line x1="${x1}" y1="${round6(y - 1.5)}" x2="${x2}" y2="${round6(y - 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${x1}" y1="${round6(y + 1.5)}" x2="${x2}" y2="${round6(y + 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
3581
+ const x1 = round9(x + 2);
3582
+ const x2 = round9(x + LEGEND_SWATCH_W - 2);
3583
+ return `<line x1="${x1}" y1="${round9(y - 1.5)}" x2="${x2}" y2="${round9(y - 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${x1}" y1="${round9(y + 1.5)}" x2="${x2}" y2="${round9(y + 1.5)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
3321
3584
  }
3322
3585
  function miniTwinSwatch(zygosity, x, y) {
3323
- const cx = round6(x + LEGEND_SWATCH_W / 2);
3324
- const apexY = round6(y - 6);
3325
- const baseY = round6(y + 6);
3326
- const left = round6(cx - 6);
3327
- const right = round6(cx + 6);
3586
+ const cx = round9(x + LEGEND_SWATCH_W / 2);
3587
+ const apexY = round9(y - 6);
3588
+ const baseY = round9(y + 6);
3589
+ const left = round9(cx - 6);
3590
+ const right = round9(cx + 6);
3328
3591
  const stub = `<line x1="${cx}" y1="${apexY}" x2="${cx}" y2="${y}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${right}" y2="${y}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${left}" y1="${y}" x2="${left}" y2="${baseY}" stroke="${EDGE_INK5}" stroke-width="1.5"/><line x1="${right}" y1="${y}" x2="${right}" y2="${baseY}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
3329
3592
  if (zygosity === "mz") {
3330
- return stub + `<line x1="${left}" y1="${round6(y + 3)}" x2="${right}" y2="${round6(y + 3)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
3593
+ return stub + `<line x1="${left}" y1="${round9(y + 3)}" x2="${right}" y2="${round9(y + 3)}" stroke="${EDGE_INK5}" stroke-width="1.5"/>`;
3331
3594
  }
3332
3595
  if (zygosity === "unknown") {
3333
- return stub + `<text x="${round6(cx + 8)}" y="${round6(y + 3)}" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL4}">?</text>`;
3596
+ return stub + `<text x="${round9(cx + 8)}" y="${round9(y + 3)}" font-family="${FONT_FAMILY}" font-size="9" fill="${LABEL_FILL4}">?</text>`;
3334
3597
  }
3335
3598
  return stub;
3336
3599
  }
@@ -3343,7 +3606,7 @@ function pedigreeLayoutSvg(layout, opts = {}) {
3343
3606
  for (const n of layout.nodes) parts.push(nodeSvg2(n, inkByCondition));
3344
3607
  for (const g of layout.generations) {
3345
3608
  parts.push(
3346
- `<text x="${round6(16)}" y="${round6(g.y + PED_LABEL_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" font-weight="bold" fill="${LABEL_FILL4}">${xmlEscape(g.roman)}</text>`
3609
+ `<text x="${round9(16)}" y="${round9(g.y + PED_LABEL_FONT * 0.32)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PED_LABEL_FONT}" font-weight="bold" fill="${LABEL_FILL4}">${xmlEscape(g.roman)}</text>`
3347
3610
  );
3348
3611
  }
3349
3612
  let width = layout.width;
@@ -3509,13 +3772,13 @@ function phyloIssues(input) {
3509
3772
  push("tip-with-children", `node ${n.id} is declared internal but has no children`);
3510
3773
  }
3511
3774
  }
3512
- const GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
3775
+ const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
3513
3776
  "duplicate-id",
3514
3777
  "unknown-endpoint",
3515
3778
  "unknown-root",
3516
3779
  "multiple-parents"
3517
3780
  ]);
3518
- if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
3781
+ if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
3519
3782
  const childrenOf = /* @__PURE__ */ new Map();
3520
3783
  for (const e of [...input.edges].sort((a, b) => a.id - b.id)) {
3521
3784
  const arr = childrenOf.get(e.parentId) ?? [];
@@ -3593,7 +3856,7 @@ var PHYLO_BRANCH_ID_BASE = 2e6;
3593
3856
  var PHYLO_EXTENSION_ID_BASE = 3e6;
3594
3857
  var PHYLO_ROOTSTUB_ID_BASE = 4e6;
3595
3858
  var PHYLO_SCALEBAR_ID = 5e6;
3596
- var round7 = (n) => Math.round(n * 100) / 100;
3859
+ var round10 = (n) => Math.round(n * 100) / 100;
3597
3860
  function niceScaleStep(maxLen) {
3598
3861
  if (!(maxLen > 0)) return 0;
3599
3862
  const target = maxLen / 4;
@@ -3719,8 +3982,8 @@ function computePhyloLayout(input, opts = {}) {
3719
3982
  edgeId: PHYLO_ROOTSTUB_ID_BASE + root.node.id,
3720
3983
  kind: "root-stub",
3721
3984
  points: [
3722
- { x: round7(Math.max(PADDING6 / 2, rootDrawX - ROOT_STUB)), y: round7(root.cy) },
3723
- { x: round7(rootDrawX), y: round7(root.cy) }
3985
+ { x: round10(Math.max(PADDING6 / 2, rootDrawX - ROOT_STUB)), y: round10(root.cy) },
3986
+ { x: round10(rootDrawX), y: round10(root.cy) }
3724
3987
  ],
3725
3988
  dotted: false,
3726
3989
  length: null,
@@ -3735,11 +3998,11 @@ function computePhyloLayout(input, opts = {}) {
3735
3998
  nodes.push({
3736
3999
  nodeId: w.node.id,
3737
4000
  isTip: tip,
3738
- cx: round7(drawX),
3739
- cy: round7(w.cy),
4001
+ cx: round10(drawX),
4002
+ cy: round10(w.cy),
3740
4003
  support,
3741
4004
  labelLines: labelLine,
3742
- labelLeft: round7(drawX + PHYLO_TIP_R + PHYLO_LABEL_GAP),
4005
+ labelLeft: round10(drawX + PHYLO_TIP_R + PHYLO_LABEL_GAP),
3743
4006
  depth: w.depth,
3744
4007
  title: nodeTitle(w.node, tip, isRoot, w.inLength, titleLabels)
3745
4008
  });
@@ -3748,8 +4011,8 @@ function computePhyloLayout(input, opts = {}) {
3748
4011
  edgeId: PHYLO_EXTENSION_ID_BASE + w.node.id,
3749
4012
  kind: "extension",
3750
4013
  points: [
3751
- { x: round7(w.cx), y: round7(w.cy) },
3752
- { x: round7(alignX), y: round7(w.cy) }
4014
+ { x: round10(w.cx), y: round10(w.cy) },
4015
+ { x: round10(alignX), y: round10(w.cy) }
3753
4016
  ],
3754
4017
  dotted: true,
3755
4018
  length: null,
@@ -3765,8 +4028,8 @@ function computePhyloLayout(input, opts = {}) {
3765
4028
  edgeId: PHYLO_CLADEBAR_ID_BASE + w.node.id,
3766
4029
  kind: "clade-bar",
3767
4030
  points: [
3768
- { x: round7(w.cx), y: round7(barTop) },
3769
- { x: round7(w.cx), y: round7(barBottom) }
4031
+ { x: round10(w.cx), y: round10(barTop) },
4032
+ { x: round10(w.cx), y: round10(barBottom) }
3770
4033
  ],
3771
4034
  dotted: false,
3772
4035
  length: null,
@@ -3778,8 +4041,8 @@ function computePhyloLayout(input, opts = {}) {
3778
4041
  edgeId: PHYLO_BRANCH_ID_BASE + c.node.id,
3779
4042
  kind: "branch",
3780
4043
  points: [
3781
- { x: round7(w.cx), y: round7(c.cy) },
3782
- { x: round7(c.cx), y: round7(c.cy) }
4044
+ { x: round10(w.cx), y: round10(c.cy) },
4045
+ { x: round10(c.cx), y: round10(c.cy) }
3783
4046
  ],
3784
4047
  dotted: false,
3785
4048
  length: c.inLength,
@@ -3796,17 +4059,17 @@ function computePhyloLayout(input, opts = {}) {
3796
4059
  const barY = treeBottom + SCALEBAR_ZONE / 2;
3797
4060
  scaleBar = {
3798
4061
  length: stepLen,
3799
- x: round7(barX),
3800
- y: round7(barY),
3801
- pxLength: round7(scaleBarPx),
4062
+ x: round10(barX),
4063
+ y: round10(barY),
4064
+ pxLength: round10(scaleBarPx),
3802
4065
  valueLabel: trimNumber(stepLen)
3803
4066
  };
3804
4067
  elements.push({
3805
4068
  edgeId: PHYLO_SCALEBAR_ID,
3806
4069
  kind: "scale-bar",
3807
4070
  points: [
3808
- { x: round7(barX), y: round7(barY) },
3809
- { x: round7(barX + scaleBarPx), y: round7(barY) }
4071
+ { x: round10(barX), y: round10(barY) },
4072
+ { x: round10(barX + scaleBarPx), y: round10(barY) }
3810
4073
  ],
3811
4074
  dotted: false,
3812
4075
  length: stepLen,
@@ -3820,18 +4083,18 @@ function computePhyloLayout(input, opts = {}) {
3820
4083
  var GLYPH_STROKE4 = "#52525b";
3821
4084
  var LABEL_FILL5 = "#3f3f46";
3822
4085
  var EDGE_INK6 = "#71717a";
3823
- var round8 = (n) => Math.round(n * 100) / 100;
4086
+ var round11 = (n) => Math.round(n * 100) / 100;
3824
4087
  function nodeSvg3(n, showSupport) {
3825
4088
  const pieces = [`<title>${xmlEscape(n.title)}</title>`];
3826
4089
  pieces.push(`<circle cx="${n.cx}" cy="${n.cy}" r="2.5" fill="${GLYPH_STROKE4}"/>`);
3827
4090
  if (n.labelLines.length > 0) {
3828
4091
  pieces.push(
3829
- `<text x="${n.labelLeft}" y="${round8(n.cy + PHYLO_LABEL_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_LABEL_FONT}" fill="${LABEL_FILL5}">${xmlEscape(n.labelLines[0])}</text>`
4092
+ `<text x="${n.labelLeft}" y="${round11(n.cy + PHYLO_LABEL_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_LABEL_FONT}" fill="${LABEL_FILL5}">${xmlEscape(n.labelLines[0])}</text>`
3830
4093
  );
3831
4094
  }
3832
4095
  if (showSupport && !n.isTip && n.support !== null) {
3833
4096
  pieces.push(
3834
- `<text x="${round8(n.cx - 4)}" y="${round8(n.cy - 3)}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(String(n.support))}</text>`
4097
+ `<text x="${round11(n.cx - 4)}" y="${round11(n.cy - 3)}" text-anchor="end" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(String(n.support))}</text>`
3835
4098
  );
3836
4099
  }
3837
4100
  return `<g data-node-id="n${n.nodeId}">${pieces.join("")}</g>`;
@@ -3846,18 +4109,18 @@ function elementSvg4(el) {
3846
4109
  function scaleBarLabelSvg(layout) {
3847
4110
  const bar = layout.scaleBar;
3848
4111
  if (bar === null) return "";
3849
- const tick = (x) => `<line x1="${x}" y1="${round8(bar.y - 3)}" x2="${x}" y2="${round8(bar.y + 3)}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
3850
- return tick(bar.x) + tick(round8(bar.x + bar.pxLength)) + `<text x="${round8(bar.x + bar.pxLength / 2)}" y="${round8(bar.y - 6)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(bar.valueLabel)}</text>`;
4112
+ const tick = (x) => `<line x1="${x}" y1="${round11(bar.y - 3)}" x2="${x}" y2="${round11(bar.y + 3)}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
4113
+ return tick(bar.x) + tick(round11(bar.x + bar.pxLength)) + `<text x="${round11(bar.x + bar.pxLength / 2)}" y="${round11(bar.y - 6)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">${xmlEscape(bar.valueLabel)}</text>`;
3851
4114
  }
3852
4115
  function supportSwatch(x, y) {
3853
- const cx = round8(x + LEGEND_SWATCH_W / 2);
3854
- return `<circle cx="${cx}" cy="${y}" r="2.5" fill="${GLYPH_STROKE4}"/><text x="${round8(cx + 5)}" y="${round8(y + PHYLO_SUPPORT_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">95</text>`;
4116
+ const cx = round11(x + LEGEND_SWATCH_W / 2);
4117
+ return `<circle cx="${cx}" cy="${y}" r="2.5" fill="${GLYPH_STROKE4}"/><text x="${round11(cx + 5)}" y="${round11(y + PHYLO_SUPPORT_FONT * 0.32)}" font-family="${FONT_FAMILY}" font-size="${PHYLO_SUPPORT_FONT}" fill="${LABEL_FILL5}">95</text>`;
3855
4118
  }
3856
4119
  function scaleSwatch(x, y) {
3857
- return `<line x1="${round8(x)}" y1="${y}" x2="${round8(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
4120
+ return `<line x1="${round11(x)}" y1="${y}" x2="${round11(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.75"/>`;
3858
4121
  }
3859
4122
  function alignedTipSwatch(x, y) {
3860
- return `<line x1="${round8(x)}" y1="${y}" x2="${round8(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.5" stroke-dasharray="2,3"/>`;
4123
+ return `<line x1="${round11(x)}" y1="${y}" x2="${round11(x + LEGEND_SWATCH_W)}" y2="${y}" stroke="${EDGE_INK6}" stroke-width="1.5" stroke-opacity="0.5" stroke-dasharray="2,3"/>`;
3861
4124
  }
3862
4125
  function phyloLayoutSvg(layout, opts = {}) {
3863
4126
  const labels = opts.labels ?? PHYLO_SVG_LABELS_EN;
@@ -3962,9 +4225,9 @@ function orgChartIssues(input) {
3962
4225
  }
3963
4226
  const reportById = /* @__PURE__ */ new Map();
3964
4227
  const dupReportIds = /* @__PURE__ */ new Set();
3965
- for (const r of input.reports) {
3966
- if (reportById.has(r.id)) dupReportIds.add(r.id);
3967
- else reportById.set(r.id, r);
4228
+ for (const r2 of input.reports) {
4229
+ if (reportById.has(r2.id)) dupReportIds.add(r2.id);
4230
+ else reportById.set(r2.id, r2);
3968
4231
  }
3969
4232
  for (const id of [...dupReportIds].sort((a, b) => a - b)) {
3970
4233
  push("duplicate-id", `duplicate report id ${id}`);
@@ -3977,32 +4240,32 @@ function orgChartIssues(input) {
3977
4240
  const assistantReportEdge = /* @__PURE__ */ new Map();
3978
4241
  const solidManagedEdges = /* @__PURE__ */ new Map();
3979
4242
  const dottedDegree = /* @__PURE__ */ new Map();
3980
- for (const r of reports) {
3981
- const hasManager = positionById.has(r.managerId);
3982
- const hasReport = positionById.has(r.reportId);
4243
+ for (const r2 of reports) {
4244
+ const hasManager = positionById.has(r2.managerId);
4245
+ const hasReport = positionById.has(r2.reportId);
3983
4246
  if (!hasManager) {
3984
- push("unknown-manager", `report ${r.id} managerId ${r.managerId} is not a declared position`);
4247
+ push("unknown-manager", `report ${r2.id} managerId ${r2.managerId} is not a declared position`);
3985
4248
  }
3986
4249
  if (!hasReport) {
3987
- push("unknown-report", `report ${r.id} reportId ${r.reportId} is not a declared position`);
4250
+ push("unknown-report", `report ${r2.id} reportId ${r2.reportId} is not a declared position`);
3988
4251
  }
3989
- if (r.managerId === r.reportId) {
3990
- push("self-report", `report ${r.id} has managerId === reportId (${r.managerId})`);
4252
+ if (r2.managerId === r2.reportId) {
4253
+ push("self-report", `report ${r2.id} has managerId === reportId (${r2.managerId})`);
3991
4254
  }
3992
- if (r.kind !== "dotted") {
3993
- const arr = solidParents.get(r.reportId) ?? [];
3994
- arr.push(r.id);
3995
- solidParents.set(r.reportId, arr);
3996
- const managed = solidManagedEdges.get(r.managerId) ?? [];
3997
- managed.push(r.id);
3998
- solidManagedEdges.set(r.managerId, managed);
4255
+ if (r2.kind !== "dotted") {
4256
+ const arr = solidParents.get(r2.reportId) ?? [];
4257
+ arr.push(r2.id);
4258
+ solidParents.set(r2.reportId, arr);
4259
+ const managed = solidManagedEdges.get(r2.managerId) ?? [];
4260
+ managed.push(r2.id);
4261
+ solidManagedEdges.set(r2.managerId, managed);
3999
4262
  }
4000
- if (r.kind === "assistant" && r.managerId !== r.reportId && !assistantReportEdge.has(r.reportId)) {
4001
- assistantReportEdge.set(r.reportId, r.id);
4263
+ if (r2.kind === "assistant" && r2.managerId !== r2.reportId && !assistantReportEdge.has(r2.reportId)) {
4264
+ assistantReportEdge.set(r2.reportId, r2.id);
4002
4265
  }
4003
- if (r.kind === "dotted" && r.managerId !== r.reportId) {
4004
- if (positionById.has(r.managerId)) dottedDegree.set(r.managerId, (dottedDegree.get(r.managerId) ?? 0) + 1);
4005
- if (positionById.has(r.reportId)) dottedDegree.set(r.reportId, (dottedDegree.get(r.reportId) ?? 0) + 1);
4266
+ if (r2.kind === "dotted" && r2.managerId !== r2.reportId) {
4267
+ if (positionById.has(r2.managerId)) dottedDegree.set(r2.managerId, (dottedDegree.get(r2.managerId) ?? 0) + 1);
4268
+ if (positionById.has(r2.reportId)) dottedDegree.set(r2.reportId, (dottedDegree.get(r2.reportId) ?? 0) + 1);
4006
4269
  }
4007
4270
  }
4008
4271
  for (const [reportId, edgeIds] of [...solidParents.entries()].sort((a, b) => a[0] - b[0])) {
@@ -4033,12 +4296,12 @@ function orgChartIssues(input) {
4033
4296
  }
4034
4297
  if (!issues.some((i) => GRAPH_BLOCKING.has(i.code))) {
4035
4298
  const solidChildren = /* @__PURE__ */ new Map();
4036
- for (const r of reports) {
4037
- if (r.kind === "dotted") continue;
4038
- if (r.managerId === r.reportId) continue;
4039
- const arr = solidChildren.get(r.managerId) ?? [];
4040
- arr.push(r.reportId);
4041
- solidChildren.set(r.managerId, arr);
4299
+ for (const r2 of reports) {
4300
+ if (r2.kind === "dotted") continue;
4301
+ if (r2.managerId === r2.reportId) continue;
4302
+ const arr = solidChildren.get(r2.managerId) ?? [];
4303
+ arr.push(r2.reportId);
4304
+ solidChildren.set(r2.managerId, arr);
4042
4305
  }
4043
4306
  for (const arr of solidChildren.values()) arr.sort((a, b) => a - b);
4044
4307
  const color = /* @__PURE__ */ new Map();
@@ -4105,26 +4368,9 @@ var ORG_BUS_ID_BASE = 2e6;
4105
4368
  var ORG_DROP_ID_BASE = 3e6;
4106
4369
  var ORG_ASSIST_ID_BASE = 4e6;
4107
4370
  var ORG_DOTTED_ID_BASE = 5e6;
4108
- var round9 = (n) => Math.round(n * 100) / 100;
4109
- var drawnLeftEdge = (cx, boxW) => round9(round9(cx) - round9(boxW) / 2);
4110
- var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) + round9(boxW);
4111
- function packSubtree(node, gaps) {
4112
- const { ownHalfL, ownHalfR, children } = node;
4113
- if (children.length === 0) {
4114
- return { halfL: ownHalfL, halfR: ownHalfR, offsets: [] };
4115
- }
4116
- const xs = [0];
4117
- for (let i = 1; i < children.length; i++) {
4118
- xs.push(xs[i - 1] + children[i - 1].halfR + gaps.siblingGap + children[i].halfL);
4119
- }
4120
- const first = children[0];
4121
- const last = children[children.length - 1];
4122
- const axis = (xs[0] + xs[xs.length - 1]) / 2;
4123
- const offsets = xs.map((x) => x - axis);
4124
- const halfL = Math.max(ownHalfL, axis - (xs[0] - first.halfL));
4125
- const halfR = Math.max(ownHalfR, xs[xs.length - 1] + last.halfR - axis);
4126
- return { halfL, halfR, offsets };
4127
- }
4371
+ var round12 = (n) => Math.round(n * 100) / 100;
4372
+ var drawnLeftEdge = (cx, boxW) => round12(round12(cx) - round12(boxW) / 2);
4373
+ var drawnRightEdge = (cx, boxW) => drawnLeftEdge(cx, boxW) + round12(boxW);
4128
4374
  function measurePosition(position, maxLabelChars, vacantWord) {
4129
4375
  const nameLines = position.name === "" ? [] : wrapLabelBalanced(clampLabel(position.name, maxLabelChars), 2);
4130
4376
  const titleLines = position.title === null ? [] : wrapLabelBalanced(clampLabel(position.title, maxLabelChars), 2);
@@ -4157,16 +4403,16 @@ function computeOrgChartLayout(input, opts = {}) {
4157
4403
  const vacantWord = titleLabels.vacant;
4158
4404
  const positionById = new Map(input.positions.map((p) => [p.id, p]));
4159
4405
  const reports = [...input.reports].sort((a, b) => a.id - b.id);
4160
- const solidReports = reports.filter((r) => r.kind !== "dotted");
4406
+ const solidReports = reports.filter((r2) => r2.kind !== "dotted");
4161
4407
  const lineChildIds = /* @__PURE__ */ new Map();
4162
4408
  const assistChildIds = /* @__PURE__ */ new Map();
4163
4409
  const solidParentReportId = /* @__PURE__ */ new Set();
4164
- for (const r of solidReports) {
4165
- solidParentReportId.add(r.reportId);
4166
- const bucket = r.kind === "assistant" ? assistChildIds : lineChildIds;
4167
- const arr = bucket.get(r.managerId) ?? [];
4168
- arr.push(r);
4169
- bucket.set(r.managerId, arr);
4410
+ for (const r2 of solidReports) {
4411
+ solidParentReportId.add(r2.reportId);
4412
+ const bucket = r2.kind === "assistant" ? assistChildIds : lineChildIds;
4413
+ const arr = bucket.get(r2.managerId) ?? [];
4414
+ arr.push(r2);
4415
+ bucket.set(r2.managerId, arr);
4170
4416
  }
4171
4417
  for (const arr of lineChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
4172
4418
  for (const arr of assistChildIds.values()) arr.sort((a, b) => a.reportId - b.reportId);
@@ -4196,12 +4442,12 @@ function computeOrgChartLayout(input, opts = {}) {
4196
4442
  const position = positionById.get(positionId);
4197
4443
  const inst = newInst(position, depth, assistReport);
4198
4444
  allInsts.push(inst);
4199
- for (const r of assistChildIds.get(positionId) ?? []) {
4200
- const a = build(r.reportId, depth + 1, r);
4201
- inst.assistants.push({ inst: a, report: r });
4445
+ for (const r2 of assistChildIds.get(positionId) ?? []) {
4446
+ const a = build(r2.reportId, depth + 1, r2);
4447
+ inst.assistants.push({ inst: a, report: r2 });
4202
4448
  }
4203
- for (const r of lineChildIds.get(positionId) ?? []) {
4204
- inst.lineChildren.push(build(r.reportId, depth + 1, null));
4449
+ for (const r2 of lineChildIds.get(positionId) ?? []) {
4450
+ inst.lineChildren.push(build(r2.reportId, depth + 1, null));
4205
4451
  }
4206
4452
  return inst;
4207
4453
  };
@@ -4275,10 +4521,10 @@ function computeOrgChartLayout(input, opts = {}) {
4275
4521
  const pushNode = (inst) => {
4276
4522
  nodes.push({
4277
4523
  positionId: inst.position.id,
4278
- cx: round9(inst.cx),
4279
- top: round9(rowTop[inst.depth]),
4280
- boxW: round9(inst.m.boxW),
4281
- boxH: round9(inst.m.boxH),
4524
+ cx: round12(inst.cx),
4525
+ top: round12(rowTop[inst.depth]),
4526
+ boxW: round12(inst.m.boxW),
4527
+ boxH: round12(inst.m.boxH),
4282
4528
  style: inst.style,
4283
4529
  nameLines: inst.m.nameLines,
4284
4530
  titleLines: inst.m.titleLines,
@@ -4286,7 +4532,8 @@ function computeOrgChartLayout(input, opts = {}) {
4286
4532
  vacantMarker: inst.m.vacantMarker,
4287
4533
  isAssistant: inst.assistReport !== null,
4288
4534
  depth: inst.depth,
4289
- title: inst.title
4535
+ title: inst.title,
4536
+ annotated: inst.position.annotated ?? false
4290
4537
  });
4291
4538
  };
4292
4539
  const emit = (inst) => {
@@ -4300,10 +4547,10 @@ function computeOrgChartLayout(input, opts = {}) {
4300
4547
  const assistMidY = assistTop + a.m.boxH / 2;
4301
4548
  nodes.push({
4302
4549
  positionId: a.position.id,
4303
- cx: round9(a.cx),
4304
- top: round9(assistTop),
4305
- boxW: round9(a.m.boxW),
4306
- boxH: round9(a.m.boxH),
4550
+ cx: round12(a.cx),
4551
+ top: round12(assistTop),
4552
+ boxW: round12(a.m.boxW),
4553
+ boxH: round12(a.m.boxH),
4307
4554
  style: a.style,
4308
4555
  nameLines: a.m.nameLines,
4309
4556
  titleLines: a.m.titleLines,
@@ -4311,17 +4558,19 @@ function computeOrgChartLayout(input, opts = {}) {
4311
4558
  vacantMarker: a.m.vacantMarker,
4312
4559
  isAssistant: true,
4313
4560
  depth: a.depth,
4314
- title: a.title
4561
+ title: a.title,
4562
+ annotated: a.position.annotated ?? false
4315
4563
  });
4316
4564
  elements.push({
4317
4565
  edgeId: ORG_ASSIST_ID_BASE + report.reportId,
4318
4566
  kind: "assist",
4319
4567
  points: [
4320
- { x: round9(inst.cx), y: round9(assistMidY) },
4321
- { x: drawnRightEdge(a.cx, a.m.boxW), y: round9(assistMidY) }
4568
+ { x: round12(inst.cx), y: round12(assistMidY) },
4569
+ { x: drawnRightEdge(a.cx, a.m.boxW), y: round12(assistMidY) }
4322
4570
  ],
4323
4571
  dashed: false,
4324
- title: reportTitle("assistant", report.label)
4572
+ title: reportTitle("assistant", report.label),
4573
+ annotated: report.annotated ?? false
4325
4574
  });
4326
4575
  bandY += a.m.boxH + ORG_ASSIST_BAND_DROP;
4327
4576
  }
@@ -4333,11 +4582,12 @@ function computeOrgChartLayout(input, opts = {}) {
4333
4582
  edgeId: ORG_STEM_ID_BASE + inst.position.id,
4334
4583
  kind: "stem",
4335
4584
  points: [
4336
- { x: round9(inst.cx), y: round9(boxBottom) },
4337
- { x: round9(inst.cx), y: round9(by) }
4585
+ { x: round12(inst.cx), y: round12(boxBottom) },
4586
+ { x: round12(inst.cx), y: round12(by) }
4338
4587
  ],
4339
4588
  dashed: false,
4340
- title: reportTitle("line", null)
4589
+ title: reportTitle("line", null),
4590
+ annotated: false
4341
4591
  });
4342
4592
  const childCxs = inst.lineChildren.map((c) => c.cx);
4343
4593
  if (inst.lineChildren.length > 1) {
@@ -4345,23 +4595,28 @@ function computeOrgChartLayout(input, opts = {}) {
4345
4595
  edgeId: ORG_BUS_ID_BASE + inst.position.id,
4346
4596
  kind: "bus",
4347
4597
  points: [
4348
- { x: round9(Math.min(...childCxs)), y: round9(by) },
4349
- { x: round9(Math.max(...childCxs)), y: round9(by) }
4598
+ { x: round12(Math.min(...childCxs)), y: round12(by) },
4599
+ { x: round12(Math.max(...childCxs)), y: round12(by) }
4350
4600
  ],
4351
4601
  dashed: false,
4352
- title: reportTitle("line", null)
4602
+ title: reportTitle("line", null),
4603
+ annotated: false
4353
4604
  });
4354
4605
  }
4606
+ const lineReports = lineChildIds.get(inst.position.id) ?? [];
4607
+ const lineReportByChildId = new Map(lineReports.map((r2) => [r2.reportId, r2]));
4355
4608
  for (const c of inst.lineChildren) {
4609
+ const lineReport = lineReportByChildId.get(c.position.id);
4356
4610
  elements.push({
4357
4611
  edgeId: ORG_DROP_ID_BASE + c.position.id,
4358
4612
  kind: "drop",
4359
4613
  points: [
4360
- { x: round9(c.cx), y: round9(by) },
4361
- { x: round9(c.cx), y: round9(rowTop[c.depth]) }
4614
+ { x: round12(c.cx), y: round12(by) },
4615
+ { x: round12(c.cx), y: round12(rowTop[c.depth]) }
4362
4616
  ],
4363
4617
  dashed: false,
4364
- title: reportTitle("line", null)
4618
+ title: reportTitle("line", null),
4619
+ annotated: lineReport?.annotated ?? false
4365
4620
  });
4366
4621
  }
4367
4622
  for (const c of inst.lineChildren) emit(c);
@@ -4395,7 +4650,7 @@ function computeOrgChartLayout(input, opts = {}) {
4395
4650
  }
4396
4651
  const nodeById = new Map(nodes.map((n) => [n.positionId, n]));
4397
4652
  const dottedReports = reports.filter(
4398
- (r) => r.kind === "dotted" && nodeById.has(r.managerId) && nodeById.has(r.reportId) && r.managerId !== r.reportId
4653
+ (r2) => r2.kind === "dotted" && nodeById.has(r2.managerId) && nodeById.has(r2.reportId) && r2.managerId !== r2.reportId
4399
4654
  );
4400
4655
  let maxBoxBottom = PADDING7;
4401
4656
  for (const n of nodes) maxBoxBottom = Math.max(maxBoxBottom, n.top + n.boxH);
@@ -4403,14 +4658,14 @@ function computeOrgChartLayout(input, opts = {}) {
4403
4658
  let channelLanes = 0;
4404
4659
  if (dottedReports.length > 0) {
4405
4660
  const laneOf = /* @__PURE__ */ new Map();
4406
- laneCount = allocateLanes2(
4407
- dottedReports.map((r) => {
4408
- const rep = escapeByPos.get(r.reportId);
4409
- const mgr = escapeByPos.get(r.managerId);
4661
+ laneCount = allocateLanes(
4662
+ dottedReports.map((r2) => {
4663
+ const rep = escapeByPos.get(r2.reportId);
4664
+ const mgr = escapeByPos.get(r2.managerId);
4410
4665
  return {
4411
4666
  lo: Math.min(rep.midY, mgr.midY),
4412
4667
  hi: Math.max(rep.midY, mgr.midY),
4413
- set: (lane) => laneOf.set(r.id, lane)
4668
+ set: (lane) => laneOf.set(r2.id, lane)
4414
4669
  };
4415
4670
  })
4416
4671
  );
@@ -4425,19 +4680,19 @@ function computeOrgChartLayout(input, opts = {}) {
4425
4680
  const sign = slot % 2 === 1 ? -1 : 1;
4426
4681
  return mid + sign * Math.ceil(slot / 2) * step;
4427
4682
  };
4428
- const geoms = dottedReports.map((r, i) => {
4429
- const rep = escapeByPos.get(r.reportId);
4430
- const mgr = escapeByPos.get(r.managerId);
4683
+ const geoms = dottedReports.map((r2, i) => {
4684
+ const rep = escapeByPos.get(r2.reportId);
4685
+ const mgr = escapeByPos.get(r2.managerId);
4431
4686
  return {
4432
- r,
4687
+ r: r2,
4433
4688
  i,
4434
- repMidY: round9(nextExitY(r.reportId, rep.midY, rep.boxH)),
4435
- mgrMidY: round9(nextExitY(r.managerId, mgr.midY, mgr.boxH)),
4436
- repBoxEdge: round9(rep.boxEdge),
4437
- mgrBoxEdge: round9(mgr.boxEdge),
4438
- repChY: round9(channelTop + 2 * i * ORG_MATRIX_LANE_PITCH),
4439
- mgrChY: round9(channelTop + (2 * i + 1) * ORG_MATRIX_LANE_PITCH),
4440
- laneX: round9(canvasRight + ORG_MATRIX_GUTTER_GAP + laneOf.get(r.id) * ORG_MATRIX_LANE_PITCH),
4689
+ repMidY: round12(nextExitY(r2.reportId, rep.midY, rep.boxH)),
4690
+ mgrMidY: round12(nextExitY(r2.managerId, mgr.midY, mgr.boxH)),
4691
+ repBoxEdge: round12(rep.boxEdge),
4692
+ mgrBoxEdge: round12(mgr.boxEdge),
4693
+ repChY: round12(channelTop + 2 * i * ORG_MATRIX_LANE_PITCH),
4694
+ mgrChY: round12(channelTop + (2 * i + 1) * ORG_MATRIX_LANE_PITCH),
4695
+ laneX: round12(canvasRight + ORG_MATRIX_GUTTER_GAP + laneOf.get(r2.id) * ORG_MATRIX_LANE_PITCH),
4441
4696
  repEscX: 0,
4442
4697
  mgrEscX: 0
4443
4698
  };
@@ -4475,14 +4730,14 @@ function computeOrgChartLayout(input, opts = {}) {
4475
4730
  else if (v.side === -1 && bRight < v.escCol - VERT_EPS) band = Math.min(band, v.escCol - bRight);
4476
4731
  }
4477
4732
  let lane = 0;
4478
- let candidate = round9(v.escCol);
4733
+ let candidate = round12(v.escCol);
4479
4734
  while (placed.some((p) => Math.abs(p.x - candidate) < VERT_EPS && yOverlap(p.lo, p.hi, v.lo, v.hi))) {
4480
4735
  lane += 1;
4481
- candidate = round9(v.escCol + v.side * lane * ORG_MATRIX_ESCAPE_STEP);
4736
+ candidate = round12(v.escCol + v.side * lane * ORG_MATRIX_ESCAPE_STEP);
4482
4737
  }
4483
4738
  if (lane > 0 && lane * ORG_MATRIX_ESCAPE_STEP >= band - VERT_EPS) {
4484
4739
  throw new Error(
4485
- `org-chart: escape-column-overflow \u2014 escape vertical nudged ${lane * ORG_MATRIX_ESCAPE_STEP}px from its base column exceeds the ${round9(band)}px box-free band`
4740
+ `org-chart: escape-column-overflow \u2014 escape vertical nudged ${lane * ORG_MATRIX_ESCAPE_STEP}px from its base column exceeds the ${round12(band)}px box-free band`
4486
4741
  );
4487
4742
  }
4488
4743
  placed.push({ x: candidate, lo: v.lo, hi: v.hi });
@@ -4511,7 +4766,8 @@ function computeOrgChartLayout(input, opts = {}) {
4511
4766
  // → into the manager box's edge
4512
4767
  ]),
4513
4768
  dashed: true,
4514
- title: reportTitle("dotted", g.r.label)
4769
+ title: reportTitle("dotted", g.r.label),
4770
+ annotated: g.r.annotated ?? false
4515
4771
  });
4516
4772
  }
4517
4773
  }
@@ -4527,25 +4783,6 @@ function collapseDegenerate(points) {
4527
4783
  }
4528
4784
  return out;
4529
4785
  }
4530
- function allocateLanes2(items) {
4531
- const lanes = [];
4532
- for (const it of items) {
4533
- let chosen = -1;
4534
- for (let l = 0; l < lanes.length; l++) {
4535
- if (lanes[l].every((o) => it.hi <= o.lo || it.lo >= o.hi)) {
4536
- chosen = l;
4537
- break;
4538
- }
4539
- }
4540
- if (chosen === -1) {
4541
- chosen = lanes.length;
4542
- lanes.push([]);
4543
- }
4544
- lanes[chosen].push({ lo: it.lo, hi: it.hi });
4545
- it.set(chosen);
4546
- }
4547
- return lanes.length;
4548
- }
4549
4786
 
4550
4787
  // src/org-chart/svg.ts
4551
4788
  var GLYPH_STROKE5 = "#52525b";
@@ -4554,25 +4791,25 @@ var EDGE_INK7 = "#71717a";
4554
4791
  var GLYPH_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="2"`;
4555
4792
  var VACANT_DASH = `stroke-dasharray="6,4"`;
4556
4793
  var DOTTED = EDGE_STROKE.distant;
4557
- var round10 = (n) => Math.round(n * 100) / 100;
4794
+ var round13 = (n) => Math.round(n * 100) / 100;
4558
4795
  var ORG_BOX_PAD_Y2 = 9;
4559
4796
  function boxSvg(n) {
4560
- const left = round10(n.cx - n.boxW / 2);
4797
+ const left = round13(n.cx - n.boxW / 2);
4561
4798
  const pieces = [`<title>${xmlEscape(n.title)}</title>`];
4562
4799
  if (n.style === "dashed") {
4563
4800
  pieces.push(
4564
4801
  `<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`
4565
4802
  );
4566
- const ix = round10(left + 3);
4567
- const iy = round10(n.top + 3);
4568
- const iw = round10(n.boxW - 6);
4569
- const ih = round10(n.boxH - 6);
4803
+ const ix = round13(left + 3);
4804
+ const iy = round13(n.top + 3);
4805
+ const iw = round13(n.boxW - 6);
4806
+ const ih = round13(n.boxH - 6);
4570
4807
  pieces.push(`<rect x="${ix}" y="${iy}" width="${iw}" height="${ih}" rx="2" ${GLYPH_ATTRS3} ${VACANT_DASH}/>`);
4571
4808
  } else {
4572
4809
  pieces.push(`<rect x="${left}" y="${n.top}" width="${n.boxW}" height="${n.boxH}" rx="2" ${GLYPH_ATTRS3}/>`);
4573
4810
  }
4574
4811
  let lineIndex = 0;
4575
- const firstBaseline = (i) => round10(n.top + ORG_BOX_PAD_Y2 + ORG_LABEL_LINE_H / 2 + i * ORG_LABEL_LINE_H + ORG_LABEL_FONT * 0.32);
4812
+ const firstBaseline = (i) => round13(n.top + ORG_BOX_PAD_Y2 + ORG_LABEL_LINE_H / 2 + i * ORG_LABEL_LINE_H + ORG_LABEL_FONT * 0.32);
4576
4813
  const textBlock = (lines, font) => {
4577
4814
  if (lines.length === 0) return "";
4578
4815
  const tspans = lines.map((line, i) => `<tspan x="${n.cx}" y="${firstBaseline(lineIndex + i)}">${xmlEscape(line)}</tspan>`).join("");
@@ -4583,43 +4820,45 @@ function boxSvg(n) {
4583
4820
  pieces.push(textBlock(n.titleLines, ORG_TITLE_FONT));
4584
4821
  pieces.push(textBlock(n.subtitleLines, ORG_TITLE_FONT));
4585
4822
  if (n.vacantMarker !== null) pieces.push(textBlock([n.vacantMarker], ORG_TITLE_FONT));
4823
+ if (n.annotated) pieces.push(annotationDot(round13(n.cx + n.boxW / 2) - 4, n.top + 4));
4586
4824
  return `<g data-node-id="p${n.positionId}">${pieces.filter((p) => p !== "").join("")}</g>`;
4587
4825
  }
4588
4826
  function elementSvg5(el) {
4589
4827
  const head = `<g data-edge-id="${el.edgeId}"><title>${xmlEscape(el.title)}</title>`;
4828
+ const tick = el.annotated ? annotationTick(el.points) : "";
4590
4829
  if (el.dashed) {
4591
4830
  const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
4592
- return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/></g>`;
4831
+ return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>${tick}</g>`;
4593
4832
  }
4594
4833
  if (el.points.length === 2) {
4595
4834
  const a = el.points[0];
4596
4835
  const b = el.points[1];
4597
- return head + `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
4836
+ return head + `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>${tick}</g>`;
4598
4837
  }
4599
- return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/></g>`;
4838
+ return head + `<path d="${pathData(el.points)}" fill="none" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>${tick}</g>`;
4600
4839
  }
4601
4840
  var MINI_ATTRS3 = `fill="transparent" stroke="${GLYPH_STROKE5}" stroke-width="1.5"`;
4602
4841
  function lineSwatch(x, y) {
4603
- const x1 = round10(x + 2);
4604
- const x2 = round10(x + LEGEND_SWATCH_W - 2);
4842
+ const x1 = round13(x + 2);
4843
+ const x2 = round13(x + LEGEND_SWATCH_W - 2);
4605
4844
  return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/>`;
4606
4845
  }
4607
4846
  function assistantSwatch(x, y) {
4608
- const stemX = round10(x + LEGEND_SWATCH_W - 4);
4609
- const boxX = round10(x + 1);
4610
- return `<line x1="${stemX}" y1="${round10(y - 4)}" x2="${stemX}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><line x1="${stemX}" y1="${y}" x2="${round10(boxX + 8)}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><rect x="${boxX}" y="${round10(y - 3.5)}" width="8" height="7" rx="1" ${MINI_ATTRS3}/>`;
4847
+ const stemX = round13(x + LEGEND_SWATCH_W - 4);
4848
+ const boxX = round13(x + 1);
4849
+ return `<line x1="${stemX}" y1="${round13(y - 4)}" x2="${stemX}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><line x1="${stemX}" y1="${y}" x2="${round13(boxX + 8)}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="1.5" stroke-opacity="0.75"/><rect x="${boxX}" y="${round13(y - 3.5)}" width="8" height="7" rx="1" ${MINI_ATTRS3}/>`;
4611
4850
  }
4612
4851
  function dottedSwatch(x, y) {
4613
- const x1 = round10(x + 2);
4614
- const x2 = round10(x + LEGEND_SWATCH_W - 2);
4852
+ const x1 = round13(x + 2);
4853
+ const x2 = round13(x + LEGEND_SWATCH_W - 2);
4615
4854
  const dash = DOTTED.dash === null ? "" : ` stroke-dasharray="${DOTTED.dash[0]},${DOTTED.dash[1]}"`;
4616
4855
  return `<line x1="${x1}" y1="${y}" x2="${x2}" y2="${y}" stroke="${EDGE_INK7}" stroke-width="${DOTTED.width}" stroke-opacity="${DOTTED.opacity}"${dash}/>`;
4617
4856
  }
4618
4857
  function vacantSwatch(x, y) {
4619
- const cx = round10(x + LEGEND_SWATCH_W / 2);
4620
- const bx = round10(cx - 8);
4621
- const by = round10(y - 5);
4622
- return `<rect x="${bx}" y="${by}" width="16" height="10" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/><rect x="${round10(bx + 2)}" y="${round10(by + 2)}" width="12" height="6" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/>`;
4858
+ const cx = round13(x + LEGEND_SWATCH_W / 2);
4859
+ const bx = round13(cx - 8);
4860
+ const by = round13(y - 5);
4861
+ return `<rect x="${bx}" y="${by}" width="16" height="10" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/><rect x="${round13(bx + 2)}" y="${round13(by + 2)}" width="12" height="6" rx="1" ${MINI_ATTRS3} stroke-dasharray="3,2"/>`;
4623
4862
  }
4624
4863
  function orgChartLayoutSvg(layout, opts = {}) {
4625
4864
  const labels = opts.labels ?? ORG_CHART_SVG_LABELS_EN;
@@ -4638,6 +4877,9 @@ function orgChartLayoutSvg(layout, opts = {}) {
4638
4877
  if (hasAssistant) entries.push({ swatch: assistantSwatch, label: labels.legend.assistant });
4639
4878
  if (hasDotted) entries.push({ swatch: dottedSwatch, label: labels.legend.dotted });
4640
4879
  if (hasVacant) entries.push({ swatch: vacantSwatch, label: labels.legend.vacant });
4880
+ if (opts.annotationLabel !== void 0 && (layout.nodes.some((n) => n.annotated) || layout.elements.some((e) => e.annotated))) {
4881
+ entries.push({ swatch: annotationSwatch, label: opts.annotationLabel });
4882
+ }
4641
4883
  const block = legendBlock(entries, layout.height);
4642
4884
  if (block.svg !== "") {
4643
4885
  parts.push(block.svg);
@@ -4658,11 +4900,1661 @@ function orgChartSvg(input, opts = {}) {
4658
4900
  });
4659
4901
  const svg = orgChartLayoutSvg(layout, {
4660
4902
  ...opts.legend === false ? { legend: false } : {},
4661
- ...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {}
4903
+ ...opts.svgLabels !== void 0 ? { labels: opts.svgLabels } : {},
4904
+ ...opts.annotationLabel !== void 0 ? { annotationLabel: opts.annotationLabel } : {}
4662
4905
  });
4663
4906
  return { svg, layout };
4664
4907
  }
4665
4908
 
4909
+ // src/prisma/types.ts
4910
+ var PRISMA_PHASES = ["identification", "screening", "included"];
4911
+ var PRISMA_BOX_KINDS = ["flow", "exclusion"];
4912
+ var PRISMA_COLUMNS = ["main", "new", "previous"];
4913
+
4914
+ // src/prisma/validate.ts
4915
+ var PrismaValidationError = class extends Error {
4916
+ issues;
4917
+ constructor(issues) {
4918
+ super(`invalid PRISMA diagram: ${issues.map((i) => i.message).join("; ")}`);
4919
+ this.name = "PrismaValidationError";
4920
+ this.issues = issues;
4921
+ }
4922
+ };
4923
+ function sortIssues5(issues) {
4924
+ const unique = /* @__PURE__ */ new Map();
4925
+ for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
4926
+ return [...unique.values()].sort(
4927
+ (a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
4928
+ );
4929
+ }
4930
+ function prismaIssues(input) {
4931
+ if (input.boxes.length === 0 && input.arrows.length === 0) return [];
4932
+ const issues = [];
4933
+ const push = (code, message) => {
4934
+ issues.push({ code, message });
4935
+ };
4936
+ const boxById = /* @__PURE__ */ new Map();
4937
+ const dupBoxIds = /* @__PURE__ */ new Set();
4938
+ for (const b of input.boxes) {
4939
+ if (boxById.has(b.id)) dupBoxIds.add(b.id);
4940
+ else boxById.set(b.id, b);
4941
+ }
4942
+ for (const id of [...dupBoxIds].sort((a, b) => a - b)) {
4943
+ push("duplicate-id", `duplicate box id ${id}`);
4944
+ }
4945
+ const arrowById = /* @__PURE__ */ new Map();
4946
+ const dupArrowIds = /* @__PURE__ */ new Set();
4947
+ for (const a of input.arrows) {
4948
+ if (arrowById.has(a.id)) dupArrowIds.add(a.id);
4949
+ else arrowById.set(a.id, a);
4950
+ }
4951
+ for (const id of [...dupArrowIds].sort((a, b) => a - b)) {
4952
+ push("duplicate-id", `duplicate arrow id ${id}`);
4953
+ }
4954
+ if (issues.length > 0) return sortIssues5(issues);
4955
+ for (const arrow of [...arrowById.values()].sort((a, b) => a.id - b.id)) {
4956
+ if (!boxById.has(arrow.fromId)) {
4957
+ push("unknown-arrow-endpoint", `arrow ${arrow.id} fromId ${arrow.fromId} is not a declared box`);
4958
+ }
4959
+ if (!boxById.has(arrow.toId)) {
4960
+ push("unknown-arrow-endpoint", `arrow ${arrow.id} toId ${arrow.toId} is not a declared box`);
4961
+ }
4962
+ if (arrow.fromId === arrow.toId) {
4963
+ push("self-arrow", `arrow ${arrow.id} has fromId === toId (${arrow.fromId})`);
4964
+ }
4965
+ }
4966
+ for (const box of [...boxById.values()].sort((a, b) => a.id - b.id)) {
4967
+ for (const count of box.counts) {
4968
+ if (count.n !== null && !Number.isFinite(count.n)) {
4969
+ push("non-finite-count", `box ${box.id} count "${count.label}" has non-finite n: ${String(count.n)}`);
4970
+ }
4971
+ }
4972
+ }
4973
+ if (input.variant !== "flow-with-prior") {
4974
+ for (const box of [...boxById.values()].sort((a, b) => a.id - b.id)) {
4975
+ if (box.column === "previous") {
4976
+ push(
4977
+ "invalid-variant-column",
4978
+ `box ${box.id} has column "previous" but variant is "${input.variant}"`
4979
+ );
4980
+ }
4981
+ }
4982
+ }
4983
+ const GRAPH_BLOCKING3 = /* @__PURE__ */ new Set([
4984
+ "duplicate-id",
4985
+ "unknown-arrow-endpoint"
4986
+ ]);
4987
+ if (!issues.some((i) => GRAPH_BLOCKING3.has(i.code))) {
4988
+ const phaseIndex = /* @__PURE__ */ new Map();
4989
+ for (let i = 0; i < PRISMA_PHASES.length; i++) phaseIndex.set(PRISMA_PHASES[i], i);
4990
+ for (const arrow of [...arrowById.values()].sort((a, b) => a.id - b.id)) {
4991
+ const fromBox = boxById.get(arrow.fromId);
4992
+ const toBox = boxById.get(arrow.toId);
4993
+ const fromPhaseIdx = phaseIndex.get(fromBox.phase);
4994
+ const toPhaseIdx = phaseIndex.get(toBox.phase);
4995
+ if (fromBox.kind === "flow" && toBox.kind === "exclusion" && fromBox.phase === toBox.phase) {
4996
+ continue;
4997
+ }
4998
+ if (toPhaseIdx < fromPhaseIdx) {
4999
+ push(
5000
+ "arrow-skips-backward",
5001
+ `arrow ${arrow.id} goes backward: from phase "${fromBox.phase}" to "${toBox.phase}"`
5002
+ );
5003
+ }
5004
+ }
5005
+ }
5006
+ return sortIssues5(issues);
5007
+ }
5008
+ function validatePrisma(input) {
5009
+ const issues = prismaIssues(input);
5010
+ if (issues.length > 0) throw new PrismaValidationError(issues);
5011
+ }
5012
+
5013
+ // src/prisma/labels.ts
5014
+ var PRISMA_TITLE_LABELS_EN = {
5015
+ phases: {
5016
+ identification: "Identification",
5017
+ screening: "Screening",
5018
+ included: "Included"
5019
+ },
5020
+ nullCount: "n = \u2014",
5021
+ arrowKinds: {
5022
+ flow: "Flow",
5023
+ exclusion: "Exclusion",
5024
+ merge: "Merge"
5025
+ }
5026
+ };
5027
+ var PRISMA_SVG_LABELS_EN = {
5028
+ legend: {
5029
+ flow: "Main flow",
5030
+ exclusion: "Excluded"
5031
+ },
5032
+ ariaLabel: "PRISMA 2020 flow diagram"
5033
+ };
5034
+
5035
+ // src/prisma/layout.ts
5036
+ var PRISMA_PADDING = 32;
5037
+ var PRISMA_BAND_LABEL_W = 40;
5038
+ var PRISMA_COL_GAP = 56;
5039
+ var PRISMA_CORRIDOR = 30;
5040
+ var PRISMA_BOX_PAD_X = 12;
5041
+ var PRISMA_BOX_PAD_Y = 10;
5042
+ var PRISMA_LINE_H = 14;
5043
+ var PRISMA_MIN_BOX_W = 200;
5044
+ var PRISMA_EXCL_GAP = 48;
5045
+ var PRISMA_HEADING_FONT = 12;
5046
+ var PRISMA_COUNT_FONT = 10;
5047
+ var PRISMA_ROUTE_MARGIN = 10;
5048
+ var PRISMA_ROUTE_PITCH = 10;
5049
+ var PRISMA_FLOW_ARROW_BASE = 1e6;
5050
+ var PRISMA_EXCL_ARROW_BASE = 2e6;
5051
+ var PRISMA_MERGE_ARROW_BASE = 3e6;
5052
+ function round14(n) {
5053
+ return Math.round(n * 100) / 100;
5054
+ }
5055
+ function wrapHeading(h) {
5056
+ if (h === null || h === "") return [];
5057
+ return wrapLabelBalanced(h);
5058
+ }
5059
+ function countLine(label, n, nullToken) {
5060
+ const nStr = n === null ? nullToken : String(n);
5061
+ return `${label}: n = ${nStr}`;
5062
+ }
5063
+ function buildCountLines(counts, nullToken) {
5064
+ return counts.map((c) => countLine(c.label, c.n, nullToken));
5065
+ }
5066
+ function boxTitle(box, nullToken) {
5067
+ if (box.title !== void 0) return box.title;
5068
+ const parts = [];
5069
+ if (box.heading !== null && box.heading !== "") parts.push(box.heading);
5070
+ for (const c of box.counts) parts.push(countLine(c.label, c.n, nullToken));
5071
+ return parts.join("; ") || "(box)";
5072
+ }
5073
+ function computePrismaLayout(input, opts = {}) {
5074
+ if (opts.validate !== false) validatePrisma(input);
5075
+ const labels = opts.labels ?? PRISMA_TITLE_LABELS_EN;
5076
+ const nullToken = labels.nullCount;
5077
+ const COMP_OPTS = {
5078
+ padX: PRISMA_BOX_PAD_X,
5079
+ padY: PRISMA_BOX_PAD_Y,
5080
+ lineH: PRISMA_LINE_H,
5081
+ minW: PRISMA_MIN_BOX_W};
5082
+ const boxMetrics = /* @__PURE__ */ new Map();
5083
+ for (const b of input.boxes) {
5084
+ const headingLines = wrapHeading(b.heading);
5085
+ const cLines = buildCountLines(b.counts, nullToken);
5086
+ const metrics = measureCompartmentBox(
5087
+ [
5088
+ { lines: headingLines, font: PRISMA_HEADING_FONT },
5089
+ { lines: cLines, font: PRISMA_COUNT_FONT }
5090
+ ],
5091
+ COMP_OPTS
5092
+ );
5093
+ boxMetrics.set(b.id, {
5094
+ boxW: metrics.boxW,
5095
+ boxH: metrics.boxH,
5096
+ rows: metrics.rows,
5097
+ dividerYs: metrics.dividerYs,
5098
+ headingLines,
5099
+ countLines: cLines
5100
+ });
5101
+ }
5102
+ const boxesByKey = /* @__PURE__ */ new Map();
5103
+ for (const b of input.boxes) {
5104
+ const key = `${b.phase}:${b.column}:${b.kind}`;
5105
+ const arr = boxesByKey.get(key) ?? [];
5106
+ arr.push(b);
5107
+ boxesByKey.set(key, arr);
5108
+ }
5109
+ for (const arr of boxesByKey.values()) {
5110
+ arr.sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.id - b.id);
5111
+ }
5112
+ const groupOf = (phase, col, kind) => boxesByKey.get(`${phase}:${col}:${kind}`) ?? [];
5113
+ const flowCols = input.variant === "flow-with-prior" ? ["new", "previous"] : input.variant === "flow-other-methods" ? ["main", "new"] : ["main"];
5114
+ const contentLeft = PRISMA_PADDING + PRISMA_BAND_LABEL_W;
5115
+ function maxColW(col, kind) {
5116
+ let w = 0;
5117
+ for (const phase of PRISMA_PHASES) {
5118
+ for (const b of groupOf(phase, col, kind)) {
5119
+ const bw = boxMetrics.get(b.id).boxW;
5120
+ if (bw > w) w = bw;
5121
+ }
5122
+ }
5123
+ return w;
5124
+ }
5125
+ const colHasFlow = (col) => PRISMA_PHASES.some((phase) => groupOf(phase, col, "flow").length > 0);
5126
+ const colHasExcl = (col) => PRISMA_PHASES.some((phase) => groupOf(phase, col, "exclusion").length > 0);
5127
+ const exclOnlyCols = ["main", "new", "previous"].filter(
5128
+ (col) => !flowCols.includes(col) && colHasExcl(col)
5129
+ );
5130
+ const placedCols = [...flowCols, ...exclOnlyCols];
5131
+ const colGeom = /* @__PURE__ */ new Map();
5132
+ for (const col of placedCols) {
5133
+ const flowW = colHasFlow(col) ? Math.max(PRISMA_MIN_BOX_W, maxColW(col, "flow")) : 0;
5134
+ const exclW = maxColW(col, "exclusion");
5135
+ const blockW = flowW > 0 && exclW > 0 ? flowW + PRISMA_EXCL_GAP + exclW : flowW > 0 ? flowW : exclW;
5136
+ colGeom.set(col, { flowW, exclW, blockW, flowCx: 0, exclCx: 0 });
5137
+ }
5138
+ function placeBlock(col, blockLeft) {
5139
+ const g = colGeom.get(col);
5140
+ if (g.flowW > 0) g.flowCx = round14(blockLeft + g.flowW / 2);
5141
+ if (g.exclW > 0) {
5142
+ const exclLeft = g.flowW > 0 ? blockLeft + g.flowW + PRISMA_EXCL_GAP : blockLeft;
5143
+ g.exclCx = round14(exclLeft + g.exclW / 2);
5144
+ }
5145
+ }
5146
+ if (placedCols.length === 1) {
5147
+ placeBlock(placedCols[0], contentLeft);
5148
+ } else {
5149
+ const packed = packSubtree(
5150
+ {
5151
+ ownHalfL: 0,
5152
+ ownHalfR: 0,
5153
+ children: placedCols.map((col) => {
5154
+ const half = colGeom.get(col).blockW / 2;
5155
+ return { halfL: half, halfR: half };
5156
+ })
5157
+ },
5158
+ { siblingGap: PRISMA_COL_GAP }
5159
+ );
5160
+ const totalW = packed.halfL + packed.halfR;
5161
+ const blockCx = round14(contentLeft + totalW / 2);
5162
+ placedCols.forEach((col, i) => {
5163
+ const w = colGeom.get(col).blockW;
5164
+ placeBlock(col, round14(blockCx + packed.offsets[i] - w / 2));
5165
+ });
5166
+ }
5167
+ let totalRight = contentLeft;
5168
+ for (const col of placedCols) {
5169
+ const g = colGeom.get(col);
5170
+ const r2 = g.exclW > 0 ? round14(g.exclCx + g.exclW / 2) : round14(g.flowCx + g.flowW / 2);
5171
+ if (r2 > totalRight) totalRight = r2;
5172
+ }
5173
+ const rowsByCol = /* @__PURE__ */ new Map();
5174
+ const rowsKey = (phase, col) => `${phase}:${col}`;
5175
+ function buildRankRows(phase, col) {
5176
+ const byRank = /* @__PURE__ */ new Map();
5177
+ for (const kind of ["flow", "exclusion"]) {
5178
+ for (const b of groupOf(phase, col, kind)) {
5179
+ const h = boxMetrics.get(b.id).boxH;
5180
+ const prev = byRank.get(b.rank) ?? 0;
5181
+ if (h > prev) byRank.set(b.rank, h);
5182
+ }
5183
+ }
5184
+ const ranks = [...byRank.keys()].sort((a, b) => a - b);
5185
+ return ranks.map((rank) => ({ rank, rowH: byRank.get(rank), top: 0 }));
5186
+ }
5187
+ for (const phase of PRISMA_PHASES) {
5188
+ for (const col of placedCols) {
5189
+ rowsByCol.set(rowsKey(phase, col), buildRankRows(phase, col));
5190
+ }
5191
+ }
5192
+ function colStackH(rows) {
5193
+ if (rows.length === 0) return 0;
5194
+ let h = 0;
5195
+ for (const r2 of rows) h += r2.rowH + PRISMA_BOX_PAD_Y;
5196
+ return h - PRISMA_BOX_PAD_Y;
5197
+ }
5198
+ function bandRowH(phase) {
5199
+ let maxH = 0;
5200
+ let any = false;
5201
+ for (const col of placedCols) {
5202
+ const rows = rowsByCol.get(rowsKey(phase, col));
5203
+ if (rows.length > 0) any = true;
5204
+ const h = colStackH(rows);
5205
+ if (h > maxH) maxH = h;
5206
+ }
5207
+ if (!any) return PRISMA_LINE_H * 2 + PRISMA_BOX_PAD_Y * 4;
5208
+ return maxH;
5209
+ }
5210
+ const bandRowHs = PRISMA_PHASES.map((phase) => ({
5211
+ rowH: bandRowH(phase),
5212
+ zoneH: 0
5213
+ }));
5214
+ const stacked = bandStack(bandRowHs, { padding: PRISMA_PADDING, corridor: PRISMA_CORRIDOR });
5215
+ const layoutBoxes = [];
5216
+ const lboxById = /* @__PURE__ */ new Map();
5217
+ function placeBox(b, phase, kind, cx, top) {
5218
+ const m = boxMetrics.get(b.id);
5219
+ const t = round14(top);
5220
+ const title = boxTitle(b, nullToken);
5221
+ const lbox = {
5222
+ id: b.id,
5223
+ phase,
5224
+ kind,
5225
+ cx: round14(cx),
5226
+ top: t,
5227
+ boxW: m.boxW,
5228
+ boxH: m.boxH,
5229
+ rows: m.rows.map((r2) => ({ ...r2, top: round14(t + r2.top) })),
5230
+ dividerYs: m.dividerYs.map((d) => round14(t + d)),
5231
+ heading: b.heading,
5232
+ countLines: m.countLines,
5233
+ title
5234
+ };
5235
+ layoutBoxes.push(lbox);
5236
+ lboxById.set(b.id, lbox);
5237
+ }
5238
+ for (let phaseIdx = 0; phaseIdx < PRISMA_PHASES.length; phaseIdx++) {
5239
+ const phase = PRISMA_PHASES[phaseIdx];
5240
+ const bandTop = stacked.rowTop[phaseIdx];
5241
+ for (const col of placedCols) {
5242
+ const g = colGeom.get(col);
5243
+ const rows = rowsByCol.get(rowsKey(phase, col));
5244
+ const topByRank = /* @__PURE__ */ new Map();
5245
+ let y = bandTop;
5246
+ for (const r2 of rows) {
5247
+ r2.top = round14(y);
5248
+ topByRank.set(r2.rank, r2.top);
5249
+ y = round14(y + r2.rowH + PRISMA_BOX_PAD_Y);
5250
+ }
5251
+ if (g.flowW > 0) {
5252
+ for (const b of groupOf(phase, col, "flow")) {
5253
+ placeBox(b, phase, "flow", g.flowCx, topByRank.get(b.rank));
5254
+ }
5255
+ }
5256
+ if (g.exclW > 0) {
5257
+ for (const b of groupOf(phase, col, "exclusion")) {
5258
+ placeBox(b, phase, "exclusion", g.exclCx, topByRank.get(b.rank));
5259
+ }
5260
+ }
5261
+ }
5262
+ }
5263
+ const elements = [];
5264
+ const boxById = new Map(input.boxes.map((b) => [b.id, b]));
5265
+ const phaseIndexMap = new Map(
5266
+ PRISMA_PHASES.map((p, i) => [p, i])
5267
+ );
5268
+ function boxBottom(lb) {
5269
+ return round14(lb.top + lb.boxH);
5270
+ }
5271
+ function bandBottom(phase) {
5272
+ const idx = PRISMA_PHASES.indexOf(phase);
5273
+ return round14(stacked.rowTop[idx] + bandRowHs[idx].rowH);
5274
+ }
5275
+ const arrowsToBox = /* @__PURE__ */ new Map();
5276
+ for (const arrow of input.arrows) {
5277
+ const toBox = boxById.get(arrow.toId);
5278
+ const fromBox = boxById.get(arrow.fromId);
5279
+ if (toBox === void 0 || fromBox === void 0) continue;
5280
+ if (fromBox.kind === "flow" && toBox.kind === "flow") {
5281
+ const arr = arrowsToBox.get(arrow.toId) ?? [];
5282
+ arr.push(arrow.id);
5283
+ arrowsToBox.set(arrow.toId, arr);
5284
+ }
5285
+ }
5286
+ const arrowToBoxArrivalIdx = /* @__PURE__ */ new Map();
5287
+ const exclArrowIndexBySource = /* @__PURE__ */ new Map();
5288
+ for (const arrow of input.arrows) {
5289
+ const fromLBox = lboxById.get(arrow.fromId);
5290
+ const toLBox = lboxById.get(arrow.toId);
5291
+ if (fromLBox === void 0 || toLBox === void 0) continue;
5292
+ const fromBox = boxById.get(arrow.fromId);
5293
+ const toBox = boxById.get(arrow.toId);
5294
+ const isExclusion = fromBox.kind === "flow" && toBox.kind === "exclusion";
5295
+ const isFlowToFlow = fromBox.kind === "flow" && toBox.kind === "flow";
5296
+ if (isExclusion) {
5297
+ const idx = exclArrowIndexBySource.get(arrow.fromId) ?? 0;
5298
+ exclArrowIndexBySource.set(arrow.fromId, idx + 1);
5299
+ const srcRight = round14(fromLBox.cx + fromLBox.boxW / 2);
5300
+ const exclMidY = round14(toLBox.top + toLBox.boxH / 2);
5301
+ const eLeft = round14(toLBox.cx - toLBox.boxW / 2);
5302
+ const sameColumn = fromBox.column === toBox.column;
5303
+ let points;
5304
+ if (sameColumn) {
5305
+ points = [
5306
+ { x: round14(srcRight), y: round14(exclMidY) },
5307
+ { x: round14(eLeft), y: round14(exclMidY) }
5308
+ ];
5309
+ } else {
5310
+ const colGapStep = round14(PRISMA_COL_GAP / 8);
5311
+ const exclGapStep = round14(PRISMA_EXCL_GAP / 8);
5312
+ const fenceX = round14(srcRight + (idx + 1) * colGapStep);
5313
+ const exclFenceX = round14(eLeft - (idx + 1) * exclGapStep);
5314
+ const bBotY = bandBottom(fromBox.phase);
5315
+ const corridorY = round14(bBotY + PRISMA_ROUTE_MARGIN + idx * PRISMA_ROUTE_PITCH);
5316
+ points = [
5317
+ { x: round14(srcRight), y: round14(exclMidY) },
5318
+ // exit source right at excl center y
5319
+ { x: round14(fenceX), y: round14(exclMidY) },
5320
+ // horizontal into inter-col gap (unique y)
5321
+ { x: round14(fenceX), y: round14(corridorY) },
5322
+ // vertical at unique x in inter-col gap
5323
+ { x: round14(exclFenceX), y: round14(corridorY) },
5324
+ // horizontal in corridor (unique y)
5325
+ { x: round14(exclFenceX), y: round14(exclMidY) },
5326
+ // vertical at unique x left of excl box
5327
+ { x: round14(eLeft), y: round14(exclMidY) }
5328
+ // horizontal into excl box (unique y)
5329
+ ];
5330
+ }
5331
+ elements.push({
5332
+ edgeId: PRISMA_EXCL_ARROW_BASE + arrow.id,
5333
+ kind: "exclusion",
5334
+ points,
5335
+ title: `${labels.arrowKinds.exclusion}: ${fromLBox.title} \u2192 ${toLBox.title}`,
5336
+ headDir: "right"
5337
+ });
5338
+ } else if (isFlowToFlow) {
5339
+ const fromPhaseIdx = phaseIndexMap.get(fromBox.phase);
5340
+ const toPhaseIdx = phaseIndexMap.get(toBox.phase);
5341
+ const isSamePhase = fromBox.phase === toBox.phase;
5342
+ const isSameCol = fromBox.column === toBox.column;
5343
+ const isForward = toPhaseIdx >= fromPhaseIdx;
5344
+ const arrivals = arrowsToBox.get(arrow.toId) ?? [];
5345
+ let arrivalIdx = arrowToBoxArrivalIdx.get(arrow.id) ?? 0;
5346
+ if (!arrowToBoxArrivalIdx.has(arrow.id)) {
5347
+ arrivalIdx = arrivals.indexOf(arrow.id);
5348
+ arrowToBoxArrivalIdx.set(arrow.id, arrivalIdx);
5349
+ }
5350
+ const nArrivals = arrivals.length;
5351
+ const APPROACH_SPREAD = round14(toLBox.boxW / 4);
5352
+ const spreadStep = nArrivals > 1 ? round14(APPROACH_SPREAD / (nArrivals - 1)) : 0;
5353
+ const approachX = round14(toLBox.cx - APPROACH_SPREAD / 2 + arrivalIdx * spreadStep);
5354
+ const toTop = toLBox.top;
5355
+ if (isSamePhase && isSameCol) {
5356
+ const fromBottom = boxBottom(fromLBox);
5357
+ const fromCx = round14(fromLBox.cx);
5358
+ const toCx = round14(toLBox.cx);
5359
+ const dropX = fromCx === toCx ? fromCx : toCx;
5360
+ elements.push({
5361
+ edgeId: PRISMA_FLOW_ARROW_BASE + arrow.id,
5362
+ kind: "flow",
5363
+ points: [
5364
+ { x: fromCx, y: round14(fromBottom) },
5365
+ { x: dropX, y: round14(toTop) }
5366
+ ],
5367
+ title: `${labels.arrowKinds.flow}: ${fromLBox.title} \u2192 ${toLBox.title}`,
5368
+ headDir: "down"
5369
+ });
5370
+ } else if (isSameCol && isForward) {
5371
+ const fromBottom = boxBottom(fromLBox);
5372
+ if (nArrivals <= 1 && round14(fromLBox.cx) === round14(toLBox.cx)) {
5373
+ elements.push({
5374
+ edgeId: PRISMA_FLOW_ARROW_BASE + arrow.id,
5375
+ kind: "flow",
5376
+ points: [
5377
+ { x: round14(fromLBox.cx), y: round14(fromBottom) },
5378
+ { x: round14(toLBox.cx), y: round14(toTop) }
5379
+ ],
5380
+ title: `${labels.arrowKinds.flow}: ${fromLBox.title} \u2192 ${toLBox.title}`,
5381
+ headDir: "down"
5382
+ });
5383
+ } else {
5384
+ const phIdx = phaseIndexMap.get(fromBox.phase);
5385
+ const elbowY = round14(stacked.rowTop[phIdx] + bandRowHs[phIdx].rowH + PRISMA_CORRIDOR / 2);
5386
+ elements.push({
5387
+ edgeId: PRISMA_FLOW_ARROW_BASE + arrow.id,
5388
+ kind: "flow",
5389
+ points: [
5390
+ { x: round14(fromLBox.cx), y: round14(fromBottom) },
5391
+ { x: round14(fromLBox.cx), y: round14(elbowY) },
5392
+ { x: round14(approachX), y: round14(elbowY) },
5393
+ { x: round14(approachX), y: round14(toTop) }
5394
+ ],
5395
+ title: `${labels.arrowKinds.flow}: ${fromLBox.title} \u2192 ${toLBox.title}`,
5396
+ headDir: "down"
5397
+ });
5398
+ }
5399
+ } else {
5400
+ const fromBottom = boxBottom(fromLBox);
5401
+ const phIdx = phaseIndexMap.get(fromBox.phase);
5402
+ const elbowY = round14(stacked.rowTop[phIdx] + bandRowHs[phIdx].rowH + PRISMA_CORRIDOR / 2);
5403
+ const fromCx = round14(fromLBox.cx);
5404
+ if (round14(fromCx) === round14(approachX)) {
5405
+ elements.push({
5406
+ edgeId: PRISMA_MERGE_ARROW_BASE + arrow.id,
5407
+ kind: "merge",
5408
+ points: [
5409
+ { x: fromCx, y: round14(fromBottom) },
5410
+ { x: round14(approachX), y: round14(toTop) }
5411
+ ],
5412
+ title: `${labels.arrowKinds.merge}: ${fromLBox.title} \u2192 ${toLBox.title}`,
5413
+ headDir: "down"
5414
+ });
5415
+ } else {
5416
+ elements.push({
5417
+ edgeId: PRISMA_MERGE_ARROW_BASE + arrow.id,
5418
+ kind: "merge",
5419
+ points: [
5420
+ { x: fromCx, y: round14(fromBottom) },
5421
+ { x: fromCx, y: round14(elbowY) },
5422
+ { x: round14(approachX), y: round14(elbowY) },
5423
+ { x: round14(approachX), y: round14(toTop) }
5424
+ ],
5425
+ title: `${labels.arrowKinds.merge}: ${fromLBox.title} \u2192 ${toLBox.title}`,
5426
+ headDir: "down"
5427
+ });
5428
+ }
5429
+ }
5430
+ }
5431
+ }
5432
+ const canvasH = stacked.height;
5433
+ const canvasW = round14(totalRight + PRISMA_PADDING);
5434
+ const bands = PRISMA_PHASES.map((phase, i) => {
5435
+ const rowTop = stacked.rowTop[i];
5436
+ const rowH = bandRowHs[i].rowH;
5437
+ return {
5438
+ phase,
5439
+ rowTop,
5440
+ rowH,
5441
+ labelX: PRISMA_PADDING,
5442
+ centerY: round14(rowTop + rowH / 2)
5443
+ };
5444
+ });
5445
+ return {
5446
+ width: canvasW,
5447
+ height: canvasH,
5448
+ boxes: layoutBoxes,
5449
+ elements,
5450
+ bands,
5451
+ variant: input.variant
5452
+ };
5453
+ }
5454
+
5455
+ // src/prisma/svg.ts
5456
+ var BOX_STROKE2 = "#52525b";
5457
+ var BOX_STROKE_W = 2;
5458
+ var BOX_FILL = "#fff";
5459
+ var BOX_RX = 2;
5460
+ var HEADING_FILL = "#3f3f46";
5461
+ var COUNT_FILL = "#52525b";
5462
+ var ARROW_STROKE = "#71717a";
5463
+ var ARROW_STROKE_W = 1.5;
5464
+ var ARROW_FILL = "#71717a";
5465
+ var BAND_LABEL_FILL = "#52525b";
5466
+ var BAND_LABEL_FONT = 11;
5467
+ function round15(n) {
5468
+ return Math.round(n * 100) / 100;
5469
+ }
5470
+ function emitBox(b) {
5471
+ const x = round15(b.cx - b.boxW / 2);
5472
+ const y = b.top;
5473
+ const parts = [];
5474
+ parts.push(
5475
+ `<rect x="${x}" y="${y}" width="${b.boxW}" height="${b.boxH}" rx="${BOX_RX}" fill="${BOX_FILL}" stroke="${BOX_STROKE2}" stroke-width="${BOX_STROKE_W}"/>`
5476
+ );
5477
+ for (const dy of b.dividerYs) {
5478
+ parts.push(
5479
+ `<line x1="${x}" y1="${dy}" x2="${round15(x + b.boxW)}" y2="${dy}" stroke="${BOX_STROKE2}" stroke-width="1"/>`
5480
+ );
5481
+ }
5482
+ for (let ri = 0; ri < b.rows.length; ri++) {
5483
+ const row = b.rows[ri];
5484
+ const isHeading = ri === 0;
5485
+ const font = row.font;
5486
+ const fill = isHeading ? HEADING_FILL : COUNT_FILL;
5487
+ const textX = isHeading ? round15(b.cx) : round15(x + 8);
5488
+ for (let li = 0; li < row.lines.length; li++) {
5489
+ const line = row.lines[li];
5490
+ const textY = round15(row.top + li * 14 + font);
5491
+ const anchorAttr = isHeading ? ` text-anchor="middle"` : "";
5492
+ const weightAttr = isHeading ? ` font-weight="bold"` : "";
5493
+ parts.push(
5494
+ `<text x="${textX}" y="${textY}" font-family="${FONT_FAMILY}" font-size="${font}" fill="${fill}"${anchorAttr}${weightAttr}>${xmlEscape(line)}</text>`
5495
+ );
5496
+ }
5497
+ }
5498
+ return parts.join("");
5499
+ }
5500
+ function emitArrow(el) {
5501
+ const points = el.points;
5502
+ if (points.length < 2) return "";
5503
+ const inset = glyphInset(el.headDir, "arrow");
5504
+ const last = points[points.length - 1];
5505
+ let lineEndX = last.x;
5506
+ let lineEndY = last.y;
5507
+ if (el.headDir === "down") lineEndY = round15(last.y - inset);
5508
+ else if (el.headDir === "right") lineEndX = round15(last.x - inset);
5509
+ else if (el.headDir === "left") lineEndX = round15(last.x + inset);
5510
+ else lineEndY = round15(last.y + inset);
5511
+ const linePoints = [
5512
+ ...points.slice(0, points.length - 1).map((p) => ({ x: p.x, y: p.y })),
5513
+ { x: lineEndX, y: lineEndY }
5514
+ ];
5515
+ const pathStr = pathData(linePoints);
5516
+ const arrowPts = arrowFilledPoints({ x: last.x, y: last.y }, el.headDir);
5517
+ return `<title>${xmlEscape(el.title)}</title><path d="${pathStr}" stroke="${ARROW_STROKE}" stroke-width="${ARROW_STROKE_W}" fill="none"/><polygon points="${arrowPts}" fill="${ARROW_FILL}" stroke="none"/>`;
5518
+ }
5519
+ function emitBandLabel(phase, labelX, centerY, bandTitleLabels) {
5520
+ const text = bandTitleLabels.phases[phase];
5521
+ const gutterCx = round15(labelX + PRISMA_BAND_LABEL_W / 2);
5522
+ return `<text x="${gutterCx}" y="${round15(centerY)}" font-family="${FONT_FAMILY}" font-size="${BAND_LABEL_FONT}" fill="${BAND_LABEL_FILL}" text-anchor="middle" transform="rotate(-90,${gutterCx},${round15(centerY)})">${xmlEscape(text)}</text>`;
5523
+ }
5524
+ function buildLegendEntries(layout, svgLabels) {
5525
+ const hasFlow = layout.boxes.some((b) => b.kind === "flow");
5526
+ const hasExcl = layout.boxes.some((b) => b.kind === "exclusion");
5527
+ const entries = [];
5528
+ if (hasFlow) {
5529
+ entries.push({
5530
+ label: svgLabels.legend.flow,
5531
+ swatch: (x, cy) => `<rect x="${x}" y="${round15(cy - 6)}" width="22" height="12" rx="2" fill="${BOX_FILL}" stroke="${BOX_STROKE2}" stroke-width="2"/>`
5532
+ });
5533
+ }
5534
+ if (hasExcl) {
5535
+ entries.push({
5536
+ label: svgLabels.legend.exclusion,
5537
+ swatch: (x, cy) => `<rect x="${x}" y="${round15(cy - 6)}" width="22" height="12" rx="2" fill="${BOX_FILL}" stroke="${BOX_STROKE2}" stroke-width="2" stroke-dasharray="4,2"/>`
5538
+ });
5539
+ }
5540
+ return entries;
5541
+ }
5542
+ function prismaLayoutSvg(layout, opts = {}) {
5543
+ const svgLabels = opts.svgLabels ?? PRISMA_SVG_LABELS_EN;
5544
+ const bandTitleLabels = opts.titleLabels ?? PRISMA_TITLE_LABELS_EN;
5545
+ const doLegend = opts.legend !== false;
5546
+ const parts = [];
5547
+ for (const band of layout.bands) {
5548
+ parts.push(emitBandLabel(band.phase, band.labelX, band.centerY, bandTitleLabels));
5549
+ }
5550
+ for (const b of layout.boxes) {
5551
+ const boxSvg3 = emitBox(b);
5552
+ parts.push(`<g data-node-id="b${b.id}"><title>${xmlEscape(b.title)}</title>${boxSvg3}</g>`);
5553
+ }
5554
+ for (const el of layout.elements) {
5555
+ const arrowSvg = emitArrow(el);
5556
+ parts.push(`<g data-edge-id="${el.edgeId}">${arrowSvg}</g>`);
5557
+ }
5558
+ let w = layout.width;
5559
+ let h = layout.height;
5560
+ let legendSvg = "";
5561
+ if (doLegend) {
5562
+ const entries = buildLegendEntries(layout, svgLabels);
5563
+ const lb = legendBlock(entries, h);
5564
+ legendSvg = lb.svg;
5565
+ if (entries.length > 0) {
5566
+ w = Math.max(w, lb.width + 32 * 2);
5567
+ h = lb.height;
5568
+ }
5569
+ }
5570
+ w = round15(w);
5571
+ h = round15(h);
5572
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" role="img" aria-label="${xmlEscape(svgLabels.ariaLabel)}">` + parts.join("") + legendSvg + `</svg>`;
5573
+ }
5574
+
5575
+ // src/prisma/render.ts
5576
+ function prismaSvg(input, opts = {}) {
5577
+ const layout = computePrismaLayout(input, opts);
5578
+ const svg = prismaLayoutSvg(layout, opts);
5579
+ return { svg, layout };
5580
+ }
5581
+
5582
+ // src/uml/types.ts
5583
+ var UML_VISIBILITIES = ["public", "private", "protected", "package"];
5584
+ var UML_VIS_GLYPH = {
5585
+ public: "+",
5586
+ private: "-",
5587
+ protected: "#",
5588
+ package: "~"
5589
+ };
5590
+ var UML_RELATIONSHIPS = [
5591
+ "association",
5592
+ "directed-association",
5593
+ "aggregation",
5594
+ "composition",
5595
+ "generalization",
5596
+ "realization",
5597
+ "dependency"
5598
+ ];
5599
+ var RELATION_GLYPHS = {
5600
+ association: { sourceEnd: "none", targetEnd: "none", line: "solid" },
5601
+ "directed-association": { sourceEnd: "none", targetEnd: "open-arrow", line: "solid" },
5602
+ aggregation: { sourceEnd: "hollow-diamond", targetEnd: "none", line: "solid" },
5603
+ composition: { sourceEnd: "solid-diamond", targetEnd: "none", line: "solid" },
5604
+ generalization: { sourceEnd: "none", targetEnd: "hollow-triangle", line: "solid" },
5605
+ realization: { sourceEnd: "none", targetEnd: "hollow-triangle", line: "dashed" },
5606
+ dependency: { sourceEnd: "none", targetEnd: "open-arrow", line: "dashed" }
5607
+ };
5608
+
5609
+ // src/uml/validate.ts
5610
+ var UmlValidationError = class extends Error {
5611
+ issues;
5612
+ constructor(issues) {
5613
+ super(`invalid UML class diagram: ${issues.map((i) => i.message).join("; ")}`);
5614
+ this.name = "UmlValidationError";
5615
+ this.issues = issues;
5616
+ }
5617
+ };
5618
+ function sortIssues6(issues) {
5619
+ const unique = /* @__PURE__ */ new Map();
5620
+ for (const issue of issues) unique.set(`${issue.code} ${issue.message}`, issue);
5621
+ return [...unique.values()].sort(
5622
+ (a, b) => a.code !== b.code ? a.code < b.code ? -1 : 1 : a.message < b.message ? -1 : a.message > b.message ? 1 : 0
5623
+ );
5624
+ }
5625
+ var GRAPH_BLOCKING2 = /* @__PURE__ */ new Set([
5626
+ "duplicate-id",
5627
+ "unknown-endpoint"
5628
+ ]);
5629
+ var INHERITANCE_KINDS = /* @__PURE__ */ new Set([
5630
+ "generalization",
5631
+ "realization"
5632
+ ]);
5633
+ function umlIssues(input) {
5634
+ if (input.classes.length === 0 && input.relationships.length === 0) return [];
5635
+ const issues = [];
5636
+ const push = (code, message) => {
5637
+ issues.push({ code, message });
5638
+ };
5639
+ const classById = /* @__PURE__ */ new Map();
5640
+ const dupClassIds = /* @__PURE__ */ new Set();
5641
+ for (const c of input.classes) {
5642
+ if (classById.has(c.id)) dupClassIds.add(c.id);
5643
+ else classById.set(c.id, c);
5644
+ }
5645
+ for (const id of [...dupClassIds].sort((a, b) => a - b)) {
5646
+ push("duplicate-id", `duplicate class id ${id}`);
5647
+ }
5648
+ const relById = /* @__PURE__ */ new Map();
5649
+ const dupRelIds = /* @__PURE__ */ new Set();
5650
+ for (const r2 of input.relationships) {
5651
+ if (relById.has(r2.id)) dupRelIds.add(r2.id);
5652
+ else relById.set(r2.id, r2);
5653
+ }
5654
+ for (const id of [...dupRelIds].sort((a, b) => a - b)) {
5655
+ push("duplicate-id", `duplicate relationship id ${id}`);
5656
+ }
5657
+ if (issues.length > 0) return sortIssues6(issues);
5658
+ const rels = [...relById.values()].sort((a, b) => a.id - b.id);
5659
+ for (const r2 of rels) {
5660
+ if (!classById.has(r2.sourceId)) {
5661
+ push("unknown-endpoint", `relationship ${r2.id} sourceId ${r2.sourceId} is not a declared class`);
5662
+ }
5663
+ if (!classById.has(r2.targetId)) {
5664
+ push("unknown-endpoint", `relationship ${r2.id} targetId ${r2.targetId} is not a declared class`);
5665
+ }
5666
+ }
5667
+ for (const c of [...classById.values()].sort((a, b) => a.id - b.id)) {
5668
+ if (!Number.isInteger(c.col) || c.col < 0) {
5669
+ push("negative-cell", `class ${c.id} col ${c.col} must be a non-negative integer`);
5670
+ }
5671
+ if (!Number.isInteger(c.row) || c.row < 0) {
5672
+ push("negative-cell", `class ${c.id} row ${c.row} must be a non-negative integer`);
5673
+ }
5674
+ }
5675
+ const cellKey = (c) => `${c.col},${c.row}`;
5676
+ const cellToClasses = /* @__PURE__ */ new Map();
5677
+ for (const c of [...classById.values()].sort((a, b) => a.id - b.id)) {
5678
+ const key = cellKey(c);
5679
+ const arr = cellToClasses.get(key) ?? [];
5680
+ arr.push(c.id);
5681
+ cellToClasses.set(key, arr);
5682
+ }
5683
+ for (const [key, ids] of [...cellToClasses.entries()].sort()) {
5684
+ if (ids.length > 1) {
5685
+ push(
5686
+ "cell-collision",
5687
+ `classes ${ids.sort((a, b) => a - b).join(", ")} all declare cell (${key})`
5688
+ );
5689
+ }
5690
+ }
5691
+ for (const r2 of rels) {
5692
+ if ((r2.kind === "generalization" || r2.kind === "realization") && r2.sourceId === r2.targetId) {
5693
+ push(
5694
+ "self-generalization",
5695
+ `relationship ${r2.id} is a ${r2.kind} from class ${r2.sourceId} to itself`
5696
+ );
5697
+ }
5698
+ }
5699
+ if (!issues.some((i) => GRAPH_BLOCKING2.has(i.code))) {
5700
+ const inheritanceChildren = /* @__PURE__ */ new Map();
5701
+ for (const r2 of rels) {
5702
+ if (!INHERITANCE_KINDS.has(r2.kind)) continue;
5703
+ if (r2.sourceId === r2.targetId) continue;
5704
+ const arr = inheritanceChildren.get(r2.sourceId) ?? [];
5705
+ arr.push(r2.targetId);
5706
+ inheritanceChildren.set(r2.sourceId, arr);
5707
+ }
5708
+ for (const arr of inheritanceChildren.values()) arr.sort((a, b) => a - b);
5709
+ const color = /* @__PURE__ */ new Map();
5710
+ const seenCycles = /* @__PURE__ */ new Set();
5711
+ const dfs = (start) => {
5712
+ const stack = [{ id: start, nextChild: 0 }];
5713
+ color.set(start, 1);
5714
+ while (stack.length > 0) {
5715
+ const frame = stack[stack.length - 1];
5716
+ const children = inheritanceChildren.get(frame.id) ?? [];
5717
+ if (frame.nextChild >= children.length) {
5718
+ color.set(frame.id, 2);
5719
+ stack.pop();
5720
+ continue;
5721
+ }
5722
+ const child = children[frame.nextChild];
5723
+ frame.nextChild += 1;
5724
+ const c = color.get(child) ?? 0;
5725
+ if (c === 1) {
5726
+ const from = stack.findIndex((f) => f.id === child);
5727
+ const cycle = stack.slice(from).map((f) => f.id);
5728
+ const minIdx = cycle.indexOf(Math.min(...cycle));
5729
+ const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
5730
+ const key = rotated.join(">");
5731
+ if (!seenCycles.has(key)) {
5732
+ seenCycles.add(key);
5733
+ push(
5734
+ "generalization-cycle",
5735
+ `generalization cycle: ${[...rotated, rotated[0]].join(" \u2192 ")}`
5736
+ );
5737
+ }
5738
+ } else if (c === 0) {
5739
+ color.set(child, 1);
5740
+ stack.push({ id: child, nextChild: 0 });
5741
+ }
5742
+ }
5743
+ };
5744
+ for (const c of [...classById.values()].sort((a, b) => a.id - b.id)) {
5745
+ if ((color.get(c.id) ?? 0) === 0) dfs(c.id);
5746
+ }
5747
+ }
5748
+ return sortIssues6(issues);
5749
+ }
5750
+ function validateUml(input) {
5751
+ const issues = umlIssues(input);
5752
+ if (issues.length > 0) throw new UmlValidationError(issues);
5753
+ }
5754
+ function sideCapacity(boxH, rowGap, pitch, glyphHalf) {
5755
+ const clear = boxH / 2 + rowGap / 2 - glyphHalf;
5756
+ const k = Math.max(0, Math.floor(clear / pitch));
5757
+ return 2 * k + 1;
5758
+ }
5759
+ function sideCapacityIssue(classId, side, count, capacity) {
5760
+ return {
5761
+ code: "too-many-side-edges",
5762
+ message: `class ${classId} has ${count} edges on its ${side} side \u2014 at most ${capacity} fit without a port stub spilling through a neighbouring box`
5763
+ };
5764
+ }
5765
+
5766
+ // src/uml/labels.ts
5767
+ var UML_TITLE_LABELS_EN = {
5768
+ kinds: {
5769
+ association: "association",
5770
+ "directed-association": "directed association",
5771
+ aggregation: "aggregation",
5772
+ composition: "composition",
5773
+ generalization: "generalization",
5774
+ realization: "realization",
5775
+ dependency: "dependency"
5776
+ },
5777
+ visibilities: {
5778
+ public: "public",
5779
+ private: "private",
5780
+ protected: "protected",
5781
+ package: "package"
5782
+ }
5783
+ };
5784
+ var UML_SVG_LABELS_EN = {
5785
+ legend: {
5786
+ association: "Association",
5787
+ "directed-association": "Directed association",
5788
+ aggregation: "Aggregation",
5789
+ composition: "Composition",
5790
+ generalization: "Generalization",
5791
+ realization: "Realization",
5792
+ dependency: "Dependency"
5793
+ },
5794
+ ariaLabel: "UML class diagram"
5795
+ };
5796
+
5797
+ // src/uml/layout.ts
5798
+ var UML_PADDING = 32;
5799
+ var UML_COL_GAP = 64;
5800
+ var UML_ROW_GAP = 56;
5801
+ var UML_BOX_PAD_X = 10;
5802
+ var UML_BOX_PAD_Y = 8;
5803
+ var UML_LINE_H = 15;
5804
+ var UML_MIN_BOX_W = 120;
5805
+ var UML_NAME_FONT = 12;
5806
+ var UML_FEAT_FONT = 11;
5807
+ var UML_SEP_PAD = 4;
5808
+ var UML_PORT_PITCH = 12;
5809
+ var UML_SELF_LOOP_W = 28;
5810
+ var UML_LABEL_FONT = 10;
5811
+ var UML_ASSOC_ROUTE_BASE = 1e6;
5812
+ var UML_GLYPH_BASE = 2e6;
5813
+ var UML_SELF_LOOP_BASE = 3e6;
5814
+ var MIN_TERMINAL_STUB = 4;
5815
+ var round16 = (n) => Math.round(n * 100) / 100;
5816
+ function featureLine(f) {
5817
+ const prefix = f.visibility !== null ? UML_VIS_GLYPH[f.visibility] + " " : "";
5818
+ return prefix + f.text;
5819
+ }
5820
+ function measureClass(cls) {
5821
+ const nameLines = [];
5822
+ if (cls.stereotype !== null) nameLines.push(cls.stereotype);
5823
+ nameLines.push(cls.name);
5824
+ const attrLines = cls.attributes.map(featureLine);
5825
+ const opLines = cls.operations.map(featureLine);
5826
+ return measureCompartmentBox(
5827
+ [
5828
+ { lines: nameLines, font: UML_NAME_FONT },
5829
+ { lines: attrLines, font: UML_FEAT_FONT },
5830
+ { lines: opLines, font: UML_FEAT_FONT }
5831
+ ],
5832
+ {
5833
+ padX: UML_BOX_PAD_X,
5834
+ padY: UML_BOX_PAD_Y,
5835
+ lineH: UML_LINE_H,
5836
+ minW: UML_MIN_BOX_W}
5837
+ );
5838
+ }
5839
+ function boxEdges(node) {
5840
+ const left = round16(node.cx - node.boxW / 2);
5841
+ const right = round16(left + node.boxW);
5842
+ const top = round16(node.cy - node.boxH / 2);
5843
+ const bottom = round16(top + node.boxH);
5844
+ return { left, right, top, bottom };
5845
+ }
5846
+ function glyphInsetForKind(gk) {
5847
+ switch (gk) {
5848
+ case "none":
5849
+ return 0;
5850
+ case "open-arrow":
5851
+ return glyphInset("right", "arrow");
5852
+ case "hollow-triangle":
5853
+ return glyphInset("right", "triangle");
5854
+ case "hollow-diamond":
5855
+ case "solid-diamond":
5856
+ return glyphInset("right", "diamond");
5857
+ }
5858
+ }
5859
+ var MAX_GLYPH_HALF = GLYPH_TRI_HALF;
5860
+ var GLYPH_SLOT_PITCH = Math.max(UML_PORT_PITCH, MAX_GLYPH_HALF * 2 + 2);
5861
+ function portOffset(slotIndex, slotSize) {
5862
+ if (slotIndex === 0) return 0;
5863
+ const sign = slotIndex % 2 === 1 ? 1 : -1;
5864
+ const dist = Math.ceil(slotIndex / 2);
5865
+ return sign * dist * slotSize;
5866
+ }
5867
+ function sidePort(node, side, dy) {
5868
+ const { left, right } = boxEdges(node);
5869
+ const x = side === "right" ? right : left;
5870
+ return { x, y: round16(node.cy + dy) };
5871
+ }
5872
+ function vGutterLeft(colIdx, colX, grid) {
5873
+ return round16(colX[colIdx] + grid.colW[colIdx]);
5874
+ }
5875
+ var V_GUTTER_MARGIN = 6;
5876
+ function horizontalGutterY(rowIdx, rowY, grid) {
5877
+ return round16(rowY[rowIdx] + grid.rowH[rowIdx] + UML_ROW_GAP / 2);
5878
+ }
5879
+ function moveAlongDir(p, dir, dist) {
5880
+ switch (dir) {
5881
+ case "right":
5882
+ return { x: round16(p.x + dist), y: p.y };
5883
+ case "left":
5884
+ return { x: round16(p.x - dist), y: p.y };
5885
+ case "down":
5886
+ return { x: p.x, y: round16(p.y + dist) };
5887
+ case "up":
5888
+ return { x: p.x, y: round16(p.y - dist) };
5889
+ }
5890
+ }
5891
+ function simplifyWaypoints(pts) {
5892
+ const dedup = [pts[0]];
5893
+ for (let i = 1; i < pts.length; i++) {
5894
+ const prev = dedup[dedup.length - 1];
5895
+ const cur = pts[i];
5896
+ if (Math.abs(cur.x - prev.x) > 1e-6 || Math.abs(cur.y - prev.y) > 1e-6) dedup.push(cur);
5897
+ }
5898
+ if (dedup.length <= 2) return dedup;
5899
+ const out = [dedup[0]];
5900
+ for (let i = 1; i < dedup.length - 1; i++) {
5901
+ const a = out[out.length - 1];
5902
+ const b = dedup[i];
5903
+ const c = dedup[i + 1];
5904
+ const abH = Math.abs(a.y - b.y) <= 1e-6;
5905
+ const bcH = Math.abs(b.y - c.y) <= 1e-6;
5906
+ const abV = Math.abs(a.x - b.x) <= 1e-6;
5907
+ const bcV = Math.abs(b.x - c.x) <= 1e-6;
5908
+ if (abH && bcH || abV && bcV) continue;
5909
+ out.push(b);
5910
+ }
5911
+ out.push(dedup[dedup.length - 1]);
5912
+ return out;
5913
+ }
5914
+ function chooseSides(srcNode, tgtNode, maxCol) {
5915
+ const sc = srcNode.col;
5916
+ const tc = tgtNode.col;
5917
+ if (tc > sc) return { srcSide: "right", tgtSide: "left", srcVCol: sc, tgtVCol: tc - 1 };
5918
+ if (tc < sc) return { srcSide: "left", tgtSide: "right", srcVCol: sc - 1, tgtVCol: tc };
5919
+ if (sc < maxCol) return { srcSide: "right", tgtSide: "right", srcVCol: sc, tgtVCol: sc };
5920
+ if (sc > 0) return { srcSide: "left", tgtSide: "left", srcVCol: sc - 1, tgtVCol: sc - 1 };
5921
+ return { srcSide: "right", tgtSide: "right", srcVCol: sc, tgtVCol: sc };
5922
+ }
5923
+ function hBaseY(plan, rowY, grid) {
5924
+ if (plan.hKind === "pad-top") return round16(UML_PADDING / 2);
5925
+ return horizontalGutterY(plan.hRow, rowY, grid);
5926
+ }
5927
+ function hBandBounds(plan, rowY, grid) {
5928
+ if (plan.hKind === "pad-top") return { lo: 0, hi: round16(rowY[0] ?? UML_PADDING) };
5929
+ const lo = round16(rowY[plan.hRow] + grid.rowH[plan.hRow]);
5930
+ const hi = round16(rowY[plan.hRow + 1]);
5931
+ return { lo, hi };
5932
+ }
5933
+ var LANE_EPS = 0.5;
5934
+ var X_OVERLAP_EPS = 1e-4;
5935
+ function allocateHorizontal(runs, base, usableHalf) {
5936
+ const floating = runs.filter((r2) => r2.y === null);
5937
+ if (floating.length === 0) return;
5938
+ const pinned = runs.filter((r2) => r2.y !== null);
5939
+ const trackCount = floating.length + pinned.length + 1;
5940
+ const candidates = trackCount <= 1 || usableHalf <= 0 ? [base] : Array.from(
5941
+ { length: trackCount },
5942
+ (_, k) => round16(base - usableHalf + (k + 0.5) * (2 * usableHalf) / trackCount)
5943
+ );
5944
+ const placed = pinned.map((r2) => ({ lo: r2.lo, hi: r2.hi, y: r2.y }));
5945
+ const ordered = [...floating].sort((a, b) => a.lo - b.lo || a.hi - b.hi);
5946
+ for (const run of ordered) {
5947
+ let chosen = candidates[0];
5948
+ for (const cy of candidates) {
5949
+ const collides = placed.some(
5950
+ (q) => Math.abs(q.y - cy) <= LANE_EPS && Math.min(run.hi, q.hi) - Math.max(run.lo, q.lo) > X_OVERLAP_EPS
5951
+ );
5952
+ if (!collides) {
5953
+ chosen = cy;
5954
+ break;
5955
+ }
5956
+ }
5957
+ run.set(chosen);
5958
+ placed.push({ lo: run.lo, hi: run.hi, y: chosen });
5959
+ }
5960
+ }
5961
+ function applyGlyphInsets(pts, srcInset, tgtInset) {
5962
+ if (pts.length < 2) return pts;
5963
+ const out = pts.map((p) => ({ ...p }));
5964
+ if (srcInset > 0) {
5965
+ const p0 = out[0];
5966
+ const p1 = out[1];
5967
+ const dir = p1.x >= p0.x ? "right" : "left";
5968
+ out[0] = moveAlongDir(p0, dir, srcInset);
5969
+ }
5970
+ if (tgtInset > 0) {
5971
+ const pn = out[out.length - 1];
5972
+ const pm = out[out.length - 2];
5973
+ const dir = pm.x >= pn.x ? "right" : "left";
5974
+ out[out.length - 1] = moveAlongDir(pn, dir, tgtInset);
5975
+ }
5976
+ return out;
5977
+ }
5978
+ function reserveTextBox(relId, slot, text, anchorX, anchorY, anchor) {
5979
+ const w = round16(estimateTextWidth(text, UML_LABEL_FONT));
5980
+ const h = round16(UML_LABEL_FONT * 1.2);
5981
+ const x = anchor === "middle" ? round16(anchorX - w / 2) : round16(anchorX);
5982
+ const y = round16(anchorY);
5983
+ return { relId, slot, text, x, y, w, h, anchor };
5984
+ }
5985
+ function computeUmlLayout(input, labels = UML_TITLE_LABELS_EN) {
5986
+ validateUml(input);
5987
+ const relTitle = (rel) => rel.title ?? labels.kinds[rel.kind];
5988
+ if (input.classes.length === 0) {
5989
+ return {
5990
+ nodes: [],
5991
+ elements: [],
5992
+ textBoxes: [],
5993
+ canvasW: UML_PADDING * 2,
5994
+ canvasH: UML_PADDING * 2,
5995
+ grid: { cellX: [], cellY: [], colW: [], rowH: [] },
5996
+ colX: [],
5997
+ rowY: []
5998
+ };
5999
+ }
6000
+ const classes = [...input.classes].sort((a, b) => a.id - b.id);
6001
+ const rels = [...input.relationships].sort((a, b) => a.id - b.id);
6002
+ const metricsMap = /* @__PURE__ */ new Map();
6003
+ for (const cls of classes) metricsMap.set(cls.id, measureClass(cls));
6004
+ const gridCells = classes.map((cls) => ({
6005
+ col: cls.col,
6006
+ row: cls.row,
6007
+ w: metricsMap.get(cls.id).boxW,
6008
+ h: metricsMap.get(cls.id).boxH
6009
+ }));
6010
+ const grid = packGrid(gridCells, { colGap: UML_COL_GAP, rowGap: UML_ROW_GAP });
6011
+ const colX = [];
6012
+ {
6013
+ let x = UML_PADDING;
6014
+ for (let c = 0; c < grid.colW.length; c++) {
6015
+ colX.push(round16(x));
6016
+ x = round16(x + grid.colW[c] + UML_COL_GAP);
6017
+ }
6018
+ }
6019
+ const rowY = [];
6020
+ {
6021
+ let y = UML_PADDING;
6022
+ for (let r2 = 0; r2 < grid.rowH.length; r2++) {
6023
+ rowY.push(round16(y));
6024
+ y = round16(y + grid.rowH[r2] + UML_ROW_GAP);
6025
+ }
6026
+ }
6027
+ const nodeByClassId = /* @__PURE__ */ new Map();
6028
+ const nodes = [];
6029
+ for (const cls of classes) {
6030
+ const metrics = metricsMap.get(cls.id);
6031
+ const cx = round16(colX[cls.col] + grid.colW[cls.col] / 2);
6032
+ const cy = round16(rowY[cls.row] + grid.rowH[cls.row] / 2);
6033
+ const title = cls.title ?? (cls.stereotype !== null ? `\xAB${cls.stereotype}\xBB ${cls.name}` : cls.name);
6034
+ const node = {
6035
+ classId: cls.id,
6036
+ cx,
6037
+ cy,
6038
+ boxW: metrics.boxW,
6039
+ boxH: metrics.boxH,
6040
+ metrics,
6041
+ title,
6042
+ col: cls.col,
6043
+ row: cls.row
6044
+ };
6045
+ nodes.push(node);
6046
+ nodeByClassId.set(cls.id, node);
6047
+ }
6048
+ const maxCol = grid.colW.length - 1;
6049
+ const maxRow = grid.rowH.length - 1;
6050
+ const sideCount = /* @__PURE__ */ new Map();
6051
+ const bumpSide = (classId, side) => {
6052
+ const key = `${classId}:${side}`;
6053
+ sideCount.set(key, (sideCount.get(key) ?? 0) + 1);
6054
+ };
6055
+ for (const rel of rels) {
6056
+ const srcNode = nodeByClassId.get(rel.sourceId);
6057
+ const tgtNode = nodeByClassId.get(rel.targetId);
6058
+ if (!srcNode || !tgtNode) continue;
6059
+ if (rel.sourceId === rel.targetId) {
6060
+ bumpSide(srcNode.classId, "right");
6061
+ bumpSide(srcNode.classId, "right");
6062
+ continue;
6063
+ }
6064
+ const { srcSide, tgtSide } = chooseSides(srcNode, tgtNode, maxCol);
6065
+ bumpSide(srcNode.classId, srcSide);
6066
+ bumpSide(tgtNode.classId, tgtSide);
6067
+ }
6068
+ {
6069
+ const capIssues = [];
6070
+ for (const node of nodes) {
6071
+ const cap = sideCapacity(node.boxH, UML_ROW_GAP, GLYPH_SLOT_PITCH, MAX_GLYPH_HALF);
6072
+ for (const side of ["left", "right"]) {
6073
+ const count = sideCount.get(`${node.classId}:${side}`) ?? 0;
6074
+ if (count > cap) capIssues.push(sideCapacityIssue(node.classId, side, count, cap));
6075
+ }
6076
+ }
6077
+ if (capIssues.length > 0) {
6078
+ throw new UmlValidationError(capIssues);
6079
+ }
6080
+ }
6081
+ const sidePortSlot = /* @__PURE__ */ new Map();
6082
+ const nextSidePort = (classId, side) => {
6083
+ const key = `${classId}:${side}`;
6084
+ const idx = sidePortSlot.get(key) ?? 0;
6085
+ sidePortSlot.set(key, idx + 1);
6086
+ return idx;
6087
+ };
6088
+ const elements = [];
6089
+ const textBoxes = [];
6090
+ const plans = [];
6091
+ for (const rel of rels) {
6092
+ const srcNode = nodeByClassId.get(rel.sourceId);
6093
+ const tgtNode = nodeByClassId.get(rel.targetId);
6094
+ if (!srcNode || !tgtNode) continue;
6095
+ const glyphs = RELATION_GLYPHS[rel.kind];
6096
+ const srcInset = glyphInsetForKind(glyphs.sourceEnd);
6097
+ const tgtInset = glyphInsetForKind(glyphs.targetEnd);
6098
+ if (rel.sourceId === rel.targetId) {
6099
+ const vCol = srcNode.col;
6100
+ const armDy1 = portOffset(nextSidePort(srcNode.classId, "right"), GLYPH_SLOT_PITCH);
6101
+ const armDy2 = portOffset(nextSidePort(srcNode.classId, "right"), GLYPH_SLOT_PITCH);
6102
+ plans.push({
6103
+ rel,
6104
+ srcNode,
6105
+ tgtNode,
6106
+ srcSide: "right",
6107
+ tgtSide: "right",
6108
+ srcVCol: vCol,
6109
+ tgtVCol: vCol,
6110
+ srcInset,
6111
+ tgtInset,
6112
+ srcPortY: round16(srcNode.cy + armDy1),
6113
+ tgtPortY: round16(srcNode.cy + armDy2),
6114
+ hKind: "row",
6115
+ hRow: 0,
6116
+ // unused for self-loops (no H-leg)
6117
+ isSelfLoop: true,
6118
+ srcVx: 0,
6119
+ tgtVx: 0,
6120
+ hY: 0
6121
+ });
6122
+ continue;
6123
+ }
6124
+ const { srcSide, tgtSide, srcVCol, tgtVCol } = chooseSides(srcNode, tgtNode, maxCol);
6125
+ const srcPortDy = portOffset(nextSidePort(srcNode.classId, srcSide), GLYPH_SLOT_PITCH);
6126
+ const tgtPortDy = portOffset(nextSidePort(tgtNode.classId, tgtSide), GLYPH_SLOT_PITCH);
6127
+ const sr = srcNode.row;
6128
+ const tr = tgtNode.row;
6129
+ let hKind;
6130
+ let hRow;
6131
+ if (sr !== tr) {
6132
+ hKind = "row";
6133
+ hRow = Math.min(sr, tr);
6134
+ } else if (maxRow >= 1) {
6135
+ hKind = "row";
6136
+ hRow = sr < maxRow ? sr : sr - 1;
6137
+ } else {
6138
+ hKind = "pad-top";
6139
+ hRow = -1;
6140
+ }
6141
+ plans.push({
6142
+ rel,
6143
+ srcNode,
6144
+ tgtNode,
6145
+ srcSide,
6146
+ tgtSide,
6147
+ srcVCol,
6148
+ tgtVCol,
6149
+ srcInset,
6150
+ tgtInset,
6151
+ srcPortY: round16(srcNode.cy + srcPortDy),
6152
+ tgtPortY: round16(tgtNode.cy + tgtPortDy),
6153
+ hKind,
6154
+ hRow,
6155
+ isSelfLoop: false,
6156
+ srcVx: 0,
6157
+ tgtVx: 0,
6158
+ hY: 0
6159
+ });
6160
+ }
6161
+ const hHalfBand = (p) => p.hKind === "pad-top" ? round16(UML_PADDING / 2) : round16(UML_ROW_GAP / 2);
6162
+ const vByKey = /* @__PURE__ */ new Map();
6163
+ const pushV = (col, side, portY, hBase, half, inset, set) => {
6164
+ const lo = Math.min(portY, round16(hBase - half));
6165
+ const hi = Math.max(portY, round16(hBase + half));
6166
+ const key = `${col}:${side}`;
6167
+ const arr = vByKey.get(key) ?? [];
6168
+ arr.push({ lo, hi, inset, lane: 0, set });
6169
+ vByKey.set(key, arr);
6170
+ };
6171
+ for (const p of plans) {
6172
+ if (p.isSelfLoop) {
6173
+ const lo = Math.min(p.srcPortY, p.tgtPortY);
6174
+ const hi = Math.max(p.srcPortY, p.tgtPortY);
6175
+ const key = `${p.srcVCol}:right`;
6176
+ const arr = vByKey.get(key) ?? [];
6177
+ arr.push({ lo, hi, inset: Math.max(p.srcInset, p.tgtInset), lane: 0, set: (x) => {
6178
+ p.srcVx = x;
6179
+ p.tgtVx = x;
6180
+ } });
6181
+ vByKey.set(key, arr);
6182
+ continue;
6183
+ }
6184
+ if (p.srcVCol === p.tgtVCol && p.srcSide === p.tgtSide) {
6185
+ const lo = Math.min(p.srcPortY, p.tgtPortY);
6186
+ const hi = Math.max(p.srcPortY, p.tgtPortY);
6187
+ const key = `${p.srcVCol}:${p.srcSide}`;
6188
+ const arr = vByKey.get(key) ?? [];
6189
+ arr.push({ lo, hi, inset: Math.max(p.srcInset, p.tgtInset), lane: 0, set: (x) => {
6190
+ p.srcVx = x;
6191
+ p.tgtVx = x;
6192
+ } });
6193
+ vByKey.set(key, arr);
6194
+ continue;
6195
+ }
6196
+ const hBase = hBaseY(p, rowY, grid);
6197
+ const half = hHalfBand(p);
6198
+ pushV(p.srcVCol, p.srcSide, p.srcPortY, hBase, half, p.srcInset, (x) => {
6199
+ p.srcVx = x;
6200
+ });
6201
+ pushV(p.tgtVCol, p.tgtSide, p.tgtPortY, hBase, half, p.tgtInset, (x) => {
6202
+ p.tgtVx = x;
6203
+ });
6204
+ }
6205
+ for (const [key, items] of vByKey.entries()) {
6206
+ const side = key.endsWith(":right") ? "right" : "left";
6207
+ const col = Number(key.slice(0, key.lastIndexOf(":")));
6208
+ const laneCount = allocateLanes(
6209
+ items.map((it) => ({ lo: it.lo, hi: it.hi, set: (l) => {
6210
+ it.lane = l;
6211
+ } }))
6212
+ );
6213
+ const left = vGutterLeft(col, colX, grid);
6214
+ const right = round16(left + UML_COL_GAP);
6215
+ const center = round16((left + right) / 2);
6216
+ const maxBaseGap = Math.max(...items.map((it) => Math.max(V_GUTTER_MARGIN, it.inset + MIN_TERMINAL_STUB)));
6217
+ for (const it of items) {
6218
+ let x;
6219
+ if (side === "right") {
6220
+ const lo = left + maxBaseGap;
6221
+ const hi = center - 1;
6222
+ x = laneCount <= 1 ? lo : lo + it.lane * (hi - lo) / (laneCount - 1);
6223
+ } else {
6224
+ const hi = right - maxBaseGap;
6225
+ const lo = center + 1;
6226
+ x = laneCount <= 1 ? hi : hi - it.lane * (hi - lo) / (laneCount - 1);
6227
+ }
6228
+ it.set(round16(x));
6229
+ }
6230
+ }
6231
+ const usableHalfOf = (kind) => Math.max(0, (kind === "pad-top" ? round16(UML_PADDING / 2) : round16(UML_ROW_GAP / 2)) - 3);
6232
+ const gutterKey = (p) => p.hKind === "pad-top" ? "pad-top" : `row:${p.hRow}`;
6233
+ const gutterMeta = /* @__PURE__ */ new Map();
6234
+ for (const p of plans) {
6235
+ if (p.isSelfLoop) continue;
6236
+ const key = gutterKey(p);
6237
+ if (!gutterMeta.has(key)) gutterMeta.set(key, { base: hBaseY(p, rowY, grid), usableHalf: usableHalfOf(p.hKind) });
6238
+ }
6239
+ const hRunsByKey = /* @__PURE__ */ new Map();
6240
+ const pushRun = (key, run) => {
6241
+ const arr = hRunsByKey.get(key) ?? [];
6242
+ arr.push(run);
6243
+ hRunsByKey.set(key, arr);
6244
+ };
6245
+ const pinStub = (yy, a, b) => {
6246
+ for (const [gk, meta] of gutterMeta.entries()) {
6247
+ if (yy >= meta.base - meta.usableHalf - LANE_EPS && yy <= meta.base + meta.usableHalf + LANE_EPS) {
6248
+ pushRun(gk, { lo: Math.min(a, b), hi: Math.max(a, b), y: yy, set: () => {
6249
+ } });
6250
+ }
6251
+ }
6252
+ };
6253
+ for (const p of plans) {
6254
+ if (p.isSelfLoop) {
6255
+ const sideX = p.srcSide === "right" ? boxEdges(p.srcNode).right : boxEdges(p.srcNode).left;
6256
+ pinStub(p.srcPortY, sideX, p.srcVx);
6257
+ pinStub(p.tgtPortY, sideX, p.tgtVx);
6258
+ continue;
6259
+ }
6260
+ const key = gutterKey(p);
6261
+ if (Math.abs(p.srcVx - p.tgtVx) > LANE_EPS) {
6262
+ pushRun(key, { lo: Math.min(p.srcVx, p.tgtVx), hi: Math.max(p.srcVx, p.tgtVx), y: null, set: (y) => {
6263
+ p.hY = round16(y);
6264
+ } });
6265
+ } else {
6266
+ p.hY = p.srcPortY;
6267
+ }
6268
+ const srcPortX = p.srcSide === "right" ? boxEdges(p.srcNode).right : boxEdges(p.srcNode).left;
6269
+ const tgtPortX = p.tgtSide === "right" ? boxEdges(p.tgtNode).right : boxEdges(p.tgtNode).left;
6270
+ pinStub(p.srcPortY, srcPortX, p.srcVx);
6271
+ pinStub(p.tgtPortY, tgtPortX, p.tgtVx);
6272
+ }
6273
+ for (const [key, runs] of hRunsByKey.entries()) {
6274
+ const meta = gutterMeta.get(key);
6275
+ allocateHorizontal(runs, meta.base, meta.usableHalf);
6276
+ }
6277
+ for (const p of plans) {
6278
+ const srcPort = sidePort(p.srcNode, p.srcSide, p.srcPortY - p.srcNode.cy);
6279
+ const tgtPort = sidePort(p.tgtNode, p.tgtSide, p.tgtPortY - p.tgtNode.cy);
6280
+ if (p.isSelfLoop) {
6281
+ const raw2 = [
6282
+ srcPort,
6283
+ { x: p.srcVx, y: srcPort.y },
6284
+ { x: p.srcVx, y: tgtPort.y },
6285
+ tgtPort
6286
+ ];
6287
+ const portPts2 = simplifyWaypoints(raw2);
6288
+ const pts2 = applyGlyphInsets(portPts2, p.srcInset, p.tgtInset);
6289
+ const edgeId2 = UML_SELF_LOOP_BASE + p.rel.id;
6290
+ elements.push({ edgeId: edgeId2, relId: p.rel.id, points: pts2, kind: "self-loop", title: relTitle(p.rel) });
6291
+ reserveSelfLoopText(p.rel, p, textBoxes);
6292
+ continue;
6293
+ }
6294
+ const raw = [
6295
+ srcPort,
6296
+ { x: p.srcVx, y: srcPort.y },
6297
+ { x: p.srcVx, y: p.hY },
6298
+ { x: p.tgtVx, y: p.hY },
6299
+ { x: p.tgtVx, y: tgtPort.y },
6300
+ tgtPort
6301
+ ];
6302
+ const portPts = simplifyWaypoints(raw);
6303
+ const pts = applyGlyphInsets(portPts, p.srcInset, p.tgtInset);
6304
+ const edgeId = UML_ASSOC_ROUTE_BASE + p.rel.id;
6305
+ elements.push({ edgeId, relId: p.rel.id, points: pts, kind: "route", title: relTitle(p.rel) });
6306
+ reserveRouteText(p.rel, p, hBandBounds(p, rowY, grid), textBoxes);
6307
+ }
6308
+ let maxRight = 0;
6309
+ let maxBottom = 0;
6310
+ for (const node of nodes) {
6311
+ const { right, bottom } = boxEdges(node);
6312
+ if (right > maxRight) maxRight = right;
6313
+ if (bottom > maxBottom) maxBottom = bottom;
6314
+ }
6315
+ for (const el of elements) {
6316
+ for (const pt2 of el.points) {
6317
+ if (pt2.x > maxRight) maxRight = pt2.x;
6318
+ if (pt2.y > maxBottom) maxBottom = pt2.y;
6319
+ }
6320
+ }
6321
+ for (const tb of textBoxes) {
6322
+ if (tb.x + tb.w > maxRight) maxRight = tb.x + tb.w;
6323
+ if (tb.y + tb.h > maxBottom) maxBottom = tb.y + tb.h;
6324
+ }
6325
+ const canvasW = round16(maxRight + UML_PADDING);
6326
+ const canvasH = round16(maxBottom + UML_PADDING);
6327
+ return { nodes, elements, textBoxes, canvasW, canvasH, grid, colX, rowY };
6328
+ }
6329
+ function reserveRouteText(rel, p, band, out) {
6330
+ const textH = round16(UML_LABEL_FONT * 1.2);
6331
+ const clampY = (y) => round16(Math.max(band.lo + 1, Math.min(y, band.hi - 1 - textH)));
6332
+ const above = clampY(p.hY - textH - 1);
6333
+ const below = clampY(p.hY + 1);
6334
+ const srcX = round16(p.srcVx + 3);
6335
+ const tgtX = round16(p.tgtVx + 3);
6336
+ if (rel.sourceMultiplicity !== null) {
6337
+ out.push(reserveTextBox(rel.id, "src-mult", rel.sourceMultiplicity, srcX, above, "start"));
6338
+ }
6339
+ if (rel.sourceRole !== null) {
6340
+ out.push(reserveTextBox(rel.id, "src-role", rel.sourceRole, srcX, below, "start"));
6341
+ }
6342
+ if (rel.targetMultiplicity !== null) {
6343
+ out.push(reserveTextBox(rel.id, "tgt-mult", rel.targetMultiplicity, tgtX, above, "start"));
6344
+ }
6345
+ if (rel.targetRole !== null) {
6346
+ out.push(reserveTextBox(rel.id, "tgt-role", rel.targetRole, tgtX, below, "start"));
6347
+ }
6348
+ if (rel.label !== null) {
6349
+ const midX = round16((p.srcVx + p.tgtVx) / 2);
6350
+ out.push(reserveTextBox(rel.id, "label", rel.label, midX, above, "middle"));
6351
+ }
6352
+ }
6353
+ function reserveSelfLoopText(rel, p, out) {
6354
+ const x0 = round16(p.srcVx + 3);
6355
+ const srcY = p.srcPortY;
6356
+ const tgtY = p.tgtPortY;
6357
+ if (rel.sourceMultiplicity !== null) {
6358
+ out.push(reserveTextBox(rel.id, "src-mult", rel.sourceMultiplicity, x0, round16(srcY - UML_LABEL_FONT - 1), "start"));
6359
+ }
6360
+ if (rel.sourceRole !== null) {
6361
+ out.push(reserveTextBox(rel.id, "src-role", rel.sourceRole, x0, round16(srcY + 1), "start"));
6362
+ }
6363
+ if (rel.targetMultiplicity !== null) {
6364
+ out.push(reserveTextBox(rel.id, "tgt-mult", rel.targetMultiplicity, x0, round16(tgtY - UML_LABEL_FONT - 1), "start"));
6365
+ }
6366
+ if (rel.targetRole !== null) {
6367
+ out.push(reserveTextBox(rel.id, "tgt-role", rel.targetRole, x0, round16(tgtY + 1), "start"));
6368
+ }
6369
+ if (rel.label !== null) {
6370
+ out.push(reserveTextBox(rel.id, "label", rel.label, x0, round16((srcY + tgtY) / 2 - UML_LABEL_FONT / 2), "start"));
6371
+ }
6372
+ }
6373
+
6374
+ // src/uml/svg.ts
6375
+ var GLYPH_STROKE6 = "#52525b";
6376
+ var LABEL_FILL7 = "#3f3f46";
6377
+ var EDGE_INK8 = "#71717a";
6378
+ var round17 = (n) => Math.round(n * 100) / 100;
6379
+ function emitGlyph(gk, tip, dir) {
6380
+ switch (gk) {
6381
+ case "none":
6382
+ return "";
6383
+ case "open-arrow": {
6384
+ const pts = arrowOpenPoints(tip, dir);
6385
+ return `<polyline points="${pts}" fill="none" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
6386
+ }
6387
+ case "hollow-triangle": {
6388
+ const pts = trianglePoints(tip, dir);
6389
+ return `<polygon points="${pts}" fill="#fff" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
6390
+ }
6391
+ case "hollow-diamond": {
6392
+ const pts = diamondPoints(tip, dir);
6393
+ return `<polygon points="${pts}" fill="#fff" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
6394
+ }
6395
+ case "solid-diamond": {
6396
+ const pts = diamondPoints(tip, dir);
6397
+ return `<polygon points="${pts}" fill="${GLYPH_STROKE6}" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`;
6398
+ }
6399
+ }
6400
+ }
6401
+ function glyphDir(firstPt, secondPt) {
6402
+ const dx = secondPt.x - firstPt.x;
6403
+ const dy = secondPt.y - firstPt.y;
6404
+ if (Math.abs(dx) >= Math.abs(dy)) {
6405
+ return dx >= 0 ? "left" : "right";
6406
+ }
6407
+ return dy >= 0 ? "up" : "down";
6408
+ }
6409
+ function featureSvg(f, x, y, font) {
6410
+ const prefix = f.visibility !== null ? UML_VIS_GLYPH[f.visibility] + " " : "";
6411
+ const text = prefix + f.text;
6412
+ const baseline = round17(y + font * 0.8);
6413
+ let out = `<text x="${x}" y="${baseline}" font-family="${FONT_FAMILY}" font-size="${font}" fill="${LABEL_FILL7}">${xmlEscape(text)}</text>`;
6414
+ if (f.isStatic) {
6415
+ const lineY = round17(baseline + 1);
6416
+ const approxW = round17(text.length * font * 0.6);
6417
+ out += `<line x1="${x}" y1="${lineY}" x2="${round17(x + approxW)}" y2="${lineY}" stroke="${LABEL_FILL7}" stroke-width="0.8"/>`;
6418
+ }
6419
+ return out;
6420
+ }
6421
+ function boxSvg2(node, cls) {
6422
+ const left = round17(node.cx - node.boxW / 2);
6423
+ const top = round17(node.cy - node.boxH / 2);
6424
+ const metrics = node.metrics;
6425
+ const pieces = [];
6426
+ pieces.push(`<title>${xmlEscape(node.title)}</title>`);
6427
+ pieces.push(
6428
+ `<rect x="${left}" y="${top}" width="${node.boxW}" height="${node.boxH}" rx="2" fill="#fff" stroke="${GLYPH_STROKE6}" stroke-width="1.5"/>`
6429
+ );
6430
+ for (const dy of metrics.dividerYs) {
6431
+ const y = round17(top + dy);
6432
+ pieces.push(
6433
+ `<line x1="${left}" y1="${y}" x2="${round17(left + node.boxW)}" y2="${y}" stroke="${GLYPH_STROKE6}" stroke-width="0.75"/>`
6434
+ );
6435
+ }
6436
+ const compartment0 = metrics.rows[0];
6437
+ const textX = round17(left + node.boxW / 2);
6438
+ let lineIdx = 0;
6439
+ if (cls.stereotype !== null) {
6440
+ const stereoY = round17(top + compartment0.top + lineIdx * UML_LINE_H + UML_NAME_FONT * 0.8);
6441
+ pieces.push(
6442
+ `<text x="${textX}" y="${stereoY}" font-family="${FONT_FAMILY}" font-size="${UML_NAME_FONT}" fill="${LABEL_FILL7}" text-anchor="middle">${xmlEscape(cls.stereotype)}</text>`
6443
+ );
6444
+ lineIdx++;
6445
+ }
6446
+ const nameY = round17(top + compartment0.top + lineIdx * UML_LINE_H + UML_NAME_FONT * 0.8);
6447
+ const nameStyle = cls.isAbstract ? ` font-style="italic"` : "";
6448
+ const nameText = metrics.rows[0].lines[lineIdx] ?? "";
6449
+ pieces.push(
6450
+ `<text x="${textX}" y="${nameY}" font-family="${FONT_FAMILY}" font-size="${UML_NAME_FONT}" fill="${LABEL_FILL7}" text-anchor="middle"${nameStyle}>${xmlEscape(nameText)}</text>`
6451
+ );
6452
+ const attrCompartment = metrics.rows[1];
6453
+ const attrX = round17(left + UML_BOX_PAD_X);
6454
+ for (let i = 0; i < cls.attributes.length; i++) {
6455
+ const f = cls.attributes[i];
6456
+ const fy = round17(top + attrCompartment.top + i * UML_LINE_H);
6457
+ pieces.push(featureSvg(f, attrX, fy, UML_FEAT_FONT));
6458
+ }
6459
+ const opCompartment = metrics.rows[2];
6460
+ const opX = round17(left + UML_BOX_PAD_X);
6461
+ for (let i = 0; i < cls.operations.length; i++) {
6462
+ const f = cls.operations[i];
6463
+ const fy = round17(top + opCompartment.top + i * UML_LINE_H);
6464
+ pieces.push(featureSvg(f, opX, fy, UML_FEAT_FONT));
6465
+ }
6466
+ return `<g data-node-id="c${node.classId}">${pieces.join("")}</g>`;
6467
+ }
6468
+ function edgeSvg(el, rel, textBoxes) {
6469
+ const glyphs = RELATION_GLYPHS[rel.kind];
6470
+ const isDashed = glyphs.line === "dashed";
6471
+ const stroke = isDashed ? EDGE_STROKE.distant : { width: 1.5, opacity: 0.7 };
6472
+ const strokeAttrs = isDashed ? `stroke="${EDGE_INK8}" stroke-width="${stroke.width}" stroke-dasharray="4,4" opacity="${stroke.opacity}"` : `stroke="${EDGE_INK8}" stroke-width="${stroke.width}" opacity="${stroke.opacity}"`;
6473
+ const pts = el.points;
6474
+ const pieces = [`<title>${xmlEscape(el.title)}</title>`];
6475
+ if (pts.length >= 2) {
6476
+ pieces.push(`<polyline points="${pts.map((p) => `${p.x},${p.y}`).join(" ")}" fill="none" ${strokeAttrs}/>`);
6477
+ if (glyphs.sourceEnd !== "none") {
6478
+ const tip = pts[0];
6479
+ const next = pts[1];
6480
+ const dir = glyphDir(tip, next);
6481
+ pieces.push(emitGlyph(glyphs.sourceEnd, tip, dir));
6482
+ }
6483
+ if (glyphs.targetEnd !== "none") {
6484
+ const tip = pts[pts.length - 1];
6485
+ const prev = pts[pts.length - 2];
6486
+ const dir = glyphDir(tip, prev);
6487
+ pieces.push(emitGlyph(glyphs.targetEnd, tip, dir));
6488
+ }
6489
+ for (const tb of textBoxes) {
6490
+ if (tb.relId !== rel.id) continue;
6491
+ const italic = tb.slot === "src-role" || tb.slot === "tgt-role" ? ` font-style="italic"` : "";
6492
+ const anchor = tb.anchor === "middle" ? ` text-anchor="middle"` : "";
6493
+ const drawX = tb.anchor === "middle" ? round17(tb.x + tb.w / 2) : tb.x;
6494
+ const baseline = round17(tb.y + UML_LABEL_FONT * 0.8);
6495
+ pieces.push(
6496
+ `<text x="${drawX}" y="${baseline}" font-family="${FONT_FAMILY}" font-size="${UML_LABEL_FONT}" fill="${LABEL_FILL7}"${anchor}${italic}>${xmlEscape(tb.text)}</text>`
6497
+ );
6498
+ }
6499
+ }
6500
+ return `<g data-edge-id="${el.edgeId}">${pieces.join("")}</g>`;
6501
+ }
6502
+ function makeSwatchLine(isDashed) {
6503
+ return (x, y) => {
6504
+ const attrs = isDashed ? `stroke="${EDGE_INK8}" stroke-width="1.5" stroke-dasharray="4,4"` : `stroke="${EDGE_INK8}" stroke-width="1.5"`;
6505
+ return `<line x1="${x}" y1="${y}" x2="${x + LEGEND_SWATCH_W}" y2="${y}" ${attrs}/>`;
6506
+ };
6507
+ }
6508
+ function umlLayoutSvg(layout, input, opts = {}) {
6509
+ const labels = opts.labels ?? UML_SVG_LABELS_EN;
6510
+ const showLegend = opts.legend !== false;
6511
+ const classMap = new Map(input.classes.map((c) => [c.id, c]));
6512
+ const relMap = new Map(input.relationships.map((r2) => [r2.id, r2]));
6513
+ const parts = [];
6514
+ for (const node of layout.nodes) {
6515
+ const cls = classMap.get(node.classId);
6516
+ parts.push(boxSvg2(node, cls));
6517
+ }
6518
+ for (const el of layout.elements) {
6519
+ const rel = relMap.get(el.relId);
6520
+ if (!rel) continue;
6521
+ parts.push(edgeSvg(el, rel, layout.textBoxes));
6522
+ }
6523
+ let canvasW = layout.canvasW;
6524
+ let canvasH = layout.canvasH;
6525
+ if (showLegend) {
6526
+ const usedKinds = new Set(
6527
+ input.relationships.map((r2) => r2.kind)
6528
+ );
6529
+ const entries = [];
6530
+ for (const kind of UML_RELATIONSHIPS) {
6531
+ if (!usedKinds.has(kind)) continue;
6532
+ const isDashed = RELATION_GLYPHS[kind].line === "dashed";
6533
+ entries.push({
6534
+ swatch: makeSwatchLine(isDashed),
6535
+ label: labels.legend[kind]
6536
+ });
6537
+ }
6538
+ if (entries.length > 0) {
6539
+ const block = legendBlock(entries, canvasH);
6540
+ parts.push(block.svg);
6541
+ canvasH = block.height;
6542
+ canvasW = Math.max(canvasW, block.width);
6543
+ }
6544
+ }
6545
+ const roundedW = round17(canvasW);
6546
+ const roundedH = round17(canvasH);
6547
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${roundedW}" height="${roundedH}" viewBox="0 0 ${roundedW} ${roundedH}" role="img" aria-label="${xmlEscape(labels.ariaLabel)}">` + parts.join("") + `</svg>`;
6548
+ }
6549
+
6550
+ // src/uml/render.ts
6551
+ function umlSvg(input, opts = {}) {
6552
+ const layout = computeUmlLayout(input, opts.titleLabels ?? UML_TITLE_LABELS_EN);
6553
+ const svg = umlLayoutSvg(layout, input, opts);
6554
+ return { svg, layout };
6555
+ }
6556
+
6557
+ exports.ANNOTATION_INK = ANNOTATION_INK;
4666
6558
  exports.CHAR_W = CHAR_W;
4667
6559
  exports.CODE_FONT = CODE_FONT;
4668
6560
  exports.ECOMAP_LABELS_EN = ECOMAP_LABELS_EN;
@@ -4685,6 +6577,12 @@ exports.FishboneValidationError = FishboneValidationError;
4685
6577
  exports.GATE_TYPES = GATE_TYPES;
4686
6578
  exports.GENOGRAM_SVG_LABELS_EN = GENOGRAM_SVG_LABELS_EN;
4687
6579
  exports.GENOGRAM_TITLE_LABELS_EN = GENOGRAM_TITLE_LABELS_EN;
6580
+ exports.GLYPH_ARROW_HALF = GLYPH_ARROW_HALF;
6581
+ exports.GLYPH_ARROW_LEN = GLYPH_ARROW_LEN;
6582
+ exports.GLYPH_DIAMOND_HALF = GLYPH_DIAMOND_HALF;
6583
+ exports.GLYPH_DIAMOND_LEN = GLYPH_DIAMOND_LEN;
6584
+ exports.GLYPH_TRI_HALF = GLYPH_TRI_HALF;
6585
+ exports.GLYPH_TRI_LEN = GLYPH_TRI_LEN;
4688
6586
  exports.KINSHIP_EN = KINSHIP_EN;
4689
6587
  exports.LABEL_FONT = LABEL_FONT;
4690
6588
  exports.LABEL_GAP = LABEL_GAP;
@@ -4736,13 +6634,63 @@ exports.PHYLO_SCALEBAR_ID = PHYLO_SCALEBAR_ID;
4736
6634
  exports.PHYLO_SUPPORT_FONT = PHYLO_SUPPORT_FONT;
4737
6635
  exports.PHYLO_SVG_LABELS_EN = PHYLO_SVG_LABELS_EN;
4738
6636
  exports.PHYLO_TITLE_LABELS_EN = PHYLO_TITLE_LABELS_EN;
6637
+ exports.PRISMA_BAND_LABEL_W = PRISMA_BAND_LABEL_W;
6638
+ exports.PRISMA_BOX_KINDS = PRISMA_BOX_KINDS;
6639
+ exports.PRISMA_BOX_PAD_X = PRISMA_BOX_PAD_X;
6640
+ exports.PRISMA_BOX_PAD_Y = PRISMA_BOX_PAD_Y;
6641
+ exports.PRISMA_COLUMNS = PRISMA_COLUMNS;
6642
+ exports.PRISMA_COL_GAP = PRISMA_COL_GAP;
6643
+ exports.PRISMA_CORRIDOR = PRISMA_CORRIDOR;
6644
+ exports.PRISMA_COUNT_FONT = PRISMA_COUNT_FONT;
6645
+ exports.PRISMA_EXCL_ARROW_BASE = PRISMA_EXCL_ARROW_BASE;
6646
+ exports.PRISMA_EXCL_GAP = PRISMA_EXCL_GAP;
6647
+ exports.PRISMA_FLOW_ARROW_BASE = PRISMA_FLOW_ARROW_BASE;
6648
+ exports.PRISMA_HEADING_FONT = PRISMA_HEADING_FONT;
6649
+ exports.PRISMA_LINE_H = PRISMA_LINE_H;
6650
+ exports.PRISMA_MERGE_ARROW_BASE = PRISMA_MERGE_ARROW_BASE;
6651
+ exports.PRISMA_MIN_BOX_W = PRISMA_MIN_BOX_W;
6652
+ exports.PRISMA_PADDING = PRISMA_PADDING;
6653
+ exports.PRISMA_PHASES = PRISMA_PHASES;
6654
+ exports.PRISMA_SVG_LABELS_EN = PRISMA_SVG_LABELS_EN;
6655
+ exports.PRISMA_TITLE_LABELS_EN = PRISMA_TITLE_LABELS_EN;
4739
6656
  exports.PROMOTED_REL_ID_BASE = PROMOTED_REL_ID_BASE;
4740
6657
  exports.PedigreeValidationError = PedigreeValidationError;
4741
6658
  exports.PhyloValidationError = PhyloValidationError;
6659
+ exports.PrismaValidationError = PrismaValidationError;
4742
6660
  exports.QUALITY_LEXICON_EN = QUALITY_LEXICON_EN;
6661
+ exports.RELATION_GLYPHS = RELATION_GLYPHS;
6662
+ exports.UML_ASSOC_ROUTE_BASE = UML_ASSOC_ROUTE_BASE;
6663
+ exports.UML_BOX_PAD_X = UML_BOX_PAD_X;
6664
+ exports.UML_BOX_PAD_Y = UML_BOX_PAD_Y;
6665
+ exports.UML_COL_GAP = UML_COL_GAP;
6666
+ exports.UML_FEAT_FONT = UML_FEAT_FONT;
6667
+ exports.UML_GLYPH_BASE = UML_GLYPH_BASE;
6668
+ exports.UML_LABEL_FONT = UML_LABEL_FONT;
6669
+ exports.UML_LINE_H = UML_LINE_H;
6670
+ exports.UML_MIN_BOX_W = UML_MIN_BOX_W;
6671
+ exports.UML_NAME_FONT = UML_NAME_FONT;
6672
+ exports.UML_PADDING = UML_PADDING;
6673
+ exports.UML_PORT_PITCH = UML_PORT_PITCH;
6674
+ exports.UML_RELATIONSHIPS = UML_RELATIONSHIPS;
6675
+ exports.UML_ROW_GAP = UML_ROW_GAP;
6676
+ exports.UML_SELF_LOOP_BASE = UML_SELF_LOOP_BASE;
6677
+ exports.UML_SELF_LOOP_W = UML_SELF_LOOP_W;
6678
+ exports.UML_SEP_PAD = UML_SEP_PAD;
6679
+ exports.UML_SVG_LABELS_EN = UML_SVG_LABELS_EN;
6680
+ exports.UML_TITLE_LABELS_EN = UML_TITLE_LABELS_EN;
6681
+ exports.UML_VISIBILITIES = UML_VISIBILITIES;
6682
+ exports.UML_VIS_GLYPH = UML_VIS_GLYPH;
4743
6683
  exports.UNION_NOTATION = UNION_NOTATION;
4744
6684
  exports.UNION_REL_ID_BASE = UNION_REL_ID_BASE;
4745
6685
  exports.UNION_STATUSES = UNION_STATUSES;
6686
+ exports.UmlValidationError = UmlValidationError;
6687
+ exports.allocateLanes = allocateLanes;
6688
+ exports.annotationDot = annotationDot;
6689
+ exports.annotationSwatch = annotationSwatch;
6690
+ exports.annotationTick = annotationTick;
6691
+ exports.arrowFilledPoints = arrowFilledPoints;
6692
+ exports.arrowOpenPoints = arrowOpenPoints;
6693
+ exports.bandStack = bandStack;
4746
6694
  exports.clampLabel = clampLabel;
4747
6695
  exports.classifyRelationshipType = classifyRelationshipType;
4748
6696
  exports.computeFaultTreeLayout = computeFaultTreeLayout;
@@ -4750,6 +6698,9 @@ exports.computeGenogramLayout = computeGenogramLayout;
4750
6698
  exports.computeOrgChartLayout = computeOrgChartLayout;
4751
6699
  exports.computePedigreeLayout = computePedigreeLayout;
4752
6700
  exports.computePhyloLayout = computePhyloLayout;
6701
+ exports.computePrismaLayout = computePrismaLayout;
6702
+ exports.computeUmlLayout = computeUmlLayout;
6703
+ exports.diamondPoints = diamondPoints;
4753
6704
  exports.ecomapSvg = ecomapSvg;
4754
6705
  exports.estimateTextWidth = estimateTextWidth;
4755
6706
  exports.faultTreeIssues = faultTreeIssues;
@@ -4758,13 +6709,16 @@ exports.faultTreeSvg = faultTreeSvg;
4758
6709
  exports.fishboneSvg = fishboneSvg;
4759
6710
  exports.genogramLayoutSvg = genogramLayoutSvg;
4760
6711
  exports.genogramSvg = genogramSvg;
6712
+ exports.glyphInset = glyphInset;
4761
6713
  exports.latestUnionPerPair = latestUnionPerPair;
4762
6714
  exports.legendBlock = legendBlock;
6715
+ exports.measureCompartmentBox = measureCompartmentBox;
4763
6716
  exports.niceScaleStep = niceScaleStep;
4764
6717
  exports.normalizeText = normalizeText;
4765
6718
  exports.orgChartIssues = orgChartIssues;
4766
6719
  exports.orgChartLayoutSvg = orgChartLayoutSvg;
4767
6720
  exports.orgChartSvg = orgChartSvg;
6721
+ exports.packGrid = packGrid;
4768
6722
  exports.packSubtree = packSubtree;
4769
6723
  exports.pathData = pathData;
4770
6724
  exports.pedigreeIssues = pedigreeIssues;
@@ -4773,13 +6727,24 @@ exports.pedigreeSvg = pedigreeSvg;
4773
6727
  exports.phyloIssues = phyloIssues;
4774
6728
  exports.phyloLayoutSvg = phyloLayoutSvg;
4775
6729
  exports.phyloSvg = phyloSvg;
6730
+ exports.prismaIssues = prismaIssues;
6731
+ exports.prismaLayoutSvg = prismaLayoutSvg;
6732
+ exports.prismaSvg = prismaSvg;
4776
6733
  exports.qualityLineStyle = qualityLineStyle;
4777
6734
  exports.relationshipTypeTokens = relationshipTypeTokens;
4778
6735
  exports.romanNumeral = romanNumeral;
6736
+ exports.sideCapacity = sideCapacity;
6737
+ exports.sideCapacityIssue = sideCapacityIssue;
6738
+ exports.trianglePoints = trianglePoints;
6739
+ exports.umlIssues = umlIssues;
6740
+ exports.umlLayoutSvg = umlLayoutSvg;
6741
+ exports.umlSvg = umlSvg;
4779
6742
  exports.validateFaultTree = validateFaultTree;
4780
6743
  exports.validateOrgChart = validateOrgChart;
4781
6744
  exports.validatePedigree = validatePedigree;
4782
6745
  exports.validatePhylo = validatePhylo;
6746
+ exports.validatePrisma = validatePrisma;
6747
+ exports.validateUml = validateUml;
4783
6748
  exports.wrapLabel = wrapLabel;
4784
6749
  exports.wrapLabelBalanced = wrapLabelBalanced;
4785
6750
  exports.xmlEscape = xmlEscape;