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