@crazyhappyone/auto-graph 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { CommanderError, Command } from 'commander';
3
- import { Graph, layout } from '@dagrejs/dagre';
4
3
  import { createRequire } from 'module';
5
4
  import { prepareWithSegments, layoutWithLines, measureNaturalWidth } from '@chenglou/pretext';
5
+ import { Graph, layout } from '@dagrejs/dagre';
6
6
  import { Buffer as Buffer$1 } from 'buffer';
7
7
  import { parseDocument } from 'yaml';
8
8
  import { z } from 'zod';
@@ -144,6 +144,15 @@ function exportExcalidraw(diagram, options = {}) {
144
144
  elements.push(text);
145
145
  }
146
146
  }
147
+ for (const matrix of diagram.matrices ?? []) {
148
+ elements.push(...renderMatrixBlock(matrix));
149
+ }
150
+ for (const table of diagram.tables ?? []) {
151
+ elements.push(...renderTableBlock(table));
152
+ }
153
+ for (const panel of diagram.evidencePanels ?? []) {
154
+ elements.push(...renderEvidencePanel(panel));
155
+ }
147
156
  for (const edge of diagram.edges) {
148
157
  elements.push(renderArrow(edge));
149
158
  }
@@ -176,6 +185,83 @@ function renderNode(node, groupIds) {
176
185
  groupIds
177
186
  };
178
187
  }
188
+ function renderMatrixBlock(matrix) {
189
+ const containerId = `matrix:${matrix.id}`;
190
+ const groupIds = [containerId];
191
+ const label = blockText([
192
+ matrix.id,
193
+ `row | ${matrix.cols.join(" | ")}`,
194
+ ...matrix.rows.map((rowId, rowIndex) => {
195
+ const row = matrix.cells[rowIndex] ?? [];
196
+ return `${rowId}: ${matrix.cols.map((_, columnIndex) => row[columnIndex]?.text ?? "").join(" | ")}`;
197
+ })
198
+ ]);
199
+ return [
200
+ {
201
+ ...baseElement(containerId, "rectangle", matrix.box),
202
+ backgroundColor: matrix.style?.fill ?? "#f8fafc",
203
+ strokeColor: matrix.style?.stroke ?? "#374151",
204
+ groupIds
205
+ },
206
+ renderTextBlock(
207
+ `matrix-text:${matrix.id}`,
208
+ label,
209
+ matrix.box,
210
+ containerId,
211
+ groupIds
212
+ )
213
+ ];
214
+ }
215
+ function renderTableBlock(table) {
216
+ const containerId = `table:${table.id}`;
217
+ const groupIds = [containerId];
218
+ const label = blockText([
219
+ table.columns.map((column) => column.label.text).join(" | "),
220
+ ...table.rows.map(
221
+ (row) => table.columns.map((column) => row.cells[column.id]?.text ?? "").join(" | ")
222
+ )
223
+ ]);
224
+ return [
225
+ {
226
+ ...baseElement(containerId, "rectangle", table.box),
227
+ backgroundColor: table.style?.fill ?? "#f8fafc",
228
+ strokeColor: table.style?.stroke ?? "#374151",
229
+ groupIds
230
+ },
231
+ renderTextBlock(
232
+ `table-text:${table.id}`,
233
+ label,
234
+ table.box,
235
+ containerId,
236
+ groupIds
237
+ )
238
+ ];
239
+ }
240
+ function renderEvidencePanel(panel) {
241
+ const containerId = `evidence-panel:${panel.id}`;
242
+ const groupIds = [containerId];
243
+ const label = blockText([
244
+ `${panel.kind}: ${panel.id}`,
245
+ ...panel.items.map(
246
+ (item) => item.detail?.text === void 0 ? item.label.text : `${item.label.text}: ${item.detail.text}`
247
+ )
248
+ ]);
249
+ return [
250
+ {
251
+ ...baseElement(containerId, "rectangle", panel.box),
252
+ backgroundColor: panel.style?.fill ?? panelKindFill(panel.kind),
253
+ strokeColor: panel.style?.stroke ?? "#374151",
254
+ groupIds
255
+ },
256
+ renderTextBlock(
257
+ `evidence-panel-text:${panel.id}`,
258
+ label,
259
+ panel.box,
260
+ containerId,
261
+ groupIds
262
+ )
263
+ ];
264
+ }
179
265
  function renderArrow(edge) {
180
266
  const first = edge.points[0];
181
267
  if (first === void 0) {
@@ -235,6 +321,34 @@ function renderText(id, label, box, containerId, groupIds) {
235
321
  versionNonce: seedFor(`${id}:nonce`)
236
322
  };
237
323
  }
324
+ function renderTextBlock(id, text, box, containerId, groupIds) {
325
+ const fontSize = 12;
326
+ return {
327
+ ...baseElement(id, "text", {
328
+ x: box.x + 8,
329
+ y: box.y + 8,
330
+ width: Math.max(0, box.width - 16),
331
+ height: Math.max(fontSize, box.height - 16)
332
+ }),
333
+ backgroundColor: "transparent",
334
+ strokeColor: "#111827",
335
+ groupIds,
336
+ text,
337
+ fontSize,
338
+ fontFamily: 1,
339
+ textAlign: "left",
340
+ verticalAlign: "top",
341
+ baseline: fontSize,
342
+ containerId,
343
+ originalText: text,
344
+ lineHeight: 1.25,
345
+ boundElements: null,
346
+ link: null,
347
+ locked: false,
348
+ seed: seedFor(id),
349
+ versionNonce: seedFor(`${id}:nonce`)
350
+ };
351
+ }
238
352
  function baseElement(id, type, box) {
239
353
  return {
240
354
  id,
@@ -312,6 +426,21 @@ function groupGroupIds(groupId) {
312
426
  function groupElementIdFor(groupId) {
313
427
  return `group:${groupId}`;
314
428
  }
429
+ function blockText(lines) {
430
+ return lines.filter((line) => line.length > 0).join("\n");
431
+ }
432
+ function panelKindFill(kind) {
433
+ switch (kind) {
434
+ case "legend":
435
+ return "#ecfdf5";
436
+ case "rule":
437
+ return "#eff6ff";
438
+ case "note":
439
+ return "#fffbeb";
440
+ case "verification":
441
+ return "#fef2f2";
442
+ }
443
+ }
315
444
  function pointsBox(points) {
316
445
  const xs = points.map((point2) => point2.x);
317
446
  const ys = points.map((point2) => point2.y);
@@ -349,42 +478,253 @@ var GROUP_FILL = "#f9fafb";
349
478
  var STROKE = "#374151";
350
479
  var EDGE_STROKE = "#111827";
351
480
  var FONT_FAMILY = "Arial, sans-serif";
481
+ var EVIDENCE_FILL = "#f8fafc";
482
+ var EVIDENCE_HEADER_FILL = "#e5e7eb";
483
+ var EVIDENCE_TEXT_FONT_SIZE = 10;
484
+ var EVIDENCE_TEXT_LINE_HEIGHT = 12;
485
+ var EVIDENCE_PANEL_KIND_FILL = {
486
+ legend: "#ecfdf5",
487
+ rule: "#eff6ff",
488
+ note: "#fffbeb",
489
+ verification: "#fef2f2"
490
+ };
352
491
  function exportSvg(diagram, options = {}) {
353
492
  const title = options.title ?? diagram.title;
354
- const lines = [
493
+ const annotations = diagram.textAnnotations ?? [];
494
+ return `${[
355
495
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
356
496
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
357
497
  ` <rect class="background" x="${formatNumber(diagram.bounds.x)}" y="${formatNumber(diagram.bounds.y)}" width="${formatNumber(diagram.bounds.width)}" height="${formatNumber(diagram.bounds.height)}" fill="#ffffff"/>`,
358
- ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame))],
498
+ ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
359
499
  ...(diagram.swimlanes ?? []).flatMap(
360
- (swimlane) => renderSwimlane(swimlane)
500
+ (swimlane) => renderSwimlane(swimlane, annotations)
361
501
  ),
362
502
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
503
+ ...(diagram.matrices ?? []).flatMap(
504
+ (matrix) => indentLines(renderMatrixBlock2(matrix))
505
+ ),
506
+ ...(diagram.tables ?? []).flatMap(
507
+ (table) => indentLines(renderTableBlock2(table))
508
+ ),
509
+ ...(diagram.evidencePanels ?? []).flatMap(
510
+ (panel) => indentLines(renderEvidencePanel2(panel))
511
+ ),
363
512
  ...diagram.edges.flatMap((edge) => {
364
513
  const path = renderEdgePath(edge);
365
- if (path === void 0) {
366
- return [];
367
- }
368
- return [indent(path), indent(renderArrowhead(edge))];
514
+ return path === void 0 ? [] : [indent(path), indent(renderArrowhead(edge))];
369
515
  }),
370
516
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
371
- ...diagram.nodes.flatMap((node) => renderCompartments(node)),
372
- ...diagram.nodes.flatMap((node) => renderPorts(node)),
517
+ ...diagram.nodes.flatMap((node) => renderCompartments(node, annotations)),
518
+ ...diagram.nodes.flatMap((node) => renderPorts(node, annotations)),
373
519
  ...diagram.groups.flatMap(
374
- (group) => renderLabel(group.label, group.box, group)
520
+ (group) => renderLabel(group.label, group.box, group, annotations, "group-label")
375
521
  ),
376
522
  ...diagram.nodes.flatMap(
377
- (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node) : []
523
+ (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node, annotations, "node-label") : []
378
524
  ),
379
- ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge)),
525
+ ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge, annotations)),
380
526
  "</svg>"
381
- ];
382
- return `${lines.join("\n")}
527
+ ].join("\n")}
383
528
  `;
384
529
  }
385
530
  function renderGroup2(group) {
386
531
  return `<rect class="group" data-id="${escapeAttribute(group.id)}" x="${formatNumber(group.box.x)}" y="${formatNumber(group.box.y)}" width="${formatNumber(group.box.width)}" height="${formatNumber(group.box.height)}" fill="${GROUP_FILL}" stroke="${STROKE}" stroke-dasharray="6 4"/>`;
387
532
  }
533
+ function renderMatrixBlock2(matrix) {
534
+ const columnCount = Math.max(1, matrix.cols.length);
535
+ const rowCount = matrix.rows.length;
536
+ const rowHeaderWidth = rowCount > 0 ? Math.min(96, matrix.box.width * 0.28) : 0;
537
+ const dataWidth = Math.max(0, matrix.box.width - rowHeaderWidth);
538
+ const cellWidth = dataWidth / columnCount;
539
+ const rowHeight = matrix.box.height / Math.max(1, rowCount + 1);
540
+ const lines = [
541
+ `<g class="matrix-block" data-id="${escapeAttribute(matrix.id)}" data-row-count="${rowCount}" data-column-count="${matrix.cols.length}">`,
542
+ ` <rect class="matrix-frame" x="${formatNumber(matrix.box.x)}" y="${formatNumber(matrix.box.y)}" width="${formatNumber(matrix.box.width)}" height="${formatNumber(matrix.box.height)}" fill="${escapeAttribute(matrix.style?.fill ?? EVIDENCE_FILL)}" stroke="${escapeAttribute(matrix.style?.stroke ?? STROKE)}"/>`
543
+ ];
544
+ if (rowHeaderWidth > 0) {
545
+ lines.push(
546
+ ` <rect class="matrix-corner-header" x="${formatNumber(matrix.box.x)}" y="${formatNumber(matrix.box.y)}" width="${formatNumber(rowHeaderWidth)}" height="${formatNumber(rowHeight)}" fill="${EVIDENCE_HEADER_FILL}" stroke="${STROKE}"/>`
547
+ );
548
+ }
549
+ for (let columnIndex = 0; columnIndex < matrix.cols.length; columnIndex += 1) {
550
+ const column = matrix.cols[columnIndex];
551
+ if (column === void 0) {
552
+ continue;
553
+ }
554
+ const x = matrix.box.x + rowHeaderWidth + columnIndex * cellWidth;
555
+ lines.push(
556
+ ` <rect class="matrix-column-header" data-col="${escapeAttribute(column)}" x="${formatNumber(x)}" y="${formatNumber(matrix.box.y)}" width="${formatNumber(cellWidth)}" height="${formatNumber(rowHeight)}" fill="${EVIDENCE_HEADER_FILL}" stroke="${STROKE}"/>`,
557
+ renderEvidenceText(
558
+ "matrix-column-label",
559
+ matrix.columnLabelLayouts?.[columnIndex]?.lines ?? [column],
560
+ {
561
+ x,
562
+ y: matrix.box.y,
563
+ width: cellWidth,
564
+ height: rowHeight
565
+ }
566
+ )
567
+ );
568
+ }
569
+ for (let rowIndex = 0; rowIndex < matrix.rows.length; rowIndex += 1) {
570
+ const row = matrix.rows[rowIndex];
571
+ const cells = matrix.cells[rowIndex] ?? [];
572
+ if (row === void 0) {
573
+ continue;
574
+ }
575
+ if (rowHeaderWidth > 0) {
576
+ const rowHeaderBox = {
577
+ x: matrix.box.x,
578
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
579
+ width: rowHeaderWidth,
580
+ height: rowHeight
581
+ };
582
+ lines.push(
583
+ ` <rect class="matrix-row-header" data-row="${escapeAttribute(row)}" x="${formatNumber(rowHeaderBox.x)}" y="${formatNumber(rowHeaderBox.y)}" width="${formatNumber(rowHeaderBox.width)}" height="${formatNumber(rowHeaderBox.height)}" fill="${EVIDENCE_HEADER_FILL}" stroke="${STROKE}"/>`,
584
+ renderEvidenceText(
585
+ "matrix-row-label",
586
+ matrix.rowLabelLayouts?.[rowIndex]?.lines ?? [row],
587
+ rowHeaderBox
588
+ )
589
+ );
590
+ }
591
+ for (let columnIndex = 0; columnIndex < matrix.cols.length; columnIndex += 1) {
592
+ const column = matrix.cols[columnIndex];
593
+ if (column === void 0) {
594
+ continue;
595
+ }
596
+ const cell2 = cells[columnIndex] ?? { text: "" };
597
+ const box = {
598
+ x: matrix.box.x + rowHeaderWidth + columnIndex * cellWidth,
599
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
600
+ width: cellWidth,
601
+ height: rowHeight
602
+ };
603
+ lines.push(
604
+ ` <rect class="matrix-cell" data-row="${escapeAttribute(row)}" data-col="${escapeAttribute(column)}" x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}" fill="${escapeAttribute(cell2.style?.fill ?? "#ffffff")}" stroke="${escapeAttribute(cell2.style?.stroke ?? STROKE)}"/>`,
605
+ renderEvidenceText(
606
+ "matrix-cell-label",
607
+ matrix.cellLabelLayouts?.[rowIndex]?.[columnIndex]?.lines ?? [
608
+ cell2.text
609
+ ],
610
+ box
611
+ )
612
+ );
613
+ }
614
+ }
615
+ lines.push("</g>");
616
+ return lines;
617
+ }
618
+ function renderTableBlock2(table) {
619
+ const columnCount = Math.max(1, table.columns.length);
620
+ const rowHeight = table.box.height / Math.max(1, table.rows.length + 1);
621
+ const lines = [
622
+ `<g class="table-block" data-id="${escapeAttribute(table.id)}" data-row-count="${table.rows.length}" data-column-count="${table.columns.length}">`,
623
+ ` <rect class="table-frame" x="${formatNumber(table.box.x)}" y="${formatNumber(table.box.y)}" width="${formatNumber(table.box.width)}" height="${formatNumber(table.box.height)}" fill="${escapeAttribute(table.style?.fill ?? EVIDENCE_FILL)}" stroke="${escapeAttribute(table.style?.stroke ?? STROKE)}"/>`,
624
+ ` <g class="table-header" data-column-count="${table.columns.length}">`
625
+ ];
626
+ for (let columnIndex = 0; columnIndex < table.columns.length; columnIndex += 1) {
627
+ const column = table.columns[columnIndex];
628
+ if (column === void 0) {
629
+ continue;
630
+ }
631
+ const columnBox = tableCellBox(
632
+ table,
633
+ columnIndex,
634
+ 0,
635
+ rowHeight,
636
+ columnCount
637
+ );
638
+ lines.push(
639
+ ` <rect class="table-header-cell" data-col="${escapeAttribute(column.id)}" x="${formatNumber(columnBox.x)}" y="${formatNumber(columnBox.y)}" width="${formatNumber(columnBox.width)}" height="${formatNumber(columnBox.height)}" fill="${EVIDENCE_HEADER_FILL}" stroke="${STROKE}"/>`,
640
+ ` ${renderEvidenceText("table-header-label", table.columnLabelLayouts?.[columnIndex]?.lines ?? [column.label.text], columnBox)}`
641
+ );
642
+ }
643
+ lines.push(" </g>");
644
+ for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) {
645
+ const row = table.rows[rowIndex];
646
+ if (row === void 0) {
647
+ continue;
648
+ }
649
+ const rowBox = {
650
+ x: table.box.x,
651
+ y: table.box.y + (rowIndex + 1) * rowHeight,
652
+ width: table.box.width,
653
+ height: rowHeight
654
+ };
655
+ const rowClass = rowIndex % 2 === 0 ? "table-row-even" : "table-row-odd";
656
+ lines.push(
657
+ ` <g class="table-row ${rowClass}" data-row="${escapeAttribute(row.id)}">`,
658
+ ` <rect class="${rowClass}" data-row="${escapeAttribute(row.id)}" x="${formatNumber(rowBox.x)}" y="${formatNumber(rowBox.y)}" width="${formatNumber(rowBox.width)}" height="${formatNumber(rowBox.height)}" fill="${rowIndex % 2 === 0 ? "#ffffff" : "#f3f4f6"}" stroke="none"/>`
659
+ );
660
+ for (let columnIndex = 0; columnIndex < table.columns.length; columnIndex += 1) {
661
+ const column = table.columns[columnIndex];
662
+ if (column === void 0) {
663
+ continue;
664
+ }
665
+ const cell2 = row.cells[column.id] ?? { text: "" };
666
+ const cellBox = tableCellBox(
667
+ table,
668
+ columnIndex,
669
+ rowIndex + 1,
670
+ rowHeight,
671
+ columnCount
672
+ );
673
+ lines.push(
674
+ ` <rect class="table-cell" data-col="${escapeAttribute(column.id)}" x="${formatNumber(cellBox.x)}" y="${formatNumber(cellBox.y)}" width="${formatNumber(cellBox.width)}" height="${formatNumber(cellBox.height)}" fill="${escapeAttribute(cell2.style?.fill ?? "transparent")}" stroke="${escapeAttribute(cell2.style?.stroke ?? STROKE)}"/>`,
675
+ ` ${renderEvidenceText("table-cell-label", table.cellLabelLayouts?.[rowIndex]?.[columnIndex]?.lines ?? [cell2.text], cellBox)}`
676
+ );
677
+ }
678
+ lines.push(" </g>");
679
+ }
680
+ lines.push("</g>");
681
+ return lines;
682
+ }
683
+ function renderEvidencePanel2(panel) {
684
+ const titleWidth = Math.min(panel.box.width * 0.36, 140);
685
+ const itemBox = {
686
+ x: panel.box.x + titleWidth,
687
+ y: panel.box.y,
688
+ width: panel.box.width - titleWidth,
689
+ height: panel.box.height
690
+ };
691
+ const titleBox = {
692
+ x: panel.box.x,
693
+ y: panel.box.y,
694
+ width: titleWidth,
695
+ height: panel.box.height
696
+ };
697
+ const itemHeight = panel.box.height / Math.max(1, panel.items.length);
698
+ const lines = [
699
+ `<g class="evidence-panel evidence-panel--${panel.kind}" data-id="${escapeAttribute(panel.id)}" data-kind="${escapeAttribute(panel.kind)}" data-item-count="${panel.items.length}">`,
700
+ ` <rect class="evidence-panel-frame" x="${formatNumber(panel.box.x)}" y="${formatNumber(panel.box.y)}" width="${formatNumber(panel.box.width)}" height="${formatNumber(panel.box.height)}" fill="${escapeAttribute(panel.style?.fill ?? EVIDENCE_PANEL_KIND_FILL[panel.kind])}" stroke="${escapeAttribute(panel.style?.stroke ?? STROKE)}"/>`,
701
+ ` <g class="evidence-panel-title-cell">`,
702
+ ` <rect class="evidence-panel-title-bg" x="${formatNumber(titleBox.x)}" y="${formatNumber(titleBox.y)}" width="${formatNumber(titleBox.width)}" height="${formatNumber(titleBox.height)}" fill="${EVIDENCE_HEADER_FILL}" stroke="${STROKE}"/>`,
703
+ ` ${renderEvidenceText("evidence-panel-title", panel.titleLayout?.lines ?? [`${panel.kind}: ${panel.id}`], titleBox)}`,
704
+ " </g>",
705
+ ` <g class="evidence-panel-items-cell">`,
706
+ ` <rect class="evidence-panel-items-bg" x="${formatNumber(itemBox.x)}" y="${formatNumber(itemBox.y)}" width="${formatNumber(itemBox.width)}" height="${formatNumber(itemBox.height)}" fill="transparent" stroke="${STROKE}"/>`
707
+ ];
708
+ for (let index = 0; index < panel.items.length; index += 1) {
709
+ const item = panel.items[index];
710
+ if (item === void 0) {
711
+ continue;
712
+ }
713
+ const text = panelItemText(item.label.text, item.detail?.text);
714
+ const box = {
715
+ x: itemBox.x,
716
+ y: itemBox.y + index * itemHeight,
717
+ width: itemBox.width,
718
+ height: itemHeight
719
+ };
720
+ lines.push(
721
+ ` <rect class="evidence-panel-item" data-item="${escapeAttribute(item.id ?? String(index))}" x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}" fill="${escapeAttribute(item.style?.fill ?? "transparent")}" stroke="${escapeAttribute(item.style?.stroke ?? "none")}"/>`,
722
+ ` ${renderEvidenceText("evidence-panel-item-label", panel.itemLayouts?.[index]?.lines ?? [text], box)}`
723
+ );
724
+ }
725
+ lines.push(" </g>", "</g>");
726
+ return lines;
727
+ }
388
728
  function renderNode2(node) {
389
729
  const fill = node.style?.fill ?? NODE_FILL;
390
730
  const stroke = node.style?.stroke ?? STROKE;
@@ -404,18 +744,27 @@ function renderNode2(node) {
404
744
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
405
745
  }
406
746
  }
407
- function renderFrame(frame) {
747
+ function renderFrame(frame, annotations) {
408
748
  const stroke = frame.style?.stroke ?? "#6b7280";
409
749
  const fill = frame.style?.fill ?? "transparent";
410
750
  return [
411
751
  `<g class="sysml-frame" data-kind="${escapeAttribute(frame.kind)}">`,
412
752
  ` <rect class="sysml-frame-border" x="${formatNumber(frame.box.x)}" y="${formatNumber(frame.box.y)}" width="${formatNumber(frame.box.width)}" height="${formatNumber(frame.box.height)}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(stroke)}"/>`,
413
753
  ` <path class="sysml-title-tab" d="M ${formatNumber(frame.titleBox.x)} ${formatNumber(frame.titleBox.y + frame.titleBox.height)} L ${formatNumber(frame.titleBox.x)} ${formatNumber(frame.titleBox.y)} L ${formatNumber(frame.titleBox.x + frame.titleBox.width - 16)} ${formatNumber(frame.titleBox.y)} L ${formatNumber(frame.titleBox.x + frame.titleBox.width)} ${formatNumber(frame.titleBox.y + frame.titleBox.height)} Z" fill="#f3f4f6" stroke="${escapeAttribute(stroke)}"/>`,
414
- ` <text class="sysml-title-tab-label" x="${formatNumber(frame.titleBox.x + 8)}" y="${formatNumber(frame.titleBox.y + frame.titleBox.height / 2)}" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(frame.titleTab)}</text>`,
754
+ ...renderSolvedTextAnnotation(
755
+ findAnnotation(annotations, "frame-title", frame.kind),
756
+ `sysml-title-tab-label`,
757
+ {
758
+ indent: " ",
759
+ mode: "center",
760
+ fallbackText: frame.titleTab}
761
+ ) ?? [
762
+ ` <text class="sysml-title-tab-label" x="${formatNumber(frame.titleBox.x + 8)}" y="${formatNumber(frame.titleBox.y + frame.titleBox.height / 2)}" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(frame.titleTab)}</text>`
763
+ ],
415
764
  "</g>"
416
765
  ].join("\n");
417
766
  }
418
- function renderSwimlane(swimlane) {
767
+ function renderSwimlane(swimlane, annotations) {
419
768
  if (swimlane.box === void 0) {
420
769
  return [];
421
770
  }
@@ -441,28 +790,49 @@ function renderSwimlane(swimlane) {
441
790
  );
442
791
  }
443
792
  if (lane.label?.text !== void 0) {
444
- const labelBox = lane.headerBox ?? lane.box;
445
- lines.push(renderSwimlaneLabel(swimlane, lane.label.text, labelBox));
793
+ const annotation = findAnnotation(
794
+ annotations,
795
+ "swimlane-label",
796
+ `${swimlane.id}.${lane.id}`
797
+ );
798
+ lines.push(
799
+ ...annotation === void 0 ? [
800
+ renderSwimlaneLabel(
801
+ swimlane,
802
+ lane.label.text,
803
+ lane.headerBox ?? lane.box
804
+ )
805
+ ] : renderSolvedTextAnnotation(annotation, "swimlane-label", {
806
+ indent: " ",
807
+ mode: "center",
808
+ rotate: swimlane.orientation === "horizontal"
809
+ }) ?? []
810
+ );
446
811
  }
447
812
  }
448
813
  lines.push(" </g>");
449
814
  return lines;
450
815
  }
451
- function renderSwimlaneLabel(swimlane, text, labelBox) {
452
- const x = labelBox.x + labelBox.width / 2;
453
- const y = labelBox.y + labelBox.height / 2;
454
- const transform = swimlane.orientation === "horizontal" ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
455
- return ` <text class="swimlane-label" x="${formatNumber(x)}" y="${formatNumber(y)}" text-anchor="middle" dominant-baseline="middle"${transform} font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(text)}</text>`;
456
- }
457
- function renderPorts(node) {
816
+ function renderPorts(node, annotations) {
458
817
  return (node.ports ?? []).flatMap((port) => [
459
818
  ` <rect class="port" data-kind="${escapeAttribute(port.kind)}" data-port="${escapeAttribute(`${node.id}.${port.id}`)}" x="${formatNumber(port.box.x)}" y="${formatNumber(port.box.y)}" width="${formatNumber(port.box.width)}" height="${formatNumber(port.box.height)}" fill="${escapeAttribute(port.style?.fill ?? "#d9ead3")}" stroke="${escapeAttribute(port.style?.stroke ?? STROKE)}"/>`,
460
- ...port.label?.text === void 0 ? [] : [
461
- ` <text class="port-label" data-for="${escapeAttribute(`${node.id}.${port.id}`)}" x="${formatNumber(portLabelX(port.anchor.x, port.side))}" y="${formatNumber(port.anchor.y - 8)}" text-anchor="${port.side === "left" ? "end" : "start"}" font-family="${FONT_FAMILY}" font-size="10" fill="#111827">${escapeXml(port.label.text)}</text>`
462
- ]
819
+ ...port.label?.text === void 0 ? [] : (() => {
820
+ const annotation = findAnnotation(
821
+ annotations,
822
+ "port-label",
823
+ `${node.id}.${port.id}`
824
+ );
825
+ return annotation === void 0 ? [
826
+ ` <text class="port-label" data-for="${escapeAttribute(`${node.id}.${port.id}`)}" x="${formatNumber(portLabelX(port.anchor.x, port.side))}" y="${formatNumber(port.anchor.y - 8)}" text-anchor="${port.side === "left" ? "end" : "start"}" font-family="${FONT_FAMILY}" font-size="10" fill="#111827">${escapeXml(port.label.text)}</text>`
827
+ ] : renderSolvedTextAnnotation(annotation, "port-label", {
828
+ indent: " ",
829
+ mode: "center",
830
+ textAnchor: port.side === "left" ? "end" : "start"
831
+ }) ?? [];
832
+ })()
463
833
  ]);
464
834
  }
465
- function renderCompartments(node) {
835
+ function renderCompartments(node, annotations) {
466
836
  const compartments2 = node.compartments;
467
837
  if (compartments2 === void 0) {
468
838
  return [];
@@ -482,7 +852,6 @@ function renderCompartments(node) {
482
852
  text
483
853
  }))
484
854
  ];
485
- const lineHeight = 16;
486
855
  const lines = [
487
856
  ` <g class="compartment" data-for="${escapeAttribute(node.id)}">`
488
857
  ];
@@ -491,32 +860,72 @@ function renderCompartments(node) {
491
860
  if (row === void 0) {
492
861
  continue;
493
862
  }
494
- const y = node.box.y + 18 + index * lineHeight;
863
+ const y = node.box.y + 18 + index * 16;
495
864
  if (index > 1) {
496
865
  lines.push(
497
866
  ` <line class="compartment-separator" x1="${formatNumber(node.box.x)}" y1="${formatNumber(y - 12)}" x2="${formatNumber(node.box.x + node.box.width)}" y2="${formatNumber(y - 12)}" stroke="${STROKE}"/>`
498
867
  );
499
868
  }
869
+ const annotation = findAnnotation(
870
+ annotations,
871
+ "compartment-row",
872
+ node.id,
873
+ index
874
+ );
500
875
  lines.push(
501
- ` <text class="compartment-${row.className}" x="${formatNumber(node.box.x + node.box.width / 2)}" y="${formatNumber(y)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="11" fill="#111827">${escapeXml(row.text)}</text>`
876
+ ...annotation === void 0 ? [
877
+ ` <text class="compartment-${row.className}" x="${formatNumber(node.box.x + node.box.width / 2)}" y="${formatNumber(y)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="11" fill="#111827">${escapeXml(row.text)}</text>`
878
+ ] : renderSolvedTextAnnotation(
879
+ annotation,
880
+ `compartment-${row.className}`,
881
+ {
882
+ indent: " ",
883
+ mode: "center"
884
+ }
885
+ ) ?? []
502
886
  );
503
887
  }
504
888
  lines.push(" </g>");
505
889
  return lines;
506
890
  }
507
- function portLabelX(x, side) {
508
- if (side === "left") {
509
- return x - 8;
510
- }
511
- if (side === "right") {
512
- return x + 8;
891
+ function tableCellBox(table, columnIndex, rowIndex, rowHeight, columnCount) {
892
+ const x = table.columnXOffsets[columnIndex] ?? table.box.x + table.box.width / columnCount * columnIndex;
893
+ const nextX = table.columnXOffsets[columnIndex + 1] ?? table.box.x + table.box.width;
894
+ return {
895
+ x,
896
+ y: table.box.y + rowIndex * rowHeight,
897
+ width: nextX - x,
898
+ height: rowHeight
899
+ };
900
+ }
901
+ function renderEvidenceText(className, lines, box) {
902
+ const fontSize = EVIDENCE_TEXT_FONT_SIZE;
903
+ const lineHeight = EVIDENCE_TEXT_LINE_HEIGHT;
904
+ const x = box.x + box.width / 2;
905
+ if (lines.length <= 1) {
906
+ return `<text class="${className}" x="${formatNumber(x)}" y="${formatNumber(box.y + box.height / 2)}" text-anchor="middle" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="${formatNumber(fontSize)}" fill="#111827">${escapeXml(lines[0] ?? "")}</text>`;
513
907
  }
514
- return x + 8;
908
+ const totalHeight = (lines.length - 1) * lineHeight;
909
+ const firstBaselineY = box.y + box.height / 2 - totalHeight / 2;
910
+ return [
911
+ `<text class="${className}" x="${formatNumber(x)}" y="${formatNumber(firstBaselineY)}" text-anchor="middle" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="${formatNumber(fontSize)}" fill="#111827">`,
912
+ ...lines.map(
913
+ (line, index) => ` <tspan x="${formatNumber(x)}" y="${formatNumber(firstBaselineY + index * lineHeight)}">${escapeXml(line)}</tspan>`
914
+ ),
915
+ "</text>"
916
+ ].join("\n");
515
917
  }
516
- function renderRect(box, attributes) {
517
- return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
918
+ function panelItemText(label, detail) {
919
+ return detail === void 0 ? label : `${label}: ${detail}`;
518
920
  }
519
- function renderLabel(label, box, item) {
921
+ function renderLabel(label, box, item, annotations, surfaceKind) {
922
+ const annotation = findAnnotation(annotations, surfaceKind, item.id);
923
+ if (annotation !== void 0) {
924
+ return renderSolvedTextAnnotation(annotation, "label", {
925
+ indent: " ",
926
+ mode: "center"
927
+ }) ?? [];
928
+ }
520
929
  const labelLayout = item.labelLayout;
521
930
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
522
931
  const offset = { x: box.x, y: box.y };
@@ -542,10 +951,17 @@ function renderEdgePath(edge) {
542
951
  const dash = edge.style === "dashed" ? ' stroke-dasharray="6 4"' : "";
543
952
  return `<path class="edge" data-id="${escapeAttribute(edge.id)}" d="${formatPath(pathPointsBeforeArrowhead(edge.points))}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"${dash}/>`;
544
953
  }
545
- function renderEdgeLabel(edge) {
954
+ function renderEdgeLabel(edge, annotations) {
546
955
  if (edge.label?.text === void 0 || edge.points.length < 2) {
547
956
  return [];
548
957
  }
958
+ const annotation = findAnnotation(annotations, "edge-label", edge.id);
959
+ if (annotation !== void 0) {
960
+ return renderSolvedTextAnnotation(annotation, "edge-label", {
961
+ indent: " ",
962
+ mode: "center"
963
+ }) ?? [];
964
+ }
549
965
  const placement = labelPlacementOnPolyline(edge.points);
550
966
  if (placement === void 0) {
551
967
  return [];
@@ -559,6 +975,85 @@ function renderArrowhead(edge) {
559
975
  const fill = edge.arrowhead === "hollowTriangle" ? "none" : EDGE_STROKE;
560
976
  return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(edge.id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${fill}" stroke="${EDGE_STROKE}"/>`;
561
977
  }
978
+ function renderSolvedTextAnnotation(annotation, className, options) {
979
+ if (annotation === void 0) {
980
+ return void 0;
981
+ }
982
+ const x = options.mode === "center" ? annotation.box.x + annotation.box.width / 2 : annotation.box.x;
983
+ const y = options.mode === "center" ? annotation.box.y + annotation.box.height / 2 : annotation.box.y + annotation.box.height;
984
+ const rotate = options.rotate ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
985
+ const attrs = [
986
+ `class="${className}"`,
987
+ `data-for="${escapeAttribute(annotation.ownerId)}"`,
988
+ `data-text-surface="${escapeAttribute(annotation.surfaceKind)}"`,
989
+ `data-owner-id="${escapeAttribute(annotation.ownerId)}"`,
990
+ `data-text-backend="${escapeAttribute(annotation.textBackend ?? "deterministic")}"`,
991
+ `font-family="${FONT_FAMILY}"`,
992
+ `font-size="${formatNumber(annotation.fontSize)}"`,
993
+ `fill="#111827"`
994
+ ];
995
+ if (options.mode === "center") {
996
+ attrs.push('text-anchor="middle"');
997
+ } else {
998
+ attrs.push(`text-anchor="${options.textAnchor ?? "start"}"`);
999
+ }
1000
+ if (annotation.lines.length > 1) {
1001
+ return [
1002
+ `${options.indent}<text ${attrs.join(" ")}${rotate}>`,
1003
+ ...annotation.lines.map(
1004
+ (line2) => `${options.indent} <tspan x="${formatNumber(textLineX(annotation, line2, options))}" y="${formatNumber(annotation.box.y + line2.baselineY)}">${escapeXml(line2.text)}</tspan>`
1005
+ ),
1006
+ `${options.indent}</text>`
1007
+ ];
1008
+ }
1009
+ const line = annotation.lines[0];
1010
+ const text = line?.text ?? options.fallbackText ?? annotation.text;
1011
+ const singleLineAttrs = options.mode === "center" ? [...attrs, 'dominant-baseline="middle"'] : attrs;
1012
+ return [
1013
+ `${options.indent}<text ${singleLineAttrs.join(" ")} x="${formatNumber(x)}" y="${formatNumber(y)}"${rotate}>${escapeXml(text)}</text>`
1014
+ ];
1015
+ }
1016
+ function textLineX(annotation, line, options) {
1017
+ if (options.mode === "center") {
1018
+ return annotation.box.x + line.box.x + line.box.width / 2;
1019
+ }
1020
+ if ((options.textAnchor ?? "start") === "end") {
1021
+ return annotation.box.x + line.box.x + line.box.width;
1022
+ }
1023
+ return annotation.box.x + line.box.x;
1024
+ }
1025
+ function findAnnotation(annotations, surfaceKind, ownerId, index) {
1026
+ return annotations.find((annotation) => {
1027
+ if (annotation.surfaceKind !== surfaceKind) {
1028
+ return false;
1029
+ }
1030
+ if (annotation.ownerId !== ownerId) {
1031
+ return false;
1032
+ }
1033
+ if (index === void 0) {
1034
+ return annotation.surfaceIndex === void 0;
1035
+ }
1036
+ return annotation.surfaceIndex === index;
1037
+ });
1038
+ }
1039
+ function renderSwimlaneLabel(swimlane, text, labelBox) {
1040
+ const x = labelBox.x + labelBox.width / 2;
1041
+ const y = labelBox.y + labelBox.height / 2;
1042
+ const transform = swimlane.orientation === "horizontal" ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
1043
+ return ` <text class="swimlane-label" x="${formatNumber(x)}" y="${formatNumber(y)}" text-anchor="middle" dominant-baseline="middle"${transform} font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(text)}</text>`;
1044
+ }
1045
+ function renderRect(box, attributes) {
1046
+ return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
1047
+ }
1048
+ function portLabelX(x, side) {
1049
+ if (side === "left") {
1050
+ return x - 8;
1051
+ }
1052
+ if (side === "right") {
1053
+ return x + 8;
1054
+ }
1055
+ return x + 8;
1056
+ }
562
1057
  function labelPlacementOnPolyline(points) {
563
1058
  const segments = nonZeroSegments(points);
564
1059
  const totalLength = segments.reduce(
@@ -641,17 +1136,15 @@ function shapePoints(shape, box) {
641
1136
  { x: right - skew, y: bottom },
642
1137
  { x: left, y: bottom }
643
1138
  ];
644
- case "hexagon": {
645
- const inset = Math.min(box.width * 0.2, 24);
1139
+ case "hexagon":
646
1140
  return [
647
- { x: left + inset, y: top },
648
- { x: right - inset, y: top },
1141
+ { x: left + skew, y: top },
1142
+ { x: right - skew, y: top },
649
1143
  { x: right, y: midY },
650
- { x: right - inset, y: bottom },
651
- { x: left + inset, y: bottom },
1144
+ { x: right - skew, y: bottom },
1145
+ { x: left + skew, y: bottom },
652
1146
  { x: left, y: midY }
653
1147
  ];
654
- }
655
1148
  }
656
1149
  }
657
1150
  function formatCylinderPath(box) {
@@ -676,10 +1169,9 @@ function formatCylinderPath(box) {
676
1169
  ].join(" ");
677
1170
  }
678
1171
  function formatPath(points) {
679
- return points.map((point2, index) => {
680
- const command = index === 0 ? "M" : "L";
681
- return `${command} ${formatNumber(point2.x)} ${formatNumber(point2.y)}`;
682
- }).join(" ");
1172
+ return points.map(
1173
+ (point2, index) => `${index === 0 ? "M" : "L"} ${formatNumber(point2.x)} ${formatNumber(point2.y)}`
1174
+ ).join(" ");
683
1175
  }
684
1176
  function formatPoints(points) {
685
1177
  return points.map((point2) => `${formatNumber(point2.x)},${formatNumber(point2.y)}`).join(" ");
@@ -702,6 +1194,9 @@ function escapeAttribute(value) {
702
1194
  function indent(value) {
703
1195
  return ` ${value}`;
704
1196
  }
1197
+ function indentLines(values) {
1198
+ return values.map(indent);
1199
+ }
705
1200
 
706
1201
  // src/geometry/boxes.ts
707
1202
  function normalizeInsets(input = 0) {
@@ -1015,27 +1510,31 @@ function applyDistribute(constraints, boxes, locks, diagnostics) {
1015
1510
  function repairOverlaps(input, boxes, locks, diagnostics) {
1016
1511
  const spacing = input.overlapSpacing ?? 40;
1017
1512
  const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
1513
+ const secondaryAxis = axis === "x" ? "y" : "x";
1018
1514
  const ids = [...boxes.keys()].sort();
1019
- for (const firstId of ids) {
1020
- for (const secondId of ids) {
1021
- if (firstId >= secondId) {
1022
- continue;
1023
- }
1024
- const first = boxes.get(firstId);
1025
- const second = boxes.get(secondId);
1026
- if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
1027
- continue;
1028
- }
1029
- const firstLocked = locks.has(firstId);
1030
- const secondLocked = locks.has(secondId);
1031
- if (firstLocked === secondLocked) {
1032
- continue;
1515
+ for (let pass = 0; pass < 2; pass += 1) {
1516
+ for (const firstId of ids) {
1517
+ for (const secondId of ids) {
1518
+ if (firstId >= secondId) {
1519
+ continue;
1520
+ }
1521
+ const first = boxes.get(firstId);
1522
+ const second = boxes.get(secondId);
1523
+ if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
1524
+ continue;
1525
+ }
1526
+ const firstLocked = locks.has(firstId);
1527
+ const secondLocked = locks.has(secondId);
1528
+ if (firstLocked && secondLocked) {
1529
+ continue;
1530
+ }
1531
+ const movingId = firstLocked ? secondId : secondLocked ? firstId : secondId;
1532
+ const moving = movingId === firstId ? first : second;
1533
+ const fixed = movingId === firstId ? second : first;
1534
+ const repairAxis = firstLocked === secondLocked && pass === 0 ? secondaryAxis : axis;
1535
+ const moved = movePastOverlap(moving, fixed, repairAxis, spacing);
1536
+ boxes.set(movingId, moved);
1033
1537
  }
1034
- const movingId = firstLocked ? secondId : firstId;
1035
- const moving = firstLocked ? second : first;
1036
- const fixed = firstLocked ? first : second;
1037
- const moved = movePastOverlap(moving, fixed, axis, spacing);
1038
- boxes.set(movingId, moved);
1039
1538
  }
1040
1539
  }
1041
1540
  for (const firstId of ids) {
@@ -1400,42 +1899,392 @@ function validateSize(value, label) {
1400
1899
  throw new TypeError(`${label} must be finite and non-negative`);
1401
1900
  }
1402
1901
  }
1403
- var DEFAULT_OPTIONS = {
1404
- nodesep: 80,
1405
- ranksep: 100,
1406
- edgesep: 40,
1407
- marginx: 0,
1408
- marginy: 0,
1409
- ranker: "network-simplex"
1410
- };
1411
- function runDagreInitialLayout(input) {
1412
- const diagnostics = [];
1413
- const boxes = /* @__PURE__ */ new Map();
1414
- const validNodeIds = /* @__PURE__ */ new Set();
1415
- const graph = new Graph({
1416
- directed: true,
1417
- multigraph: true,
1418
- compound: false
1419
- });
1420
- const options = { ...DEFAULT_OPTIONS, ...input.options };
1421
- graph.setGraph({
1422
- rankdir: input.direction,
1423
- nodesep: options.nodesep,
1424
- ranksep: options.ranksep,
1425
- edgesep: options.edgesep,
1426
- marginx: options.marginx,
1427
- marginy: options.marginy,
1428
- ranker: options.ranker
1429
- });
1430
- graph.setDefaultEdgeLabel(() => ({}));
1431
- for (const node of input.nodes) {
1432
- if (!isValidDimension(node.size.width) || !isValidDimension(node.size.height)) {
1433
- diagnostics.push({
1434
- severity: "error",
1435
- code: "layout.node-size.invalid",
1436
- message: `Node ${node.id} has invalid layout dimensions.`,
1437
- path: ["nodes", node.id, "size"],
1438
- detail: { nodeId: node.id }
1902
+
1903
+ // src/text/types.ts
1904
+ function assertFinitePositive(value, label) {
1905
+ if (!Number.isFinite(value) || value <= 0) {
1906
+ throw new TypeError(`${label} must be finite and positive`);
1907
+ }
1908
+ }
1909
+ function assertFiniteNonNegative(value, label) {
1910
+ if (!Number.isFinite(value) || value < 0) {
1911
+ throw new TypeError(`${label} must be a finite non-negative width`);
1912
+ }
1913
+ }
1914
+ function validateTextStyle(style2) {
1915
+ assertFinitePositive(style2.fontSize, "fontSize");
1916
+ if (style2.lineHeight !== void 0) {
1917
+ assertFinitePositive(style2.lineHeight, "lineHeight");
1918
+ }
1919
+ if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
1920
+ throw new TypeError("letterSpacing must be finite");
1921
+ }
1922
+ }
1923
+ function resolveLineHeight(style2) {
1924
+ validateTextStyle(style2);
1925
+ return style2.lineHeight ?? style2.fontSize * 1.2;
1926
+ }
1927
+ function toCanvasFont(style2) {
1928
+ validateTextStyle(style2);
1929
+ const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
1930
+ const fontWeight = style2.fontWeight ?? 400;
1931
+ return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
1932
+ }
1933
+
1934
+ // src/text/fallback.ts
1935
+ var DeterministicTextMeasurer = class {
1936
+ prepare(text, style2) {
1937
+ validateTextStyle(style2);
1938
+ return {
1939
+ text,
1940
+ font: toCanvasFont(style2),
1941
+ style: { ...style2 },
1942
+ backend: "deterministic"
1943
+ };
1944
+ }
1945
+ layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
1946
+ assertFiniteNonNegative(maxWidth, "maxWidth");
1947
+ assertFinitePositiveLineHeight(lineHeight);
1948
+ const lines = this.wrap(prepared, maxWidth);
1949
+ const width = lines.reduce(
1950
+ (current, line) => Math.max(current, line.width),
1951
+ 0
1952
+ );
1953
+ return {
1954
+ width,
1955
+ height: lines.length * lineHeight,
1956
+ lineHeight,
1957
+ lineCount: lines.length,
1958
+ lines,
1959
+ diagnostics: []
1960
+ };
1961
+ }
1962
+ naturalWidth(prepared) {
1963
+ const charWidth = getCharacterWidth(prepared.style);
1964
+ return prepared.text.split("\n").reduce((width, line) => {
1965
+ return Math.max(width, line.length * charWidth);
1966
+ }, 0);
1967
+ }
1968
+ wrap(prepared, maxWidth) {
1969
+ const charWidth = getCharacterWidth(prepared.style);
1970
+ const sourceLines = prepared.text.split("\n");
1971
+ const output = [];
1972
+ let segmentIndex = 0;
1973
+ for (const sourceLine of sourceLines) {
1974
+ if (sourceLine.length === 0) {
1975
+ output.push(createLine("", 0, segmentIndex, 0, 0));
1976
+ segmentIndex += 1;
1977
+ continue;
1978
+ }
1979
+ const maxChars = maxWidth <= 0 ? 1 : Math.max(1, Math.floor(maxWidth / charWidth));
1980
+ for (let start = 0; start < sourceLine.length; start += maxChars) {
1981
+ const text = sourceLine.slice(start, start + maxChars);
1982
+ output.push(
1983
+ createLine(
1984
+ text,
1985
+ text.length * charWidth,
1986
+ segmentIndex,
1987
+ start,
1988
+ start + text.length
1989
+ )
1990
+ );
1991
+ }
1992
+ segmentIndex += 1;
1993
+ }
1994
+ if (output.length === 0) {
1995
+ output.push(createLine("", 0, 0, 0, 0));
1996
+ }
1997
+ return output;
1998
+ }
1999
+ };
2000
+ function getCharacterWidth(style2) {
2001
+ const letterSpacing = style2.letterSpacing ?? 0;
2002
+ return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
2003
+ }
2004
+ function createLine(text, width, segmentIndex, start, end) {
2005
+ return {
2006
+ text,
2007
+ width,
2008
+ start: {
2009
+ segmentIndex,
2010
+ graphemeIndex: start
2011
+ },
2012
+ end: {
2013
+ segmentIndex,
2014
+ graphemeIndex: end
2015
+ }
2016
+ };
2017
+ }
2018
+ function assertFinitePositiveLineHeight(lineHeight) {
2019
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2020
+ throw new TypeError("lineHeight must be finite and positive");
2021
+ }
2022
+ }
2023
+ var require2 = createRequire(import.meta.url);
2024
+ function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
2025
+ if (typeof globalThis.OffscreenCanvas === "function") {
2026
+ return true;
2027
+ }
2028
+ try {
2029
+ const canvasModule = loadNodeCanvasModule();
2030
+ const { createCanvas } = canvasModule;
2031
+ const NodeOffscreenCanvas = class {
2032
+ canvas;
2033
+ constructor(width, height) {
2034
+ this.canvas = createCanvas(width, height);
2035
+ }
2036
+ getContext(contextId) {
2037
+ return contextId === "2d" ? this.canvas.getContext("2d") : null;
2038
+ }
2039
+ };
2040
+ globalThis.OffscreenCanvas = NodeOffscreenCanvas;
2041
+ return true;
2042
+ } catch {
2043
+ return false;
2044
+ }
2045
+ }
2046
+ function loadDefaultNodeCanvasModule() {
2047
+ return require2("@napi-rs/canvas");
2048
+ }
2049
+ var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
2050
+ function isPretextRuntimeAvailable() {
2051
+ return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
2052
+ }
2053
+ var PretextTextMeasurer = class {
2054
+ prepare(text, style2) {
2055
+ if (!isPretextRuntimeAvailable()) {
2056
+ throw new TypeError(RUNTIME_UNAVAILABLE);
2057
+ }
2058
+ validateTextStyle(style2);
2059
+ const font = toCanvasFont(style2);
2060
+ const options = {
2061
+ ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
2062
+ ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
2063
+ ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
2064
+ };
2065
+ const prepared = prepareWithSegments(text, font, options);
2066
+ return {
2067
+ text,
2068
+ font,
2069
+ style: { ...style2 },
2070
+ backend: "pretext",
2071
+ pretextPrepared: prepared
2072
+ };
2073
+ }
2074
+ layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
2075
+ assertFiniteNonNegative(maxWidth, "maxWidth");
2076
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2077
+ throw new TypeError("lineHeight must be finite and positive");
2078
+ }
2079
+ const result = layoutWithLines(
2080
+ toInternalPrepared(prepared),
2081
+ maxWidth,
2082
+ lineHeight
2083
+ );
2084
+ const width = result.lines.reduce(
2085
+ (current, line) => Math.max(current, line.width),
2086
+ 0
2087
+ );
2088
+ return {
2089
+ width,
2090
+ height: result.height,
2091
+ lineHeight,
2092
+ lineCount: result.lineCount,
2093
+ lines: result.lines.map((line) => ({
2094
+ text: line.text,
2095
+ width: line.width,
2096
+ start: {
2097
+ segmentIndex: line.start.segmentIndex,
2098
+ graphemeIndex: line.start.graphemeIndex
2099
+ },
2100
+ end: {
2101
+ segmentIndex: line.end.segmentIndex,
2102
+ graphemeIndex: line.end.graphemeIndex
2103
+ }
2104
+ })),
2105
+ diagnostics: []
2106
+ };
2107
+ }
2108
+ naturalWidth(prepared) {
2109
+ return measureNaturalWidth(toInternalPrepared(prepared));
2110
+ }
2111
+ };
2112
+ function toInternalPrepared(prepared) {
2113
+ if (prepared.backend !== "pretext" || !("pretextPrepared" in prepared)) {
2114
+ throw new TypeError("prepared text was not created by PretextTextMeasurer");
2115
+ }
2116
+ return prepared.pretextPrepared;
2117
+ }
2118
+
2119
+ // src/text/default.ts
2120
+ function createDefaultTextMeasurer(options = {}) {
2121
+ const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
2122
+ installRuntime();
2123
+ return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
2124
+ }
2125
+
2126
+ // src/labels/fit.ts
2127
+ function fitLabel(text, options, measurer) {
2128
+ return computeLabelLayout(text, options, measurer);
2129
+ }
2130
+ function computeLabelLayout(text, options, measurer) {
2131
+ const padding = normalizeInsets(options.padding);
2132
+ const minSize = normalizeMinSize2(options.minSize);
2133
+ const lineHeight = resolveLineHeight(options.font);
2134
+ const maxWidth = normalizeMaxWidth(options.maxWidth);
2135
+ const prepared = measurer.prepare(text, options.font);
2136
+ const naturalTextWidth = measurer.naturalWidth(prepared);
2137
+ const contentMaxWidth = maxWidth === void 0 ? naturalTextWidth : Math.max(0, maxWidth - padding.left - padding.right);
2138
+ const textLayout = measurer.layout(prepared, contentMaxWidth, lineHeight);
2139
+ const naturalSize = {
2140
+ width: naturalTextWidth,
2141
+ height: textLayout.height
2142
+ };
2143
+ const contentWidth = Math.max(
2144
+ textLayout.width,
2145
+ minContentWidth(minSize, padding)
2146
+ );
2147
+ const contentHeight = Math.max(
2148
+ textLayout.height,
2149
+ minContentHeight(minSize, padding)
2150
+ );
2151
+ const idealWidth = contentWidth + padding.left + padding.right;
2152
+ const idealHeight = contentHeight + padding.top + padding.bottom;
2153
+ const fittedSize = {
2154
+ width: maxWidth === void 0 ? idealWidth : Math.min(maxWidth, idealWidth),
2155
+ height: idealHeight
2156
+ };
2157
+ const box = {
2158
+ x: 0,
2159
+ y: 0,
2160
+ width: fittedSize.width,
2161
+ height: fittedSize.height
2162
+ };
2163
+ const contentBox2 = {
2164
+ x: padding.left,
2165
+ y: padding.top,
2166
+ width: Math.max(0, box.width - padding.left - padding.right),
2167
+ height: Math.max(0, box.height - padding.top - padding.bottom)
2168
+ };
2169
+ const overflow = {
2170
+ horizontal: textLayout.width > contentBox2.width,
2171
+ vertical: textLayout.height > contentBox2.height || diagnosedHeightConstraintOverflow(textLayout.height, padding, minSize),
2172
+ truncated: options.overflow === "truncate" && textLayout.width > contentBox2.width
2173
+ };
2174
+ const diagnostics = buildDiagnostics(overflow, options.overflow);
2175
+ return {
2176
+ text,
2177
+ box,
2178
+ contentBox: contentBox2,
2179
+ naturalSize,
2180
+ fittedSize,
2181
+ padding,
2182
+ font: { ...options.font },
2183
+ textBackend: prepared.backend,
2184
+ lineHeight,
2185
+ lines: buildLines(textLayout, contentBox2, lineHeight),
2186
+ overflow,
2187
+ diagnostics
2188
+ };
2189
+ }
2190
+ function buildLines(textLayout, contentBox2, lineHeight) {
2191
+ return textLayout.lines.map((line, lineIndex) => ({
2192
+ text: line.text,
2193
+ box: {
2194
+ x: contentBox2.x,
2195
+ y: contentBox2.y + lineIndex * lineHeight,
2196
+ width: line.width,
2197
+ height: lineHeight
2198
+ },
2199
+ baselineY: contentBox2.y + lineIndex * lineHeight + lineHeight * 0.8,
2200
+ width: line.width,
2201
+ lineIndex,
2202
+ sourceStart: { ...line.start },
2203
+ sourceEnd: { ...line.end }
2204
+ }));
2205
+ }
2206
+ function normalizeMinSize2(minSize = {}) {
2207
+ if (minSize.width !== void 0) {
2208
+ assertFiniteNonNegative(minSize.width, "minSize.width");
2209
+ }
2210
+ if (minSize.height !== void 0) {
2211
+ assertFiniteNonNegative(minSize.height, "minSize.height");
2212
+ }
2213
+ return { ...minSize };
2214
+ }
2215
+ function normalizeMaxWidth(maxWidth) {
2216
+ if (maxWidth === void 0) {
2217
+ return void 0;
2218
+ }
2219
+ assertFiniteNonNegative(maxWidth, "maxWidth");
2220
+ return maxWidth;
2221
+ }
2222
+ function minContentWidth(minSize, padding) {
2223
+ return Math.max(0, (minSize.width ?? 0) - padding.left - padding.right);
2224
+ }
2225
+ function minContentHeight(minSize, padding) {
2226
+ return Math.max(0, (minSize.height ?? 0) - padding.top - padding.bottom);
2227
+ }
2228
+ function diagnosedHeightConstraintOverflow(textHeight, padding, minSize) {
2229
+ return minSize.height !== void 0 && textHeight + padding.top + padding.bottom > minSize.height;
2230
+ }
2231
+ function buildDiagnostics(overflow, mode = "allow") {
2232
+ if (mode !== "diagnose") {
2233
+ return [];
2234
+ }
2235
+ const diagnostics = [];
2236
+ if (overflow.horizontal) {
2237
+ diagnostics.push({
2238
+ severity: "warning",
2239
+ code: "label.overflow.horizontal",
2240
+ message: "Label text exceeds the fitted content width."
2241
+ });
2242
+ }
2243
+ if (overflow.vertical) {
2244
+ diagnostics.push({
2245
+ severity: "warning",
2246
+ code: "label.overflow.vertical",
2247
+ message: "Label text exceeds the fitted content height."
2248
+ });
2249
+ }
2250
+ return diagnostics;
2251
+ }
2252
+ var DEFAULT_OPTIONS = {
2253
+ nodesep: 80,
2254
+ ranksep: 100,
2255
+ edgesep: 40,
2256
+ marginx: 0,
2257
+ marginy: 0,
2258
+ ranker: "network-simplex"
2259
+ };
2260
+ function runDagreInitialLayout(input) {
2261
+ const diagnostics = [];
2262
+ const boxes = /* @__PURE__ */ new Map();
2263
+ const validNodeIds = /* @__PURE__ */ new Set();
2264
+ const graph = new Graph({
2265
+ directed: true,
2266
+ multigraph: true,
2267
+ compound: false
2268
+ });
2269
+ const options = { ...DEFAULT_OPTIONS, ...input.options };
2270
+ graph.setGraph({
2271
+ rankdir: input.direction,
2272
+ nodesep: options.nodesep,
2273
+ ranksep: options.ranksep,
2274
+ edgesep: options.edgesep,
2275
+ marginx: options.marginx,
2276
+ marginy: options.marginy,
2277
+ ranker: options.ranker
2278
+ });
2279
+ graph.setDefaultEdgeLabel(() => ({}));
2280
+ for (const node of input.nodes) {
2281
+ if (!isValidDimension(node.size.width) || !isValidDimension(node.size.height)) {
2282
+ diagnostics.push({
2283
+ severity: "error",
2284
+ code: "layout.node-size.invalid",
2285
+ message: `Node ${node.id} has invalid layout dimensions.`,
2286
+ path: ["nodes", node.id, "size"],
2287
+ detail: { nodeId: node.id }
1439
2288
  });
1440
2289
  continue;
1441
2290
  }
@@ -1501,37 +2350,112 @@ function isValidDimension(value) {
1501
2350
  // src/routing/routes.ts
1502
2351
  function routeEdge(input) {
1503
2352
  const diagnostics = [];
2353
+ const softObstacles = input.obstacles ?? [];
2354
+ const hardObstacles = input.hardObstacles ?? [];
1504
2355
  const defaultAnchors = defaultAnchorsForGeometry(
1505
2356
  input.source.box,
1506
2357
  input.target.box,
1507
2358
  input.direction
1508
2359
  );
1509
- const source = getEdgePort(
1510
- input.source,
1511
- input.target.center,
1512
- input.sourceAnchor ?? defaultAnchors.sourceAnchor
1513
- );
1514
- const target = getEdgePort(
1515
- input.target,
1516
- input.source.center,
1517
- input.targetAnchor ?? defaultAnchors.targetAnchor
1518
- );
1519
2360
  if ((input.kind ?? "orthogonal") === "straight") {
1520
- return { points: simplifyRoute([source, target]), diagnostics };
2361
+ const source = getEdgePort(
2362
+ input.source,
2363
+ input.target.center,
2364
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
2365
+ );
2366
+ const target = getEdgePort(
2367
+ input.target,
2368
+ input.source.center,
2369
+ input.targetAnchor ?? defaultAnchors.targetAnchor
2370
+ );
2371
+ const points = simplifyRoute([source, target]);
2372
+ if (routeCrossesBoxes(points, hardObstacles)) {
2373
+ diagnostics.push({
2374
+ severity: "error",
2375
+ code: "routing.evidence.crossing_forbidden",
2376
+ message: "Straight route crosses hard evidence block obstacles."
2377
+ });
2378
+ return { points, diagnostics };
2379
+ }
2380
+ if (routeCrossesBoxes(points, softObstacles)) {
2381
+ diagnostics.push({
2382
+ severity: "warning",
2383
+ code: "routing.obstacle.unavoidable",
2384
+ message: "Straight route crosses soft obstacles."
2385
+ });
2386
+ }
2387
+ return { points, diagnostics };
2388
+ }
2389
+ const routeLaneObstacles = [...softObstacles, ...hardObstacles];
2390
+ const anchorPairs = routeAnchorPairs(input, defaultAnchors);
2391
+ const candidateRoutes = anchorPairs.flatMap(
2392
+ ({ sourceAnchor, targetAnchor }) => {
2393
+ const source = getEdgePort(
2394
+ input.source,
2395
+ input.target.center,
2396
+ sourceAnchor
2397
+ );
2398
+ const target = getEdgePort(
2399
+ input.target,
2400
+ input.source.center,
2401
+ targetAnchor
2402
+ );
2403
+ const routes = [
2404
+ ...orthogonalCandidates(source, target, input.direction),
2405
+ ...expandedObstacleCandidates(
2406
+ source,
2407
+ target,
2408
+ input.direction,
2409
+ routeLaneObstacles
2410
+ ),
2411
+ ...outerDoglegCandidates(
2412
+ source,
2413
+ target,
2414
+ input.direction,
2415
+ routeLaneObstacles
2416
+ )
2417
+ ];
2418
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
2419
+ return routes.map((points) => ({ points, endpointObstacles }));
2420
+ }
2421
+ );
2422
+ for (const candidate of candidateRoutes) {
2423
+ if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
2424
+ candidate.points,
2425
+ candidate.endpointObstacles
2426
+ )) {
2427
+ return { points: simplifyRoute(candidate.points), diagnostics };
2428
+ }
1521
2429
  }
1522
- const candidates = orthogonalCandidates(source, target, input.direction);
1523
- candidates.push(
1524
- ...expandedObstacleCandidates(
1525
- source,
1526
- target,
1527
- input.direction,
1528
- input.obstacles ?? []
2430
+ const hardClearCandidate = candidateRoutes.find(
2431
+ (candidate) => !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
2432
+ candidate.points,
2433
+ candidate.endpointObstacles
1529
2434
  )
1530
2435
  );
1531
- for (const candidate of candidates) {
1532
- if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
1533
- return { points: simplifyRoute(candidate), diagnostics };
1534
- }
2436
+ if (hardClearCandidate !== void 0) {
2437
+ diagnostics.push({
2438
+ severity: "warning",
2439
+ code: "routing.obstacle.unavoidable",
2440
+ message: "No bounded orthogonal route candidate avoided all soft obstacles."
2441
+ });
2442
+ return {
2443
+ points: simplifyRoute(hardClearCandidate.points),
2444
+ diagnostics
2445
+ };
2446
+ }
2447
+ if (hardObstacles.length > 0) {
2448
+ diagnostics.push({
2449
+ severity: "error",
2450
+ code: "routing.evidence.crossing_forbidden",
2451
+ message: "No bounded orthogonal route candidate avoided hard evidence block obstacles."
2452
+ });
2453
+ return {
2454
+ points: simplifyRoute(
2455
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors)
2456
+ ),
2457
+ diagnostics
2458
+ };
1535
2459
  }
1536
2460
  diagnostics.push({
1537
2461
  severity: "warning",
@@ -1539,10 +2463,111 @@ function routeEdge(input) {
1539
2463
  message: "No bounded orthogonal route candidate avoided all obstacles."
1540
2464
  });
1541
2465
  return {
1542
- points: simplifyRoute(candidates[0] ?? [source, target]),
2466
+ points: simplifyRoute(
2467
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors)
2468
+ ),
1543
2469
  diagnostics
1544
2470
  };
1545
2471
  }
2472
+ function endpointObstaclesForAutoAnchors(input) {
2473
+ const boxes = [];
2474
+ if (input.sourceAnchor === void 0 && hasDistinctAnchors(input.source)) {
2475
+ boxes.push(insetBox(input.source.box, 1));
2476
+ }
2477
+ if (input.targetAnchor === void 0 && hasDistinctAnchors(input.target)) {
2478
+ boxes.push(insetBox(input.target.box, 1));
2479
+ }
2480
+ return boxes.filter((box) => box.width > 0 && box.height > 0);
2481
+ }
2482
+ function hasDistinctAnchors(geometry) {
2483
+ const points = new Set(
2484
+ geometry.anchors.map((anchor) => `${anchor.point.x},${anchor.point.y}`)
2485
+ );
2486
+ return points.size > 1;
2487
+ }
2488
+ function insetBox(box, margin) {
2489
+ return {
2490
+ x: box.x + margin,
2491
+ y: box.y + margin,
2492
+ width: box.width - margin * 2,
2493
+ height: box.height - margin * 2
2494
+ };
2495
+ }
2496
+ function fallbackRoute(input, defaultAnchors) {
2497
+ return [
2498
+ getEdgePort(
2499
+ input.source,
2500
+ input.target.center,
2501
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
2502
+ ),
2503
+ getEdgePort(
2504
+ input.target,
2505
+ input.source.center,
2506
+ input.targetAnchor ?? defaultAnchors.targetAnchor
2507
+ )
2508
+ ];
2509
+ }
2510
+ function routeAnchorPairs(input, defaultAnchors) {
2511
+ const sourceAnchors = routeAnchorCandidates(
2512
+ input.sourceAnchor,
2513
+ defaultAnchors.sourceAnchor,
2514
+ input.source,
2515
+ input.target.center
2516
+ );
2517
+ const targetAnchors = routeAnchorCandidates(
2518
+ input.targetAnchor,
2519
+ defaultAnchors.targetAnchor,
2520
+ input.target,
2521
+ input.source.center
2522
+ );
2523
+ const pairs = sourceAnchors.flatMap(
2524
+ (sourceAnchor) => targetAnchors.map((targetAnchor) => ({ sourceAnchor, targetAnchor }))
2525
+ );
2526
+ const seen = /* @__PURE__ */ new Set();
2527
+ return pairs.filter((pair) => {
2528
+ const key = `${pair.sourceAnchor}->${pair.targetAnchor}`;
2529
+ if (seen.has(key)) {
2530
+ return false;
2531
+ }
2532
+ seen.add(key);
2533
+ return true;
2534
+ });
2535
+ }
2536
+ function routeAnchorCandidates(explicitAnchor, defaultAnchor, geometry, toward) {
2537
+ if (explicitAnchor !== void 0) {
2538
+ return [explicitAnchor];
2539
+ }
2540
+ const ranked = rankedSideAnchors(geometry, toward);
2541
+ return [defaultAnchor, ...ranked].filter(
2542
+ (anchor, index, anchors) => anchors.indexOf(anchor) === index
2543
+ );
2544
+ }
2545
+ function rankedSideAnchors(geometry, toward) {
2546
+ const anchors = outwardSideAnchors(geometry.box, toward);
2547
+ return anchors.sort((left, right) => {
2548
+ const leftPoint = getEdgePort(geometry, toward, left);
2549
+ const rightPoint = getEdgePort(geometry, toward, right);
2550
+ const distance = squaredDistance2(leftPoint, toward) - squaredDistance2(rightPoint, toward);
2551
+ return distance === 0 ? left.localeCompare(right) : distance;
2552
+ });
2553
+ }
2554
+ function outwardSideAnchors(box, toward) {
2555
+ const center = {
2556
+ x: box.x + box.width / 2,
2557
+ y: box.y + box.height / 2
2558
+ };
2559
+ const dx = toward.x - center.x;
2560
+ const dy = toward.y - center.y;
2561
+ if (Math.abs(dx) >= Math.abs(dy)) {
2562
+ return dx >= 0 ? ["right", "top", "bottom"] : ["left", "top", "bottom"];
2563
+ }
2564
+ return dy >= 0 ? ["bottom", "left", "right"] : ["top", "left", "right"];
2565
+ }
2566
+ function squaredDistance2(a, b) {
2567
+ const dx = a.x - b.x;
2568
+ const dy = a.y - b.y;
2569
+ return dx * dx + dy * dy;
2570
+ }
1546
2571
  function simplifyRoute(points) {
1547
2572
  const withoutDuplicates = [];
1548
2573
  for (const point2 of points) {
@@ -1667,6 +2692,44 @@ function expandedObstacleCandidates(source, target, direction, obstacles) {
1667
2692
  }
1668
2693
  return candidates;
1669
2694
  }
2695
+ function outerDoglegCandidates(source, target, direction, obstacles) {
2696
+ if (obstacles.length === 0) {
2697
+ return [];
2698
+ }
2699
+ const margin = 24;
2700
+ const minX = Math.min(...obstacles.map((obstacle) => obstacle.x)) - margin;
2701
+ const maxX = Math.max(...obstacles.map((obstacle) => obstacle.x + obstacle.width)) + margin;
2702
+ const minY = Math.min(...obstacles.map((obstacle) => obstacle.y)) - margin;
2703
+ const maxY = Math.max(...obstacles.map((obstacle) => obstacle.y + obstacle.height)) + margin;
2704
+ if (direction === "TB" || direction === "BT") {
2705
+ const exit2 = exitDelta(source, target, "y");
2706
+ return sortedUniqueLanes([minX, maxX], (source.x + target.x) / 2).map(
2707
+ (laneX) => [
2708
+ source,
2709
+ { x: source.x, y: source.y + exit2 },
2710
+ { x: laneX, y: source.y + exit2 },
2711
+ { x: laneX, y: target.y - exit2 },
2712
+ { x: target.x, y: target.y - exit2 },
2713
+ target
2714
+ ]
2715
+ );
2716
+ }
2717
+ const exit = exitDelta(source, target, "x");
2718
+ return sortedUniqueLanes([minY, maxY], (source.y + target.y) / 2).map(
2719
+ (laneY) => [
2720
+ source,
2721
+ { x: source.x + exit, y: source.y },
2722
+ { x: source.x + exit, y: laneY },
2723
+ { x: target.x - exit, y: laneY },
2724
+ { x: target.x - exit, y: target.y },
2725
+ target
2726
+ ]
2727
+ );
2728
+ }
2729
+ function exitDelta(source, target, axis) {
2730
+ const delta = axis === "x" ? target.x - source.x : target.y - source.y;
2731
+ return (delta >= 0 ? 1 : -1) * 24;
2732
+ }
1670
2733
  function sortedUniqueLanes(lanes, midpoint) {
1671
2734
  return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
1672
2735
  const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
@@ -1690,6 +2753,72 @@ function routeIntersectsObstacles(points, obstacles) {
1690
2753
  }
1691
2754
  return false;
1692
2755
  }
2756
+ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
2757
+ for (let index = 0; index < points.length - 1; index += 1) {
2758
+ const a = points[index];
2759
+ const b = points[index + 1];
2760
+ if (a === void 0 || b === void 0) {
2761
+ continue;
2762
+ }
2763
+ const segment = segmentBox(a, b);
2764
+ for (const endpointInterior of endpointInteriors) {
2765
+ validateBox(endpointInterior);
2766
+ if (intersectsAabb(segment, endpointInterior)) {
2767
+ return true;
2768
+ }
2769
+ }
2770
+ }
2771
+ return false;
2772
+ }
2773
+ function routeCrossesBoxes(points, obstacles) {
2774
+ for (let index = 0; index < points.length - 1; index += 1) {
2775
+ const a = points[index];
2776
+ const b = points[index + 1];
2777
+ if (a === void 0 || b === void 0) {
2778
+ continue;
2779
+ }
2780
+ for (const obstacle of obstacles) {
2781
+ validateBox(obstacle);
2782
+ if (segmentIntersectsBox(a, b, obstacle)) {
2783
+ return true;
2784
+ }
2785
+ }
2786
+ }
2787
+ return false;
2788
+ }
2789
+ function segmentIntersectsBox(start, end, box) {
2790
+ const left = box.x;
2791
+ const right = box.x + box.width;
2792
+ const top = box.y;
2793
+ const bottom = box.y + box.height;
2794
+ if (pointInsideBox(start, box) || pointInsideBox(end, box)) {
2795
+ return true;
2796
+ }
2797
+ if (start.x === end.x) {
2798
+ return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
2799
+ }
2800
+ if (start.y === end.y) {
2801
+ return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
2802
+ }
2803
+ return segmentIntersectsBoxEdge(start, end, left, top, right, top) || segmentIntersectsBoxEdge(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge(start, end, left, bottom, left, top);
2804
+ }
2805
+ function pointInsideBox(point2, box) {
2806
+ return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
2807
+ }
2808
+ function rangesOverlap(a, b, min, max) {
2809
+ const low = Math.min(a, b);
2810
+ const high = Math.max(a, b);
2811
+ return high > min && low < max;
2812
+ }
2813
+ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
2814
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
2815
+ if (denominator === 0) {
2816
+ return false;
2817
+ }
2818
+ const t = ((x1 - start.x) * (y2 - y1) - (y1 - start.y) * (x2 - x1)) / denominator;
2819
+ const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
2820
+ return t > 0 && t < 1 && u > 0 && u < 1;
2821
+ }
1693
2822
  function segmentBox(a, b) {
1694
2823
  const minX = Math.min(a.x, b.x);
1695
2824
  const minY = Math.min(a.y, b.y);
@@ -1705,6 +2834,17 @@ function areCollinear(a, b, c) {
1705
2834
  }
1706
2835
 
1707
2836
  // src/solver/solve.ts
2837
+ var DEFAULT_MATRIX_CELL_SIZE = { width: 120, height: 36 };
2838
+ var DEFAULT_TABLE_CELL_SIZE = { width: 128, height: 34 };
2839
+ var DEFAULT_PANEL_WIDTH = 320;
2840
+ var DEFAULT_PANEL_ITEM_HEIGHT = 28;
2841
+ var DEFAULT_EVIDENCE_BLOCK_GAP = 24;
2842
+ var EDGE_LABEL_CLEARANCE = 8;
2843
+ var EVIDENCE_TEXT_FONT = {
2844
+ fontFamily: "Arial, sans-serif",
2845
+ fontSize: 10,
2846
+ lineHeight: 12
2847
+ };
1708
2848
  function solveDiagram(diagram, options = {}) {
1709
2849
  const diagnostics = [...diagram.diagnostics];
1710
2850
  const nodes = stableById(diagram.nodes);
@@ -1769,19 +2909,21 @@ function solveDiagram(diagram, options = {}) {
1769
2909
  constrained.boxes,
1770
2910
  swimlaneContracts.layouts
1771
2911
  );
2912
+ const coordinatedMatrices = coordinateMatrices(diagram.matrices ?? []);
2913
+ const coordinatedTables = coordinateTables(diagram.tables ?? []);
2914
+ const coordinatedEvidencePanels = coordinateEvidencePanels(
2915
+ diagram.evidencePanels ?? []
2916
+ );
1772
2917
  const groupBoxes = new Map(
1773
2918
  coordinatedGroups.map((group) => [group.id, group.box])
1774
2919
  );
1775
- const coordinatedEdges = coordinateEdges(
1776
- edges,
1777
- nodeGeometryById,
1778
- coordinatedNodes,
1779
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
1780
- diagram.direction,
1781
- options,
1782
- diagnostics
1783
- );
1784
- const allBoxes = [
2920
+ const baseTextAnnotations = coordinateBaseTextAnnotations({
2921
+ nodes: coordinatedNodes,
2922
+ groups: coordinatedGroups,
2923
+ swimlanes: coordinatedSwimlanes,
2924
+ ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
2925
+ });
2926
+ const layoutBoxes = [
1785
2927
  ...coordinatedNodes.map((node) => node.box),
1786
2928
  ...coordinatedNodes.flatMap(
1787
2929
  (node) => (node.ports ?? []).flatMap(
@@ -1791,10 +2933,111 @@ function solveDiagram(diagram, options = {}) {
1791
2933
  ...groupBoxes.values(),
1792
2934
  ...coordinatedSwimlanes.flatMap(
1793
2935
  (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
1794
- )
2936
+ ),
2937
+ ...baseTextAnnotations.map((annotation) => annotation.box)
2938
+ ];
2939
+ const initialContentBounds = layoutBoxes.length === 0 ? { x: 0, y: 0, width: 0} : unionBoxes(layoutBoxes);
2940
+ placeEvidenceBlocks(
2941
+ [
2942
+ ...coordinatedMatrices,
2943
+ ...coordinatedTables,
2944
+ ...coordinatedEvidencePanels
2945
+ ],
2946
+ initialContentBounds
2947
+ );
2948
+ refreshTableColumnXOffsets(coordinatedTables);
2949
+ measureEvidenceTextBlocks(
2950
+ coordinatedMatrices,
2951
+ coordinatedTables,
2952
+ coordinatedEvidencePanels,
2953
+ options.textMeasurer
2954
+ );
2955
+ const evidenceBoxes = [
2956
+ ...coordinatedMatrices.map((matrix) => matrix.box),
2957
+ ...coordinatedTables.map((table) => table.box),
2958
+ ...coordinatedEvidencePanels.map((panel) => panel.box)
1795
2959
  ];
2960
+ diagnostics.push(
2961
+ ...reportEvidenceBlockOverlaps(
2962
+ [
2963
+ ...coordinatedMatrices.map((matrix) => ({
2964
+ id: matrix.id,
2965
+ kind: "matrix",
2966
+ ...matrix.position === void 0 ? {} : { position: matrix.position },
2967
+ box: matrix.box
2968
+ })),
2969
+ ...coordinatedTables.map((table) => ({
2970
+ id: table.id,
2971
+ kind: "table",
2972
+ ...table.position === void 0 ? {} : { position: table.position },
2973
+ box: table.box
2974
+ })),
2975
+ ...coordinatedEvidencePanels.map((panel) => ({
2976
+ id: panel.id,
2977
+ kind: "evidence-panel",
2978
+ ...panel.position === void 0 ? {} : { position: panel.position },
2979
+ box: panel.box
2980
+ }))
2981
+ ],
2982
+ [
2983
+ ...coordinatedNodes.map((node) => ({
2984
+ id: node.id,
2985
+ kind: "node",
2986
+ box: node.box
2987
+ })),
2988
+ ...coordinatedGroups.map((group) => ({
2989
+ id: group.id,
2990
+ kind: "group",
2991
+ box: group.box
2992
+ })),
2993
+ ...coordinatedSwimlanes.flatMap(
2994
+ (swimlane) => swimlane.box === void 0 ? [] : [{ id: swimlane.id, kind: "swimlane", box: swimlane.box }]
2995
+ )
2996
+ ]
2997
+ )
2998
+ );
2999
+ const allBoxes = [...layoutBoxes, ...evidenceBoxes];
1796
3000
  const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
1797
3001
  const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
3002
+ const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
3003
+ const routingTextObstacles = [
3004
+ ...baseTextAnnotations.filter(isPreRouteTextObstacle),
3005
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle)
3006
+ ];
3007
+ const coordinatedEdges = coordinateEdges(
3008
+ edges,
3009
+ nodeGeometryById,
3010
+ coordinatedNodes,
3011
+ [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
3012
+ [
3013
+ ...coordinatedTables.map((table) => table.box),
3014
+ ...coordinatedEvidencePanels.map((panel) => panel.box)
3015
+ ],
3016
+ routingTextObstacles,
3017
+ coordinatedMatrices.map((matrix) => matrix.box),
3018
+ diagram.direction,
3019
+ options,
3020
+ diagnostics
3021
+ );
3022
+ const edgeTextAnnotations = coordinateEdgeTextAnnotations(
3023
+ coordinatedEdges,
3024
+ options.textMeasurer
3025
+ );
3026
+ const textAnnotations = [
3027
+ ...baseTextAnnotations,
3028
+ ...frameTextAnnotation,
3029
+ ...edgeTextAnnotations
3030
+ ];
3031
+ diagnostics.push(...reportTextAnnotationCollisions(textAnnotations));
3032
+ diagnostics.push(
3033
+ ...reportRouteTextClearance(coordinatedEdges, textAnnotations)
3034
+ );
3035
+ const edgePointBounds = edgeBounds(coordinatedEdges);
3036
+ const boundsBase = [
3037
+ contentBounds,
3038
+ ...edgePointBounds,
3039
+ ...edgeTextAnnotations.map((annotation) => annotation.box)
3040
+ ];
1798
3041
  return {
1799
3042
  id: diagram.id,
1800
3043
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -1803,9 +3046,13 @@ function solveDiagram(diagram, options = {}) {
1803
3046
  edges: coordinatedEdges,
1804
3047
  groups: coordinatedGroups,
1805
3048
  ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
3049
+ ...coordinatedMatrices.length === 0 ? {} : { matrices: coordinatedMatrices },
3050
+ ...coordinatedTables.length === 0 ? {} : { tables: coordinatedTables },
3051
+ ...coordinatedEvidencePanels.length === 0 ? {} : { evidencePanels: coordinatedEvidencePanels },
1806
3052
  diagnostics,
1807
- bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
3053
+ bounds: frame === void 0 ? unionBoxes(boundsBase) : unionBoxes([...boundsBase, frame.box, frame.titleBox]),
1808
3054
  ...frame === void 0 ? {} : { frame },
3055
+ ...textAnnotations.length === 0 ? {} : { textAnnotations },
1809
3056
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
1810
3057
  };
1811
3058
  }
@@ -2700,7 +3947,364 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2700
3947
  }
2701
3948
  return coordinated;
2702
3949
  }
2703
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
3950
+ function coordinateMatrices(matrices) {
3951
+ return matrices.map((block) => ({
3952
+ ...block,
3953
+ box: blockBox(block, {
3954
+ width: defaultMatrixRowHeaderWidth(block) + Math.max(1, block.cols.length) * DEFAULT_MATRIX_CELL_SIZE.width,
3955
+ height: Math.max(1, block.rows.length + 1) * DEFAULT_MATRIX_CELL_SIZE.height
3956
+ })
3957
+ }));
3958
+ }
3959
+ function defaultMatrixRowHeaderWidth(block) {
3960
+ return block.rows.length === 0 ? 0 : Math.min(96, DEFAULT_MATRIX_CELL_SIZE.width);
3961
+ }
3962
+ function coordinateTables(tables) {
3963
+ return tables.map((table) => {
3964
+ const box = blockBox(table, {
3965
+ width: Math.max(1, table.columns.length) * DEFAULT_TABLE_CELL_SIZE.width,
3966
+ height: Math.max(1, table.rows.length + 1) * DEFAULT_TABLE_CELL_SIZE.height
3967
+ });
3968
+ return {
3969
+ ...table,
3970
+ box,
3971
+ columnXOffsets: columnXOffsets(table, box)
3972
+ };
3973
+ });
3974
+ }
3975
+ function coordinateEvidencePanels(panels) {
3976
+ return panels.map((block) => ({
3977
+ ...block,
3978
+ box: blockBox(block, {
3979
+ width: DEFAULT_PANEL_WIDTH,
3980
+ height: Math.max(1, block.items.length) * DEFAULT_PANEL_ITEM_HEIGHT
3981
+ })
3982
+ }));
3983
+ }
3984
+ function edgeBounds(edges) {
3985
+ return edges.flatMap((edge) => {
3986
+ if (edge.points.length === 0) {
3987
+ return [];
3988
+ }
3989
+ const minX = Math.min(...edge.points.map((point2) => point2.x));
3990
+ const minY = Math.min(...edge.points.map((point2) => point2.y));
3991
+ const maxX = Math.max(...edge.points.map((point2) => point2.x));
3992
+ const maxY = Math.max(...edge.points.map((point2) => point2.y));
3993
+ return [
3994
+ {
3995
+ x: minX,
3996
+ y: minY,
3997
+ width: maxX - minX,
3998
+ height: maxY - minY
3999
+ }
4000
+ ];
4001
+ });
4002
+ }
4003
+ function blockBox(block, defaultSize) {
4004
+ return {
4005
+ x: block.position?.x ?? 0,
4006
+ y: block.position?.y ?? 0,
4007
+ width: block.size?.width ?? defaultSize.width,
4008
+ height: block.size?.height ?? defaultSize.height
4009
+ };
4010
+ }
4011
+ function placeEvidenceBlocks(blocks, contentBounds) {
4012
+ let nextY = contentBounds.y;
4013
+ const x = contentBounds.x + contentBounds.width + DEFAULT_EVIDENCE_BLOCK_GAP;
4014
+ for (const block of blocks) {
4015
+ if (block.position !== void 0) {
4016
+ continue;
4017
+ }
4018
+ block.box.x = x;
4019
+ block.box.y = nextY;
4020
+ nextY += block.box.height + DEFAULT_EVIDENCE_BLOCK_GAP;
4021
+ }
4022
+ }
4023
+ function columnXOffsets(table, box) {
4024
+ if (table.columns.length === 0) {
4025
+ return [];
4026
+ }
4027
+ const columnWidth = box.width / table.columns.length;
4028
+ return table.columns.map((_, index) => box.x + index * columnWidth);
4029
+ }
4030
+ function tableCellBox2(table, columnIndex, rowIndex, rowHeight, columnCount) {
4031
+ const x = table.columnXOffsets[columnIndex] ?? table.box.x + table.box.width / columnCount * columnIndex;
4032
+ const nextX = table.columnXOffsets[columnIndex + 1] ?? table.box.x + table.box.width;
4033
+ return {
4034
+ x,
4035
+ y: table.box.y + rowIndex * rowHeight,
4036
+ width: nextX - x,
4037
+ height: rowHeight
4038
+ };
4039
+ }
4040
+ function refreshTableColumnXOffsets(tables) {
4041
+ for (const table of tables) {
4042
+ table.columnXOffsets = columnXOffsets(table, table.box);
4043
+ }
4044
+ }
4045
+ function measureEvidenceTextBlocks(matrices, tables, panels, textMeasurer) {
4046
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
4047
+ for (const matrix of matrices) {
4048
+ const geometry = matrixGeometry(matrix);
4049
+ matrix.columnLabelLayouts = matrix.cols.map(
4050
+ (column) => measureEvidenceTextLayout(column, geometry.columnHeaderBox, measurer)
4051
+ );
4052
+ matrix.rowLabelLayouts = matrix.rows.map(
4053
+ (row, index) => measureEvidenceTextLayout(row, geometry.rowHeaderBox(index), measurer)
4054
+ );
4055
+ matrix.cellLabelLayouts = matrix.rows.map(
4056
+ (_, rowIndex) => matrix.cols.map((_2, columnIndex) => {
4057
+ const cell2 = matrix.cells[rowIndex]?.[columnIndex] ?? { text: "" };
4058
+ return measureEvidenceTextLayout(
4059
+ cell2.text,
4060
+ geometry.cellBox(rowIndex, columnIndex),
4061
+ measurer
4062
+ );
4063
+ })
4064
+ );
4065
+ }
4066
+ for (const table of tables) {
4067
+ const rowHeight = table.box.height / Math.max(1, table.rows.length + 1);
4068
+ const columnCount = Math.max(1, table.columns.length);
4069
+ table.columnLabelLayouts = table.columns.map(
4070
+ (column, columnIndex) => measureEvidenceTextLayout(
4071
+ column.label.text,
4072
+ tableCellBox2(table, columnIndex, 0, rowHeight, columnCount),
4073
+ measurer
4074
+ )
4075
+ );
4076
+ table.cellLabelLayouts = table.rows.map(
4077
+ (row, rowIndex) => table.columns.map((column, columnIndex) => {
4078
+ const cell2 = row.cells[column.id] ?? { text: "" };
4079
+ return measureEvidenceTextLayout(
4080
+ cell2.text,
4081
+ tableCellBox2(
4082
+ table,
4083
+ columnIndex,
4084
+ rowIndex + 1,
4085
+ rowHeight,
4086
+ columnCount
4087
+ ),
4088
+ measurer
4089
+ );
4090
+ })
4091
+ );
4092
+ }
4093
+ for (const panel of panels) {
4094
+ const geometry = panelGeometry(panel);
4095
+ panel.titleLayout = measureEvidenceTextLayout(
4096
+ `${panel.kind}: ${panel.id}`,
4097
+ geometry.titleBox,
4098
+ measurer
4099
+ );
4100
+ panel.itemLayouts = panel.items.map(
4101
+ (item, index) => measureEvidenceTextLayout(
4102
+ panelItemText2(item.label.text, item.detail?.text),
4103
+ geometry.itemRowBox(index),
4104
+ measurer
4105
+ )
4106
+ );
4107
+ }
4108
+ }
4109
+ function measureEvidenceTextLayout(text, box, textMeasurer) {
4110
+ const lineHeight = EVIDENCE_TEXT_FONT.lineHeight;
4111
+ return {
4112
+ lines: wrapEvidenceText(text, {
4113
+ maxWidth: Math.max(0, box.width - 8),
4114
+ maxLines: Math.max(1, Math.floor((box.height - 4) / lineHeight)),
4115
+ textMeasurer
4116
+ })
4117
+ };
4118
+ }
4119
+ function wrapEvidenceText(text, options) {
4120
+ const normalized = text.trim().replace(/\s+/g, " ");
4121
+ if (normalized.length === 0) {
4122
+ return [""];
4123
+ }
4124
+ const lines = [];
4125
+ let current = "";
4126
+ let overflow = false;
4127
+ for (const word of normalized.split(" ")) {
4128
+ const chunks = chunkEvidenceWord(
4129
+ word,
4130
+ options.maxWidth,
4131
+ options.textMeasurer
4132
+ );
4133
+ for (const chunk of chunks) {
4134
+ const candidate = current.length === 0 ? chunk : `${current} ${chunk}`;
4135
+ if (measureEvidenceText(candidate, options.textMeasurer) <= options.maxWidth) {
4136
+ current = candidate;
4137
+ continue;
4138
+ }
4139
+ if (current.length > 0) {
4140
+ lines.push(current);
4141
+ current = chunk;
4142
+ } else {
4143
+ lines.push(chunk);
4144
+ current = "";
4145
+ }
4146
+ if (lines.length >= options.maxLines) {
4147
+ overflow = true;
4148
+ break;
4149
+ }
4150
+ }
4151
+ if (overflow) {
4152
+ break;
4153
+ }
4154
+ }
4155
+ if (!overflow && current.length > 0) {
4156
+ lines.push(current);
4157
+ }
4158
+ if (lines.length > options.maxLines) {
4159
+ overflow = true;
4160
+ lines.length = options.maxLines;
4161
+ }
4162
+ if (overflow || lines.length === options.maxLines) {
4163
+ const rendered = lines.join(" ");
4164
+ if (rendered.length < normalized.length) {
4165
+ lines[lines.length - 1] = ellipsizeMeasuredEvidenceLine(
4166
+ lines[lines.length - 1] ?? "",
4167
+ options.maxWidth,
4168
+ options.textMeasurer
4169
+ );
4170
+ }
4171
+ }
4172
+ return lines.length === 0 ? [""] : lines;
4173
+ }
4174
+ function chunkEvidenceWord(word, maxWidth, textMeasurer) {
4175
+ if (measureEvidenceText(word, textMeasurer) <= maxWidth) {
4176
+ return [word];
4177
+ }
4178
+ const chunks = [];
4179
+ let current = "";
4180
+ for (const char of Array.from(word)) {
4181
+ const candidate = `${current}${char}`;
4182
+ if (current.length > 0 && measureEvidenceText(candidate, textMeasurer) > maxWidth) {
4183
+ chunks.push(current);
4184
+ current = char;
4185
+ continue;
4186
+ }
4187
+ current = candidate;
4188
+ }
4189
+ if (current.length > 0) {
4190
+ chunks.push(current);
4191
+ }
4192
+ return chunks.length === 0 ? [word] : chunks;
4193
+ }
4194
+ function ellipsizeMeasuredEvidenceLine(line, maxWidth, textMeasurer) {
4195
+ const ellipsis = "...";
4196
+ if (measureEvidenceText(ellipsis, textMeasurer) > maxWidth) {
4197
+ return "";
4198
+ }
4199
+ let candidate = line.trimEnd();
4200
+ while (candidate.length > 0 && measureEvidenceText(`${candidate}${ellipsis}`, textMeasurer) > maxWidth) {
4201
+ candidate = Array.from(candidate).slice(0, -1).join("").trimEnd();
4202
+ }
4203
+ return `${candidate}${ellipsis}`;
4204
+ }
4205
+ function measureEvidenceText(text, textMeasurer) {
4206
+ return textMeasurer.naturalWidth(
4207
+ textMeasurer.prepare(text, EVIDENCE_TEXT_FONT)
4208
+ );
4209
+ }
4210
+ function matrixGeometry(matrix) {
4211
+ const columnCount = Math.max(1, matrix.cols.length);
4212
+ const rowCount = matrix.rows.length;
4213
+ const rowHeaderWidth = rowCount > 0 ? Math.min(96, matrix.box.width * 0.28) : 0;
4214
+ const dataWidth = Math.max(0, matrix.box.width - rowHeaderWidth);
4215
+ const cellWidth = dataWidth / columnCount;
4216
+ const rowHeight = matrix.box.height / Math.max(1, rowCount + 1);
4217
+ return {
4218
+ rowHeaderWidth,
4219
+ cellWidth,
4220
+ rowHeight,
4221
+ columnHeaderBox: {
4222
+ x: matrix.box.x + rowHeaderWidth,
4223
+ y: matrix.box.y,
4224
+ width: cellWidth,
4225
+ height: rowHeight
4226
+ },
4227
+ rowHeaderBox: (rowIndex) => ({
4228
+ x: matrix.box.x,
4229
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
4230
+ width: rowHeaderWidth,
4231
+ height: rowHeight
4232
+ }),
4233
+ cellBox: (rowIndex, columnIndex) => ({
4234
+ x: matrix.box.x + rowHeaderWidth + columnIndex * cellWidth,
4235
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
4236
+ width: cellWidth,
4237
+ height: rowHeight
4238
+ })
4239
+ };
4240
+ }
4241
+ function panelGeometry(panel) {
4242
+ const titleWidth = Math.min(panel.box.width * 0.36, 140);
4243
+ const itemBox = {
4244
+ x: panel.box.x + titleWidth,
4245
+ y: panel.box.y,
4246
+ width: panel.box.width - titleWidth,
4247
+ height: panel.box.height
4248
+ };
4249
+ const itemHeight = panel.box.height / Math.max(1, panel.items.length);
4250
+ return {
4251
+ titleBox: {
4252
+ x: panel.box.x,
4253
+ y: panel.box.y,
4254
+ width: titleWidth,
4255
+ height: panel.box.height
4256
+ },
4257
+ itemRowBox: (index) => ({
4258
+ x: itemBox.x,
4259
+ y: itemBox.y + index * itemHeight,
4260
+ width: itemBox.width,
4261
+ height: itemHeight
4262
+ })
4263
+ };
4264
+ }
4265
+ function panelItemText2(label, detail) {
4266
+ return detail === void 0 ? label : `${label}: ${detail}`;
4267
+ }
4268
+ function reportEvidenceBlockOverlaps(evidenceBlocks, contentBlocks) {
4269
+ const diagnostics = [];
4270
+ for (let index = 0; index < evidenceBlocks.length; index += 1) {
4271
+ const block = evidenceBlocks[index];
4272
+ if (block === void 0 || block.position === void 0) {
4273
+ continue;
4274
+ }
4275
+ for (const content of contentBlocks) {
4276
+ if (intersectsAabb(block.box, content.box)) {
4277
+ diagnostics.push(evidenceOverlapDiagnostic(block, content));
4278
+ }
4279
+ }
4280
+ for (let otherIndex = 0; otherIndex < evidenceBlocks.length; otherIndex += 1) {
4281
+ if (otherIndex === index) {
4282
+ continue;
4283
+ }
4284
+ const other = evidenceBlocks[otherIndex];
4285
+ if (other === void 0 || other.position !== void 0 && otherIndex < index || !intersectsAabb(block.box, other.box)) {
4286
+ continue;
4287
+ }
4288
+ diagnostics.push(evidenceOverlapDiagnostic(block, other));
4289
+ }
4290
+ }
4291
+ return diagnostics;
4292
+ }
4293
+ function evidenceOverlapDiagnostic(block, conflict) {
4294
+ return {
4295
+ severity: "warning",
4296
+ code: "constraints.overlap.unresolved",
4297
+ message: `Evidence block ${block.id} overlaps ${conflict.kind} ${conflict.id}.`,
4298
+ path: ["evidence", block.id],
4299
+ detail: {
4300
+ evidenceBlockId: block.id,
4301
+ evidenceBlockKind: block.kind,
4302
+ conflictingObjectId: conflict.id,
4303
+ conflictingObjectKind: conflict.kind
4304
+ }
4305
+ };
4306
+ }
4307
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
2704
4308
  const coordinated = [];
2705
4309
  const coordinatedNodeById = new Map(
2706
4310
  coordinatedNodes.map((node) => [node.id, node])
@@ -2724,6 +4328,8 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, o
2724
4328
  }
2725
4329
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
2726
4330
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
4331
+ const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
4332
+ const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
2727
4333
  const route = routeEdge({
2728
4334
  kind: options.routeKind ?? "orthogonal",
2729
4335
  direction,
@@ -2731,9 +4337,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, o
2731
4337
  target: portGeometry(target, targetPort),
2732
4338
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
2733
4339
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
2734
- obstacles: obstacles.filter(
2735
- (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
2736
- )
4340
+ obstacles: [
4341
+ ...obstacles.filter(
4342
+ (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
4343
+ ),
4344
+ ...softObstacles,
4345
+ ...routeTextObstacles
4346
+ ],
4347
+ hardObstacles
2737
4348
  });
2738
4349
  diagnostics.push(
2739
4350
  ...route.diagnostics.map((diagnostic) => ({
@@ -2748,387 +4359,581 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, o
2748
4359
  }
2749
4360
  return coordinated;
2750
4361
  }
2751
- function portGeometry(nodeGeometry, port) {
2752
- if (port === void 0) {
2753
- return nodeGeometry;
2754
- }
2755
- return {
2756
- ...nodeGeometry,
2757
- box: port.box,
2758
- center: port.anchor,
2759
- anchors: nodeGeometry.anchors.map((anchor) => ({
2760
- name: anchor.name,
2761
- point: port.anchor
2762
- })),
2763
- obstacleBox: port.box
2764
- };
2765
- }
2766
- function stableById(items) {
2767
- return [...items].sort((a, b) => a.id.localeCompare(b.id));
2768
- }
2769
- function stableByConstraintId(items) {
2770
- return [...items].sort(
2771
- (a, b) => `${a.id ?? a.kind}`.localeCompare(`${b.id ?? b.kind}`)
2772
- );
2773
- }
2774
- function groupReferenceMissing(groupId, referenceKind, id) {
2775
- return {
2776
- severity: "error",
2777
- code: "solver.group-reference.missing",
2778
- message: `Group ${groupId} references a missing ${referenceKind}.`,
2779
- path: ["groups", groupId],
2780
- detail: id === void 0 ? { groupId } : { groupId, id }
2781
- };
2782
- }
2783
-
2784
- // src/text/types.ts
2785
- function assertFinitePositive(value, label) {
2786
- if (!Number.isFinite(value) || value <= 0) {
2787
- throw new TypeError(`${label} must be finite and positive`);
2788
- }
2789
- }
2790
- function assertFiniteNonNegative(value, label) {
2791
- if (!Number.isFinite(value) || value < 0) {
2792
- throw new TypeError(`${label} must be a finite non-negative width`);
2793
- }
2794
- }
2795
- function validateTextStyle(style2) {
2796
- assertFinitePositive(style2.fontSize, "fontSize");
2797
- if (style2.lineHeight !== void 0) {
2798
- assertFinitePositive(style2.lineHeight, "lineHeight");
4362
+ function edgeConnectedTextOwnerIds(edge) {
4363
+ const owners = /* @__PURE__ */ new Set();
4364
+ if (edge.source.portId !== void 0) {
4365
+ owners.add(`${edge.source.nodeId}.${edge.source.portId}`);
2799
4366
  }
2800
- if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
2801
- throw new TypeError("letterSpacing must be finite");
4367
+ if (edge.target.portId !== void 0) {
4368
+ owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
2802
4369
  }
4370
+ return owners;
2803
4371
  }
2804
- function resolveLineHeight(style2) {
2805
- validateTextStyle(style2);
2806
- return style2.lineHeight ?? style2.fontSize * 1.2;
2807
- }
2808
- function toCanvasFont(style2) {
2809
- validateTextStyle(style2);
2810
- const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
2811
- const fontWeight = style2.fontWeight ?? 400;
2812
- return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
2813
- }
2814
-
2815
- // src/text/fallback.ts
2816
- var DeterministicTextMeasurer = class {
2817
- prepare(text, style2) {
2818
- validateTextStyle(style2);
2819
- return {
2820
- text,
2821
- font: toCanvasFont(style2),
2822
- style: { ...style2 },
2823
- backend: "deterministic"
2824
- };
4372
+ function coordinateBaseTextAnnotations(input) {
4373
+ const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
4374
+ const annotations = [];
4375
+ for (const node of input.nodes) {
4376
+ if (node.compartments !== void 0) {
4377
+ continue;
4378
+ }
4379
+ if (node.labelLayout === void 0 && node.label === void 0) {
4380
+ continue;
4381
+ }
4382
+ const layout2 = node.labelLayout ?? fallbackLabelLayout(node.label?.text ?? "");
4383
+ const buildAnnotation = node.labelLayout === void 0 ? buildAnchorCenteredTextAnnotation : buildTextAnnotation;
4384
+ annotations.push(
4385
+ buildAnnotation({
4386
+ ownerId: node.id,
4387
+ surfaceKind: "node-label",
4388
+ layout: layout2,
4389
+ anchor: node.box
4390
+ })
4391
+ );
2825
4392
  }
2826
- layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
2827
- assertFiniteNonNegative(maxWidth, "maxWidth");
2828
- assertFinitePositiveLineHeight(lineHeight);
2829
- const lines = this.wrap(prepared, maxWidth);
2830
- const width = lines.reduce(
2831
- (current, line) => Math.max(current, line.width),
2832
- 0
4393
+ for (const group of input.groups) {
4394
+ if (group.labelLayout === void 0 && group.label === void 0) {
4395
+ continue;
4396
+ }
4397
+ const layout2 = group.labelLayout ?? fallbackLabelLayout(group.label?.text ?? "");
4398
+ const buildAnnotation = group.labelLayout === void 0 ? buildAnchorCenteredTextAnnotation : buildTextAnnotation;
4399
+ annotations.push(
4400
+ buildAnnotation({
4401
+ ownerId: group.id,
4402
+ surfaceKind: "group-label",
4403
+ layout: layout2,
4404
+ anchor: group.box
4405
+ })
2833
4406
  );
2834
- return {
2835
- width,
2836
- height: lines.length * lineHeight,
2837
- lineHeight,
2838
- lineCount: lines.length,
2839
- lines,
2840
- diagnostics: []
2841
- };
2842
4407
  }
2843
- naturalWidth(prepared) {
2844
- const charWidth = getCharacterWidth(prepared.style);
2845
- return prepared.text.split("\n").reduce((width, line) => {
2846
- return Math.max(width, line.length * charWidth);
2847
- }, 0);
4408
+ for (const node of input.nodes) {
4409
+ for (const port of node.ports ?? []) {
4410
+ if (port.label?.text === void 0) {
4411
+ continue;
4412
+ }
4413
+ const layout2 = fitLabel(
4414
+ port.label.text,
4415
+ {
4416
+ font: { fontFamily: "Arial", fontSize: 10, lineHeight: 12 },
4417
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
4418
+ minSize: { width: 0, height: 0 },
4419
+ maxWidth: 160
4420
+ },
4421
+ measurer
4422
+ );
4423
+ annotations.push(
4424
+ buildTextAnnotation({
4425
+ ownerId: `${node.id}.${port.id}`,
4426
+ surfaceKind: "port-label",
4427
+ layout: layout2,
4428
+ anchor: portLabelBox(port)
4429
+ })
4430
+ );
4431
+ }
2848
4432
  }
2849
- wrap(prepared, maxWidth) {
2850
- const charWidth = getCharacterWidth(prepared.style);
2851
- const sourceLines = prepared.text.split("\n");
2852
- const output = [];
2853
- let segmentIndex = 0;
2854
- for (const sourceLine of sourceLines) {
2855
- if (sourceLine.length === 0) {
2856
- output.push(createLine("", 0, segmentIndex, 0, 0));
2857
- segmentIndex += 1;
4433
+ for (const node of input.nodes) {
4434
+ if (node.compartments === void 0) {
4435
+ continue;
4436
+ }
4437
+ const rows = compartmentRows(node);
4438
+ for (let index = 0; index < rows.length; index += 1) {
4439
+ const row = rows[index];
4440
+ if (row === void 0) {
2858
4441
  continue;
2859
4442
  }
2860
- const maxChars = maxWidth <= 0 ? 1 : Math.max(1, Math.floor(maxWidth / charWidth));
2861
- for (let start = 0; start < sourceLine.length; start += maxChars) {
2862
- const text = sourceLine.slice(start, start + maxChars);
2863
- output.push(
2864
- createLine(
2865
- text,
2866
- text.length * charWidth,
2867
- segmentIndex,
2868
- start,
2869
- start + text.length
2870
- )
2871
- );
4443
+ const layout2 = fitLabel(
4444
+ row,
4445
+ {
4446
+ font: { fontFamily: "Arial", fontSize: 11, lineHeight: 13 },
4447
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
4448
+ minSize: { width: 0, height: 0 },
4449
+ maxWidth: node.box.width
4450
+ },
4451
+ measurer
4452
+ );
4453
+ annotations.push(
4454
+ buildAnchorCenteredTextAnnotation({
4455
+ ownerId: node.id,
4456
+ surfaceKind: "compartment-row",
4457
+ surfaceIndex: index,
4458
+ layout: layout2,
4459
+ anchor: {
4460
+ x: node.box.x,
4461
+ y: node.box.y + 18 + index * 16,
4462
+ width: node.box.width,
4463
+ height: 16
4464
+ }
4465
+ })
4466
+ );
4467
+ }
4468
+ }
4469
+ for (const swimlane of input.swimlanes) {
4470
+ for (const lane of swimlane.lanes) {
4471
+ if (lane.label?.text === void 0 || lane.box === void 0) {
4472
+ continue;
2872
4473
  }
2873
- segmentIndex += 1;
4474
+ const labelBox = lane.headerBox ?? lane.box;
4475
+ const layout2 = fitLabel(
4476
+ lane.label.text,
4477
+ {
4478
+ font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
4479
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
4480
+ minSize: { width: 0, height: 0 },
4481
+ maxWidth: swimlane.orientation === "horizontal" ? labelBox.height : labelBox.width
4482
+ },
4483
+ measurer
4484
+ );
4485
+ annotations.push(
4486
+ buildAnchorCenteredTextAnnotation({
4487
+ ownerId: `${swimlane.id}.${lane.id}`,
4488
+ surfaceKind: "swimlane-label",
4489
+ layout: layout2,
4490
+ anchor: labelBox
4491
+ })
4492
+ );
2874
4493
  }
2875
- if (output.length === 0) {
2876
- output.push(createLine("", 0, 0, 0, 0));
4494
+ }
4495
+ return annotations;
4496
+ }
4497
+ function coordinateEdgeTextAnnotations(edges, textMeasurer) {
4498
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
4499
+ const annotations = [];
4500
+ for (const edge of edges) {
4501
+ if (edge.label?.text === void 0) {
4502
+ continue;
2877
4503
  }
2878
- return output;
4504
+ const layout2 = fitLabel(
4505
+ edge.label.text,
4506
+ {
4507
+ font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
4508
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
4509
+ minSize: { width: 0, height: 0 },
4510
+ maxWidth: 200
4511
+ },
4512
+ measurer
4513
+ );
4514
+ annotations.push(
4515
+ buildCenteredTextAnnotation({
4516
+ ownerId: edge.id,
4517
+ surfaceKind: "edge-label",
4518
+ layout: layout2,
4519
+ center: edgeLabelAnchor(edge, layout2, edges)
4520
+ })
4521
+ );
2879
4522
  }
2880
- };
2881
- function getCharacterWidth(style2) {
2882
- const letterSpacing = style2.letterSpacing ?? 0;
2883
- return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
4523
+ return annotations;
2884
4524
  }
2885
- function createLine(text, width, segmentIndex, start, end) {
4525
+ function coordinateFrameTextAnnotation(frame, textMeasurer) {
4526
+ const layout2 = fitLabel(
4527
+ frame.titleTab,
4528
+ {
4529
+ font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
4530
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
4531
+ minSize: { width: 0, height: 0 },
4532
+ maxWidth: frame.titleBox.width
4533
+ },
4534
+ textMeasurer ?? createDefaultTextMeasurer()
4535
+ );
4536
+ return buildAnchorCenteredTextAnnotation({
4537
+ ownerId: frame.kind,
4538
+ surfaceKind: "frame-title",
4539
+ layout: layout2,
4540
+ anchor: frame.titleBox
4541
+ });
4542
+ }
4543
+ function buildTextAnnotation(input) {
2886
4544
  return {
2887
- text,
2888
- width,
2889
- start: {
2890
- segmentIndex,
2891
- graphemeIndex: start
4545
+ text: input.layout.text,
4546
+ ownerId: input.ownerId,
4547
+ surfaceKind: input.surfaceKind,
4548
+ ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
4549
+ box: {
4550
+ x: input.anchor.x + input.layout.box.x,
4551
+ y: input.anchor.y + input.layout.box.y,
4552
+ width: input.layout.box.width,
4553
+ height: input.layout.box.height
2892
4554
  },
2893
- end: {
2894
- segmentIndex,
2895
- graphemeIndex: end
2896
- }
4555
+ anchor: input.anchor,
4556
+ paddings: input.layout.padding,
4557
+ lines: input.layout.lines,
4558
+ fontSize: input.layout.font.fontSize,
4559
+ textBackend: input.layout.textBackend
2897
4560
  };
2898
4561
  }
2899
- function assertFinitePositiveLineHeight(lineHeight) {
2900
- if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2901
- throw new TypeError("lineHeight must be finite and positive");
2902
- }
4562
+ function buildAnchorCenteredTextAnnotation(input) {
4563
+ return buildCenteredTextAnnotation({
4564
+ ownerId: input.ownerId,
4565
+ surfaceKind: input.surfaceKind,
4566
+ ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
4567
+ layout: input.layout,
4568
+ center: {
4569
+ x: input.anchor.x + input.anchor.width / 2,
4570
+ y: input.anchor.y + input.anchor.height / 2
4571
+ },
4572
+ anchor: input.anchor
4573
+ });
2903
4574
  }
2904
- var require2 = createRequire(import.meta.url);
2905
- function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
2906
- if (typeof globalThis.OffscreenCanvas === "function") {
2907
- return true;
4575
+ function buildCenteredTextAnnotation(input) {
4576
+ return {
4577
+ text: input.layout.text,
4578
+ ownerId: input.ownerId,
4579
+ surfaceKind: input.surfaceKind,
4580
+ ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
4581
+ box: {
4582
+ x: input.center.x - input.layout.box.width / 2,
4583
+ y: input.center.y - input.layout.box.height / 2,
4584
+ width: input.layout.box.width,
4585
+ height: input.layout.box.height
4586
+ },
4587
+ anchor: input.anchor ?? input.center,
4588
+ paddings: input.layout.padding,
4589
+ lines: input.layout.lines,
4590
+ fontSize: input.layout.font.fontSize,
4591
+ textBackend: input.layout.textBackend
4592
+ };
4593
+ }
4594
+ function reportTextAnnotationCollisions(annotations) {
4595
+ const diagnostics = [];
4596
+ const relevantAnnotations = annotations.filter(
4597
+ (annotation) => isExternallyPlacedText(annotation.surfaceKind)
4598
+ );
4599
+ for (let annotationIndex = 0; annotationIndex < relevantAnnotations.length; annotationIndex += 1) {
4600
+ const annotation = relevantAnnotations[annotationIndex];
4601
+ if (annotation === void 0) {
4602
+ continue;
4603
+ }
4604
+ for (let otherIndex = annotationIndex + 1; otherIndex < relevantAnnotations.length; otherIndex += 1) {
4605
+ const other = relevantAnnotations[otherIndex];
4606
+ if (other === void 0) {
4607
+ continue;
4608
+ }
4609
+ if (!intersectsAabb(annotation.box, other.box)) {
4610
+ continue;
4611
+ }
4612
+ if (annotation.ownerId === other.ownerId && annotation.surfaceKind === other.surfaceKind) {
4613
+ continue;
4614
+ }
4615
+ diagnostics.push({
4616
+ severity: "warning",
4617
+ code: "constraints.overlap.unresolved",
4618
+ message: `Text surface ${annotation.surfaceKind} for ${annotation.ownerId} overlaps text surface ${other.surfaceKind} for ${other.ownerId}.`,
4619
+ path: ["textAnnotations", annotation.surfaceKind, annotation.ownerId],
4620
+ detail: compactDetail({
4621
+ textSurfaceKind: annotation.surfaceKind,
4622
+ ownerId: annotation.ownerId,
4623
+ conflictingObjectId: other.ownerId,
4624
+ conflictingObjectKind: other.surfaceKind,
4625
+ surfaceIndex: annotation.surfaceIndex,
4626
+ otherSurfaceKind: other.surfaceKind,
4627
+ otherSurfaceIndex: other.surfaceIndex,
4628
+ textBackend: annotation.textBackend
4629
+ })
4630
+ });
4631
+ }
2908
4632
  }
2909
- try {
2910
- const canvasModule = loadNodeCanvasModule();
2911
- const { createCanvas } = canvasModule;
2912
- const NodeOffscreenCanvas = class {
2913
- canvas;
2914
- constructor(width, height) {
2915
- this.canvas = createCanvas(width, height);
4633
+ return diagnostics;
4634
+ }
4635
+ function reportRouteTextClearance(edges, annotations) {
4636
+ const diagnostics = [];
4637
+ const relevantAnnotations = annotations.filter(isRouteClearanceText);
4638
+ for (const edge of edges) {
4639
+ const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
4640
+ for (const annotation of relevantAnnotations) {
4641
+ if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
4642
+ continue;
2916
4643
  }
2917
- getContext(contextId) {
2918
- return contextId === "2d" ? this.canvas.getContext("2d") : null;
4644
+ if (!routeIntersectsTextBox(edge.points, annotation.box)) {
4645
+ continue;
2919
4646
  }
2920
- };
2921
- globalThis.OffscreenCanvas = NodeOffscreenCanvas;
2922
- return true;
2923
- } catch {
4647
+ diagnostics.push({
4648
+ severity: "warning",
4649
+ code: "routing.text-clearance.unresolved",
4650
+ message: `Edge ${edge.id} intersects solved text surface ${annotation.surfaceKind} for ${annotation.ownerId}.`,
4651
+ path: ["edges", edge.id],
4652
+ detail: compactDetail({
4653
+ edgeId: edge.id,
4654
+ textSurfaceKind: annotation.surfaceKind,
4655
+ conflictingObjectId: annotation.ownerId,
4656
+ surfaceIndex: annotation.surfaceIndex,
4657
+ textBackend: annotation.textBackend
4658
+ })
4659
+ });
4660
+ }
4661
+ }
4662
+ return diagnostics;
4663
+ }
4664
+ function isPreRouteTextObstacle(annotation) {
4665
+ if (annotation.surfaceKind === "edge-label") {
2924
4666
  return false;
2925
4667
  }
4668
+ return isRouteClearanceText(annotation);
2926
4669
  }
2927
- function loadDefaultNodeCanvasModule() {
2928
- return require2("@napi-rs/canvas");
4670
+ function isRouteClearanceText(annotation) {
4671
+ switch (annotation.surfaceKind) {
4672
+ case "port-label":
4673
+ case "edge-label":
4674
+ case "swimlane-label":
4675
+ case "frame-title":
4676
+ return true;
4677
+ case "node-label":
4678
+ case "group-label":
4679
+ case "compartment-row":
4680
+ return textExtendsOutsideAnchor(annotation);
4681
+ }
2929
4682
  }
2930
- var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
2931
- function isPretextRuntimeAvailable() {
2932
- return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
4683
+ function textExtendsOutsideAnchor(annotation) {
4684
+ if (!("width" in annotation.anchor)) {
4685
+ return true;
4686
+ }
4687
+ const epsilon = 1e-3;
4688
+ return annotation.box.x < annotation.anchor.x - epsilon || annotation.box.y < annotation.anchor.y - epsilon || annotation.box.x + annotation.box.width > annotation.anchor.x + annotation.anchor.width + epsilon || annotation.box.y + annotation.box.height > annotation.anchor.y + annotation.anchor.height + epsilon;
2933
4689
  }
2934
- var PretextTextMeasurer = class {
2935
- prepare(text, style2) {
2936
- if (!isPretextRuntimeAvailable()) {
2937
- throw new TypeError(RUNTIME_UNAVAILABLE);
4690
+ function routeIntersectsTextBox(points, box) {
4691
+ for (let index = 0; index < points.length - 1; index += 1) {
4692
+ const start = points[index];
4693
+ const end = points[index + 1];
4694
+ if (start === void 0 || end === void 0) {
4695
+ continue;
2938
4696
  }
2939
- validateTextStyle(style2);
2940
- const font = toCanvasFont(style2);
2941
- const options = {
2942
- ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
2943
- ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
2944
- ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
2945
- };
2946
- const prepared = prepareWithSegments(text, font, options);
2947
- return {
2948
- text,
2949
- font,
2950
- style: { ...style2 },
2951
- backend: "pretext",
2952
- pretextPrepared: prepared
2953
- };
2954
- }
2955
- layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
2956
- assertFiniteNonNegative(maxWidth, "maxWidth");
2957
- if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2958
- throw new TypeError("lineHeight must be finite and positive");
4697
+ if (segmentIntersectsBox2(start, end, box)) {
4698
+ return true;
2959
4699
  }
2960
- const result = layoutWithLines(
2961
- toInternalPrepared(prepared),
2962
- maxWidth,
2963
- lineHeight
2964
- );
2965
- const width = result.lines.reduce(
2966
- (current, line) => Math.max(current, line.width),
2967
- 0
2968
- );
2969
- return {
2970
- width,
2971
- height: result.height,
2972
- lineHeight,
2973
- lineCount: result.lineCount,
2974
- lines: result.lines.map((line) => ({
2975
- text: line.text,
2976
- width: line.width,
2977
- start: {
2978
- segmentIndex: line.start.segmentIndex,
2979
- graphemeIndex: line.start.graphemeIndex
2980
- },
2981
- end: {
2982
- segmentIndex: line.end.segmentIndex,
2983
- graphemeIndex: line.end.graphemeIndex
2984
- }
2985
- })),
2986
- diagnostics: []
2987
- };
2988
4700
  }
2989
- naturalWidth(prepared) {
2990
- return measureNaturalWidth(toInternalPrepared(prepared));
4701
+ return false;
4702
+ }
4703
+ function segmentIntersectsBox2(start, end, box) {
4704
+ const left = box.x;
4705
+ const right = box.x + box.width;
4706
+ const top = box.y;
4707
+ const bottom = box.y + box.height;
4708
+ if (pointInsideBox2(start, box) || pointInsideBox2(end, box)) {
4709
+ return true;
2991
4710
  }
2992
- };
2993
- function toInternalPrepared(prepared) {
2994
- if (prepared.backend !== "pretext" || !("pretextPrepared" in prepared)) {
2995
- throw new TypeError("prepared text was not created by PretextTextMeasurer");
4711
+ if (start.x === end.x) {
4712
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
2996
4713
  }
2997
- return prepared.pretextPrepared;
4714
+ if (start.y === end.y) {
4715
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4716
+ }
4717
+ return segmentIntersectsBoxEdge2(start, end, left, top, right, top) || segmentIntersectsBoxEdge2(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge2(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge2(start, end, left, bottom, left, top);
2998
4718
  }
2999
-
3000
- // src/text/default.ts
3001
- function createDefaultTextMeasurer(options = {}) {
3002
- const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
3003
- installRuntime();
3004
- return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
4719
+ function pointInsideBox2(point2, box) {
4720
+ return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
3005
4721
  }
3006
-
3007
- // src/labels/fit.ts
3008
- function fitLabel(text, options, measurer) {
3009
- return computeLabelLayout(text, options, measurer);
4722
+ function rangesOverlap2(a, b, min, max) {
4723
+ const low = Math.min(a, b);
4724
+ const high = Math.max(a, b);
4725
+ return high > min && low < max;
3010
4726
  }
3011
- function computeLabelLayout(text, options, measurer) {
3012
- const padding = normalizeInsets(options.padding);
3013
- const minSize = normalizeMinSize2(options.minSize);
3014
- const lineHeight = resolveLineHeight(options.font);
3015
- const maxWidth = normalizeMaxWidth(options.maxWidth);
3016
- const prepared = measurer.prepare(text, options.font);
3017
- const naturalTextWidth = measurer.naturalWidth(prepared);
3018
- const contentMaxWidth = maxWidth === void 0 ? naturalTextWidth : Math.max(0, maxWidth - padding.left - padding.right);
3019
- const textLayout = measurer.layout(prepared, contentMaxWidth, lineHeight);
3020
- const naturalSize = {
3021
- width: naturalTextWidth,
3022
- height: textLayout.height
3023
- };
3024
- const contentWidth = Math.max(
3025
- textLayout.width,
3026
- minContentWidth(minSize, padding)
3027
- );
3028
- const contentHeight = Math.max(
3029
- textLayout.height,
3030
- minContentHeight(minSize, padding)
4727
+ function segmentIntersectsBoxEdge2(start, end, x1, y1, x2, y2) {
4728
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4729
+ if (denominator === 0) {
4730
+ return false;
4731
+ }
4732
+ const t = ((x1 - start.x) * (y2 - y1) - (y1 - start.y) * (x2 - x1)) / denominator;
4733
+ const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
4734
+ return t > 0 && t < 1 && u > 0 && u < 1;
4735
+ }
4736
+ function compactDetail(detail) {
4737
+ return Object.fromEntries(
4738
+ Object.entries(detail).filter(
4739
+ (entry) => entry[1] !== void 0
4740
+ )
3031
4741
  );
3032
- const idealWidth = contentWidth + padding.left + padding.right;
3033
- const idealHeight = contentHeight + padding.top + padding.bottom;
3034
- const fittedSize = {
3035
- width: maxWidth === void 0 ? idealWidth : Math.min(maxWidth, idealWidth),
3036
- height: idealHeight
3037
- };
3038
- const box = {
3039
- x: 0,
3040
- y: 0,
3041
- width: fittedSize.width,
3042
- height: fittedSize.height
3043
- };
3044
- const contentBox2 = {
3045
- x: padding.left,
3046
- y: padding.top,
3047
- width: Math.max(0, box.width - padding.left - padding.right),
3048
- height: Math.max(0, box.height - padding.top - padding.bottom)
3049
- };
3050
- const overflow = {
3051
- horizontal: textLayout.width > contentBox2.width,
3052
- vertical: textLayout.height > contentBox2.height || diagnosedHeightConstraintOverflow(textLayout.height, padding, minSize),
3053
- truncated: options.overflow === "truncate" && textLayout.width > contentBox2.width
3054
- };
3055
- const diagnostics = buildDiagnostics(overflow, options.overflow);
3056
- return {
3057
- text,
3058
- box,
3059
- contentBox: contentBox2,
3060
- naturalSize,
3061
- fittedSize,
3062
- padding,
3063
- font: { ...options.font },
3064
- textBackend: prepared.backend,
3065
- lineHeight,
3066
- lines: buildLines(textLayout, contentBox2, lineHeight),
3067
- overflow,
3068
- diagnostics
4742
+ }
4743
+ function isExternallyPlacedText(surfaceKind) {
4744
+ switch (surfaceKind) {
4745
+ case "port-label":
4746
+ return true;
4747
+ case "edge-label":
4748
+ return false;
4749
+ case "swimlane-label":
4750
+ return true;
4751
+ case "frame-title":
4752
+ return true;
4753
+ case "node-label":
4754
+ case "group-label":
4755
+ case "compartment-row":
4756
+ return false;
4757
+ }
4758
+ }
4759
+ function fallbackLabelLayout(text) {
4760
+ const width = Math.max(0, text.length * 7);
4761
+ return {
4762
+ text,
4763
+ box: { x: 0, y: 0, width, height: 14 },
4764
+ contentBox: { x: 0, y: 0, width, height: 14 },
4765
+ naturalSize: { width, height: 14 },
4766
+ fittedSize: { width, height: 14 },
4767
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
4768
+ font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
4769
+ lineHeight: 14,
4770
+ lines: [
4771
+ {
4772
+ text,
4773
+ box: { x: 0, y: 0, width, height: 14 },
4774
+ baselineY: 11.2,
4775
+ width,
4776
+ lineIndex: 0
4777
+ }
4778
+ ],
4779
+ overflow: { horizontal: false, vertical: false, truncated: false },
4780
+ diagnostics: []
3069
4781
  };
3070
4782
  }
3071
- function buildLines(textLayout, contentBox2, lineHeight) {
3072
- return textLayout.lines.map((line, lineIndex) => ({
3073
- text: line.text,
3074
- box: {
3075
- x: contentBox2.x,
3076
- y: contentBox2.y + lineIndex * lineHeight,
3077
- width: line.width,
3078
- height: lineHeight
3079
- },
3080
- baselineY: contentBox2.y + lineIndex * lineHeight + lineHeight * 0.8,
3081
- width: line.width,
3082
- lineIndex,
3083
- sourceStart: { ...line.start },
3084
- sourceEnd: { ...line.end }
3085
- }));
4783
+ function edgeLabelAnchor(edge, layout2, edges) {
4784
+ const placement = labelPlacementOnPolyline2(edge.points);
4785
+ if (placement === void 0) {
4786
+ return { x: 0, y: 0 };
4787
+ }
4788
+ for (const candidate of edgeLabelAnchorCandidates(edge.points, placement)) {
4789
+ const labelBox = {
4790
+ x: candidate.x - layout2.box.width / 2,
4791
+ y: candidate.y - layout2.box.height / 2,
4792
+ width: layout2.box.width,
4793
+ height: layout2.box.height
4794
+ };
4795
+ if (routeIntersectsTextBox(edge.points, labelBox)) {
4796
+ continue;
4797
+ }
4798
+ const crossesOtherRoute = edges.some(
4799
+ (other) => other.id !== edge.id && routeIntersectsTextBox(other.points, labelBox)
4800
+ );
4801
+ if (!crossesOtherRoute) {
4802
+ return candidate;
4803
+ }
4804
+ }
4805
+ return placement;
3086
4806
  }
3087
- function normalizeMinSize2(minSize = {}) {
3088
- if (minSize.width !== void 0) {
3089
- assertFiniteNonNegative(minSize.width, "minSize.width");
4807
+ function edgeLabelAnchorCandidates(points, placement) {
4808
+ const segment = labelSegmentOnPolyline(points);
4809
+ if (segment === void 0) {
4810
+ return [placement];
3090
4811
  }
3091
- if (minSize.height !== void 0) {
3092
- assertFiniteNonNegative(minSize.height, "minSize.height");
4812
+ if (segment.start.y === segment.end.y) {
4813
+ return [
4814
+ placement,
4815
+ { x: placement.x, y: placement.y - EDGE_LABEL_CLEARANCE },
4816
+ { x: placement.x, y: placement.y + EDGE_LABEL_CLEARANCE },
4817
+ { x: placement.x, y: placement.y - EDGE_LABEL_CLEARANCE * 2 },
4818
+ { x: placement.x, y: placement.y + EDGE_LABEL_CLEARANCE * 2 }
4819
+ ];
3093
4820
  }
3094
- return { ...minSize };
4821
+ if (segment.start.x === segment.end.x) {
4822
+ return [
4823
+ placement,
4824
+ { x: placement.x + EDGE_LABEL_CLEARANCE, y: placement.y },
4825
+ { x: placement.x - EDGE_LABEL_CLEARANCE, y: placement.y },
4826
+ { x: placement.x + EDGE_LABEL_CLEARANCE * 2, y: placement.y },
4827
+ { x: placement.x - EDGE_LABEL_CLEARANCE * 2, y: placement.y }
4828
+ ];
4829
+ }
4830
+ return [placement];
3095
4831
  }
3096
- function normalizeMaxWidth(maxWidth) {
3097
- if (maxWidth === void 0) {
4832
+ function labelPlacementOnPolyline2(points) {
4833
+ return labelSegmentOnPolyline(points)?.placement;
4834
+ }
4835
+ function labelSegmentOnPolyline(points) {
4836
+ const segments = nonZeroSegments2(points);
4837
+ const totalLength = segments.reduce(
4838
+ (sum, segment) => sum + segment.length,
4839
+ 0
4840
+ );
4841
+ if (totalLength <= 0) {
3098
4842
  return void 0;
3099
4843
  }
3100
- assertFiniteNonNegative(maxWidth, "maxWidth");
3101
- return maxWidth;
3102
- }
3103
- function minContentWidth(minSize, padding) {
3104
- return Math.max(0, (minSize.width ?? 0) - padding.left - padding.right);
4844
+ let remaining = totalLength / 2;
4845
+ for (const segment of segments) {
4846
+ if (remaining <= segment.length) {
4847
+ const ratio = remaining / segment.length;
4848
+ const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
4849
+ const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
4850
+ const offset2 = labelOffset2(segment);
4851
+ return {
4852
+ start: segment.start,
4853
+ end: segment.end,
4854
+ placement: { x: x + offset2.x, y: y + offset2.y }
4855
+ };
4856
+ }
4857
+ remaining -= segment.length;
4858
+ }
4859
+ const last = segments.at(-1);
4860
+ if (last === void 0) {
4861
+ return void 0;
4862
+ }
4863
+ const offset = labelOffset2(last);
4864
+ return {
4865
+ start: last.start,
4866
+ end: last.end,
4867
+ placement: { x: last.end.x + offset.x, y: last.end.y + offset.y }
4868
+ };
3105
4869
  }
3106
- function minContentHeight(minSize, padding) {
3107
- return Math.max(0, (minSize.height ?? 0) - padding.top - padding.bottom);
4870
+ function nonZeroSegments2(points) {
4871
+ const segments = [];
4872
+ for (let index = 0; index < points.length - 1; index += 1) {
4873
+ const start = points[index];
4874
+ const end = points[index + 1];
4875
+ if (start === void 0 || end === void 0) {
4876
+ continue;
4877
+ }
4878
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
4879
+ if (length > 0) {
4880
+ segments.push({ start, end, length });
4881
+ }
4882
+ }
4883
+ return segments;
3108
4884
  }
3109
- function diagnosedHeightConstraintOverflow(textHeight, padding, minSize) {
3110
- return minSize.height !== void 0 && textHeight + padding.top + padding.bottom > minSize.height;
4885
+ function labelOffset2(segment) {
4886
+ const offset = 10;
4887
+ const dx = segment.end.x - segment.start.x;
4888
+ const dy = segment.end.y - segment.start.y;
4889
+ return {
4890
+ x: -dy / segment.length * offset,
4891
+ y: dx / segment.length * offset
4892
+ };
3111
4893
  }
3112
- function buildDiagnostics(overflow, mode = "allow") {
3113
- if (mode !== "diagnose") {
4894
+ function compartmentRows(node) {
4895
+ const compartments2 = node.compartments;
4896
+ if (compartments2 === void 0) {
3114
4897
  return [];
3115
4898
  }
3116
- const diagnostics = [];
3117
- if (overflow.horizontal) {
3118
- diagnostics.push({
3119
- severity: "warning",
3120
- code: "label.overflow.horizontal",
3121
- message: "Label text exceeds the fitted content width."
3122
- });
3123
- }
3124
- if (overflow.vertical) {
3125
- diagnostics.push({
3126
- severity: "warning",
3127
- code: "label.overflow.vertical",
3128
- message: "Label text exceeds the fitted content height."
3129
- });
4899
+ return [
4900
+ ...compartments2.stereotype === void 0 ? [] : [compartments2.stereotype],
4901
+ ...compartments2.name === void 0 ? [node.label?.text ?? node.id] : [compartments2.name],
4902
+ ...compartments2.properties ?? [],
4903
+ ...compartments2.constraints ?? []
4904
+ ];
4905
+ }
4906
+ function portGeometry(nodeGeometry, port) {
4907
+ if (port === void 0) {
4908
+ return nodeGeometry;
3130
4909
  }
3131
- return diagnostics;
4910
+ return {
4911
+ ...nodeGeometry,
4912
+ box: port.box,
4913
+ center: port.anchor,
4914
+ anchors: nodeGeometry.anchors.map((anchor) => ({
4915
+ name: anchor.name,
4916
+ point: port.anchor
4917
+ })),
4918
+ obstacleBox: port.box
4919
+ };
4920
+ }
4921
+ function stableById(items) {
4922
+ return [...items].sort((a, b) => a.id.localeCompare(b.id));
4923
+ }
4924
+ function stableByConstraintId(items) {
4925
+ return [...items].sort(
4926
+ (a, b) => `${a.id ?? a.kind}`.localeCompare(`${b.id ?? b.kind}`)
4927
+ );
4928
+ }
4929
+ function groupReferenceMissing(groupId, referenceKind, id) {
4930
+ return {
4931
+ severity: "error",
4932
+ code: "solver.group-reference.missing",
4933
+ message: `Group ${groupId} references a missing ${referenceKind}.`,
4934
+ path: ["groups", groupId],
4935
+ detail: id === void 0 ? { groupId } : { groupId, id }
4936
+ };
3132
4937
  }
3133
4938
 
3134
4939
  // src/dsl/normalize.ts
@@ -3147,6 +4952,9 @@ var DEFAULT_GROUP_PADDING = {
3147
4952
  };
3148
4953
  var DEFAULT_LABEL_MAX_WIDTH = 160;
3149
4954
  var DEFAULT_FONT = { fontFamily: "Arial", fontSize: 14, lineHeight: 18 };
4955
+ var DEFAULT_MATRIX_CELL_SIZE2 = { width: 120, height: 36 };
4956
+ var DEFAULT_TABLE_CELL_SIZE2 = { width: 128, height: 34 };
4957
+ var DEFAULT_PANEL_ITEM_HEIGHT2 = 28;
3150
4958
  function normalizeDiagramDsl(dslValue, options = {}) {
3151
4959
  const dsl = dslValue;
3152
4960
  const diagnostics = validateReferences(dsl);
@@ -3160,6 +4968,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
3160
4968
  const routeKind = dsl.routing?.kind ?? "orthogonal";
3161
4969
  const portShifting = normalizePortShifting(dsl.routing?.portShifting);
3162
4970
  const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
4971
+ const matrices = normalizeMatrices(dsl);
4972
+ const tables = normalizeTables(dsl);
4973
+ const evidencePanels = normalizeEvidencePanels(dsl);
3163
4974
  const diagram = {
3164
4975
  id: options.id ?? dsl.id ?? "diagram",
3165
4976
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -3168,6 +4979,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
3168
4979
  edges: normalizeEdges(dsl),
3169
4980
  groups: normalizeGroups(dsl, measurer),
3170
4981
  swimlanes: normalizeSwimlanes(dsl),
4982
+ ...matrices === void 0 ? {} : { matrices },
4983
+ ...tables === void 0 ? {} : { tables },
4984
+ ...evidencePanels === void 0 ? {} : { evidencePanels },
3171
4985
  constraints: normalizeConstraints(dsl),
3172
4986
  diagnostics: [],
3173
4987
  ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
@@ -3237,7 +5051,7 @@ function compartmentHeight(value) {
3237
5051
  );
3238
5052
  }
3239
5053
  function compartmentNaturalWidth(id, label, value, measurer) {
3240
- const rows = compartmentRows(id, label, value);
5054
+ const rows = compartmentRows2(id, label, value);
3241
5055
  const maxRowWidth = rows.reduce((width, row) => {
3242
5056
  const prepared = measurer.prepare(row, DEFAULT_FONT);
3243
5057
  return Math.max(width, measurer.naturalWidth(prepared));
@@ -3246,7 +5060,7 @@ function compartmentNaturalWidth(id, label, value, measurer) {
3246
5060
  maxRowWidth + DEFAULT_NODE_PADDING.left + DEFAULT_NODE_PADDING.right
3247
5061
  );
3248
5062
  }
3249
- function compartmentRows(id, label, value) {
5063
+ function compartmentRows2(id, label, value) {
3250
5064
  return [
3251
5065
  ...value.stereotype === void 0 ? [] : [value.stereotype],
3252
5066
  value.name ?? label?.text ?? id,
@@ -3366,6 +5180,102 @@ function normalizeSwimlanes(dsl) {
3366
5180
  };
3367
5181
  });
3368
5182
  }
5183
+ function normalizeMatrices(dsl) {
5184
+ if (dsl.matrices === void 0) {
5185
+ return void 0;
5186
+ }
5187
+ return dsl.matrices.map((matrix) => ({
5188
+ id: matrix.id,
5189
+ rows: [...matrix.rows],
5190
+ cols: [...matrix.cols],
5191
+ cells: matrix.cells.map((row) => row.map(cell)),
5192
+ ...matrix.position === void 0 ? {} : { position: point(matrix.position) },
5193
+ size: matrix.size ?? {
5194
+ width: defaultMatrixRowHeaderWidth2(matrix) + Math.max(1, matrix.cols.length) * DEFAULT_MATRIX_CELL_SIZE2.width,
5195
+ height: Math.max(1, matrix.rows.length + 1) * DEFAULT_MATRIX_CELL_SIZE2.height
5196
+ },
5197
+ ...matrix.style === void 0 ? {} : { style: style(matrix.style) }
5198
+ }));
5199
+ }
5200
+ function defaultMatrixRowHeaderWidth2(matrix) {
5201
+ return matrix.rows.length === 0 ? 0 : Math.min(96, DEFAULT_MATRIX_CELL_SIZE2.width);
5202
+ }
5203
+ function normalizeTables(dsl) {
5204
+ if (dsl.tables === void 0) {
5205
+ return void 0;
5206
+ }
5207
+ return dsl.tables.map((table) => ({
5208
+ id: table.id,
5209
+ columns: table.columns.map(tableColumn),
5210
+ rows: table.rows.map(tableRow),
5211
+ ...table.position === void 0 ? {} : { position: point(table.position) },
5212
+ size: table.size ?? {
5213
+ width: Math.max(1, table.columns.length) * DEFAULT_TABLE_CELL_SIZE2.width,
5214
+ height: Math.max(1, table.rows.length + 1) * DEFAULT_TABLE_CELL_SIZE2.height
5215
+ },
5216
+ ...table.style === void 0 ? {} : { style: style(table.style) }
5217
+ }));
5218
+ }
5219
+ function normalizeEvidencePanels(dsl) {
5220
+ if (dsl.evidencePanels === void 0) {
5221
+ return void 0;
5222
+ }
5223
+ return dsl.evidencePanels.map((panel) => ({
5224
+ id: panel.id,
5225
+ kind: panel.kind,
5226
+ items: panel.items.map(panelItem),
5227
+ ...panel.position === void 0 ? {} : { position: point(panel.position) },
5228
+ size: panel.size ?? {
5229
+ width: 320,
5230
+ height: Math.max(1, panel.items.length) * DEFAULT_PANEL_ITEM_HEIGHT2
5231
+ },
5232
+ ...panel.style === void 0 ? {} : { style: style(panel.style) }
5233
+ }));
5234
+ }
5235
+ function cell(value) {
5236
+ if (typeof value === "string") {
5237
+ return { text: value };
5238
+ }
5239
+ return {
5240
+ text: value.text,
5241
+ ...value.style === void 0 && value.fill === void 0 && value.stroke === void 0 ? {} : {
5242
+ style: style({
5243
+ ...value.style,
5244
+ ...value.fill === void 0 ? {} : { fill: value.fill },
5245
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
5246
+ })
5247
+ }
5248
+ };
5249
+ }
5250
+ function tableColumn(value) {
5251
+ return {
5252
+ id: value.id,
5253
+ label: toLabel(value.label) ?? { text: value.id }
5254
+ };
5255
+ }
5256
+ function tableRow(value) {
5257
+ return {
5258
+ id: value.id,
5259
+ cells: Object.fromEntries(
5260
+ Object.keys(value.cells).map((columnId) => [
5261
+ columnId,
5262
+ cell(value.cells[columnId] ?? "")
5263
+ ])
5264
+ )
5265
+ };
5266
+ }
5267
+ function panelItem(value) {
5268
+ if (typeof value === "string") {
5269
+ return { label: { text: value } };
5270
+ }
5271
+ const detail = toLabel(value.detail);
5272
+ return {
5273
+ ...value.id === void 0 ? {} : { id: value.id },
5274
+ label: toLabel(value.label) ?? { text: "" },
5275
+ ...detail === void 0 ? {} : { detail },
5276
+ ...value.style === void 0 ? {} : { style: style(value.style) }
5277
+ };
5278
+ }
3369
5279
  function normalizeGroups(dsl, measurer) {
3370
5280
  return Object.keys(dsl.groups ?? {}).sort().map((id) => {
3371
5281
  const group = dsl.groups?.[id];
@@ -3698,6 +5608,15 @@ var styleSchema = z.object({
3698
5608
  fill: z.string().optional(),
3699
5609
  stroke: z.string().optional()
3700
5610
  });
5611
+ var blockCellSchema = z.union([
5612
+ z.string(),
5613
+ z.object({
5614
+ text: z.string(),
5615
+ fill: z.string().optional(),
5616
+ stroke: z.string().optional(),
5617
+ style: styleSchema.optional()
5618
+ })
5619
+ ]);
3701
5620
  var portSideSchema = z.enum(["top", "right", "bottom", "left"]);
3702
5621
  var portKindSchema = z.enum(["proxy", "flow"]);
3703
5622
  var portSchema = z.object({
@@ -3774,6 +5693,122 @@ var swimlaneSchema = z.object({
3774
5693
  })
3775
5694
  )
3776
5695
  });
5696
+ var matrixSchema = z.object({
5697
+ id: z.string(),
5698
+ rows: z.array(z.string()),
5699
+ cols: z.array(z.string()),
5700
+ cells: z.array(z.array(blockCellSchema)),
5701
+ position: pointSchema.optional(),
5702
+ size: z.object({
5703
+ width: nonNegativeNumberSchema,
5704
+ height: nonNegativeNumberSchema
5705
+ }).optional(),
5706
+ style: styleSchema.optional()
5707
+ }).superRefine((matrix, context) => {
5708
+ checkDuplicateValues("matrix row", matrix.rows, context, (index) => [
5709
+ "rows",
5710
+ index
5711
+ ]);
5712
+ checkDuplicateValues("matrix column", matrix.cols, context, (index) => [
5713
+ "cols",
5714
+ index
5715
+ ]);
5716
+ if (matrix.cells.length !== matrix.rows.length) {
5717
+ context.addIssue({
5718
+ code: "custom",
5719
+ message: `Matrix cells must contain exactly ${matrix.rows.length} row(s).`,
5720
+ path: ["cells"]
5721
+ });
5722
+ }
5723
+ matrix.cells.forEach((row, rowIndex) => {
5724
+ if (row.length !== matrix.cols.length) {
5725
+ context.addIssue({
5726
+ code: "custom",
5727
+ message: `Matrix cell row must contain exactly ${matrix.cols.length} column(s).`,
5728
+ path: ["cells", rowIndex]
5729
+ });
5730
+ }
5731
+ });
5732
+ });
5733
+ var tableColumnSchema = z.object({
5734
+ id: z.string(),
5735
+ label: labelSchema
5736
+ });
5737
+ var tableRowSchema = z.object({
5738
+ id: z.string(),
5739
+ cells: z.record(z.string(), blockCellSchema)
5740
+ });
5741
+ var tableSchema = z.object({
5742
+ id: z.string(),
5743
+ columns: z.array(tableColumnSchema),
5744
+ rows: z.array(tableRowSchema),
5745
+ position: pointSchema.optional(),
5746
+ size: z.object({
5747
+ width: nonNegativeNumberSchema,
5748
+ height: nonNegativeNumberSchema
5749
+ }).optional(),
5750
+ style: styleSchema.optional()
5751
+ }).superRefine((table, context) => {
5752
+ checkDuplicateIds("column", table.columns, context, (index) => [
5753
+ "columns",
5754
+ index,
5755
+ "id"
5756
+ ]);
5757
+ checkDuplicateIds("row", table.rows, context, (index) => [
5758
+ "rows",
5759
+ index,
5760
+ "id"
5761
+ ]);
5762
+ const columnIds = new Set(table.columns.map((column) => column.id));
5763
+ table.rows.forEach((row, rowIndex) => {
5764
+ for (const columnId of Object.keys(row.cells)) {
5765
+ if (!columnIds.has(columnId)) {
5766
+ context.addIssue({
5767
+ code: "custom",
5768
+ message: `Table row cell references undeclared column "${columnId}".`,
5769
+ path: ["rows", rowIndex, "cells", columnId]
5770
+ });
5771
+ }
5772
+ }
5773
+ });
5774
+ });
5775
+ var panelItemSchema = z.union([
5776
+ z.string(),
5777
+ z.object({
5778
+ id: z.string().optional(),
5779
+ label: labelSchema,
5780
+ detail: labelSchema.optional(),
5781
+ style: styleSchema.optional()
5782
+ })
5783
+ ]);
5784
+ var evidencePanelSchema = z.object({
5785
+ id: z.string(),
5786
+ kind: z.enum(["legend", "rule", "note", "verification"]),
5787
+ items: z.array(panelItemSchema),
5788
+ position: pointSchema.optional(),
5789
+ size: z.object({
5790
+ width: nonNegativeNumberSchema,
5791
+ height: nonNegativeNumberSchema
5792
+ }).optional(),
5793
+ style: styleSchema.optional()
5794
+ }).superRefine((panel, context) => {
5795
+ const firstIndexByItemId = /* @__PURE__ */ new Map();
5796
+ panel.items.forEach((item, index) => {
5797
+ if (typeof item === "string" || item.id === void 0) {
5798
+ return;
5799
+ }
5800
+ const firstIndex = firstIndexByItemId.get(item.id);
5801
+ if (firstIndex === void 0) {
5802
+ firstIndexByItemId.set(item.id, index);
5803
+ return;
5804
+ }
5805
+ context.addIssue({
5806
+ code: "custom",
5807
+ message: `Duplicate evidence panel item id "${item.id}".`,
5808
+ path: ["items", index, "id"]
5809
+ });
5810
+ });
5811
+ });
3777
5812
  var exactPositionConstraintSchema = z.object({
3778
5813
  kind: z.literal("exact-position"),
3779
5814
  target: z.string().optional(),
@@ -3845,6 +5880,9 @@ var diagramDslSchema = z.object({
3845
5880
  edges: z.array(edgeSchema).optional(),
3846
5881
  groups: z.record(z.string(), groupSchema).optional(),
3847
5882
  swimlanes: z.record(z.string(), swimlaneSchema).optional(),
5883
+ matrices: z.array(matrixSchema).optional(),
5884
+ tables: z.array(tableSchema).optional(),
5885
+ evidencePanels: z.array(evidencePanelSchema).optional(),
3848
5886
  constraints: z.array(constraintSchema).optional(),
3849
5887
  frame: z.object({
3850
5888
  kind: z.string(),
@@ -3856,6 +5894,15 @@ var diagramDslSchema = z.object({
3856
5894
  output: z.object({
3857
5895
  format: outputFormatSchema.optional()
3858
5896
  }).optional()
5897
+ }).superRefine((diagram, context) => {
5898
+ checkDuplicateEvidenceBlockIds("matrices", diagram.matrices, context);
5899
+ checkDuplicateEvidenceBlockIds("tables", diagram.tables, context);
5900
+ checkDuplicateEvidenceBlockIds(
5901
+ "evidencePanels",
5902
+ diagram.evidencePanels,
5903
+ context
5904
+ );
5905
+ checkDuplicateEvidenceBlockIdsAcrossTypes(diagram, context);
3859
5906
  });
3860
5907
  function validateDiagramDsl(value) {
3861
5908
  const result = diagramDslSchema.safeParse(value);
@@ -3875,6 +5922,62 @@ function toDiagnosticPath(path) {
3875
5922
  (segment) => typeof segment === "string" || typeof segment === "number" ? [segment] : []
3876
5923
  );
3877
5924
  }
5925
+ function checkDuplicateEvidenceBlockIds(collection, blocks, context) {
5926
+ checkDuplicateIds(
5927
+ `evidence block id in ${collection}`,
5928
+ blocks ?? [],
5929
+ context,
5930
+ (index) => [collection, index, "id"]
5931
+ );
5932
+ }
5933
+ function checkDuplicateIds(label, items, context, pathForIndex) {
5934
+ const firstIndexById = /* @__PURE__ */ new Map();
5935
+ items.forEach((item, index) => {
5936
+ const firstIndex = firstIndexById.get(item.id);
5937
+ if (firstIndex === void 0) {
5938
+ firstIndexById.set(item.id, index);
5939
+ return;
5940
+ }
5941
+ context.addIssue({
5942
+ code: "custom",
5943
+ message: `Duplicate ${label} "${item.id}".`,
5944
+ path: pathForIndex(index)
5945
+ });
5946
+ });
5947
+ }
5948
+ function checkDuplicateValues(label, values, context, pathForIndex) {
5949
+ const firstIndexByValue = /* @__PURE__ */ new Map();
5950
+ values.forEach((value, index) => {
5951
+ const firstIndex = firstIndexByValue.get(value);
5952
+ if (firstIndex === void 0) {
5953
+ firstIndexByValue.set(value, index);
5954
+ return;
5955
+ }
5956
+ context.addIssue({
5957
+ code: "custom",
5958
+ message: `Duplicate ${label} "${value}".`,
5959
+ path: pathForIndex(index)
5960
+ });
5961
+ });
5962
+ }
5963
+ function checkDuplicateEvidenceBlockIdsAcrossTypes(diagram, context) {
5964
+ const firstById = /* @__PURE__ */ new Map();
5965
+ for (const collection of ["matrices", "tables", "evidencePanels"]) {
5966
+ const blocks = diagram[collection] ?? [];
5967
+ blocks.forEach((block, index) => {
5968
+ const first = firstById.get(block.id);
5969
+ if (first === void 0) {
5970
+ firstById.set(block.id, { collection, index });
5971
+ return;
5972
+ }
5973
+ context.addIssue({
5974
+ code: "custom",
5975
+ message: `Duplicate evidence block id "${block.id}" across ${first.collection} and ${collection}.`,
5976
+ path: [collection, index, "id"]
5977
+ });
5978
+ });
5979
+ }
5980
+ }
3878
5981
 
3879
5982
  // src/dsl/parse.ts
3880
5983
  var DEFAULT_DSL_MAX_BYTES = 1e6;
@@ -4050,7 +6153,8 @@ function renderDiagramDsl(source, options = {}) {
4050
6153
  }
4051
6154
  const solved = solveDiagram(normalized.diagram, {
4052
6155
  routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
4053
- ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
6156
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
6157
+ ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4054
6158
  });
4055
6159
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
4056
6160
  if (hasErrorDiagnostics2(solveDiagnostics)) {