@crazyhappyone/auto-graph 0.0.21 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -99,11 +99,21 @@ function applyLayoutConstraints(input) {
99
99
  const nodeById = new Map(input.nodes.map((node) => [node.id, node]));
100
100
  applyFixedPositionLocks(input.nodes, boxes, locks, diagnostics);
101
101
  applyExactPositions(input.constraints, boxes, locks, diagnostics, nodeById);
102
- applyContainment(input.constraints, boxes, locks, diagnostics);
102
+ applyContainment(input.constraints, boxes, locks, diagnostics, false);
103
103
  applyRelative(input.constraints, boxes, locks, diagnostics);
104
104
  applyAlign(input.constraints, boxes, locks, diagnostics);
105
105
  applyDistribute(input.constraints, boxes, locks, diagnostics);
106
- repairOverlaps(input, boxes, locks, diagnostics);
106
+ repairOverlaps(
107
+ input,
108
+ boxes,
109
+ locks,
110
+ diagnostics,
111
+ siblingOverlapKeys(input.constraints)
112
+ );
113
+ applyContainment(input.constraints, boxes, locks, diagnostics, true);
114
+ applyDistributeContained(input, boxes, locks, diagnostics);
115
+ reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
116
+ reportIntraContainerOverflow(input, boxes, diagnostics);
107
117
  return { boxes, locks, diagnostics };
108
118
  }
109
119
  function cloneValidBoxes(input, diagnostics) {
@@ -191,7 +201,7 @@ function applyExactPositions(constraints, boxes, locks, diagnostics, nodeById) {
191
201
  locks.set(targetId, { nodeId: targetId, source: "exact-position" });
192
202
  }
193
203
  }
194
- function applyContainment(constraints, boxes, locks, diagnostics) {
204
+ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow) {
195
205
  for (const constraint of constraints) {
196
206
  if (constraint.kind !== "containment") {
197
207
  continue;
@@ -213,21 +223,23 @@ function applyContainment(constraints, boxes, locks, diagnostics) {
213
223
  continue;
214
224
  }
215
225
  if (locks.has(childId)) {
216
- diagnostics.push({
217
- severity: "warning",
218
- code: "constraints.locked-target-not-moved",
219
- message: `Locked child ${childId} was not moved into containment.`,
220
- path: ["constraints", constraint.id ?? constraint.containerId],
221
- detail: { nodeId: childId }
222
- });
223
- if (!isInside(child, content)) {
226
+ if (!reportOverflow) {
224
227
  diagnostics.push({
225
- severity: "error",
226
- code: "constraints.containment.impossible",
227
- message: `Locked child ${childId} cannot fit inside ${constraint.containerId}.`,
228
+ severity: "warning",
229
+ code: "constraints.locked-target-not-moved",
230
+ message: `Locked child ${childId} was not moved into containment.`,
228
231
  path: ["constraints", constraint.id ?? constraint.containerId],
229
- detail: { nodeId: childId, containerId: constraint.containerId }
232
+ detail: { nodeId: childId }
230
233
  });
234
+ if (!isInside(child, content)) {
235
+ diagnostics.push({
236
+ severity: "error",
237
+ code: "constraints.containment.impossible",
238
+ message: `Locked child ${childId} cannot fit inside ${constraint.containerId}.`,
239
+ path: ["constraints", constraint.id ?? constraint.containerId],
240
+ detail: { nodeId: childId, containerId: constraint.containerId }
241
+ });
242
+ }
231
243
  }
232
244
  continue;
233
245
  }
@@ -242,6 +254,15 @@ function applyContainment(constraints, boxes, locks, diagnostics) {
242
254
  continue;
243
255
  }
244
256
  boxes.set(childId, next);
257
+ if (reportOverflow) {
258
+ diagnostics.push({
259
+ severity: "warning",
260
+ code: "containment_overflow",
261
+ message: `Child ${childId} was clamped back inside ${constraint.containerId} after constraint solving.`,
262
+ path: ["constraints", constraint.id ?? constraint.containerId],
263
+ detail: { nodeId: childId, containerId: constraint.containerId }
264
+ });
265
+ }
245
266
  }
246
267
  }
247
268
  }
@@ -317,37 +338,69 @@ function applyDistribute(constraints, boxes, locks, diagnostics) {
317
338
  }
318
339
  }
319
340
  }
320
- function repairOverlaps(input, boxes, locks, diagnostics) {
341
+ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
321
342
  const spacing = input.overlapSpacing ?? 40;
322
343
  const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
344
+ const secondaryAxis = axis === "x" ? "y" : "x";
345
+ const ignoredPairs = containmentOverlapKeys(input.constraints);
323
346
  const ids = [...boxes.keys()].sort();
324
- for (const firstId of ids) {
325
- for (const secondId of ids) {
326
- if (firstId >= secondId) {
327
- continue;
328
- }
329
- const first = boxes.get(firstId);
330
- const second = boxes.get(secondId);
331
- if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
332
- continue;
333
- }
334
- const firstLocked = locks.has(firstId);
335
- const secondLocked = locks.has(secondId);
336
- if (firstLocked === secondLocked) {
337
- continue;
347
+ for (let pass = 0; pass < 2; pass += 1) {
348
+ for (const firstId of ids) {
349
+ for (const secondId of ids) {
350
+ if (firstId >= secondId) {
351
+ continue;
352
+ }
353
+ if (ignoredPairs.has(overlapKey(firstId, secondId))) {
354
+ continue;
355
+ }
356
+ const first = boxes.get(firstId);
357
+ const second = boxes.get(secondId);
358
+ if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
359
+ continue;
360
+ }
361
+ const firstLocked = locks.has(firstId);
362
+ const secondLocked = locks.has(secondId);
363
+ if (firstLocked && secondLocked) {
364
+ continue;
365
+ }
366
+ const movingId = firstLocked ? secondId : secondLocked ? firstId : secondId;
367
+ const moving = movingId === firstId ? first : second;
368
+ const fixed = movingId === firstId ? second : first;
369
+ const repairAxis = firstLocked === secondLocked && pass === 0 ? secondaryAxis : axis;
370
+ const pairKey = overlapKey(firstId, secondId);
371
+ const effectiveSpacing = siblingPairs.has(pairKey) ? Math.max(spacing, input.minSiblingGap ?? 0) : spacing;
372
+ const moved = movePastOverlap(
373
+ moving,
374
+ fixed,
375
+ repairAxis,
376
+ effectiveSpacing
377
+ );
378
+ boxes.set(movingId, moved);
338
379
  }
339
- const movingId = firstLocked ? secondId : firstId;
340
- const moving = firstLocked ? second : first;
341
- const fixed = firstLocked ? first : second;
342
- const moved = movePastOverlap(moving, fixed, axis, spacing);
343
- boxes.set(movingId, moved);
344
380
  }
345
381
  }
382
+ reportOverlaps(boxes, diagnostics, ignoredPairs);
383
+ }
384
+ function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
385
+ const ids = [...boxes.keys()].sort();
386
+ const reported = new Set(
387
+ diagnostics.filter(
388
+ (diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
389
+ ).map((diagnostic) => {
390
+ const firstId = diagnostic.detail?.firstId;
391
+ const secondId = diagnostic.detail?.secondId;
392
+ return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
393
+ }).filter((key) => key !== void 0)
394
+ );
346
395
  for (const firstId of ids) {
347
396
  for (const secondId of ids) {
348
397
  if (firstId >= secondId) {
349
398
  continue;
350
399
  }
400
+ const key = overlapKey(firstId, secondId);
401
+ if (reported.has(key) || ignoredPairs.has(key)) {
402
+ continue;
403
+ }
351
404
  const first = boxes.get(firstId);
352
405
  const second = boxes.get(secondId);
353
406
  if (first !== void 0 && second !== void 0 && intersectsAabb(first, second)) {
@@ -358,9 +411,136 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
358
411
  path: ["boxes"],
359
412
  detail: { firstId, secondId }
360
413
  });
414
+ reported.add(key);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ function reportIntraContainerOverflow(input, boxes, diagnostics) {
420
+ if (input.minSiblingGap === void 0) {
421
+ return;
422
+ }
423
+ const minGap = input.minSiblingGap;
424
+ const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
425
+ for (const constraint of input.constraints) {
426
+ if (constraint.kind !== "containment") {
427
+ continue;
428
+ }
429
+ const container = boxes.get(constraint.containerId);
430
+ if (container === void 0) {
431
+ continue;
432
+ }
433
+ const children = [];
434
+ for (const childId of constraint.childIds) {
435
+ const child = boxes.get(childId);
436
+ if (child !== void 0) {
437
+ children.push(child);
438
+ }
439
+ }
440
+ if (children.length < 2) {
441
+ continue;
442
+ }
443
+ const sorted = [...children].sort((a, b) => a[axis] - b[axis]);
444
+ const mainDim = axis === "x" ? "width" : "height";
445
+ let overlapPairs = 0;
446
+ for (let i = 0; i < sorted.length; i += 1) {
447
+ const first = sorted[i];
448
+ if (first === void 0) {
449
+ continue;
450
+ }
451
+ for (let j = i + 1; j < sorted.length; j += 1) {
452
+ const second = sorted[j];
453
+ if (second === void 0) {
454
+ continue;
455
+ }
456
+ if (second[axis] >= first[axis] + first[mainDim]) {
457
+ break;
458
+ }
459
+ if (intersectsAabb(first, second)) {
460
+ overlapPairs += 1;
461
+ }
462
+ }
463
+ }
464
+ if (overlapPairs > 0) {
465
+ diagnostics.push({
466
+ severity: "warning",
467
+ code: "intra_container_overflow",
468
+ message: `${overlapPairs} sibling pair(s) overlap inside ${constraint.containerId}.`,
469
+ path: ["constraints", constraint.id ?? constraint.containerId],
470
+ detail: {
471
+ containerId: constraint.containerId,
472
+ overlapPairs,
473
+ minGap
474
+ }
475
+ });
476
+ }
477
+ const pad = constraint.padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
478
+ const contentMain = mainDim === "width" ? Math.max(0, container.width - pad.left - pad.right) : Math.max(0, container.height - pad.top - pad.bottom);
479
+ let childStart = Infinity;
480
+ let childEnd = -Infinity;
481
+ for (const child of sorted) {
482
+ const start = child[axis];
483
+ const end = start + child[mainDim];
484
+ if (start < childStart) childStart = start;
485
+ if (end > childEnd) childEnd = end;
486
+ }
487
+ if (sorted.length === 0) {
488
+ childStart = 0;
489
+ childEnd = 0;
490
+ }
491
+ const actualExtent = childEnd - childStart;
492
+ if (actualExtent > contentMain) {
493
+ diagnostics.push({
494
+ severity: "error",
495
+ code: "intra_container_overflow_total",
496
+ message: `Container ${constraint.containerId} cannot fit ${sorted.length} siblings along ${axis} (extent ${actualExtent}, available ${contentMain}).`,
497
+ path: ["constraints", constraint.id ?? constraint.containerId],
498
+ detail: {
499
+ containerId: constraint.containerId,
500
+ axis,
501
+ needed: actualExtent,
502
+ available: contentMain,
503
+ siblingCount: sorted.length,
504
+ minGap
505
+ }
506
+ });
507
+ }
508
+ }
509
+ }
510
+ function overlapKey(firstId, secondId) {
511
+ return firstId < secondId ? `${firstId}\0${secondId}` : `${secondId}\0${firstId}`;
512
+ }
513
+ function containmentOverlapKeys(constraints) {
514
+ const keys = /* @__PURE__ */ new Set();
515
+ for (const constraint of constraints) {
516
+ if (constraint.kind !== "containment") {
517
+ continue;
518
+ }
519
+ for (const childId of constraint.childIds) {
520
+ keys.add(overlapKey(constraint.containerId, childId));
521
+ }
522
+ }
523
+ return keys;
524
+ }
525
+ function siblingOverlapKeys(constraints) {
526
+ const keys = /* @__PURE__ */ new Set();
527
+ for (const constraint of constraints) {
528
+ if (constraint.kind !== "containment") {
529
+ continue;
530
+ }
531
+ const { childIds } = constraint;
532
+ for (let i = 0; i < childIds.length; i += 1) {
533
+ for (let j = i + 1; j < childIds.length; j += 1) {
534
+ const a = childIds[i];
535
+ const b = childIds[j];
536
+ if (a === void 0 || b === void 0) {
537
+ continue;
538
+ }
539
+ keys.add(overlapKey(a, b));
361
540
  }
362
541
  }
363
542
  }
543
+ return keys;
364
544
  }
365
545
  function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
366
546
  const current = boxes.get(id);
@@ -478,10 +658,125 @@ function contentBox(container, padding) {
478
658
  return {
479
659
  x: container.x + margin.left,
480
660
  y: container.y + margin.top,
481
- width: container.width - margin.left - margin.right,
482
- height: container.height - margin.top - margin.bottom
661
+ width: Math.max(0, container.width - margin.left - margin.right),
662
+ height: Math.max(0, container.height - margin.top - margin.bottom)
483
663
  };
484
664
  }
665
+ function applyDistributeContained(input, boxes, locks, diagnostics) {
666
+ if (!input.distributeContainedChildren) {
667
+ return;
668
+ }
669
+ const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
670
+ const crossAxis = axis === "x" ? "y" : "x";
671
+ const mainSize = axis === "x" ? "width" : "height";
672
+ const crossSize = axis === "x" ? "height" : "width";
673
+ const minGap = input.minSiblingGap ?? 8;
674
+ for (const constraint of input.constraints) {
675
+ if (constraint.kind !== "containment") {
676
+ continue;
677
+ }
678
+ const container = boxes.get(constraint.containerId);
679
+ if (container === void 0) {
680
+ continue;
681
+ }
682
+ const content = contentBox(container, constraint.padding);
683
+ const unlocked = [];
684
+ const reserved = [];
685
+ for (const childId of constraint.childIds) {
686
+ const box = boxes.get(childId);
687
+ if (box === void 0) {
688
+ continue;
689
+ }
690
+ if (locks.has(childId)) {
691
+ diagnostics.push({
692
+ severity: "warning",
693
+ code: "constraints.locked-target-not-moved",
694
+ message: `Locked child ${childId} skipped during containment distribution.`,
695
+ path: ["constraints", constraint.id ?? constraint.containerId],
696
+ detail: { nodeId: childId }
697
+ });
698
+ reserved.push(intervalForBox(box, axis, mainSize));
699
+ continue;
700
+ }
701
+ unlocked.push({ id: childId, box });
702
+ }
703
+ if (unlocked.length < 2) {
704
+ continue;
705
+ }
706
+ const oversized = unlocked.filter(
707
+ (child) => child.box[mainSize] > content[mainSize] || child.box[crossSize] > content[crossSize]
708
+ );
709
+ if (oversized.length > 0) {
710
+ diagnostics.push({
711
+ severity: "warning",
712
+ code: "constraints.containment.impossible",
713
+ message: `Skipped ${oversized.length} oversized child(ren) during distribution in ${constraint.containerId}.`,
714
+ path: ["constraints", constraint.id ?? constraint.containerId],
715
+ detail: {
716
+ containerId: constraint.containerId,
717
+ oversized: oversized.map((c) => c.id)
718
+ }
719
+ });
720
+ }
721
+ for (const child of oversized) {
722
+ reserved.push(intervalForBox(child.box, axis, mainSize));
723
+ }
724
+ reserved.sort((a, b) => a.start - b.start || a.end - b.end);
725
+ const distributable = unlocked.filter(
726
+ (child) => child.box[mainSize] <= content[mainSize] && child.box[crossSize] <= content[crossSize]
727
+ );
728
+ if (distributable.length < 2) {
729
+ continue;
730
+ }
731
+ let pos = content[axis];
732
+ for (const child of distributable) {
733
+ pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
734
+ const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
735
+ const next = { ...child.box };
736
+ next[axis] = pos;
737
+ next[crossAxis] = crossPos;
738
+ const clamped = moveInside(next, content);
739
+ if (clamped[axis] !== next[axis]) {
740
+ diagnostics.push({
741
+ severity: "warning",
742
+ code: "intra_container_distributed_clamped",
743
+ message: `Distribution gap clamped for ${child.id} in ${constraint.containerId}.`,
744
+ path: ["constraints", constraint.id ?? constraint.containerId],
745
+ detail: { nodeId: child.id, containerId: constraint.containerId }
746
+ });
747
+ }
748
+ boxes.set(child.id, clamped);
749
+ pos = clamped[axis] + clamped[mainSize] + minGap;
750
+ }
751
+ diagnostics.push({
752
+ severity: "info",
753
+ code: "intra_container_distributed",
754
+ message: `Distributed ${distributable.length} children in ${constraint.containerId} along ${axis}.`,
755
+ path: ["constraints", constraint.id ?? constraint.containerId],
756
+ detail: {
757
+ containerId: constraint.containerId,
758
+ count: distributable.length,
759
+ axis
760
+ }
761
+ });
762
+ }
763
+ }
764
+ function intervalForBox(box, axis, mainSize) {
765
+ return { start: box[axis], end: box[axis] + box[mainSize] };
766
+ }
767
+ function advancePastReserved(pos, size, reserved, minGap) {
768
+ let next = pos;
769
+ for (const interval of reserved) {
770
+ if (next + size + minGap <= interval.start) {
771
+ break;
772
+ }
773
+ if (next >= interval.end + minGap) {
774
+ continue;
775
+ }
776
+ next = interval.end + minGap;
777
+ }
778
+ return next;
779
+ }
485
780
  function moveInside(child, container) {
486
781
  return {
487
782
  ...child,
@@ -1187,7 +1482,14 @@ var DEFAULT_GROUP_PADDING = {
1187
1482
  left: 16
1188
1483
  };
1189
1484
  var DEFAULT_LABEL_MAX_WIDTH = 160;
1190
- var DEFAULT_FONT = { fontFamily: "Arial", fontSize: 14, lineHeight: 18 };
1485
+ var DEFAULT_FONT = {
1486
+ fontFamily: "Arial",
1487
+ fontSize: 14,
1488
+ lineHeight: 18
1489
+ };
1490
+ var DEFAULT_MATRIX_CELL_SIZE = { width: 120, height: 36 };
1491
+ var DEFAULT_TABLE_CELL_SIZE = { width: 128, height: 34 };
1492
+ var DEFAULT_PANEL_ITEM_HEIGHT = 28;
1191
1493
  function normalizeDiagramDsl(dslValue, options = {}) {
1192
1494
  const dsl = dslValue;
1193
1495
  const diagnostics = validateReferences(dsl);
@@ -1200,6 +1502,10 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1200
1502
  const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1201
1503
  const routeKind = dsl.routing?.kind ?? "orthogonal";
1202
1504
  const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1505
+ const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
1506
+ const matrices = normalizeMatrices(dsl);
1507
+ const tables = normalizeTables(dsl);
1508
+ const evidencePanels = normalizeEvidencePanels(dsl);
1203
1509
  const diagram = {
1204
1510
  id: options.id ?? dsl.id ?? "diagram",
1205
1511
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1208,11 +1514,15 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1208
1514
  edges: normalizeEdges(dsl),
1209
1515
  groups: normalizeGroups(dsl, measurer),
1210
1516
  swimlanes: normalizeSwimlanes(dsl),
1517
+ ...matrices === void 0 ? {} : { matrices },
1518
+ ...tables === void 0 ? {} : { tables },
1519
+ ...evidencePanels === void 0 ? {} : { evidencePanels },
1211
1520
  constraints: normalizeConstraints(dsl),
1212
1521
  diagnostics: [],
1213
1522
  ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
1214
1523
  metadata: {
1215
1524
  routeKind,
1525
+ ...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
1216
1526
  ...portShifting === void 0 ? {} : { portShifting }
1217
1527
  }
1218
1528
  };
@@ -1352,7 +1662,9 @@ function endpoint(value, nodeIdOverride) {
1352
1662
  function style(value) {
1353
1663
  return {
1354
1664
  ...value.fill === void 0 ? {} : { fill: value.fill },
1355
- ...value.stroke === void 0 ? {} : { stroke: value.stroke }
1665
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke },
1666
+ ...value.fontFamily === void 0 ? {} : { fontFamily: value.fontFamily },
1667
+ ...value.fontSize === void 0 ? {} : { fontSize: value.fontSize }
1356
1668
  };
1357
1669
  }
1358
1670
  function compartments(value) {
@@ -1369,6 +1681,10 @@ function normalizeFrame(frame) {
1369
1681
  ...frame.context === void 0 ? {} : { context: frame.context },
1370
1682
  ...frame.name === void 0 ? {} : { name: frame.name },
1371
1683
  titleTab: frame.titleTab,
1684
+ ...frame.headerHeight === void 0 ? {} : { headerHeight: frame.headerHeight },
1685
+ ...frame.padding === void 0 ? {} : { padding: frame.padding },
1686
+ ...frame.labelPosition === void 0 ? {} : { labelPosition: frame.labelPosition },
1687
+ ...frame.direction === void 0 ? {} : { direction: frame.direction },
1372
1688
  ...frame.style === void 0 ? {} : { style: style(frame.style) }
1373
1689
  };
1374
1690
  }
@@ -1383,14 +1699,17 @@ function formatCompartmentEntry(value) {
1383
1699
  return `${entry[0]}: ${entry[1]}`;
1384
1700
  }
1385
1701
  function normalizeSwimlanes(dsl) {
1386
- return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
1702
+ return Object.keys(dsl.swimlanes ?? {}).map((id) => {
1387
1703
  const swimlane = dsl.swimlanes?.[id];
1388
1704
  const label = toLabel(swimlane?.label);
1389
1705
  return {
1390
1706
  id,
1391
1707
  ...label === void 0 ? {} : { label },
1392
1708
  orientation: swimlane?.orientation ?? "vertical",
1393
- lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
1709
+ layout: swimlane?.layout ?? "overlay",
1710
+ ...swimlane?.headerHeight === void 0 ? {} : { headerHeight: swimlane.headerHeight },
1711
+ ...swimlane?.padding === void 0 ? {} : { padding: swimlane.padding },
1712
+ lanes: Object.keys(swimlane?.lanes ?? {}).map((laneId) => {
1394
1713
  const lane = swimlane?.lanes[laneId];
1395
1714
  const laneLabel = toLabel(lane?.label);
1396
1715
  return {
@@ -1402,6 +1721,102 @@ function normalizeSwimlanes(dsl) {
1402
1721
  };
1403
1722
  });
1404
1723
  }
1724
+ function normalizeMatrices(dsl) {
1725
+ if (dsl.matrices === void 0) {
1726
+ return void 0;
1727
+ }
1728
+ return dsl.matrices.map((matrix) => ({
1729
+ id: matrix.id,
1730
+ rows: [...matrix.rows],
1731
+ cols: [...matrix.cols],
1732
+ cells: matrix.cells.map((row) => row.map(cell)),
1733
+ ...matrix.position === void 0 ? {} : { position: point(matrix.position) },
1734
+ size: matrix.size ?? {
1735
+ width: defaultMatrixRowHeaderWidth(matrix) + Math.max(1, matrix.cols.length) * DEFAULT_MATRIX_CELL_SIZE.width,
1736
+ height: Math.max(1, matrix.rows.length + 1) * DEFAULT_MATRIX_CELL_SIZE.height
1737
+ },
1738
+ ...matrix.style === void 0 ? {} : { style: style(matrix.style) }
1739
+ }));
1740
+ }
1741
+ function defaultMatrixRowHeaderWidth(matrix) {
1742
+ return matrix.rows.length === 0 ? 0 : Math.min(96, DEFAULT_MATRIX_CELL_SIZE.width);
1743
+ }
1744
+ function normalizeTables(dsl) {
1745
+ if (dsl.tables === void 0) {
1746
+ return void 0;
1747
+ }
1748
+ return dsl.tables.map((table) => ({
1749
+ id: table.id,
1750
+ columns: table.columns.map(tableColumn),
1751
+ rows: table.rows.map(tableRow),
1752
+ ...table.position === void 0 ? {} : { position: point(table.position) },
1753
+ size: table.size ?? {
1754
+ width: Math.max(1, table.columns.length) * DEFAULT_TABLE_CELL_SIZE.width,
1755
+ height: Math.max(1, table.rows.length + 1) * DEFAULT_TABLE_CELL_SIZE.height
1756
+ },
1757
+ ...table.style === void 0 ? {} : { style: style(table.style) }
1758
+ }));
1759
+ }
1760
+ function normalizeEvidencePanels(dsl) {
1761
+ if (dsl.evidencePanels === void 0) {
1762
+ return void 0;
1763
+ }
1764
+ return dsl.evidencePanels.map((panel) => ({
1765
+ id: panel.id,
1766
+ kind: panel.kind,
1767
+ items: panel.items.map(panelItem),
1768
+ ...panel.position === void 0 ? {} : { position: point(panel.position) },
1769
+ size: panel.size ?? {
1770
+ width: 320,
1771
+ height: Math.max(1, panel.items.length) * DEFAULT_PANEL_ITEM_HEIGHT
1772
+ },
1773
+ ...panel.style === void 0 ? {} : { style: style(panel.style) }
1774
+ }));
1775
+ }
1776
+ function cell(value) {
1777
+ if (typeof value === "string") {
1778
+ return { text: value };
1779
+ }
1780
+ return {
1781
+ text: value.text,
1782
+ ...value.style === void 0 && value.fill === void 0 && value.stroke === void 0 ? {} : {
1783
+ style: style({
1784
+ ...value.style,
1785
+ ...value.fill === void 0 ? {} : { fill: value.fill },
1786
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
1787
+ })
1788
+ }
1789
+ };
1790
+ }
1791
+ function tableColumn(value) {
1792
+ return {
1793
+ id: value.id,
1794
+ label: toLabel(value.label) ?? { text: value.id }
1795
+ };
1796
+ }
1797
+ function tableRow(value) {
1798
+ return {
1799
+ id: value.id,
1800
+ cells: Object.fromEntries(
1801
+ Object.keys(value.cells).map((columnId) => [
1802
+ columnId,
1803
+ cell(value.cells[columnId] ?? "")
1804
+ ])
1805
+ )
1806
+ };
1807
+ }
1808
+ function panelItem(value) {
1809
+ if (typeof value === "string") {
1810
+ return { label: { text: value } };
1811
+ }
1812
+ const detail = toLabel(value.detail);
1813
+ return {
1814
+ ...value.id === void 0 ? {} : { id: value.id },
1815
+ label: toLabel(value.label) ?? { text: "" },
1816
+ ...detail === void 0 ? {} : { detail },
1817
+ ...value.style === void 0 ? {} : { style: style(value.style) }
1818
+ };
1819
+ }
1405
1820
  function normalizeGroups(dsl, measurer) {
1406
1821
  return Object.keys(dsl.groups ?? {}).sort().map((id) => {
1407
1822
  const group = dsl.groups?.[id];
@@ -1413,6 +1828,9 @@ function normalizeGroups(dsl, measurer) {
1413
1828
  nodeIds: [...group?.nodes ?? []],
1414
1829
  groupIds: [...group?.groups ?? []],
1415
1830
  padding: group?.padding ?? { ...DEFAULT_GROUP_PADDING },
1831
+ ...group?.headerHeight === void 0 ? {} : { headerHeight: group.headerHeight },
1832
+ ...group?.labelPosition === void 0 ? {} : { labelPosition: group.labelPosition },
1833
+ ...group?.direction === void 0 ? {} : { direction: group.direction },
1416
1834
  ...labelLayout === void 0 ? {} : { labelLayout }
1417
1835
  };
1418
1836
  });
@@ -1657,6 +2075,10 @@ var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
1657
2075
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
1658
2076
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
1659
2077
  var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
2078
+ var primaryReadingDirectionSchema = zod.z.enum([
2079
+ "top_to_bottom",
2080
+ "top-to-bottom"
2081
+ ]);
1660
2082
  var nodeShapeSchema = zod.z.enum([
1661
2083
  "rectangle",
1662
2084
  "rounded-rectangle",
@@ -1667,6 +2089,7 @@ var nodeShapeSchema = zod.z.enum([
1667
2089
  "cylinder"
1668
2090
  ]);
1669
2091
  var finiteNumberSchema = zod.z.number().finite();
2092
+ var nonNegativeNumberSchema = finiteNumberSchema.min(0);
1670
2093
  var pointSchema = zod.z.object({
1671
2094
  x: finiteNumberSchema,
1672
2095
  y: finiteNumberSchema
@@ -1677,6 +2100,12 @@ var insetsSchema = zod.z.object({
1677
2100
  bottom: finiteNumberSchema,
1678
2101
  left: finiteNumberSchema
1679
2102
  });
2103
+ var nonNegativeInsetsSchema = zod.z.object({
2104
+ top: nonNegativeNumberSchema,
2105
+ right: nonNegativeNumberSchema,
2106
+ bottom: nonNegativeNumberSchema,
2107
+ left: nonNegativeNumberSchema
2108
+ });
1680
2109
  var labelSchema = zod.z.union([
1681
2110
  zod.z.string(),
1682
2111
  zod.z.object({
@@ -1686,8 +2115,19 @@ var labelSchema = zod.z.union([
1686
2115
  ]);
1687
2116
  var styleSchema = zod.z.object({
1688
2117
  fill: zod.z.string().optional(),
1689
- stroke: zod.z.string().optional()
2118
+ stroke: zod.z.string().optional(),
2119
+ fontFamily: zod.z.string().optional(),
2120
+ fontSize: finiteNumberSchema.optional()
1690
2121
  });
2122
+ var blockCellSchema = zod.z.union([
2123
+ zod.z.string(),
2124
+ zod.z.object({
2125
+ text: zod.z.string(),
2126
+ fill: zod.z.string().optional(),
2127
+ stroke: zod.z.string().optional(),
2128
+ style: styleSchema.optional()
2129
+ })
2130
+ ]);
1691
2131
  var portSideSchema = zod.z.enum(["top", "right", "bottom", "left"]);
1692
2132
  var portKindSchema = zod.z.enum(["proxy", "flow"]);
1693
2133
  var portSchema = zod.z.object({
@@ -1748,11 +2188,17 @@ var groupSchema = zod.z.object({
1748
2188
  label: labelSchema.optional(),
1749
2189
  nodes: zod.z.array(zod.z.string()).optional(),
1750
2190
  groups: zod.z.array(zod.z.string()).optional(),
1751
- padding: insetsSchema.optional()
2191
+ padding: insetsSchema.optional(),
2192
+ headerHeight: nonNegativeNumberSchema.optional(),
2193
+ labelPosition: zod.z.enum(["top", "inside", "outside"]).optional(),
2194
+ direction: zod.z.enum(["horizontal", "vertical"]).optional()
1752
2195
  });
1753
2196
  var swimlaneSchema = zod.z.object({
1754
2197
  label: labelSchema.optional(),
1755
2198
  orientation: zod.z.enum(["vertical", "horizontal"]).optional(),
2199
+ layout: zod.z.enum(["overlay", "contract"]).optional(),
2200
+ headerHeight: nonNegativeNumberSchema.optional(),
2201
+ padding: nonNegativeNumberSchema.optional(),
1756
2202
  lanes: zod.z.record(
1757
2203
  zod.z.string(),
1758
2204
  zod.z.object({
@@ -1761,6 +2207,122 @@ var swimlaneSchema = zod.z.object({
1761
2207
  })
1762
2208
  )
1763
2209
  });
2210
+ var matrixSchema = zod.z.object({
2211
+ id: zod.z.string(),
2212
+ rows: zod.z.array(zod.z.string()),
2213
+ cols: zod.z.array(zod.z.string()),
2214
+ cells: zod.z.array(zod.z.array(blockCellSchema)),
2215
+ position: pointSchema.optional(),
2216
+ size: zod.z.object({
2217
+ width: nonNegativeNumberSchema,
2218
+ height: nonNegativeNumberSchema
2219
+ }).optional(),
2220
+ style: styleSchema.optional()
2221
+ }).superRefine((matrix, context) => {
2222
+ checkDuplicateValues("matrix row", matrix.rows, context, (index) => [
2223
+ "rows",
2224
+ index
2225
+ ]);
2226
+ checkDuplicateValues("matrix column", matrix.cols, context, (index) => [
2227
+ "cols",
2228
+ index
2229
+ ]);
2230
+ if (matrix.cells.length !== matrix.rows.length) {
2231
+ context.addIssue({
2232
+ code: "custom",
2233
+ message: `Matrix cells must contain exactly ${matrix.rows.length} row(s).`,
2234
+ path: ["cells"]
2235
+ });
2236
+ }
2237
+ matrix.cells.forEach((row, rowIndex) => {
2238
+ if (row.length !== matrix.cols.length) {
2239
+ context.addIssue({
2240
+ code: "custom",
2241
+ message: `Matrix cell row must contain exactly ${matrix.cols.length} column(s).`,
2242
+ path: ["cells", rowIndex]
2243
+ });
2244
+ }
2245
+ });
2246
+ });
2247
+ var tableColumnSchema = zod.z.object({
2248
+ id: zod.z.string(),
2249
+ label: labelSchema
2250
+ });
2251
+ var tableRowSchema = zod.z.object({
2252
+ id: zod.z.string(),
2253
+ cells: zod.z.record(zod.z.string(), blockCellSchema)
2254
+ });
2255
+ var tableSchema = zod.z.object({
2256
+ id: zod.z.string(),
2257
+ columns: zod.z.array(tableColumnSchema),
2258
+ rows: zod.z.array(tableRowSchema),
2259
+ position: pointSchema.optional(),
2260
+ size: zod.z.object({
2261
+ width: nonNegativeNumberSchema,
2262
+ height: nonNegativeNumberSchema
2263
+ }).optional(),
2264
+ style: styleSchema.optional()
2265
+ }).superRefine((table, context) => {
2266
+ checkDuplicateIds("column", table.columns, context, (index) => [
2267
+ "columns",
2268
+ index,
2269
+ "id"
2270
+ ]);
2271
+ checkDuplicateIds("row", table.rows, context, (index) => [
2272
+ "rows",
2273
+ index,
2274
+ "id"
2275
+ ]);
2276
+ const columnIds = new Set(table.columns.map((column) => column.id));
2277
+ table.rows.forEach((row, rowIndex) => {
2278
+ for (const columnId of Object.keys(row.cells)) {
2279
+ if (!columnIds.has(columnId)) {
2280
+ context.addIssue({
2281
+ code: "custom",
2282
+ message: `Table row cell references undeclared column "${columnId}".`,
2283
+ path: ["rows", rowIndex, "cells", columnId]
2284
+ });
2285
+ }
2286
+ }
2287
+ });
2288
+ });
2289
+ var panelItemSchema = zod.z.union([
2290
+ zod.z.string(),
2291
+ zod.z.object({
2292
+ id: zod.z.string().optional(),
2293
+ label: labelSchema,
2294
+ detail: labelSchema.optional(),
2295
+ style: styleSchema.optional()
2296
+ })
2297
+ ]);
2298
+ var evidencePanelSchema = zod.z.object({
2299
+ id: zod.z.string(),
2300
+ kind: zod.z.enum(["legend", "rule", "note", "verification"]),
2301
+ items: zod.z.array(panelItemSchema),
2302
+ position: pointSchema.optional(),
2303
+ size: zod.z.object({
2304
+ width: nonNegativeNumberSchema,
2305
+ height: nonNegativeNumberSchema
2306
+ }).optional(),
2307
+ style: styleSchema.optional()
2308
+ }).superRefine((panel, context) => {
2309
+ const firstIndexByItemId = /* @__PURE__ */ new Map();
2310
+ panel.items.forEach((item, index) => {
2311
+ if (typeof item === "string" || item.id === void 0) {
2312
+ return;
2313
+ }
2314
+ const firstIndex = firstIndexByItemId.get(item.id);
2315
+ if (firstIndex === void 0) {
2316
+ firstIndexByItemId.set(item.id, index);
2317
+ return;
2318
+ }
2319
+ context.addIssue({
2320
+ code: "custom",
2321
+ message: `Duplicate evidence panel item id "${item.id}".`,
2322
+ path: ["items", index, "id"]
2323
+ });
2324
+ });
2325
+ });
1764
2326
  var exactPositionConstraintSchema = zod.z.object({
1765
2327
  kind: zod.z.literal("exact-position"),
1766
2328
  target: zod.z.string().optional(),
@@ -1818,7 +2380,8 @@ var diagramDslSchema = zod.z.object({
1818
2380
  title: zod.z.string().optional(),
1819
2381
  direction: directionSchema.optional(),
1820
2382
  layout: zod.z.object({
1821
- direction: directionSchema.optional()
2383
+ direction: directionSchema.optional(),
2384
+ primaryReadingDirection: primaryReadingDirectionSchema.optional()
1822
2385
  }).optional(),
1823
2386
  routing: zod.z.object({
1824
2387
  kind: routeKindSchema.optional(),
@@ -1831,17 +2394,33 @@ var diagramDslSchema = zod.z.object({
1831
2394
  edges: zod.z.array(edgeSchema).optional(),
1832
2395
  groups: zod.z.record(zod.z.string(), groupSchema).optional(),
1833
2396
  swimlanes: zod.z.record(zod.z.string(), swimlaneSchema).optional(),
2397
+ matrices: zod.z.array(matrixSchema).optional(),
2398
+ tables: zod.z.array(tableSchema).optional(),
2399
+ evidencePanels: zod.z.array(evidencePanelSchema).optional(),
1834
2400
  constraints: zod.z.array(constraintSchema).optional(),
1835
2401
  frame: zod.z.object({
1836
2402
  kind: zod.z.string(),
1837
2403
  context: zod.z.string().optional(),
1838
2404
  name: zod.z.string().optional(),
1839
2405
  titleTab: zod.z.string(),
2406
+ headerHeight: nonNegativeNumberSchema.optional(),
2407
+ padding: zod.z.union([nonNegativeNumberSchema, nonNegativeInsetsSchema]).optional(),
2408
+ labelPosition: zod.z.enum(["top", "inside", "outside"]).optional(),
2409
+ direction: zod.z.enum(["horizontal", "vertical"]).optional(),
1840
2410
  style: styleSchema.optional()
1841
2411
  }).optional(),
1842
2412
  output: zod.z.object({
1843
2413
  format: outputFormatSchema.optional()
1844
2414
  }).optional()
2415
+ }).superRefine((diagram, context) => {
2416
+ checkDuplicateEvidenceBlockIds("matrices", diagram.matrices, context);
2417
+ checkDuplicateEvidenceBlockIds("tables", diagram.tables, context);
2418
+ checkDuplicateEvidenceBlockIds(
2419
+ "evidencePanels",
2420
+ diagram.evidencePanels,
2421
+ context
2422
+ );
2423
+ checkDuplicateEvidenceBlockIdsAcrossTypes(diagram, context);
1845
2424
  });
1846
2425
  function validateDiagramDsl(value) {
1847
2426
  const result = diagramDslSchema.safeParse(value);
@@ -1861,25 +2440,81 @@ function toDiagnosticPath(path) {
1861
2440
  (segment) => typeof segment === "string" || typeof segment === "number" ? [segment] : []
1862
2441
  );
1863
2442
  }
1864
-
1865
- // src/dsl/parse.ts
1866
- var DEFAULT_DSL_MAX_BYTES = 1e6;
1867
- function parseDiagramDsl(source, options = {}) {
1868
- const maxBytes = options.maxBytes ?? DEFAULT_DSL_MAX_BYTES;
1869
- if (buffer.Buffer.byteLength(source, "utf8") > maxBytes) {
1870
- return {
1871
- diagnostics: [
1872
- createParseDiagnostic(
1873
- "parse.input.too-large",
1874
- `Input exceeds the ${maxBytes} byte limit.`,
1875
- "Split the diagram into smaller inputs or raise maxBytes for trusted sources."
1876
- )
1877
- ]
1878
- };
1879
- }
1880
- const parsed = parseSource(source, options);
1881
- if (parsed.value === void 0 || hasErrorDiagnostics(parsed.diagnostics)) {
1882
- return { diagnostics: sortDslDiagnostics(parsed.diagnostics) };
2443
+ function checkDuplicateEvidenceBlockIds(collection, blocks, context) {
2444
+ checkDuplicateIds(
2445
+ `evidence block id in ${collection}`,
2446
+ blocks ?? [],
2447
+ context,
2448
+ (index) => [collection, index, "id"]
2449
+ );
2450
+ }
2451
+ function checkDuplicateIds(label, items, context, pathForIndex) {
2452
+ const firstIndexById = /* @__PURE__ */ new Map();
2453
+ items.forEach((item, index) => {
2454
+ const firstIndex = firstIndexById.get(item.id);
2455
+ if (firstIndex === void 0) {
2456
+ firstIndexById.set(item.id, index);
2457
+ return;
2458
+ }
2459
+ context.addIssue({
2460
+ code: "custom",
2461
+ message: `Duplicate ${label} "${item.id}".`,
2462
+ path: pathForIndex(index)
2463
+ });
2464
+ });
2465
+ }
2466
+ function checkDuplicateValues(label, values, context, pathForIndex) {
2467
+ const firstIndexByValue = /* @__PURE__ */ new Map();
2468
+ values.forEach((value, index) => {
2469
+ const firstIndex = firstIndexByValue.get(value);
2470
+ if (firstIndex === void 0) {
2471
+ firstIndexByValue.set(value, index);
2472
+ return;
2473
+ }
2474
+ context.addIssue({
2475
+ code: "custom",
2476
+ message: `Duplicate ${label} "${value}".`,
2477
+ path: pathForIndex(index)
2478
+ });
2479
+ });
2480
+ }
2481
+ function checkDuplicateEvidenceBlockIdsAcrossTypes(diagram, context) {
2482
+ const firstById = /* @__PURE__ */ new Map();
2483
+ for (const collection of ["matrices", "tables", "evidencePanels"]) {
2484
+ const blocks = diagram[collection] ?? [];
2485
+ blocks.forEach((block, index) => {
2486
+ const first = firstById.get(block.id);
2487
+ if (first === void 0) {
2488
+ firstById.set(block.id, { collection, index });
2489
+ return;
2490
+ }
2491
+ context.addIssue({
2492
+ code: "custom",
2493
+ message: `Duplicate evidence block id "${block.id}" across ${first.collection} and ${collection}.`,
2494
+ path: [collection, index, "id"]
2495
+ });
2496
+ });
2497
+ }
2498
+ }
2499
+
2500
+ // src/dsl/parse.ts
2501
+ var DEFAULT_DSL_MAX_BYTES = 1e6;
2502
+ function parseDiagramDsl(source, options = {}) {
2503
+ const maxBytes = options.maxBytes ?? DEFAULT_DSL_MAX_BYTES;
2504
+ if (buffer.Buffer.byteLength(source, "utf8") > maxBytes) {
2505
+ return {
2506
+ diagnostics: [
2507
+ createParseDiagnostic(
2508
+ "parse.input.too-large",
2509
+ `Input exceeds the ${maxBytes} byte limit.`,
2510
+ "Split the diagram into smaller inputs or raise maxBytes for trusted sources."
2511
+ )
2512
+ ]
2513
+ };
2514
+ }
2515
+ const parsed = parseSource(source, options);
2516
+ if (parsed.value === void 0 || hasErrorDiagnostics(parsed.diagnostics)) {
2517
+ return { diagnostics: sortDslDiagnostics(parsed.diagnostics) };
1883
2518
  }
1884
2519
  const expanded = expandEdgeShorthand(parsed.value);
1885
2520
  if (hasErrorDiagnostics(expanded.diagnostics)) {
@@ -2062,6 +2697,15 @@ function exportExcalidraw(diagram, options = {}) {
2062
2697
  elements.push(text);
2063
2698
  }
2064
2699
  }
2700
+ for (const matrix of diagram.matrices ?? []) {
2701
+ elements.push(...renderMatrixBlock(matrix));
2702
+ }
2703
+ for (const table of diagram.tables ?? []) {
2704
+ elements.push(...renderTableBlock(table));
2705
+ }
2706
+ for (const panel of diagram.evidencePanels ?? []) {
2707
+ elements.push(...renderEvidencePanel(panel));
2708
+ }
2065
2709
  for (const edge of diagram.edges) {
2066
2710
  elements.push(renderArrow(edge));
2067
2711
  }
@@ -2094,6 +2738,83 @@ function renderNode(node, groupIds) {
2094
2738
  groupIds
2095
2739
  };
2096
2740
  }
2741
+ function renderMatrixBlock(matrix) {
2742
+ const containerId = `matrix:${matrix.id}`;
2743
+ const groupIds = [containerId];
2744
+ const label = blockText([
2745
+ matrix.id,
2746
+ `row | ${matrix.cols.join(" | ")}`,
2747
+ ...matrix.rows.map((rowId, rowIndex) => {
2748
+ const row = matrix.cells[rowIndex] ?? [];
2749
+ return `${rowId}: ${matrix.cols.map((_, columnIndex) => row[columnIndex]?.text ?? "").join(" | ")}`;
2750
+ })
2751
+ ]);
2752
+ return [
2753
+ {
2754
+ ...baseElement(containerId, "rectangle", matrix.box),
2755
+ backgroundColor: matrix.style?.fill ?? "#f8fafc",
2756
+ strokeColor: matrix.style?.stroke ?? "#374151",
2757
+ groupIds
2758
+ },
2759
+ renderTextBlock(
2760
+ `matrix-text:${matrix.id}`,
2761
+ label,
2762
+ matrix.box,
2763
+ containerId,
2764
+ groupIds
2765
+ )
2766
+ ];
2767
+ }
2768
+ function renderTableBlock(table) {
2769
+ const containerId = `table:${table.id}`;
2770
+ const groupIds = [containerId];
2771
+ const label = blockText([
2772
+ table.columns.map((column) => column.label.text).join(" | "),
2773
+ ...table.rows.map(
2774
+ (row) => table.columns.map((column) => row.cells[column.id]?.text ?? "").join(" | ")
2775
+ )
2776
+ ]);
2777
+ return [
2778
+ {
2779
+ ...baseElement(containerId, "rectangle", table.box),
2780
+ backgroundColor: table.style?.fill ?? "#f8fafc",
2781
+ strokeColor: table.style?.stroke ?? "#374151",
2782
+ groupIds
2783
+ },
2784
+ renderTextBlock(
2785
+ `table-text:${table.id}`,
2786
+ label,
2787
+ table.box,
2788
+ containerId,
2789
+ groupIds
2790
+ )
2791
+ ];
2792
+ }
2793
+ function renderEvidencePanel(panel) {
2794
+ const containerId = `evidence-panel:${panel.id}`;
2795
+ const groupIds = [containerId];
2796
+ const label = blockText([
2797
+ `${panel.kind}: ${panel.id}`,
2798
+ ...panel.items.map(
2799
+ (item) => item.detail?.text === void 0 ? item.label.text : `${item.label.text}: ${item.detail.text}`
2800
+ )
2801
+ ]);
2802
+ return [
2803
+ {
2804
+ ...baseElement(containerId, "rectangle", panel.box),
2805
+ backgroundColor: panel.style?.fill ?? panelKindFill(panel.kind),
2806
+ strokeColor: panel.style?.stroke ?? "#374151",
2807
+ groupIds
2808
+ },
2809
+ renderTextBlock(
2810
+ `evidence-panel-text:${panel.id}`,
2811
+ label,
2812
+ panel.box,
2813
+ containerId,
2814
+ groupIds
2815
+ )
2816
+ ];
2817
+ }
2097
2818
  function renderArrow(edge) {
2098
2819
  const first = edge.points[0];
2099
2820
  if (first === void 0) {
@@ -2153,6 +2874,34 @@ function renderText(id, label, box, containerId, groupIds) {
2153
2874
  versionNonce: seedFor(`${id}:nonce`)
2154
2875
  };
2155
2876
  }
2877
+ function renderTextBlock(id, text, box, containerId, groupIds) {
2878
+ const fontSize = 12;
2879
+ return {
2880
+ ...baseElement(id, "text", {
2881
+ x: box.x + 8,
2882
+ y: box.y + 8,
2883
+ width: Math.max(0, box.width - 16),
2884
+ height: Math.max(fontSize, box.height - 16)
2885
+ }),
2886
+ backgroundColor: "transparent",
2887
+ strokeColor: "#111827",
2888
+ groupIds,
2889
+ text,
2890
+ fontSize,
2891
+ fontFamily: 1,
2892
+ textAlign: "left",
2893
+ verticalAlign: "top",
2894
+ baseline: fontSize,
2895
+ containerId,
2896
+ originalText: text,
2897
+ lineHeight: 1.25,
2898
+ boundElements: null,
2899
+ link: null,
2900
+ locked: false,
2901
+ seed: seedFor(id),
2902
+ versionNonce: seedFor(`${id}:nonce`)
2903
+ };
2904
+ }
2156
2905
  function baseElement(id, type, box) {
2157
2906
  return {
2158
2907
  id,
@@ -2230,6 +2979,21 @@ function groupGroupIds(groupId) {
2230
2979
  function groupElementIdFor(groupId) {
2231
2980
  return `group:${groupId}`;
2232
2981
  }
2982
+ function blockText(lines) {
2983
+ return lines.filter((line) => line.length > 0).join("\n");
2984
+ }
2985
+ function panelKindFill(kind) {
2986
+ switch (kind) {
2987
+ case "legend":
2988
+ return "#ecfdf5";
2989
+ case "rule":
2990
+ return "#eff6ff";
2991
+ case "note":
2992
+ return "#fffbeb";
2993
+ case "verification":
2994
+ return "#fef2f2";
2995
+ }
2996
+ }
2233
2997
  function pointsBox(points) {
2234
2998
  const xs = points.map((point2) => point2.x);
2235
2999
  const ys = points.map((point2) => point2.y);
@@ -2267,42 +3031,253 @@ var GROUP_FILL = "#f9fafb";
2267
3031
  var STROKE = "#374151";
2268
3032
  var EDGE_STROKE = "#111827";
2269
3033
  var FONT_FAMILY = "Arial, sans-serif";
3034
+ var EVIDENCE_FILL = "#f8fafc";
3035
+ var EVIDENCE_HEADER_FILL = "#e5e7eb";
3036
+ var EVIDENCE_TEXT_FONT_SIZE = 10;
3037
+ var EVIDENCE_TEXT_LINE_HEIGHT = 12;
3038
+ var EVIDENCE_PANEL_KIND_FILL = {
3039
+ legend: "#ecfdf5",
3040
+ rule: "#eff6ff",
3041
+ note: "#fffbeb",
3042
+ verification: "#fef2f2"
3043
+ };
2270
3044
  function exportSvg(diagram, options = {}) {
2271
3045
  const title = options.title ?? diagram.title;
2272
- const lines = [
3046
+ const annotations = diagram.textAnnotations ?? [];
3047
+ return `${[
2273
3048
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
2274
3049
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
2275
3050
  ` <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"/>`,
2276
- ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame))],
3051
+ ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
2277
3052
  ...(diagram.swimlanes ?? []).flatMap(
2278
- (swimlane) => renderSwimlane(swimlane)
3053
+ (swimlane) => renderSwimlane(swimlane, annotations)
2279
3054
  ),
2280
3055
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
3056
+ ...(diagram.matrices ?? []).flatMap(
3057
+ (matrix) => indentLines(renderMatrixBlock2(matrix))
3058
+ ),
3059
+ ...(diagram.tables ?? []).flatMap(
3060
+ (table) => indentLines(renderTableBlock2(table))
3061
+ ),
3062
+ ...(diagram.evidencePanels ?? []).flatMap(
3063
+ (panel) => indentLines(renderEvidencePanel2(panel))
3064
+ ),
2281
3065
  ...diagram.edges.flatMap((edge) => {
2282
3066
  const path = renderEdgePath(edge);
2283
- if (path === void 0) {
2284
- return [];
2285
- }
2286
- return [indent(path), indent(renderArrowhead(edge))];
3067
+ return path === void 0 ? [] : [indent(path), indent(renderArrowhead(edge))];
2287
3068
  }),
2288
3069
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
2289
- ...diagram.nodes.flatMap((node) => renderCompartments(node)),
2290
- ...diagram.nodes.flatMap((node) => renderPorts(node)),
3070
+ ...diagram.nodes.flatMap((node) => renderCompartments(node, annotations)),
3071
+ ...diagram.nodes.flatMap((node) => renderPorts(node, annotations)),
2291
3072
  ...diagram.groups.flatMap(
2292
- (group) => renderLabel(group.label, group.box, group)
3073
+ (group) => renderLabel(group.label, group.box, group, annotations, "group-label")
2293
3074
  ),
2294
3075
  ...diagram.nodes.flatMap(
2295
- (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node) : []
3076
+ (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node, annotations, "node-label") : []
2296
3077
  ),
2297
- ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge)),
3078
+ ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge, annotations)),
2298
3079
  "</svg>"
2299
- ];
2300
- return `${lines.join("\n")}
3080
+ ].join("\n")}
2301
3081
  `;
2302
3082
  }
2303
3083
  function renderGroup2(group) {
2304
3084
  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"/>`;
2305
3085
  }
3086
+ function renderMatrixBlock2(matrix) {
3087
+ const columnCount = Math.max(1, matrix.cols.length);
3088
+ const rowCount = matrix.rows.length;
3089
+ const rowHeaderWidth = rowCount > 0 ? Math.min(96, matrix.box.width * 0.28) : 0;
3090
+ const dataWidth = Math.max(0, matrix.box.width - rowHeaderWidth);
3091
+ const cellWidth = dataWidth / columnCount;
3092
+ const rowHeight = matrix.box.height / Math.max(1, rowCount + 1);
3093
+ const lines = [
3094
+ `<g class="matrix-block" data-id="${escapeAttribute(matrix.id)}" data-row-count="${rowCount}" data-column-count="${matrix.cols.length}">`,
3095
+ ` <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)}"/>`
3096
+ ];
3097
+ if (rowHeaderWidth > 0) {
3098
+ lines.push(
3099
+ ` <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}"/>`
3100
+ );
3101
+ }
3102
+ for (let columnIndex = 0; columnIndex < matrix.cols.length; columnIndex += 1) {
3103
+ const column = matrix.cols[columnIndex];
3104
+ if (column === void 0) {
3105
+ continue;
3106
+ }
3107
+ const x = matrix.box.x + rowHeaderWidth + columnIndex * cellWidth;
3108
+ lines.push(
3109
+ ` <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}"/>`,
3110
+ renderEvidenceText(
3111
+ "matrix-column-label",
3112
+ matrix.columnLabelLayouts?.[columnIndex]?.lines ?? [column],
3113
+ {
3114
+ x,
3115
+ y: matrix.box.y,
3116
+ width: cellWidth,
3117
+ height: rowHeight
3118
+ }
3119
+ )
3120
+ );
3121
+ }
3122
+ for (let rowIndex = 0; rowIndex < matrix.rows.length; rowIndex += 1) {
3123
+ const row = matrix.rows[rowIndex];
3124
+ const cells = matrix.cells[rowIndex] ?? [];
3125
+ if (row === void 0) {
3126
+ continue;
3127
+ }
3128
+ if (rowHeaderWidth > 0) {
3129
+ const rowHeaderBox = {
3130
+ x: matrix.box.x,
3131
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
3132
+ width: rowHeaderWidth,
3133
+ height: rowHeight
3134
+ };
3135
+ lines.push(
3136
+ ` <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}"/>`,
3137
+ renderEvidenceText(
3138
+ "matrix-row-label",
3139
+ matrix.rowLabelLayouts?.[rowIndex]?.lines ?? [row],
3140
+ rowHeaderBox
3141
+ )
3142
+ );
3143
+ }
3144
+ for (let columnIndex = 0; columnIndex < matrix.cols.length; columnIndex += 1) {
3145
+ const column = matrix.cols[columnIndex];
3146
+ if (column === void 0) {
3147
+ continue;
3148
+ }
3149
+ const cell2 = cells[columnIndex] ?? { text: "" };
3150
+ const box = {
3151
+ x: matrix.box.x + rowHeaderWidth + columnIndex * cellWidth,
3152
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
3153
+ width: cellWidth,
3154
+ height: rowHeight
3155
+ };
3156
+ lines.push(
3157
+ ` <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)}"/>`,
3158
+ renderEvidenceText(
3159
+ "matrix-cell-label",
3160
+ matrix.cellLabelLayouts?.[rowIndex]?.[columnIndex]?.lines ?? [
3161
+ cell2.text
3162
+ ],
3163
+ box
3164
+ )
3165
+ );
3166
+ }
3167
+ }
3168
+ lines.push("</g>");
3169
+ return lines;
3170
+ }
3171
+ function renderTableBlock2(table) {
3172
+ const columnCount = Math.max(1, table.columns.length);
3173
+ const rowHeight = table.box.height / Math.max(1, table.rows.length + 1);
3174
+ const lines = [
3175
+ `<g class="table-block" data-id="${escapeAttribute(table.id)}" data-row-count="${table.rows.length}" data-column-count="${table.columns.length}">`,
3176
+ ` <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)}"/>`,
3177
+ ` <g class="table-header" data-column-count="${table.columns.length}">`
3178
+ ];
3179
+ for (let columnIndex = 0; columnIndex < table.columns.length; columnIndex += 1) {
3180
+ const column = table.columns[columnIndex];
3181
+ if (column === void 0) {
3182
+ continue;
3183
+ }
3184
+ const columnBox = tableCellBox(
3185
+ table,
3186
+ columnIndex,
3187
+ 0,
3188
+ rowHeight,
3189
+ columnCount
3190
+ );
3191
+ lines.push(
3192
+ ` <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}"/>`,
3193
+ ` ${renderEvidenceText("table-header-label", table.columnLabelLayouts?.[columnIndex]?.lines ?? [column.label.text], columnBox)}`
3194
+ );
3195
+ }
3196
+ lines.push(" </g>");
3197
+ for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) {
3198
+ const row = table.rows[rowIndex];
3199
+ if (row === void 0) {
3200
+ continue;
3201
+ }
3202
+ const rowBox = {
3203
+ x: table.box.x,
3204
+ y: table.box.y + (rowIndex + 1) * rowHeight,
3205
+ width: table.box.width,
3206
+ height: rowHeight
3207
+ };
3208
+ const rowClass = rowIndex % 2 === 0 ? "table-row-even" : "table-row-odd";
3209
+ lines.push(
3210
+ ` <g class="table-row ${rowClass}" data-row="${escapeAttribute(row.id)}">`,
3211
+ ` <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"/>`
3212
+ );
3213
+ for (let columnIndex = 0; columnIndex < table.columns.length; columnIndex += 1) {
3214
+ const column = table.columns[columnIndex];
3215
+ if (column === void 0) {
3216
+ continue;
3217
+ }
3218
+ const cell2 = row.cells[column.id] ?? { text: "" };
3219
+ const cellBox = tableCellBox(
3220
+ table,
3221
+ columnIndex,
3222
+ rowIndex + 1,
3223
+ rowHeight,
3224
+ columnCount
3225
+ );
3226
+ lines.push(
3227
+ ` <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)}"/>`,
3228
+ ` ${renderEvidenceText("table-cell-label", table.cellLabelLayouts?.[rowIndex]?.[columnIndex]?.lines ?? [cell2.text], cellBox)}`
3229
+ );
3230
+ }
3231
+ lines.push(" </g>");
3232
+ }
3233
+ lines.push("</g>");
3234
+ return lines;
3235
+ }
3236
+ function renderEvidencePanel2(panel) {
3237
+ const titleWidth = Math.min(panel.box.width * 0.36, 140);
3238
+ const itemBox = {
3239
+ x: panel.box.x + titleWidth,
3240
+ y: panel.box.y,
3241
+ width: panel.box.width - titleWidth,
3242
+ height: panel.box.height
3243
+ };
3244
+ const titleBox = {
3245
+ x: panel.box.x,
3246
+ y: panel.box.y,
3247
+ width: titleWidth,
3248
+ height: panel.box.height
3249
+ };
3250
+ const itemHeight = panel.box.height / Math.max(1, panel.items.length);
3251
+ const lines = [
3252
+ `<g class="evidence-panel evidence-panel--${panel.kind}" data-id="${escapeAttribute(panel.id)}" data-kind="${escapeAttribute(panel.kind)}" data-item-count="${panel.items.length}">`,
3253
+ ` <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)}"/>`,
3254
+ ` <g class="evidence-panel-title-cell">`,
3255
+ ` <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}"/>`,
3256
+ ` ${renderEvidenceText("evidence-panel-title", panel.titleLayout?.lines ?? [`${panel.kind}: ${panel.id}`], titleBox)}`,
3257
+ " </g>",
3258
+ ` <g class="evidence-panel-items-cell">`,
3259
+ ` <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}"/>`
3260
+ ];
3261
+ for (let index = 0; index < panel.items.length; index += 1) {
3262
+ const item = panel.items[index];
3263
+ if (item === void 0) {
3264
+ continue;
3265
+ }
3266
+ const text = panelItemText(item.label.text, item.detail?.text);
3267
+ const box = {
3268
+ x: itemBox.x,
3269
+ y: itemBox.y + index * itemHeight,
3270
+ width: itemBox.width,
3271
+ height: itemHeight
3272
+ };
3273
+ lines.push(
3274
+ ` <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")}"/>`,
3275
+ ` ${renderEvidenceText("evidence-panel-item-label", panel.itemLayouts?.[index]?.lines ?? [text], box)}`
3276
+ );
3277
+ }
3278
+ lines.push(" </g>", "</g>");
3279
+ return lines;
3280
+ }
2306
3281
  function renderNode2(node) {
2307
3282
  const fill = node.style?.fill ?? NODE_FILL;
2308
3283
  const stroke = node.style?.stroke ?? STROKE;
@@ -2322,18 +3297,27 @@ function renderNode2(node) {
2322
3297
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
2323
3298
  }
2324
3299
  }
2325
- function renderFrame(frame) {
3300
+ function renderFrame(frame, annotations) {
2326
3301
  const stroke = frame.style?.stroke ?? "#6b7280";
2327
3302
  const fill = frame.style?.fill ?? "transparent";
2328
3303
  return [
2329
3304
  `<g class="sysml-frame" data-kind="${escapeAttribute(frame.kind)}">`,
2330
3305
  ` <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)}"/>`,
2331
3306
  ` <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)}"/>`,
2332
- ` <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>`,
3307
+ ...renderSolvedTextAnnotation(
3308
+ findAnnotation(annotations, "frame-title", frame.kind),
3309
+ `sysml-title-tab-label`,
3310
+ {
3311
+ indent: " ",
3312
+ mode: "center",
3313
+ fallbackText: frame.titleTab}
3314
+ ) ?? [
3315
+ ` <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>`
3316
+ ],
2333
3317
  "</g>"
2334
3318
  ].join("\n");
2335
3319
  }
2336
- function renderSwimlane(swimlane) {
3320
+ function renderSwimlane(swimlane, annotations) {
2337
3321
  if (swimlane.box === void 0) {
2338
3322
  return [];
2339
3323
  }
@@ -2348,24 +3332,60 @@ function renderSwimlane(swimlane) {
2348
3332
  lines.push(
2349
3333
  ` <rect class="swimlane-lane" data-lane="${escapeAttribute(`${swimlane.id}.${lane.id}`)}" x="${formatNumber(lane.box.x)}" y="${formatNumber(lane.box.y)}" width="${formatNumber(lane.box.width)}" height="${formatNumber(lane.box.height)}" fill="none" stroke="${STROKE}"/>`
2350
3334
  );
3335
+ if (lane.headerBox !== void 0) {
3336
+ lines.push(
3337
+ ` <rect class="swimlane-header" data-lane-header="${escapeAttribute(`${swimlane.id}.${lane.id}`)}" x="${formatNumber(lane.headerBox.x)}" y="${formatNumber(lane.headerBox.y)}" width="${formatNumber(lane.headerBox.width)}" height="${formatNumber(lane.headerBox.height)}" fill="#f3f4f6" stroke="${STROKE}"/>`
3338
+ );
3339
+ }
3340
+ if (lane.contentBox !== void 0) {
3341
+ lines.push(
3342
+ ` <rect class="swimlane-content" data-lane-content="${escapeAttribute(`${swimlane.id}.${lane.id}`)}" x="${formatNumber(lane.contentBox.x)}" y="${formatNumber(lane.contentBox.y)}" width="${formatNumber(lane.contentBox.width)}" height="${formatNumber(lane.contentBox.height)}" fill="none" stroke="none"/>`
3343
+ );
3344
+ }
2351
3345
  if (lane.label?.text !== void 0) {
3346
+ const annotation = findAnnotation(
3347
+ annotations,
3348
+ "swimlane-label",
3349
+ `${swimlane.id}.${lane.id}`
3350
+ );
2352
3351
  lines.push(
2353
- ` <text class="swimlane-label" x="${formatNumber(lane.box.x + lane.box.width / 2)}" y="${formatNumber(lane.box.y + 16)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(lane.label.text)}</text>`
3352
+ ...annotation === void 0 ? [
3353
+ renderSwimlaneLabel(
3354
+ swimlane,
3355
+ lane.label.text,
3356
+ lane.headerBox ?? lane.box
3357
+ )
3358
+ ] : renderSolvedTextAnnotation(annotation, "swimlane-label", {
3359
+ indent: " ",
3360
+ mode: "center",
3361
+ rotate: swimlane.orientation === "horizontal"
3362
+ }) ?? []
2354
3363
  );
2355
3364
  }
2356
3365
  }
2357
3366
  lines.push(" </g>");
2358
3367
  return lines;
2359
3368
  }
2360
- function renderPorts(node) {
3369
+ function renderPorts(node, annotations) {
2361
3370
  return (node.ports ?? []).flatMap((port) => [
2362
3371
  ` <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)}"/>`,
2363
- ...port.label?.text === void 0 ? [] : [
2364
- ` <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>`
2365
- ]
3372
+ ...port.label?.text === void 0 ? [] : (() => {
3373
+ const annotation = findAnnotation(
3374
+ annotations,
3375
+ "port-label",
3376
+ `${node.id}.${port.id}`
3377
+ );
3378
+ return annotation === void 0 ? [
3379
+ ` <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>`
3380
+ ] : renderSolvedTextAnnotation(annotation, "port-label", {
3381
+ indent: " ",
3382
+ mode: "center",
3383
+ textAnchor: port.side === "left" ? "end" : "start"
3384
+ }) ?? [];
3385
+ })()
2366
3386
  ]);
2367
3387
  }
2368
- function renderCompartments(node) {
3388
+ function renderCompartments(node, annotations) {
2369
3389
  const compartments2 = node.compartments;
2370
3390
  if (compartments2 === void 0) {
2371
3391
  return [];
@@ -2385,7 +3405,6 @@ function renderCompartments(node) {
2385
3405
  text
2386
3406
  }))
2387
3407
  ];
2388
- const lineHeight = 16;
2389
3408
  const lines = [
2390
3409
  ` <g class="compartment" data-for="${escapeAttribute(node.id)}">`
2391
3410
  ];
@@ -2394,32 +3413,72 @@ function renderCompartments(node) {
2394
3413
  if (row === void 0) {
2395
3414
  continue;
2396
3415
  }
2397
- const y = node.box.y + 18 + index * lineHeight;
3416
+ const y = node.box.y + 18 + index * 16;
2398
3417
  if (index > 1) {
2399
3418
  lines.push(
2400
3419
  ` <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}"/>`
2401
3420
  );
2402
3421
  }
3422
+ const annotation = findAnnotation(
3423
+ annotations,
3424
+ "compartment-row",
3425
+ node.id,
3426
+ index
3427
+ );
2403
3428
  lines.push(
2404
- ` <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>`
3429
+ ...annotation === void 0 ? [
3430
+ ` <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>`
3431
+ ] : renderSolvedTextAnnotation(
3432
+ annotation,
3433
+ `compartment-${row.className}`,
3434
+ {
3435
+ indent: " ",
3436
+ mode: "center"
3437
+ }
3438
+ ) ?? []
2405
3439
  );
2406
3440
  }
2407
3441
  lines.push(" </g>");
2408
3442
  return lines;
2409
3443
  }
2410
- function portLabelX(x, side) {
2411
- if (side === "left") {
2412
- return x - 8;
2413
- }
2414
- if (side === "right") {
2415
- return x + 8;
3444
+ function tableCellBox(table, columnIndex, rowIndex, rowHeight, columnCount) {
3445
+ const x = table.columnXOffsets[columnIndex] ?? table.box.x + table.box.width / columnCount * columnIndex;
3446
+ const nextX = table.columnXOffsets[columnIndex + 1] ?? table.box.x + table.box.width;
3447
+ return {
3448
+ x,
3449
+ y: table.box.y + rowIndex * rowHeight,
3450
+ width: nextX - x,
3451
+ height: rowHeight
3452
+ };
3453
+ }
3454
+ function renderEvidenceText(className, lines, box) {
3455
+ const fontSize = EVIDENCE_TEXT_FONT_SIZE;
3456
+ const lineHeight = EVIDENCE_TEXT_LINE_HEIGHT;
3457
+ const x = box.x + box.width / 2;
3458
+ if (lines.length <= 1) {
3459
+ 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>`;
2416
3460
  }
2417
- return x + 8;
3461
+ const totalHeight = (lines.length - 1) * lineHeight;
3462
+ const firstBaselineY = box.y + box.height / 2 - totalHeight / 2;
3463
+ return [
3464
+ `<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">`,
3465
+ ...lines.map(
3466
+ (line, index) => ` <tspan x="${formatNumber(x)}" y="${formatNumber(firstBaselineY + index * lineHeight)}">${escapeXml(line)}</tspan>`
3467
+ ),
3468
+ "</text>"
3469
+ ].join("\n");
2418
3470
  }
2419
- function renderRect(box, attributes) {
2420
- return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
3471
+ function panelItemText(label, detail) {
3472
+ return detail === void 0 ? label : `${label}: ${detail}`;
2421
3473
  }
2422
- function renderLabel(label, box, item) {
3474
+ function renderLabel(label, box, item, annotations, surfaceKind) {
3475
+ const annotation = findAnnotation(annotations, surfaceKind, item.id);
3476
+ if (annotation !== void 0) {
3477
+ return renderSolvedTextAnnotation(annotation, "label", {
3478
+ indent: " ",
3479
+ mode: "center"
3480
+ }) ?? [];
3481
+ }
2423
3482
  const labelLayout = item.labelLayout;
2424
3483
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
2425
3484
  const offset = { x: box.x, y: box.y };
@@ -2445,10 +3504,17 @@ function renderEdgePath(edge) {
2445
3504
  const dash = edge.style === "dashed" ? ' stroke-dasharray="6 4"' : "";
2446
3505
  return `<path class="edge" data-id="${escapeAttribute(edge.id)}" d="${formatPath(pathPointsBeforeArrowhead(edge.points))}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"${dash}/>`;
2447
3506
  }
2448
- function renderEdgeLabel(edge) {
3507
+ function renderEdgeLabel(edge, annotations) {
2449
3508
  if (edge.label?.text === void 0 || edge.points.length < 2) {
2450
3509
  return [];
2451
3510
  }
3511
+ const annotation = findAnnotation(annotations, "edge-label", edge.id);
3512
+ if (annotation !== void 0) {
3513
+ return renderSolvedTextAnnotation(annotation, "edge-label", {
3514
+ indent: " ",
3515
+ mode: "center"
3516
+ }) ?? [];
3517
+ }
2452
3518
  const placement = labelPlacementOnPolyline(edge.points);
2453
3519
  if (placement === void 0) {
2454
3520
  return [];
@@ -2462,6 +3528,85 @@ function renderArrowhead(edge) {
2462
3528
  const fill = edge.arrowhead === "hollowTriangle" ? "none" : EDGE_STROKE;
2463
3529
  return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(edge.id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${fill}" stroke="${EDGE_STROKE}"/>`;
2464
3530
  }
3531
+ function renderSolvedTextAnnotation(annotation, className, options) {
3532
+ if (annotation === void 0) {
3533
+ return void 0;
3534
+ }
3535
+ const x = options.mode === "center" ? annotation.box.x + annotation.box.width / 2 : annotation.box.x;
3536
+ const y = options.mode === "center" ? annotation.box.y + annotation.box.height / 2 : annotation.box.y + annotation.box.height;
3537
+ const rotate = options.rotate ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
3538
+ const attrs = [
3539
+ `class="${className}"`,
3540
+ `data-for="${escapeAttribute(annotation.ownerId)}"`,
3541
+ `data-text-surface="${escapeAttribute(annotation.surfaceKind)}"`,
3542
+ `data-owner-id="${escapeAttribute(annotation.ownerId)}"`,
3543
+ `data-text-backend="${escapeAttribute(annotation.textBackend ?? "deterministic")}"`,
3544
+ `font-family="${escapeAttribute(annotation.fontFamily)}"`,
3545
+ `font-size="${formatNumber(annotation.fontSize)}"`,
3546
+ `fill="#111827"`
3547
+ ];
3548
+ if (options.mode === "center") {
3549
+ attrs.push('text-anchor="middle"');
3550
+ } else {
3551
+ attrs.push(`text-anchor="${options.textAnchor ?? "start"}"`);
3552
+ }
3553
+ if (annotation.lines.length > 1) {
3554
+ return [
3555
+ `${options.indent}<text ${attrs.join(" ")}${rotate}>`,
3556
+ ...annotation.lines.map(
3557
+ (line2) => `${options.indent} <tspan x="${formatNumber(textLineX(annotation, line2, options))}" y="${formatNumber(annotation.box.y + line2.baselineY)}">${escapeXml(line2.text)}</tspan>`
3558
+ ),
3559
+ `${options.indent}</text>`
3560
+ ];
3561
+ }
3562
+ const line = annotation.lines[0];
3563
+ const text = line?.text ?? options.fallbackText ?? annotation.text;
3564
+ const singleLineAttrs = options.mode === "center" ? [...attrs, 'dominant-baseline="middle"'] : attrs;
3565
+ return [
3566
+ `${options.indent}<text ${singleLineAttrs.join(" ")} x="${formatNumber(x)}" y="${formatNumber(y)}"${rotate}>${escapeXml(text)}</text>`
3567
+ ];
3568
+ }
3569
+ function textLineX(annotation, line, options) {
3570
+ if (options.mode === "center") {
3571
+ return annotation.box.x + line.box.x + line.box.width / 2;
3572
+ }
3573
+ if ((options.textAnchor ?? "start") === "end") {
3574
+ return annotation.box.x + line.box.x + line.box.width;
3575
+ }
3576
+ return annotation.box.x + line.box.x;
3577
+ }
3578
+ function findAnnotation(annotations, surfaceKind, ownerId, index) {
3579
+ return annotations.find((annotation) => {
3580
+ if (annotation.surfaceKind !== surfaceKind) {
3581
+ return false;
3582
+ }
3583
+ if (annotation.ownerId !== ownerId) {
3584
+ return false;
3585
+ }
3586
+ if (index === void 0) {
3587
+ return annotation.surfaceIndex === void 0;
3588
+ }
3589
+ return annotation.surfaceIndex === index;
3590
+ });
3591
+ }
3592
+ function renderSwimlaneLabel(swimlane, text, labelBox) {
3593
+ const x = labelBox.x + labelBox.width / 2;
3594
+ const y = labelBox.y + labelBox.height / 2;
3595
+ const transform = swimlane.orientation === "horizontal" ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
3596
+ 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>`;
3597
+ }
3598
+ function renderRect(box, attributes) {
3599
+ return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
3600
+ }
3601
+ function portLabelX(x, side) {
3602
+ if (side === "left") {
3603
+ return x - 8;
3604
+ }
3605
+ if (side === "right") {
3606
+ return x + 8;
3607
+ }
3608
+ return x + 8;
3609
+ }
2465
3610
  function labelPlacementOnPolyline(points) {
2466
3611
  const segments = nonZeroSegments(points);
2467
3612
  const totalLength = segments.reduce(
@@ -2544,17 +3689,15 @@ function shapePoints(shape, box) {
2544
3689
  { x: right - skew, y: bottom },
2545
3690
  { x: left, y: bottom }
2546
3691
  ];
2547
- case "hexagon": {
2548
- const inset = Math.min(box.width * 0.2, 24);
3692
+ case "hexagon":
2549
3693
  return [
2550
- { x: left + inset, y: top },
2551
- { x: right - inset, y: top },
3694
+ { x: left + skew, y: top },
3695
+ { x: right - skew, y: top },
2552
3696
  { x: right, y: midY },
2553
- { x: right - inset, y: bottom },
2554
- { x: left + inset, y: bottom },
3697
+ { x: right - skew, y: bottom },
3698
+ { x: left + skew, y: bottom },
2555
3699
  { x: left, y: midY }
2556
3700
  ];
2557
- }
2558
3701
  }
2559
3702
  }
2560
3703
  function formatCylinderPath(box) {
@@ -2579,10 +3722,9 @@ function formatCylinderPath(box) {
2579
3722
  ].join(" ");
2580
3723
  }
2581
3724
  function formatPath(points) {
2582
- return points.map((point2, index) => {
2583
- const command = index === 0 ? "M" : "L";
2584
- return `${command} ${formatNumber(point2.x)} ${formatNumber(point2.y)}`;
2585
- }).join(" ");
3725
+ return points.map(
3726
+ (point2, index) => `${index === 0 ? "M" : "L"} ${formatNumber(point2.x)} ${formatNumber(point2.y)}`
3727
+ ).join(" ");
2586
3728
  }
2587
3729
  function formatPoints(points) {
2588
3730
  return points.map((point2) => `${formatNumber(point2.x)},${formatNumber(point2.y)}`).join(" ");
@@ -2605,6 +3747,9 @@ function escapeAttribute(value) {
2605
3747
  function indent(value) {
2606
3748
  return ` ${value}`;
2607
3749
  }
3750
+ function indentLines(values) {
3751
+ return values.map(indent);
3752
+ }
2608
3753
  var DEFAULT_OPTIONS = {
2609
3754
  nodesep: 80,
2610
3755
  ranksep: 100,
@@ -2706,48 +3851,366 @@ function isValidDimension(value) {
2706
3851
  // src/routing/routes.ts
2707
3852
  function routeEdge(input) {
2708
3853
  const diagnostics = [];
3854
+ const softObstacles = input.obstacles ?? [];
3855
+ const hardObstacles = input.hardObstacles ?? [];
2709
3856
  const defaultAnchors = defaultAnchorsForGeometry(
2710
3857
  input.source.box,
2711
3858
  input.target.box,
2712
3859
  input.direction
2713
3860
  );
2714
- const source = getEdgePort(
2715
- input.source,
2716
- input.target.center,
2717
- input.sourceAnchor ?? defaultAnchors.sourceAnchor
2718
- );
2719
- const target = getEdgePort(
2720
- input.target,
2721
- input.source.center,
2722
- input.targetAnchor ?? defaultAnchors.targetAnchor
2723
- );
2724
3861
  if ((input.kind ?? "orthogonal") === "straight") {
2725
- return { points: simplifyRoute([source, target]), diagnostics };
2726
- }
2727
- const candidates = orthogonalCandidates(source, target, input.direction);
2728
- candidates.push(
2729
- ...expandedObstacleCandidates(
2730
- source,
2731
- target,
2732
- input.direction,
2733
- input.obstacles ?? []
2734
- )
2735
- );
2736
- for (const candidate of candidates) {
2737
- if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
2738
- return { points: simplifyRoute(candidate), diagnostics };
3862
+ const source = getEdgePort(
3863
+ input.source,
3864
+ input.target.center,
3865
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
3866
+ );
3867
+ const target = getEdgePort(
3868
+ input.target,
3869
+ input.source.center,
3870
+ input.targetAnchor ?? defaultAnchors.targetAnchor
3871
+ );
3872
+ const points = finalizeRoute(
3873
+ [source, target],
3874
+ softObstacles,
3875
+ hardObstacles,
3876
+ diagnostics
3877
+ );
3878
+ if (routeCrossesBoxes(points, hardObstacles)) {
3879
+ diagnostics.push({
3880
+ severity: "error",
3881
+ code: "routing.evidence.crossing_forbidden",
3882
+ message: "Straight route crosses hard evidence block obstacles."
3883
+ });
3884
+ return { points, diagnostics };
2739
3885
  }
2740
- }
2741
- diagnostics.push({
2742
- severity: "warning",
2743
- code: "routing.obstacle.unavoidable",
2744
- message: "No bounded orthogonal route candidate avoided all obstacles."
2745
- });
3886
+ if (routeCrossesBoxes(points, softObstacles)) {
3887
+ diagnostics.push({
3888
+ severity: "warning",
3889
+ code: "routing.obstacle.unavoidable",
3890
+ message: "Straight route crosses soft obstacles."
3891
+ });
3892
+ }
3893
+ return { points, diagnostics };
3894
+ }
3895
+ const routeLaneObstacles = [...softObstacles, ...hardObstacles];
3896
+ const anchorPairs = routeAnchorPairs(input, defaultAnchors);
3897
+ const candidateRoutes = anchorPairs.flatMap(
3898
+ ({ sourceAnchor, targetAnchor }) => {
3899
+ const source = getEdgePort(
3900
+ input.source,
3901
+ input.target.center,
3902
+ sourceAnchor
3903
+ );
3904
+ const target = getEdgePort(
3905
+ input.target,
3906
+ input.source.center,
3907
+ targetAnchor
3908
+ );
3909
+ const routes = [
3910
+ ...orthogonalCandidates(source, target, input.direction),
3911
+ ...expandedObstacleCandidates(
3912
+ source,
3913
+ target,
3914
+ input.direction,
3915
+ routeLaneObstacles
3916
+ ),
3917
+ ...outerDoglegCandidates(
3918
+ source,
3919
+ target,
3920
+ input.direction,
3921
+ routeLaneObstacles
3922
+ )
3923
+ ];
3924
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
3925
+ return routes.map((points) => ({ points, endpointObstacles }));
3926
+ }
3927
+ );
3928
+ for (const candidate of candidateRoutes) {
3929
+ if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
3930
+ candidate.points,
3931
+ candidate.endpointObstacles
3932
+ )) {
3933
+ return {
3934
+ points: finalizeRoute(
3935
+ candidate.points,
3936
+ softObstacles,
3937
+ hardObstacles,
3938
+ diagnostics
3939
+ ),
3940
+ diagnostics
3941
+ };
3942
+ }
3943
+ }
3944
+ const hardClearCandidate = candidateRoutes.find(
3945
+ (candidate) => !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
3946
+ candidate.points,
3947
+ candidate.endpointObstacles
3948
+ )
3949
+ );
3950
+ if (hardClearCandidate !== void 0) {
3951
+ diagnostics.push({
3952
+ severity: "warning",
3953
+ code: "routing.obstacle.unavoidable",
3954
+ message: "No bounded orthogonal route candidate avoided all soft obstacles."
3955
+ });
3956
+ return {
3957
+ points: finalizeRoute(
3958
+ hardClearCandidate.points,
3959
+ softObstacles,
3960
+ hardObstacles,
3961
+ diagnostics
3962
+ ),
3963
+ diagnostics
3964
+ };
3965
+ }
3966
+ if (hardObstacles.length > 0) {
3967
+ diagnostics.push({
3968
+ severity: "error",
3969
+ code: "routing.evidence.crossing_forbidden",
3970
+ message: "No bounded orthogonal route candidate avoided hard evidence block obstacles."
3971
+ });
3972
+ return {
3973
+ points: finalizeRoute(
3974
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3975
+ softObstacles,
3976
+ hardObstacles,
3977
+ diagnostics
3978
+ ),
3979
+ diagnostics
3980
+ };
3981
+ }
3982
+ diagnostics.push({
3983
+ severity: "warning",
3984
+ code: "routing.obstacle.unavoidable",
3985
+ message: "No bounded orthogonal route candidate avoided all obstacles."
3986
+ });
2746
3987
  return {
2747
- points: simplifyRoute(candidates[0] ?? [source, target]),
3988
+ points: finalizeRoute(
3989
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3990
+ softObstacles,
3991
+ hardObstacles,
3992
+ diagnostics
3993
+ ),
2748
3994
  diagnostics
2749
3995
  };
2750
3996
  }
3997
+ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3998
+ const simplified = simplifyRoute(points);
3999
+ const crossesHardObstacles = routeCrossesBoxes(simplified, hardObstacles);
4000
+ const crossesSoftObstacles = routeCrossesBoxes(simplified, softObstacles);
4001
+ if (simplified.length < 3 && (crossesHardObstacles || crossesSoftObstacles)) {
4002
+ diagnostics.push({
4003
+ severity: crossesHardObstacles ? "error" : "warning",
4004
+ code: "route_obstacle_fallback",
4005
+ message: "Obstacle-aware routing fell back to fewer than three route points.",
4006
+ detail: { pointCount: simplified.length }
4007
+ });
4008
+ return expandFallbackRoute(simplified, [
4009
+ ...softObstacles,
4010
+ ...hardObstacles
4011
+ ]);
4012
+ }
4013
+ return simplified;
4014
+ }
4015
+ function expandFallbackRoute(points, obstacles) {
4016
+ if (points.length !== 2) {
4017
+ return points.map((point2) => ({ ...point2 }));
4018
+ }
4019
+ const [source, target] = points;
4020
+ if (source === void 0 || target === void 0) {
4021
+ return points.map((point2) => ({ ...point2 }));
4022
+ }
4023
+ if (source.y === target.y) {
4024
+ const detourY = horizontalDetourLane(source, target, obstacles);
4025
+ return [
4026
+ { ...source },
4027
+ { x: source.x, y: detourY },
4028
+ { x: target.x, y: detourY },
4029
+ { ...target }
4030
+ ];
4031
+ }
4032
+ if (source.x === target.x) {
4033
+ const detourX = verticalDetourLane(source, target, obstacles);
4034
+ return [
4035
+ { ...source },
4036
+ { x: detourX, y: source.y },
4037
+ { x: detourX, y: target.y },
4038
+ { ...target }
4039
+ ];
4040
+ }
4041
+ const hv = diagonalDetourHV(source, target, obstacles);
4042
+ const vh = diagonalDetourVH(source, target, obstacles);
4043
+ const viable = [hv, vh].filter((c) => !routeCrossesBoxes(c, obstacles));
4044
+ if (viable.length > 0) {
4045
+ const directLen = Math.hypot(target.x - source.x, target.y - source.y);
4046
+ let best = viable[0];
4047
+ for (let i = 1; i < viable.length; i += 1) {
4048
+ const cand = viable[i];
4049
+ if (cand !== void 0 && pathLength(cand) - directLen < pathLength(best) - directLen) {
4050
+ best = cand;
4051
+ }
4052
+ }
4053
+ return best;
4054
+ }
4055
+ return [
4056
+ { ...source },
4057
+ { x: (source.x + target.x) / 2, y: source.y },
4058
+ { x: (source.x + target.x) / 2, y: target.y },
4059
+ { ...target }
4060
+ ];
4061
+ }
4062
+ function horizontalDetourLane(source, target, obstacles) {
4063
+ const crossing = obstacles.filter(
4064
+ (obstacle) => segmentIntersectsBox(source, target, obstacle)
4065
+ );
4066
+ if (crossing.length === 0) {
4067
+ return source.y + (source.x <= target.x ? 1 : -1) * 24;
4068
+ }
4069
+ const margin = 24;
4070
+ const above = Math.min(...crossing.map((obstacle) => obstacle.y)) - margin;
4071
+ const below = Math.max(...crossing.map((obstacle) => obstacle.y + obstacle.height)) + margin;
4072
+ return Math.abs(above - source.y) <= Math.abs(below - source.y) ? above : below;
4073
+ }
4074
+ function verticalDetourLane(source, target, obstacles) {
4075
+ const crossing = obstacles.filter(
4076
+ (obstacle) => segmentIntersectsBox(source, target, obstacle)
4077
+ );
4078
+ if (crossing.length === 0) {
4079
+ return source.x + (source.y <= target.y ? 1 : -1) * 24;
4080
+ }
4081
+ const margin = 24;
4082
+ const left = Math.min(...crossing.map((obstacle) => obstacle.x)) - margin;
4083
+ const right = Math.max(...crossing.map((obstacle) => obstacle.x + obstacle.width)) + margin;
4084
+ return Math.abs(left - source.x) <= Math.abs(right - source.x) ? left : right;
4085
+ }
4086
+ function diagonalDetourHV(source, target, obstacles) {
4087
+ const detourY = horizontalDetourLane(source, target, obstacles);
4088
+ return [
4089
+ { ...source },
4090
+ { x: source.x, y: detourY },
4091
+ { x: target.x, y: detourY },
4092
+ { ...target }
4093
+ ];
4094
+ }
4095
+ function diagonalDetourVH(source, target, obstacles) {
4096
+ const detourX = verticalDetourLane(source, target, obstacles);
4097
+ return [
4098
+ { ...source },
4099
+ { x: detourX, y: source.y },
4100
+ { x: detourX, y: target.y },
4101
+ { ...target }
4102
+ ];
4103
+ }
4104
+ function pathLength(points) {
4105
+ let len = 0;
4106
+ for (let i = 1; i < points.length; i += 1) {
4107
+ const a = points[i - 1];
4108
+ const b = points[i];
4109
+ if (a !== void 0 && b !== void 0) {
4110
+ len += Math.hypot(b.x - a.x, b.y - a.y);
4111
+ }
4112
+ }
4113
+ return len;
4114
+ }
4115
+ function endpointObstaclesForAutoAnchors(input) {
4116
+ const boxes = [];
4117
+ if (input.sourceAnchor === void 0 && hasDistinctAnchors(input.source)) {
4118
+ boxes.push(insetBox(input.source.box, 1));
4119
+ }
4120
+ if (input.targetAnchor === void 0 && hasDistinctAnchors(input.target)) {
4121
+ boxes.push(insetBox(input.target.box, 1));
4122
+ }
4123
+ return boxes.filter((box) => box.width > 0 && box.height > 0);
4124
+ }
4125
+ function hasDistinctAnchors(geometry) {
4126
+ const points = new Set(
4127
+ geometry.anchors.map((anchor) => `${anchor.point.x},${anchor.point.y}`)
4128
+ );
4129
+ return points.size > 1;
4130
+ }
4131
+ function insetBox(box, margin) {
4132
+ return {
4133
+ x: box.x + margin,
4134
+ y: box.y + margin,
4135
+ width: box.width - margin * 2,
4136
+ height: box.height - margin * 2
4137
+ };
4138
+ }
4139
+ function fallbackRoute(input, defaultAnchors) {
4140
+ return [
4141
+ getEdgePort(
4142
+ input.source,
4143
+ input.target.center,
4144
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
4145
+ ),
4146
+ getEdgePort(
4147
+ input.target,
4148
+ input.source.center,
4149
+ input.targetAnchor ?? defaultAnchors.targetAnchor
4150
+ )
4151
+ ];
4152
+ }
4153
+ function routeAnchorPairs(input, defaultAnchors) {
4154
+ const sourceAnchors = routeAnchorCandidates(
4155
+ input.sourceAnchor,
4156
+ defaultAnchors.sourceAnchor,
4157
+ input.source,
4158
+ input.target.center
4159
+ );
4160
+ const targetAnchors = routeAnchorCandidates(
4161
+ input.targetAnchor,
4162
+ defaultAnchors.targetAnchor,
4163
+ input.target,
4164
+ input.source.center
4165
+ );
4166
+ const pairs = sourceAnchors.flatMap(
4167
+ (sourceAnchor) => targetAnchors.map((targetAnchor) => ({ sourceAnchor, targetAnchor }))
4168
+ );
4169
+ const seen = /* @__PURE__ */ new Set();
4170
+ return pairs.filter((pair) => {
4171
+ const key = `${pair.sourceAnchor}->${pair.targetAnchor}`;
4172
+ if (seen.has(key)) {
4173
+ return false;
4174
+ }
4175
+ seen.add(key);
4176
+ return true;
4177
+ });
4178
+ }
4179
+ function routeAnchorCandidates(explicitAnchor, defaultAnchor, geometry, toward) {
4180
+ if (explicitAnchor !== void 0) {
4181
+ return [explicitAnchor];
4182
+ }
4183
+ const ranked = rankedSideAnchors(geometry, toward);
4184
+ return [defaultAnchor, ...ranked].filter(
4185
+ (anchor, index, anchors) => anchors.indexOf(anchor) === index
4186
+ );
4187
+ }
4188
+ function rankedSideAnchors(geometry, toward) {
4189
+ const anchors = outwardSideAnchors(geometry.box, toward);
4190
+ return anchors.sort((left, right) => {
4191
+ const leftPoint = getEdgePort(geometry, toward, left);
4192
+ const rightPoint = getEdgePort(geometry, toward, right);
4193
+ const distance = squaredDistance2(leftPoint, toward) - squaredDistance2(rightPoint, toward);
4194
+ return distance === 0 ? left.localeCompare(right) : distance;
4195
+ });
4196
+ }
4197
+ function outwardSideAnchors(box, toward) {
4198
+ const center = {
4199
+ x: box.x + box.width / 2,
4200
+ y: box.y + box.height / 2
4201
+ };
4202
+ const dx = toward.x - center.x;
4203
+ const dy = toward.y - center.y;
4204
+ if (Math.abs(dx) >= Math.abs(dy)) {
4205
+ return dx >= 0 ? ["right", "top", "bottom"] : ["left", "top", "bottom"];
4206
+ }
4207
+ return dy >= 0 ? ["bottom", "left", "right"] : ["top", "left", "right"];
4208
+ }
4209
+ function squaredDistance2(a, b) {
4210
+ const dx = a.x - b.x;
4211
+ const dy = a.y - b.y;
4212
+ return dx * dx + dy * dy;
4213
+ }
2751
4214
  function simplifyRoute(points) {
2752
4215
  const withoutDuplicates = [];
2753
4216
  for (const point2 of points) {
@@ -2872,6 +4335,44 @@ function expandedObstacleCandidates(source, target, direction, obstacles) {
2872
4335
  }
2873
4336
  return candidates;
2874
4337
  }
4338
+ function outerDoglegCandidates(source, target, direction, obstacles) {
4339
+ if (obstacles.length === 0) {
4340
+ return [];
4341
+ }
4342
+ const margin = 24;
4343
+ const minX = Math.min(...obstacles.map((obstacle) => obstacle.x)) - margin;
4344
+ const maxX = Math.max(...obstacles.map((obstacle) => obstacle.x + obstacle.width)) + margin;
4345
+ const minY = Math.min(...obstacles.map((obstacle) => obstacle.y)) - margin;
4346
+ const maxY = Math.max(...obstacles.map((obstacle) => obstacle.y + obstacle.height)) + margin;
4347
+ if (direction === "TB" || direction === "BT") {
4348
+ const exit2 = exitDelta(source, target, "y");
4349
+ return sortedUniqueLanes([minX, maxX], (source.x + target.x) / 2).map(
4350
+ (laneX) => [
4351
+ source,
4352
+ { x: source.x, y: source.y + exit2 },
4353
+ { x: laneX, y: source.y + exit2 },
4354
+ { x: laneX, y: target.y - exit2 },
4355
+ { x: target.x, y: target.y - exit2 },
4356
+ target
4357
+ ]
4358
+ );
4359
+ }
4360
+ const exit = exitDelta(source, target, "x");
4361
+ return sortedUniqueLanes([minY, maxY], (source.y + target.y) / 2).map(
4362
+ (laneY) => [
4363
+ source,
4364
+ { x: source.x + exit, y: source.y },
4365
+ { x: source.x + exit, y: laneY },
4366
+ { x: target.x - exit, y: laneY },
4367
+ { x: target.x - exit, y: target.y },
4368
+ target
4369
+ ]
4370
+ );
4371
+ }
4372
+ function exitDelta(source, target, axis) {
4373
+ const delta = axis === "x" ? target.x - source.x : target.y - source.y;
4374
+ return (delta >= 0 ? 1 : -1) * 24;
4375
+ }
2875
4376
  function sortedUniqueLanes(lanes, midpoint) {
2876
4377
  return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
2877
4378
  const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
@@ -2895,6 +4396,72 @@ function routeIntersectsObstacles(points, obstacles) {
2895
4396
  }
2896
4397
  return false;
2897
4398
  }
4399
+ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4400
+ for (let index = 0; index < points.length - 1; index += 1) {
4401
+ const a = points[index];
4402
+ const b = points[index + 1];
4403
+ if (a === void 0 || b === void 0) {
4404
+ continue;
4405
+ }
4406
+ const segment = segmentBox(a, b);
4407
+ for (const endpointInterior of endpointInteriors) {
4408
+ validateBox(endpointInterior);
4409
+ if (intersectsAabb(segment, endpointInterior)) {
4410
+ return true;
4411
+ }
4412
+ }
4413
+ }
4414
+ return false;
4415
+ }
4416
+ function routeCrossesBoxes(points, obstacles) {
4417
+ for (let index = 0; index < points.length - 1; index += 1) {
4418
+ const a = points[index];
4419
+ const b = points[index + 1];
4420
+ if (a === void 0 || b === void 0) {
4421
+ continue;
4422
+ }
4423
+ for (const obstacle of obstacles) {
4424
+ validateBox(obstacle);
4425
+ if (segmentIntersectsBox(a, b, obstacle)) {
4426
+ return true;
4427
+ }
4428
+ }
4429
+ }
4430
+ return false;
4431
+ }
4432
+ function segmentIntersectsBox(start, end, box) {
4433
+ const left = box.x;
4434
+ const right = box.x + box.width;
4435
+ const top = box.y;
4436
+ const bottom = box.y + box.height;
4437
+ if (pointInsideBox(start, box) || pointInsideBox(end, box)) {
4438
+ return true;
4439
+ }
4440
+ if (start.x === end.x) {
4441
+ return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
4442
+ }
4443
+ if (start.y === end.y) {
4444
+ return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
4445
+ }
4446
+ 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);
4447
+ }
4448
+ function pointInsideBox(point2, box) {
4449
+ return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4450
+ }
4451
+ function rangesOverlap(a, b, min, max) {
4452
+ const low = Math.min(a, b);
4453
+ const high = Math.max(a, b);
4454
+ return high > min && low < max;
4455
+ }
4456
+ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
4457
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4458
+ if (denominator === 0) {
4459
+ return false;
4460
+ }
4461
+ const t = ((x1 - start.x) * (y2 - y1) - (y1 - start.y) * (x2 - x1)) / denominator;
4462
+ const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
4463
+ return t > 0 && t < 1 && u > 0 && u < 1;
4464
+ }
2898
4465
  function segmentBox(a, b) {
2899
4466
  const minX = Math.min(a.x, b.x);
2900
4467
  const minY = Math.min(a.y, b.y);
@@ -2910,32 +4477,107 @@ function areCollinear(a, b, c) {
2910
4477
  }
2911
4478
 
2912
4479
  // src/solver/solve.ts
4480
+ var DEFAULT_MATRIX_CELL_SIZE2 = { width: 120, height: 36 };
4481
+ var DEFAULT_TABLE_CELL_SIZE2 = { width: 128, height: 34 };
4482
+ var DEFAULT_PANEL_WIDTH = 320;
4483
+ var DEFAULT_PANEL_ITEM_HEIGHT2 = 28;
4484
+ var DEFAULT_EVIDENCE_BLOCK_GAP = 24;
4485
+ var EDGE_LABEL_CLEARANCE = 8;
4486
+ var DEFAULT_CJK_FONT_FAMILY = "YaHei,SimSun,sans-serif";
4487
+ var DEFAULT_MIN_CJK_FONT_SIZE = 14;
4488
+ function prefitLabelFont(node, _options) {
4489
+ const cjk = labelCjkTypography(node.label?.metadata);
4490
+ const fontFamily = cjk.fontFamily ?? DEFAULT_FONT.fontFamily;
4491
+ const fontSize = cjk.fontSize ?? DEFAULT_FONT.fontSize;
4492
+ const lineHeight = fontSize !== DEFAULT_FONT.fontSize ? Math.max(DEFAULT_FONT.lineHeight ?? 18, fontSize * 1.2) : DEFAULT_FONT.lineHeight ?? 18;
4493
+ return { fontFamily, fontSize, lineHeight };
4494
+ }
4495
+ var EVIDENCE_TEXT_FONT = {
4496
+ fontFamily: "Arial, sans-serif",
4497
+ fontSize: 10,
4498
+ lineHeight: 12
4499
+ };
2913
4500
  function solveDiagram(diagram, options = {}) {
2914
4501
  const diagnostics = [...diagram.diagnostics];
2915
- const nodes = stableById(diagram.nodes);
2916
- const edges = stableById(diagram.edges);
2917
- const groups = stableById(diagram.groups);
4502
+ const nodes = stableUniqueById(
4503
+ diagram.nodes,
4504
+ diagnostics,
4505
+ "nodes",
4506
+ "duplicate_node_id"
4507
+ );
4508
+ const edges = stableUniqueById(
4509
+ diagram.edges,
4510
+ diagnostics,
4511
+ "edges",
4512
+ "duplicate_edge_id"
4513
+ );
4514
+ const groups = stableUniqueById(
4515
+ diagram.groups,
4516
+ diagnostics,
4517
+ "groups",
4518
+ "duplicate_group_id"
4519
+ );
4520
+ const cjkTypography = createCjkTypographyOptions(options);
4521
+ const cjkStyledNodes = nodes.map(
4522
+ (node) => enhanceNodeCjkTypography(node, cjkTypography, diagnostics)
4523
+ );
4524
+ const styledNodes = options.prefitLabelSize === true ? cjkStyledNodes.map(
4525
+ (node) => prefitNodeLabelSize(node, options, diagnostics)
4526
+ ) : cjkStyledNodes;
4527
+ const styledEdges = edges.map(
4528
+ (edge) => enhanceEdgeCjkTypography(edge, cjkTypography, diagnostics)
4529
+ );
4530
+ const styledGroups = groups.map(
4531
+ (group) => enhanceGroupCjkTypography(group, cjkTypography, diagnostics)
4532
+ );
4533
+ const styledSwimlanes = (diagram.swimlanes ?? []).map(
4534
+ (swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
4535
+ );
2918
4536
  const constraints = stableByConstraintId(diagram.constraints);
2919
4537
  const layout2 = runDagreInitialLayout({
2920
4538
  direction: diagram.direction,
2921
- nodes: nodes.map((node) => ({ id: node.id, size: node.size })),
2922
- edges: edges.map((edge) => ({
4539
+ nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4540
+ edges: styledEdges.map((edge) => ({
2923
4541
  id: edge.id,
2924
4542
  sourceId: edge.source.nodeId,
2925
4543
  targetId: edge.target.nodeId
2926
4544
  }))
2927
4545
  });
2928
4546
  diagnostics.push(...layout2.diagnostics);
4547
+ const initialNodeBoxes = wrapVerticalStackIfNeeded(
4548
+ layout2.boxes,
4549
+ styledNodes,
4550
+ styledEdges,
4551
+ diagram.direction,
4552
+ options,
4553
+ diagnostics
4554
+ );
2929
4555
  const constrained = applyLayoutConstraints({
2930
4556
  direction: diagram.direction,
2931
4557
  overlapSpacing: options?.overlapSpacing ?? 40,
2932
- boxes: layout2.boxes,
2933
- nodes,
4558
+ ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4559
+ ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
4560
+ boxes: initialNodeBoxes,
4561
+ nodes: styledNodes,
2934
4562
  constraints
2935
4563
  });
2936
4564
  diagnostics.push(...constrained.diagnostics);
4565
+ const swimlaneContracts = applySwimlaneLayoutContracts(
4566
+ styledSwimlanes,
4567
+ constraints,
4568
+ styledEdges,
4569
+ isTopToBottomReadingDirection(diagram.metadata?.primaryReadingDirection),
4570
+ constrained.boxes,
4571
+ constrained.locks,
4572
+ options?.overlapSpacing ?? 40,
4573
+ Math.max(0, options?.minLaneGutter ?? 0)
4574
+ );
4575
+ if (swimlaneContracts.layouts.size > 0) {
4576
+ removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4577
+ }
4578
+ diagnostics.push(...swimlaneContracts.diagnostics);
2937
4579
  const coordinatedNodes = coordinateNodes(
2938
- nodes,
4580
+ styledNodes,
2939
4581
  constrained.boxes,
2940
4582
  options,
2941
4583
  diagnostics
@@ -2951,28 +4593,31 @@ function solveDiagram(diagram, options = {}) {
2951
4593
  ])
2952
4594
  );
2953
4595
  const coordinatedGroups = coordinateGroups(
2954
- groups,
4596
+ styledGroups,
2955
4597
  constrained.boxes,
2956
4598
  options,
2957
4599
  diagnostics
2958
4600
  );
2959
4601
  const coordinatedSwimlanes = coordinateSwimlanes(
2960
- diagram.swimlanes ?? [],
2961
- constrained.boxes
4602
+ styledSwimlanes,
4603
+ constrained.boxes,
4604
+ swimlaneContracts.layouts
4605
+ );
4606
+ const coordinatedMatrices = coordinateMatrices(diagram.matrices ?? []);
4607
+ const coordinatedTables = coordinateTables(diagram.tables ?? []);
4608
+ const coordinatedEvidencePanels = coordinateEvidencePanels(
4609
+ diagram.evidencePanels ?? []
2962
4610
  );
2963
4611
  const groupBoxes = new Map(
2964
4612
  coordinatedGroups.map((group) => [group.id, group.box])
2965
4613
  );
2966
- const coordinatedEdges = coordinateEdges(
2967
- edges,
2968
- nodeGeometryById,
2969
- coordinatedNodes,
2970
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
2971
- diagram.direction,
2972
- options,
2973
- diagnostics
2974
- );
2975
- const allBoxes = [
4614
+ const baseTextAnnotations = coordinateBaseTextAnnotations({
4615
+ nodes: coordinatedNodes,
4616
+ groups: coordinatedGroups,
4617
+ swimlanes: coordinatedSwimlanes,
4618
+ ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4619
+ });
4620
+ const layoutBoxes = [
2976
4621
  ...coordinatedNodes.map((node) => node.box),
2977
4622
  ...coordinatedNodes.flatMap(
2978
4623
  (node) => (node.ports ?? []).flatMap(
@@ -2982,10 +4627,122 @@ function solveDiagram(diagram, options = {}) {
2982
4627
  ...groupBoxes.values(),
2983
4628
  ...coordinatedSwimlanes.flatMap(
2984
4629
  (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
2985
- )
4630
+ ),
4631
+ ...baseTextAnnotations.map((annotation) => annotation.box)
4632
+ ];
4633
+ const initialContentBounds = layoutBoxes.length === 0 ? { x: 0, y: 0, width: 0} : unionBoxes(layoutBoxes);
4634
+ placeEvidenceBlocks(
4635
+ [
4636
+ ...coordinatedMatrices,
4637
+ ...coordinatedTables,
4638
+ ...coordinatedEvidencePanels
4639
+ ],
4640
+ initialContentBounds
4641
+ );
4642
+ refreshTableColumnXOffsets(coordinatedTables);
4643
+ measureEvidenceTextBlocks(
4644
+ coordinatedMatrices,
4645
+ coordinatedTables,
4646
+ coordinatedEvidencePanels,
4647
+ options.textMeasurer
4648
+ );
4649
+ const evidenceBoxes = [
4650
+ ...coordinatedMatrices.map((matrix) => matrix.box),
4651
+ ...coordinatedTables.map((table) => table.box),
4652
+ ...coordinatedEvidencePanels.map((panel) => panel.box)
2986
4653
  ];
4654
+ diagnostics.push(
4655
+ ...reportEvidenceBlockOverlaps(
4656
+ [
4657
+ ...coordinatedMatrices.map((matrix) => ({
4658
+ id: matrix.id,
4659
+ kind: "matrix",
4660
+ ...matrix.position === void 0 ? {} : { position: matrix.position },
4661
+ box: matrix.box
4662
+ })),
4663
+ ...coordinatedTables.map((table) => ({
4664
+ id: table.id,
4665
+ kind: "table",
4666
+ ...table.position === void 0 ? {} : { position: table.position },
4667
+ box: table.box
4668
+ })),
4669
+ ...coordinatedEvidencePanels.map((panel) => ({
4670
+ id: panel.id,
4671
+ kind: "evidence-panel",
4672
+ ...panel.position === void 0 ? {} : { position: panel.position },
4673
+ box: panel.box
4674
+ }))
4675
+ ],
4676
+ [
4677
+ ...coordinatedNodes.map((node) => ({
4678
+ id: node.id,
4679
+ kind: "node",
4680
+ box: node.box
4681
+ })),
4682
+ ...coordinatedGroups.map((group) => ({
4683
+ id: group.id,
4684
+ kind: "group",
4685
+ box: group.box
4686
+ })),
4687
+ ...coordinatedSwimlanes.flatMap(
4688
+ (swimlane) => swimlane.box === void 0 ? [] : [{ id: swimlane.id, kind: "swimlane", box: swimlane.box }]
4689
+ )
4690
+ ]
4691
+ )
4692
+ );
4693
+ const allBoxes = [...layoutBoxes, ...evidenceBoxes];
2987
4694
  const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
2988
4695
  const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
4696
+ const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
4697
+ const routingTextObstacles = [
4698
+ ...baseTextAnnotations.filter(isPreRouteTextObstacle),
4699
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle)
4700
+ ];
4701
+ const coordinatedEdges = coordinateEdges(
4702
+ styledEdges,
4703
+ nodeGeometryById,
4704
+ coordinatedNodes,
4705
+ [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
4706
+ [
4707
+ ...coordinatedTables.map((table) => table.box),
4708
+ ...coordinatedEvidencePanels.map((panel) => panel.box)
4709
+ ],
4710
+ routingTextObstacles,
4711
+ coordinatedMatrices.map((matrix) => matrix.box),
4712
+ diagram.direction,
4713
+ options,
4714
+ diagnostics
4715
+ );
4716
+ const edgeTextAnnotations = coordinateEdgeTextAnnotations(
4717
+ coordinatedEdges,
4718
+ [
4719
+ ...coordinatedNodes.map((node) => node.box),
4720
+ ...baseTextAnnotations.map((annotation) => annotation.box),
4721
+ ...frameTextAnnotation.map((annotation) => annotation.box)
4722
+ ],
4723
+ options.textMeasurer
4724
+ );
4725
+ const textAnnotations = [
4726
+ ...baseTextAnnotations,
4727
+ ...frameTextAnnotation,
4728
+ ...edgeTextAnnotations
4729
+ ];
4730
+ diagnostics.push(...reportTextAnnotationCollisions(textAnnotations));
4731
+ diagnostics.push(
4732
+ ...reportRouteTextClearance(coordinatedEdges, textAnnotations)
4733
+ );
4734
+ const edgePointBounds = edgeBounds(coordinatedEdges);
4735
+ const boundsBase = [
4736
+ contentBounds,
4737
+ ...edgePointBounds,
4738
+ ...edgeTextAnnotations.map((annotation) => annotation.box)
4739
+ ];
4740
+ diagnostics.push(
4741
+ ...reportPageOverflow(
4742
+ frame === void 0 ? unionBoxes(boundsBase) : unionBoxes([...boundsBase, frame.box, frame.titleBox]),
4743
+ options.pageBounds
4744
+ )
4745
+ );
2989
4746
  return {
2990
4747
  id: diagram.id,
2991
4748
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -2994,34 +4751,1039 @@ function solveDiagram(diagram, options = {}) {
2994
4751
  edges: coordinatedEdges,
2995
4752
  groups: coordinatedGroups,
2996
4753
  ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
4754
+ ...coordinatedMatrices.length === 0 ? {} : { matrices: coordinatedMatrices },
4755
+ ...coordinatedTables.length === 0 ? {} : { tables: coordinatedTables },
4756
+ ...coordinatedEvidencePanels.length === 0 ? {} : { evidencePanels: coordinatedEvidencePanels },
2997
4757
  diagnostics,
2998
- bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
4758
+ bounds: frame === void 0 ? unionBoxes(boundsBase) : unionBoxes([...boundsBase, frame.box, frame.titleBox]),
2999
4759
  ...frame === void 0 ? {} : { frame },
4760
+ ...textAnnotations.length === 0 ? {} : { textAnnotations },
3000
4761
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
3001
4762
  };
3002
4763
  }
3003
- function coordinateNodes(nodes, boxes, options, diagnostics) {
3004
- const coordinated = [];
3005
- for (const node of nodes) {
3006
- const box = boxes.get(node.id);
3007
- if (box === void 0) {
3008
- diagnostics.push({
3009
- severity: "error",
3010
- code: "solver.node-box.missing",
3011
- message: `Node ${node.id} has no solved box.`,
3012
- path: ["nodes", node.id],
3013
- detail: { nodeId: node.id }
3014
- });
3015
- continue;
3016
- }
3017
- const geometry = computeShapeGeometry({
3018
- shape: node.shape,
3019
- box,
3020
- obstacleMargin: options.obstacleMargin ?? 0
4764
+ function solveDiagramSafe(diagram, options = {}) {
4765
+ return solveDiagram(diagram, { ...options, prefitLabelSize: true });
4766
+ }
4767
+ function prefitNodeLabelSize(node, options, diagnostics) {
4768
+ if (node.label === void 0) {
4769
+ return node;
4770
+ }
4771
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
4772
+ const layout2 = fitLabel(
4773
+ node.label.text,
4774
+ {
4775
+ font: prefitLabelFont(node),
4776
+ padding: DEFAULT_NODE_PADDING,
4777
+ minSize: DEFAULT_NODE_MIN_SIZE,
4778
+ maxWidth: node.label.maxWidth ?? Math.max(node.size.width, DEFAULT_LABEL_MAX_WIDTH)
4779
+ },
4780
+ measurer
4781
+ );
4782
+ const width = Math.max(node.size.width, layout2.fittedSize.width);
4783
+ const height = Math.max(node.size.height, layout2.fittedSize.height);
4784
+ const resized = width !== node.size.width || height !== node.size.height;
4785
+ if (resized) {
4786
+ diagnostics.push({
4787
+ severity: "info",
4788
+ code: "prefit_label_resized",
4789
+ message: `Node ${node.id} size expanded to fit its label.`,
4790
+ path: ["nodes", node.id],
4791
+ detail: {
4792
+ nodeId: node.id,
4793
+ from: { width: node.size.width, height: node.size.height },
4794
+ to: { width, height }
4795
+ }
3021
4796
  });
3022
- coordinated.push({
3023
- id: node.id,
3024
- ...node.label === void 0 ? {} : { label: node.label },
4797
+ }
4798
+ const centeredLayout = expandLabelLayoutToNode(layout2, { width, height });
4799
+ return { ...node, size: { width, height }, labelLayout: centeredLayout };
4800
+ }
4801
+ function expandLabelLayoutToNode(layout2, nodeSize) {
4802
+ if (layout2.box.width >= nodeSize.width && layout2.box.height >= nodeSize.height) {
4803
+ return layout2;
4804
+ }
4805
+ const offsetX = Math.max(0, (nodeSize.width - layout2.box.width) / 2);
4806
+ const offsetY = Math.max(0, (nodeSize.height - layout2.box.height) / 2);
4807
+ if (offsetX === 0 && offsetY === 0) {
4808
+ return layout2;
4809
+ }
4810
+ return {
4811
+ ...layout2,
4812
+ box: {
4813
+ x: layout2.box.x + offsetX,
4814
+ y: layout2.box.y + offsetY,
4815
+ width: layout2.box.width,
4816
+ height: layout2.box.height
4817
+ },
4818
+ contentBox: {
4819
+ x: layout2.contentBox.x + offsetX,
4820
+ y: layout2.contentBox.y + offsetY,
4821
+ width: layout2.contentBox.width,
4822
+ height: layout2.contentBox.height
4823
+ },
4824
+ lines: layout2.lines.map((line) => ({
4825
+ ...line,
4826
+ box: {
4827
+ x: line.box.x + offsetX,
4828
+ y: line.box.y + offsetY,
4829
+ width: line.box.width,
4830
+ height: line.box.height
4831
+ }
4832
+ }))
4833
+ };
4834
+ }
4835
+ function reportPageOverflow(contentBounds, pageBounds) {
4836
+ if (pageBounds === void 0) {
4837
+ return [];
4838
+ }
4839
+ const overflowRight = Math.max(
4840
+ 0,
4841
+ contentBounds.x + contentBounds.width - pageBounds.width
4842
+ );
4843
+ const overflowBottom = Math.max(
4844
+ 0,
4845
+ contentBounds.y + contentBounds.height - pageBounds.height
4846
+ );
4847
+ const overflowLeft = Math.max(0, -contentBounds.x);
4848
+ const overflowTop = Math.max(0, -contentBounds.y);
4849
+ if (overflowRight === 0 && overflowBottom === 0 && overflowLeft === 0 && overflowTop === 0) {
4850
+ return [];
4851
+ }
4852
+ return [
4853
+ {
4854
+ severity: "warning",
4855
+ code: "page_overflow",
4856
+ message: `Content ${contentBounds.width}x${contentBounds.height} exceeds page ${pageBounds.width}x${pageBounds.height}.`,
4857
+ path: ["bounds"],
4858
+ detail: {
4859
+ page: { width: pageBounds.width, height: pageBounds.height },
4860
+ content: {
4861
+ width: contentBounds.width,
4862
+ height: contentBounds.height
4863
+ },
4864
+ overflow: {
4865
+ right: overflowRight,
4866
+ bottom: overflowBottom,
4867
+ left: overflowLeft,
4868
+ top: overflowTop
4869
+ }
4870
+ }
4871
+ }
4872
+ ];
4873
+ }
4874
+ function createCjkTypographyOptions(options) {
4875
+ const fontFamily = options.cjkFontFamily === false ? void 0 : options.cjkFontFamily ?? DEFAULT_CJK_FONT_FAMILY;
4876
+ const minFontSize = options.minCjkFontSize === false ? void 0 : options.minCjkFontSize ?? DEFAULT_MIN_CJK_FONT_SIZE;
4877
+ return {
4878
+ ...fontFamily === void 0 ? {} : { fontFamily },
4879
+ ...minFontSize === void 0 ? {} : { minFontSize }
4880
+ };
4881
+ }
4882
+ function enhanceNodeCjkTypography(node, options, diagnostics) {
4883
+ const nodeWithStyle = enhanceStyledLabelOwner(
4884
+ node,
4885
+ ["nodes", node.id],
4886
+ options,
4887
+ diagnostics
4888
+ );
4889
+ const ports = nodeWithStyle.ports === void 0 ? void 0 : nodeWithStyle.ports.map(
4890
+ (port) => enhanceStyledLabelOwner(
4891
+ port,
4892
+ ["nodes", node.id, "ports", port.id],
4893
+ options,
4894
+ diagnostics
4895
+ )
4896
+ );
4897
+ return ports === void 0 ? nodeWithStyle : { ...nodeWithStyle, ports };
4898
+ }
4899
+ function enhanceEdgeCjkTypography(edge, options, diagnostics) {
4900
+ return enhanceStyledLabelOwner(
4901
+ edge,
4902
+ ["edges", edge.id],
4903
+ options,
4904
+ diagnostics
4905
+ );
4906
+ }
4907
+ function enhanceGroupCjkTypography(group, options, diagnostics) {
4908
+ return enhanceStyledLabelOwner(
4909
+ group,
4910
+ ["groups", group.id],
4911
+ options,
4912
+ diagnostics
4913
+ );
4914
+ }
4915
+ function enhanceSwimlaneCjkTypography(swimlane, options, diagnostics) {
4916
+ const root = enhanceStyledLabelOwner(
4917
+ swimlane,
4918
+ ["swimlanes", swimlane.id],
4919
+ options,
4920
+ diagnostics
4921
+ );
4922
+ const lanes = root.lanes.map(
4923
+ (lane) => enhanceSwimlaneLaneCjkTypography(swimlane.id, lane, options, diagnostics)
4924
+ );
4925
+ return { ...root, lanes };
4926
+ }
4927
+ function enhanceSwimlaneLaneCjkTypography(swimlaneId, lane, options, diagnostics) {
4928
+ return enhanceStyledLabelOwner(
4929
+ lane,
4930
+ ["swimlanes", swimlaneId, "lanes", lane.id],
4931
+ options,
4932
+ diagnostics
4933
+ );
4934
+ }
4935
+ function enhanceStyledLabelOwner(owner, path, options, diagnostics) {
4936
+ const text = owner.label?.text;
4937
+ if (text === void 0 || !containsCjk(text)) {
4938
+ return owner;
4939
+ }
4940
+ const typography = cjkTypographyForOwner(owner, options);
4941
+ if (typography.fontFamily === void 0 && typography.fontSize === void 0) {
4942
+ return owner;
4943
+ }
4944
+ const label = owner.label;
4945
+ if (label === void 0) {
4946
+ return owner;
4947
+ }
4948
+ const nextLabel = {
4949
+ ...label,
4950
+ metadata: {
4951
+ ...metadataObject(label.metadata),
4952
+ cjkTypography: typography
4953
+ }
4954
+ };
4955
+ const nextOwner = { ...owner, label: nextLabel };
4956
+ const maybeStyled = nextOwner;
4957
+ const nextStyle = enhanceCjkStyle(maybeStyled.style, typography);
4958
+ reportCjkTypographyDiagnostics(
4959
+ path,
4960
+ typography,
4961
+ maybeStyled.style,
4962
+ diagnostics
4963
+ );
4964
+ return nextStyle === maybeStyled.style ? nextOwner : { ...nextOwner, style: nextStyle };
4965
+ }
4966
+ function cjkTypographyForOwner(owner, options) {
4967
+ const metadataTypography = labelCjkTypography(owner.label?.metadata);
4968
+ const fontFamily = metadataTypography.fontFamily ?? owner.style?.fontFamily ?? options.fontFamily;
4969
+ const fontSize = boostedCjkFontSize(
4970
+ metadataTypography.fontSize ?? owner.style?.fontSize,
4971
+ options.minFontSize
4972
+ );
4973
+ return {
4974
+ ...fontFamily === void 0 ? {} : { fontFamily },
4975
+ ...fontSize === void 0 ? {} : { fontSize }
4976
+ };
4977
+ }
4978
+ function labelCjkTypography(metadata) {
4979
+ const metadataRecord = metadataObject(metadata);
4980
+ if (metadataRecord === void 0) {
4981
+ return {};
4982
+ }
4983
+ const value = metadataRecord.cjkTypography;
4984
+ if (value === void 0 || value === null || typeof value !== "object") {
4985
+ return {};
4986
+ }
4987
+ const typography = value;
4988
+ const fontFamily = typeof typography.fontFamily === "string" ? typography.fontFamily : void 0;
4989
+ const fontSize = typeof typography.fontSize === "number" && Number.isFinite(typography.fontSize) && typography.fontSize > 0 ? typography.fontSize : void 0;
4990
+ return {
4991
+ ...fontFamily === void 0 ? {} : { fontFamily },
4992
+ ...fontSize === void 0 ? {} : { fontSize }
4993
+ };
4994
+ }
4995
+ function metadataObject(metadata) {
4996
+ if (metadata === void 0 || metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
4997
+ return void 0;
4998
+ }
4999
+ return metadata;
5000
+ }
5001
+ function typographyForLabel(label) {
5002
+ return labelCjkTypography(label?.metadata);
5003
+ }
5004
+ function typographyTextStyle(label, base) {
5005
+ const typography = typographyForLabel(label);
5006
+ return {
5007
+ ...base,
5008
+ ...typography.fontFamily === void 0 ? {} : { fontFamily: typography.fontFamily },
5009
+ ...typography.fontSize === void 0 ? {} : {
5010
+ fontSize: typography.fontSize,
5011
+ lineHeight: Math.max(base.lineHeight ?? 0, typography.fontSize * 1.2)
5012
+ }
5013
+ };
5014
+ }
5015
+ function boostedCjkFontSize(current, minFontSize) {
5016
+ if (minFontSize === void 0) {
5017
+ return current;
5018
+ }
5019
+ if (current === void 0 || current < minFontSize) {
5020
+ return minFontSize;
5021
+ }
5022
+ return current;
5023
+ }
5024
+ function enhanceCjkStyle(style2, typography) {
5025
+ let next = style2;
5026
+ if (typography.fontFamily !== void 0 && next?.fontFamily === void 0) {
5027
+ next = { ...next, fontFamily: typography.fontFamily };
5028
+ }
5029
+ if (typography.fontSize !== void 0 && (next?.fontSize === void 0 || next.fontSize < typography.fontSize)) {
5030
+ next = { ...next, fontSize: typography.fontSize };
5031
+ }
5032
+ return next;
5033
+ }
5034
+ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnostics) {
5035
+ if (typography.fontFamily !== void 0 && previousStyle?.fontFamily === void 0) {
5036
+ diagnostics.push({
5037
+ severity: "info",
5038
+ code: "cjk_font_family_applied",
5039
+ message: `Applied CJK font family ${typography.fontFamily}.`,
5040
+ path: [...path, "label", "metadata", "cjkTypography", "fontFamily"],
5041
+ detail: { fontFamily: typography.fontFamily }
5042
+ });
5043
+ }
5044
+ if (typography.fontSize !== void 0 && (previousStyle?.fontSize === void 0 || previousStyle.fontSize < typography.fontSize)) {
5045
+ diagnostics.push({
5046
+ severity: "info",
5047
+ code: "cjk_font_size_boosted",
5048
+ message: `Raised CJK font size to ${typography.fontSize}.`,
5049
+ path: [...path, "label", "metadata", "cjkTypography", "fontSize"],
5050
+ detail: {
5051
+ minFontSize: typography.fontSize,
5052
+ ...previousStyle?.fontSize === void 0 ? {} : { previousFontSize: previousStyle.fontSize }
5053
+ }
5054
+ });
5055
+ }
5056
+ }
5057
+ function containsCjk(value) {
5058
+ return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
5059
+ }
5060
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
5061
+ const layouts = /* @__PURE__ */ new Map();
5062
+ const diagnostics = [];
5063
+ const movedChildIds = /* @__PURE__ */ new Set();
5064
+ for (const swimlane of swimlanes) {
5065
+ if ((swimlane.layout ?? "overlay") !== "contract") {
5066
+ continue;
5067
+ }
5068
+ if (swimlane.lanes.length === 0) {
5069
+ continue;
5070
+ }
5071
+ const layout2 = applySingleSwimlaneContract(
5072
+ swimlane,
5073
+ edges,
5074
+ topToBottomFlow,
5075
+ nodeBoxes,
5076
+ locks,
5077
+ diagnostics,
5078
+ movedChildIds,
5079
+ laneGutter
5080
+ );
5081
+ if (layout2 !== void 0) {
5082
+ layouts.set(swimlane.id, layout2);
5083
+ }
5084
+ }
5085
+ if (layouts.size > 0) {
5086
+ diagnostics.push(
5087
+ ...reportSwimlaneOverlaps(nodeBoxes, locks, overlapSpacing),
5088
+ ...reportSwimlaneConstraintInvalidations(
5089
+ constraints,
5090
+ nodeBoxes,
5091
+ movedChildIds
5092
+ )
5093
+ );
5094
+ if (laneGutter > 0) {
5095
+ diagnostics.push({
5096
+ severity: "info",
5097
+ code: "lane_gutter_applied",
5098
+ message: `Applied ${laneGutter}px gutter between ${layouts.size} contract swimlane lane(s).`,
5099
+ path: ["swimlanes"],
5100
+ detail: { laneGutter, swimlaneCount: layouts.size }
5101
+ });
5102
+ }
5103
+ }
5104
+ return { layouts, diagnostics, movedChildIds };
5105
+ }
5106
+ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diagnostics) {
5107
+ const wrapped = new Map([...boxes].map(([id, box]) => [id, { ...box }]));
5108
+ const maxStackDepth = options.maxStackDepth;
5109
+ if (maxStackDepth === void 0 || maxStackDepth <= 0 || nodes.length <= maxStackDepth) {
5110
+ reportVerticalRunaway(
5111
+ wrapped,
5112
+ nodes,
5113
+ edges,
5114
+ direction,
5115
+ options,
5116
+ diagnostics
5117
+ );
5118
+ return wrapped;
5119
+ }
5120
+ if (edges.length > 0 || !isVerticalRunaway(wrapped, nodes, direction, options)) {
5121
+ reportVerticalRunaway(
5122
+ wrapped,
5123
+ nodes,
5124
+ edges,
5125
+ direction,
5126
+ options,
5127
+ diagnostics
5128
+ );
5129
+ return wrapped;
5130
+ }
5131
+ const ordered = nodes.map((node) => ({ node, box: wrapped.get(node.id) })).filter(
5132
+ (item) => item.box !== void 0
5133
+ ).sort((a, b) => {
5134
+ const delta = a.box.y - b.box.y;
5135
+ return delta === 0 ? a.node.id.localeCompare(b.node.id) : delta;
5136
+ });
5137
+ const columns = Math.ceil(ordered.length / maxStackDepth);
5138
+ const horizontalGap = options.overlapSpacing ?? 40;
5139
+ const verticalGap = Math.max(24, horizontalGap / 2);
5140
+ const columnWidths = Array.from(
5141
+ { length: columns },
5142
+ (_, column) => Math.max(
5143
+ 0,
5144
+ ...ordered.slice(column * maxStackDepth, (column + 1) * maxStackDepth).map((item) => item.box.width)
5145
+ )
5146
+ );
5147
+ const startX = Math.min(...ordered.map((item) => item.box.x));
5148
+ const startY = Math.min(...ordered.map((item) => item.box.y));
5149
+ let columnX = startX;
5150
+ for (let column = 0; column < columns; column += 1) {
5151
+ let y = startY;
5152
+ const items = ordered.slice(
5153
+ column * maxStackDepth,
5154
+ (column + 1) * maxStackDepth
5155
+ );
5156
+ for (const item of items) {
5157
+ wrapped.set(item.node.id, { ...item.box, x: columnX, y });
5158
+ y += item.box.height + verticalGap;
5159
+ }
5160
+ columnX += (columnWidths[column] ?? 0) + horizontalGap;
5161
+ }
5162
+ diagnostics.push({
5163
+ severity: "warning",
5164
+ code: "vertical_runaway",
5165
+ message: `Single-column layout exceeded maxStackDepth ${maxStackDepth}; wrapped into ${columns} columns.`,
5166
+ path: ["nodes"],
5167
+ detail: { nodeCount: ordered.length, maxStackDepth, columns }
5168
+ });
5169
+ return wrapped;
5170
+ }
5171
+ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnostics) {
5172
+ if (!isVerticalRunaway(boxes, nodes, direction, options)) {
5173
+ return;
5174
+ }
5175
+ diagnostics.push({
5176
+ severity: "warning",
5177
+ code: "vertical_runaway",
5178
+ message: "Layout produced a tall vertical stack beyond the preferred aspect ratio.",
5179
+ path: ["nodes"],
5180
+ detail: {
5181
+ nodeCount: nodes.length,
5182
+ edgeCount: edges.length,
5183
+ ...options.preferredAspectRatio === void 0 ? {} : { preferredAspectRatio: options.preferredAspectRatio },
5184
+ ...options.maxStackDepth === void 0 ? {} : { maxStackDepth: options.maxStackDepth }
5185
+ }
5186
+ });
5187
+ }
5188
+ function isVerticalRunaway(boxes, nodes, direction, options) {
5189
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
5190
+ return false;
5191
+ }
5192
+ if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
5193
+ return false;
5194
+ }
5195
+ const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
5196
+ if (nodeBoxes.length < 2) {
5197
+ return false;
5198
+ }
5199
+ const bounds = unionBoxes(nodeBoxes);
5200
+ const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
5201
+ const preferred = options.preferredAspectRatio ?? 3;
5202
+ if (aspectRatio < preferred) {
5203
+ return false;
5204
+ }
5205
+ const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
5206
+ const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
5207
+ const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
5208
+ return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
5209
+ }
5210
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
5211
+ const headerHeight = swimlane.headerHeight ?? 28;
5212
+ const padding = swimlane.padding ?? 16;
5213
+ const laneBounds = swimlane.lanes.map((lane) => {
5214
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
5215
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
5216
+ });
5217
+ const populatedBounds = laneBounds.filter(
5218
+ (box) => box !== void 0
5219
+ );
5220
+ if (populatedBounds.length === 0) {
5221
+ return void 0;
5222
+ }
5223
+ if (swimlane.orientation === "vertical") {
5224
+ return applyVerticalSwimlaneContract(
5225
+ swimlane,
5226
+ edges,
5227
+ topToBottomFlow,
5228
+ nodeBoxes,
5229
+ laneBounds,
5230
+ headerHeight,
5231
+ padding,
5232
+ locks,
5233
+ diagnostics,
5234
+ movedChildIds,
5235
+ laneGutter
5236
+ );
5237
+ }
5238
+ return applyHorizontalSwimlaneContract(
5239
+ swimlane,
5240
+ nodeBoxes,
5241
+ laneBounds,
5242
+ headerHeight,
5243
+ padding,
5244
+ locks,
5245
+ diagnostics,
5246
+ movedChildIds,
5247
+ laneGutter
5248
+ );
5249
+ }
5250
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
5251
+ const populatedBounds = laneBounds.filter(
5252
+ (box) => box !== void 0
5253
+ );
5254
+ const top = Math.min(...populatedBounds.map((box) => box.y));
5255
+ const left = Math.min(...populatedBounds.map((box) => box.x));
5256
+ const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
5257
+ const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
5258
+ const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
5259
+ const rankStackGap = Math.max(8, padding / 2);
5260
+ const maxRankStackHeight = maxVerticalRankStackHeight(
5261
+ swimlane,
5262
+ nodeBoxes,
5263
+ flowRanks,
5264
+ rankStackGap
5265
+ );
5266
+ const rankSpacing = Math.max(96, maxRankStackHeight + padding);
5267
+ const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
5268
+ const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
5269
+ const laneStep = slotWidth + laneGutter;
5270
+ const laneContentTop = top + headerHeight + padding;
5271
+ for (let index = 0; index < swimlane.lanes.length; index += 1) {
5272
+ const lane = swimlane.lanes[index];
5273
+ const bounds = laneBounds[index];
5274
+ if (lane === void 0 || bounds === void 0) {
5275
+ continue;
5276
+ }
5277
+ const target = {
5278
+ x: left + laneStep * index + padding,
5279
+ y: laneContentTop
5280
+ };
5281
+ if (maxRank === 0) {
5282
+ moveLaneChildren(
5283
+ lane.children,
5284
+ nodeBoxes,
5285
+ locks,
5286
+ diagnostics,
5287
+ movedChildIds,
5288
+ {
5289
+ x: target.x - bounds.x,
5290
+ y: target.y - bounds.y
5291
+ }
5292
+ );
5293
+ continue;
5294
+ }
5295
+ moveRankedVerticalLaneChildren(
5296
+ lane.children,
5297
+ nodeBoxes,
5298
+ locks,
5299
+ diagnostics,
5300
+ movedChildIds,
5301
+ flowRanks,
5302
+ rankSpacing,
5303
+ rankStackGap,
5304
+ {
5305
+ x: target.x - bounds.x,
5306
+ y: laneContentTop
5307
+ }
5308
+ );
5309
+ }
5310
+ return {
5311
+ box: {
5312
+ x: left,
5313
+ y: top,
5314
+ width: laneStep * (swimlane.lanes.length - 1) + slotWidth,
5315
+ height: contentHeight + padding * 2 + headerHeight
5316
+ },
5317
+ slotWidth,
5318
+ slotHeight: contentHeight + padding * 2 + headerHeight,
5319
+ laneStep
5320
+ };
5321
+ }
5322
+ function isTopToBottomReadingDirection(value) {
5323
+ return value === "top_to_bottom" || value === "top-to-bottom";
5324
+ }
5325
+ function rankVerticalSwimlaneChildren(swimlane, edges) {
5326
+ const childOrder = /* @__PURE__ */ new Map();
5327
+ for (const lane of swimlane.lanes) {
5328
+ for (const childId of lane.children) {
5329
+ if (!childOrder.has(childId)) {
5330
+ childOrder.set(childId, childOrder.size);
5331
+ }
5332
+ }
5333
+ }
5334
+ if (childOrder.size === 0) {
5335
+ return /* @__PURE__ */ new Map();
5336
+ }
5337
+ const childIds = new Set(childOrder.keys());
5338
+ const relevantEdges = edges.filter(
5339
+ (edge) => childIds.has(edge.source.nodeId) && childIds.has(edge.target.nodeId) && edge.source.nodeId !== edge.target.nodeId
5340
+ );
5341
+ if (relevantEdges.length === 0) {
5342
+ return /* @__PURE__ */ new Map();
5343
+ }
5344
+ const ranks = new Map([...childIds].map((id) => [id, 0]));
5345
+ const outgoing = /* @__PURE__ */ new Map();
5346
+ const inDegree = new Map([...childIds].map((id) => [id, 0]));
5347
+ for (const edge of relevantEdges) {
5348
+ const targets = outgoing.get(edge.source.nodeId) ?? [];
5349
+ targets.push(edge.target.nodeId);
5350
+ outgoing.set(edge.source.nodeId, targets);
5351
+ inDegree.set(
5352
+ edge.target.nodeId,
5353
+ (inDegree.get(edge.target.nodeId) ?? 0) + 1
5354
+ );
5355
+ }
5356
+ const queue = [...childIds].filter((id) => (inDegree.get(id) ?? 0) === 0).sort((a, b) => (childOrder.get(a) ?? 0) - (childOrder.get(b) ?? 0));
5357
+ let visited = 0;
5358
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
5359
+ const sourceId = queue[cursor];
5360
+ if (sourceId === void 0) {
5361
+ continue;
5362
+ }
5363
+ visited += 1;
5364
+ for (const targetId of outgoing.get(sourceId) ?? []) {
5365
+ ranks.set(
5366
+ targetId,
5367
+ Math.max(ranks.get(targetId) ?? 0, (ranks.get(sourceId) ?? 0) + 1)
5368
+ );
5369
+ const nextInDegree = (inDegree.get(targetId) ?? 0) - 1;
5370
+ inDegree.set(targetId, nextInDegree);
5371
+ if (nextInDegree === 0) {
5372
+ queue.push(targetId);
5373
+ }
5374
+ }
5375
+ }
5376
+ return visited === childIds.size ? ranks : rankCyclicSwimlaneChildren(childIds, relevantEdges);
5377
+ }
5378
+ function rankCyclicSwimlaneChildren(childIds, edges) {
5379
+ const maxRank = Math.max(0, childIds.size - 1);
5380
+ const ranks = new Map([...childIds].map((id) => [id, 0]));
5381
+ for (let iteration = 0; iteration < childIds.size; iteration += 1) {
5382
+ let changed = false;
5383
+ for (const edge of edges) {
5384
+ const nextRank = Math.min(
5385
+ maxRank,
5386
+ (ranks.get(edge.source.nodeId) ?? 0) + 1
5387
+ );
5388
+ if (nextRank > (ranks.get(edge.target.nodeId) ?? 0)) {
5389
+ ranks.set(edge.target.nodeId, nextRank);
5390
+ changed = true;
5391
+ }
5392
+ }
5393
+ if (!changed) {
5394
+ break;
5395
+ }
5396
+ }
5397
+ return ranks;
5398
+ }
5399
+ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
5400
+ let maxHeight = 0;
5401
+ for (const lane of swimlane.lanes) {
5402
+ for (const stack of rankStacks(
5403
+ lane.children,
5404
+ nodeBoxes,
5405
+ flowRanks
5406
+ ).values()) {
5407
+ const height = stack.reduce(
5408
+ (total, item, index) => total + item.box.height + (index === 0 ? 0 : gap),
5409
+ 0
5410
+ );
5411
+ maxHeight = Math.max(maxHeight, height);
5412
+ }
5413
+ }
5414
+ return maxHeight;
5415
+ }
5416
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
5417
+ for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
5418
+ let yOffset = 0;
5419
+ for (const item of stack) {
5420
+ const { childId, box } = item;
5421
+ if (locks.has(childId)) {
5422
+ diagnostics.push({
5423
+ severity: "warning",
5424
+ code: "constraints.locked-target-not-moved",
5425
+ message: `Locked child ${childId} was not moved into contract swimlane slot.`,
5426
+ path: ["swimlanes"],
5427
+ detail: { nodeId: childId }
5428
+ });
5429
+ continue;
5430
+ }
5431
+ const next = {
5432
+ ...box,
5433
+ x: box.x + target.x,
5434
+ y: target.y + rank * rankSpacing + yOffset
5435
+ };
5436
+ if (next.x !== box.x || next.y !== box.y) {
5437
+ movedChildIds.add(childId);
5438
+ }
5439
+ nodeBoxes.set(childId, next);
5440
+ yOffset += box.height + rankStackGap;
5441
+ }
5442
+ }
5443
+ }
5444
+ function rankStacks(childIds, nodeBoxes, flowRanks) {
5445
+ const stacks = /* @__PURE__ */ new Map();
5446
+ for (const childId of childIds) {
5447
+ const box = nodeBoxes.get(childId);
5448
+ if (box === void 0) {
5449
+ continue;
5450
+ }
5451
+ const rank = flowRanks.get(childId) ?? 0;
5452
+ const stack = stacks.get(rank) ?? [];
5453
+ stack.push({ childId, box });
5454
+ stacks.set(rank, stack);
5455
+ }
5456
+ for (const stack of stacks.values()) {
5457
+ stack.sort((a, b) => {
5458
+ const deltaY = a.box.y - b.box.y;
5459
+ return deltaY === 0 ? a.childId.localeCompare(b.childId) : deltaY;
5460
+ });
5461
+ }
5462
+ return stacks;
5463
+ }
5464
+ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
5465
+ const populatedBounds = laneBounds.filter(
5466
+ (box) => box !== void 0
5467
+ );
5468
+ const top = Math.min(...populatedBounds.map((box) => box.y));
5469
+ const left = Math.min(...populatedBounds.map((box) => box.x));
5470
+ const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + headerHeight + padding * 2;
5471
+ const slotHeight = Math.max(...populatedBounds.map((box) => box.height)) + padding * 2;
5472
+ const laneStep = slotHeight + laneGutter;
5473
+ for (let index = 0; index < swimlane.lanes.length; index += 1) {
5474
+ const lane = swimlane.lanes[index];
5475
+ const bounds = laneBounds[index];
5476
+ if (lane === void 0 || bounds === void 0) {
5477
+ continue;
5478
+ }
5479
+ const target = {
5480
+ x: left + headerHeight + padding,
5481
+ y: top + laneStep * index + padding
5482
+ };
5483
+ moveLaneChildren(
5484
+ lane.children,
5485
+ nodeBoxes,
5486
+ locks,
5487
+ diagnostics,
5488
+ movedChildIds,
5489
+ {
5490
+ x: target.x - bounds.x,
5491
+ y: target.y - bounds.y
5492
+ }
5493
+ );
5494
+ }
5495
+ return {
5496
+ box: {
5497
+ x: left,
5498
+ y: top,
5499
+ width: slotWidth,
5500
+ height: laneStep * (swimlane.lanes.length - 1) + slotHeight
5501
+ },
5502
+ slotWidth,
5503
+ slotHeight,
5504
+ laneStep
5505
+ };
5506
+ }
5507
+ function moveLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, offset) {
5508
+ for (const childId of childIds) {
5509
+ const box = nodeBoxes.get(childId);
5510
+ if (box === void 0) {
5511
+ continue;
5512
+ }
5513
+ if (locks.has(childId)) {
5514
+ diagnostics.push({
5515
+ severity: "warning",
5516
+ code: "constraints.locked-target-not-moved",
5517
+ message: `Locked child ${childId} was not moved into contract swimlane slot.`,
5518
+ path: ["swimlanes"],
5519
+ detail: { nodeId: childId }
5520
+ });
5521
+ continue;
5522
+ }
5523
+ if (offset.x !== 0 || offset.y !== 0) {
5524
+ movedChildIds.add(childId);
5525
+ }
5526
+ nodeBoxes.set(childId, {
5527
+ ...box,
5528
+ x: box.x + offset.x,
5529
+ y: box.y + offset.y
5530
+ });
5531
+ }
5532
+ }
5533
+ function removeResolvedOverlapDiagnostics(diagnostics, nodeBoxes) {
5534
+ for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
5535
+ const diagnostic = diagnostics[index];
5536
+ if (diagnostic?.code !== "constraints.overlap.unresolved") {
5537
+ continue;
5538
+ }
5539
+ const firstId = detailString(diagnostic, "firstId");
5540
+ const secondId = detailString(diagnostic, "secondId");
5541
+ const first = firstId === void 0 ? void 0 : nodeBoxes.get(firstId);
5542
+ const second = secondId === void 0 ? void 0 : nodeBoxes.get(secondId);
5543
+ if (first !== void 0 && second !== void 0 && !intersectsAabb(first, second)) {
5544
+ diagnostics.splice(index, 1);
5545
+ }
5546
+ }
5547
+ }
5548
+ function reportSwimlaneConstraintInvalidations(constraints, nodeBoxes, movedChildIds) {
5549
+ const diagnostics = [];
5550
+ for (const constraint of constraints) {
5551
+ const invalidatedNodeIds = movedConstraintNodeIds(
5552
+ constraint,
5553
+ nodeBoxes,
5554
+ movedChildIds
5555
+ );
5556
+ if (invalidatedNodeIds.length === 0) {
5557
+ continue;
5558
+ }
5559
+ diagnostics.push({
5560
+ severity: "warning",
5561
+ code: "constraints.swimlane-contract.invalidated",
5562
+ message: `Contract swimlane placement moved node(s) after ${constraint.kind} constraint solving; final geometry no longer satisfies that constraint.`,
5563
+ path: ["swimlanes"],
5564
+ detail: {
5565
+ constraintKind: constraint.kind,
5566
+ ...constraint.id === void 0 ? {} : { constraintId: constraint.id },
5567
+ nodeIds: invalidatedNodeIds
5568
+ }
5569
+ });
5570
+ }
5571
+ return diagnostics;
5572
+ }
5573
+ function movedConstraintNodeIds(constraint, nodeBoxes, movedChildIds) {
5574
+ switch (constraint.kind) {
5575
+ case "exact-position":
5576
+ return [];
5577
+ case "containment":
5578
+ return movedContainmentViolations(constraint, nodeBoxes, movedChildIds);
5579
+ case "relative-position":
5580
+ return movedRelativeViolations(constraint, nodeBoxes, movedChildIds);
5581
+ case "align":
5582
+ return movedAlignViolations(constraint, nodeBoxes, movedChildIds);
5583
+ case "distribute":
5584
+ return movedDistributeViolations(constraint, nodeBoxes, movedChildIds);
5585
+ }
5586
+ }
5587
+ function movedContainmentViolations(constraint, nodeBoxes, movedChildIds) {
5588
+ const container = nodeBoxes.get(constraint.containerId);
5589
+ if (container === void 0) {
5590
+ return [];
5591
+ }
5592
+ const content = paddedContentBox(container, constraint.padding);
5593
+ return constraint.childIds.filter((childId) => {
5594
+ if (!movedChildIds.has(childId)) {
5595
+ return false;
5596
+ }
5597
+ const child = nodeBoxes.get(childId);
5598
+ return child !== void 0 && !boxInside(child, content);
5599
+ });
5600
+ }
5601
+ function movedRelativeViolations(constraint, nodeBoxes, movedChildIds) {
5602
+ if (!movedChildIds.has(constraint.sourceId) && !movedChildIds.has(constraint.referenceId)) {
5603
+ return [];
5604
+ }
5605
+ const source = nodeBoxes.get(constraint.sourceId);
5606
+ const reference = nodeBoxes.get(constraint.referenceId);
5607
+ if (source === void 0 || reference === void 0) {
5608
+ return [];
5609
+ }
5610
+ return sameBoxPosition(
5611
+ source,
5612
+ expectedRelativeBox(source, reference, constraint)
5613
+ ) ? [] : [constraint.sourceId];
5614
+ }
5615
+ function movedAlignViolations(constraint, nodeBoxes, movedChildIds) {
5616
+ if (!constraint.targetIds.some((id) => movedChildIds.has(id))) {
5617
+ return [];
5618
+ }
5619
+ const targets = constraint.targetIds.map((id) => ({ id, box: nodeBoxes.get(id) })).filter(
5620
+ (target) => target.box !== void 0
5621
+ );
5622
+ const anchor = targets[0];
5623
+ if (anchor === void 0) {
5624
+ return [];
5625
+ }
5626
+ const expected = alignmentValue2(anchor.box, constraint.axis);
5627
+ return targets.filter(
5628
+ (target) => movedChildIds.has(target.id) && !sameNumber(alignmentValue2(target.box, constraint.axis), expected)
5629
+ ).map((target) => target.id);
5630
+ }
5631
+ function movedDistributeViolations(constraint, nodeBoxes, movedChildIds) {
5632
+ if (!constraint.targetIds.some((id) => movedChildIds.has(id))) {
5633
+ return [];
5634
+ }
5635
+ const targets = constraint.targetIds.map((id) => ({ id, box: nodeBoxes.get(id) })).filter(
5636
+ (target) => target.box !== void 0
5637
+ ).sort((a, b) => {
5638
+ const delta = constraint.axis === "horizontal" ? a.box.x - b.box.x : a.box.y - b.box.y;
5639
+ return delta === 0 ? a.id.localeCompare(b.id) : delta;
5640
+ });
5641
+ if (targets.length < 3) {
5642
+ return [];
5643
+ }
5644
+ const first = targets[0];
5645
+ const last = targets.at(-1);
5646
+ if (first === void 0 || last === void 0) {
5647
+ return [];
5648
+ }
5649
+ const expectedSpacing = constraint.spacing ?? (distributionStart2(last.box, constraint.axis) - distributionStart2(first.box, constraint.axis)) / (targets.length - 1);
5650
+ return targets.slice(1).filter((target, index) => {
5651
+ const previous = targets[index];
5652
+ if (previous === void 0 || !movedChildIds.has(target.id)) {
5653
+ return false;
5654
+ }
5655
+ return !sameNumber(
5656
+ distributionStart2(target.box, constraint.axis) - distributionStart2(previous.box, constraint.axis),
5657
+ expectedSpacing
5658
+ );
5659
+ }).map((target) => target.id);
5660
+ }
5661
+ function expectedRelativeBox(source, reference, constraint) {
5662
+ const offset = constraint.offset ?? { x: 0, y: 0 };
5663
+ switch (constraint.relation) {
5664
+ case "above":
5665
+ return {
5666
+ ...source,
5667
+ x: reference.x + offset.x,
5668
+ y: reference.y - source.height + offset.y
5669
+ };
5670
+ case "right-of":
5671
+ return {
5672
+ ...source,
5673
+ x: reference.x + reference.width + offset.x,
5674
+ y: reference.y + offset.y
5675
+ };
5676
+ case "below":
5677
+ return {
5678
+ ...source,
5679
+ x: reference.x + offset.x,
5680
+ y: reference.y + reference.height + offset.y
5681
+ };
5682
+ case "left-of":
5683
+ return {
5684
+ ...source,
5685
+ x: reference.x - source.width + offset.x,
5686
+ y: reference.y + offset.y
5687
+ };
5688
+ }
5689
+ }
5690
+ function paddedContentBox(container, padding) {
5691
+ const margin = padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
5692
+ return {
5693
+ x: container.x + margin.left,
5694
+ y: container.y + margin.top,
5695
+ width: container.width - margin.left - margin.right,
5696
+ height: container.height - margin.top - margin.bottom
5697
+ };
5698
+ }
5699
+ function boxInside(child, container) {
5700
+ return child.x >= container.x && child.y >= container.y && child.x + child.width <= container.x + container.width && child.y + child.height <= container.y + container.height;
5701
+ }
5702
+ function sameBoxPosition(first, second) {
5703
+ return sameNumber(first.x, second.x) && sameNumber(first.y, second.y);
5704
+ }
5705
+ function sameNumber(first, second) {
5706
+ return Math.abs(first - second) < 1e-3;
5707
+ }
5708
+ function alignmentValue2(box, axis) {
5709
+ switch (axis) {
5710
+ case "x":
5711
+ case "left":
5712
+ return box.x;
5713
+ case "y":
5714
+ case "top":
5715
+ return box.y;
5716
+ case "center-x":
5717
+ return box.x + box.width / 2;
5718
+ case "center-y":
5719
+ return box.y + box.height / 2;
5720
+ case "right":
5721
+ return box.x + box.width;
5722
+ case "bottom":
5723
+ return box.y + box.height;
5724
+ }
5725
+ }
5726
+ function distributionStart2(box, axis) {
5727
+ return axis === "horizontal" ? box.x : box.y;
5728
+ }
5729
+ function detailString(diagnostic, key) {
5730
+ const value = diagnostic.detail?.[key];
5731
+ return typeof value === "string" ? value : void 0;
5732
+ }
5733
+ function reportSwimlaneOverlaps(nodeBoxes, locks, overlapSpacing) {
5734
+ const diagnostics = [];
5735
+ const ids = [...nodeBoxes.keys()].sort();
5736
+ for (const firstId of ids) {
5737
+ for (const secondId of ids) {
5738
+ if (firstId >= secondId) {
5739
+ continue;
5740
+ }
5741
+ const first = nodeBoxes.get(firstId);
5742
+ const second = nodeBoxes.get(secondId);
5743
+ if (first === void 0 || second === void 0) {
5744
+ continue;
5745
+ }
5746
+ if (!intersectsAabb(first, second)) {
5747
+ continue;
5748
+ }
5749
+ diagnostics.push({
5750
+ severity: "warning",
5751
+ code: "constraints.overlap.unresolved",
5752
+ message: `Boxes ${firstId} and ${secondId} still overlap after contract swimlane placement with configured spacing ${overlapSpacing}.`,
5753
+ path: ["swimlanes"],
5754
+ detail: {
5755
+ firstId,
5756
+ secondId,
5757
+ firstLocked: locks.has(firstId),
5758
+ secondLocked: locks.has(secondId)
5759
+ }
5760
+ });
5761
+ }
5762
+ }
5763
+ return diagnostics;
5764
+ }
5765
+ function coordinateNodes(nodes, boxes, options, diagnostics) {
5766
+ const coordinated = [];
5767
+ for (const node of nodes) {
5768
+ const box = boxes.get(node.id);
5769
+ if (box === void 0) {
5770
+ diagnostics.push({
5771
+ severity: "error",
5772
+ code: "solver.node-box.missing",
5773
+ message: `Node ${node.id} has no solved box.`,
5774
+ path: ["nodes", node.id],
5775
+ detail: { nodeId: node.id }
5776
+ });
5777
+ continue;
5778
+ }
5779
+ const geometry = computeShapeGeometry({
5780
+ shape: node.shape,
5781
+ box,
5782
+ obstacleMargin: options.obstacleMargin ?? 0
5783
+ });
5784
+ coordinated.push({
5785
+ id: node.id,
5786
+ ...node.label === void 0 ? {} : { label: node.label },
3025
5787
  ...node.style === void 0 ? {} : { style: node.style },
3026
5788
  ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
3027
5789
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
@@ -3068,7 +5830,10 @@ function coordinatePorts(node, nodeBox, portShifting) {
3068
5830
  }
3069
5831
  function portAnchor(nodeBox, side, index, count, portShifting) {
3070
5832
  const shiftingEnabled = portShifting?.enabled ?? true;
3071
- const spacing = portShifting?.spacing ?? 24;
5833
+ const requestedSpacing = portShifting?.spacing ?? 24;
5834
+ const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
5835
+ const availableSpan = 2 * maxOffset;
5836
+ const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
3072
5837
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
3073
5838
  switch (side) {
3074
5839
  case "left":
@@ -3114,16 +5879,70 @@ function portLabelBox(port) {
3114
5879
  height
3115
5880
  };
3116
5881
  }
3117
- function coordinateSwimlanes(swimlanes, nodeBoxes) {
3118
- const titleSize = 28;
3119
- const padding = 16;
5882
+ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
3120
5883
  return swimlanes.map((swimlane) => {
3121
- const laneBoxes = swimlane.lanes.flatMap((lane) => {
5884
+ const layout2 = swimlane.layout ?? "overlay";
5885
+ const headerHeight = swimlane.headerHeight ?? 28;
5886
+ const padding = swimlane.padding ?? 16;
5887
+ const contractLayout = layouts.get(swimlane.id);
5888
+ if (layout2 === "contract" && contractLayout !== void 0) {
5889
+ const lanes2 = swimlane.lanes.map((lane, index) => {
5890
+ const box = swimlane.orientation === "vertical" ? {
5891
+ x: contractLayout.box.x + contractLayout.laneStep * index,
5892
+ y: contractLayout.box.y,
5893
+ width: contractLayout.slotWidth,
5894
+ height: contractLayout.box.height
5895
+ } : {
5896
+ x: contractLayout.box.x,
5897
+ y: contractLayout.box.y + contractLayout.laneStep * index,
5898
+ width: contractLayout.box.width,
5899
+ height: contractLayout.slotHeight
5900
+ };
5901
+ const headerBox = swimlane.orientation === "vertical" ? {
5902
+ x: box.x,
5903
+ y: box.y,
5904
+ width: box.width,
5905
+ height: headerHeight
5906
+ } : {
5907
+ x: box.x,
5908
+ y: box.y,
5909
+ width: headerHeight,
5910
+ height: box.height
5911
+ };
5912
+ const contentBox2 = swimlane.orientation === "vertical" ? {
5913
+ x: box.x,
5914
+ y: box.y + headerHeight,
5915
+ width: box.width,
5916
+ height: Math.max(0, box.height - headerHeight)
5917
+ } : {
5918
+ x: box.x + headerHeight,
5919
+ y: box.y,
5920
+ width: Math.max(0, box.width - headerHeight),
5921
+ height: box.height
5922
+ };
5923
+ return {
5924
+ ...lane,
5925
+ box,
5926
+ headerBox,
5927
+ contentBox: contentBox2
5928
+ };
5929
+ });
5930
+ return {
5931
+ ...swimlane,
5932
+ lanes: lanes2,
5933
+ box: contractLayout.box,
5934
+ ...headerHeight === void 0 ? {} : { headerHeight },
5935
+ ...padding === void 0 ? {} : { padding }
5936
+ };
5937
+ }
5938
+ const laneContentBoxes = swimlane.lanes.map((lane) => {
3122
5939
  const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3123
- return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
5940
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
3124
5941
  });
3125
- const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
3126
- const outer = expand(laneUnion, padding, titleSize);
5942
+ const laneUnion = laneContentBoxes.filter((box) => box !== void 0).length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(
5943
+ laneContentBoxes.filter((box) => box !== void 0)
5944
+ );
5945
+ const outer = expand(laneUnion, padding, headerHeight);
3127
5946
  const laneCount = Math.max(1, swimlane.lanes.length);
3128
5947
  const lanes = swimlane.lanes.map((lane, index) => {
3129
5948
  const box = swimlane.orientation === "vertical" ? {
@@ -3137,23 +5956,58 @@ function coordinateSwimlanes(swimlanes, nodeBoxes) {
3137
5956
  width: outer.width,
3138
5957
  height: outer.height / laneCount
3139
5958
  };
3140
- return { ...lane, box };
5959
+ const headerBox = layout2 === "contract" ? swimlane.orientation === "vertical" ? {
5960
+ x: box.x,
5961
+ y: box.y,
5962
+ width: box.width,
5963
+ height: headerHeight
5964
+ } : {
5965
+ x: box.x,
5966
+ y: box.y,
5967
+ width: headerHeight,
5968
+ height: box.height
5969
+ } : void 0;
5970
+ const contentBox2 = layout2 === "contract" ? swimlane.orientation === "vertical" ? {
5971
+ x: box.x,
5972
+ y: box.y + headerHeight,
5973
+ width: box.width,
5974
+ height: Math.max(0, box.height - headerHeight)
5975
+ } : {
5976
+ x: box.x + headerHeight,
5977
+ y: box.y,
5978
+ width: Math.max(0, box.width - headerHeight),
5979
+ height: box.height
5980
+ } : void 0;
5981
+ return {
5982
+ ...lane,
5983
+ box,
5984
+ ...headerBox === void 0 ? {} : { headerBox },
5985
+ ...contentBox2 === void 0 ? {} : { contentBox: contentBox2 }
5986
+ };
3141
5987
  });
3142
- return { ...swimlane, lanes, box: outer };
5988
+ return {
5989
+ ...swimlane,
5990
+ lanes,
5991
+ box: outer,
5992
+ ...headerHeight === void 0 ? {} : { headerHeight },
5993
+ ...padding === void 0 ? {} : { padding }
5994
+ };
3143
5995
  });
3144
5996
  }
3145
5997
  function coordinateFrame(frame, contentBounds) {
3146
- const padding = 32;
3147
- const titleHeight = 28;
5998
+ const padding = framePadding(frame.padding);
5999
+ const titleHeight = frame.headerHeight ?? 28;
3148
6000
  const titleWidth = Math.max(180, frame.titleTab.length * 7);
3149
6001
  const box = {
3150
- x: contentBounds.x - padding,
3151
- y: contentBounds.y - padding - titleHeight,
3152
- width: contentBounds.width + padding * 2,
3153
- height: contentBounds.height + padding * 2 + titleHeight
6002
+ x: contentBounds.x - padding.left,
6003
+ y: contentBounds.y - padding.top - titleHeight,
6004
+ width: contentBounds.width + padding.left + padding.right,
6005
+ height: contentBounds.height + padding.top + padding.bottom + titleHeight
3154
6006
  };
3155
6007
  return {
3156
6008
  ...frame,
6009
+ headerHeight: titleHeight,
6010
+ padding: frame.padding ?? 32,
3157
6011
  box,
3158
6012
  titleBox: {
3159
6013
  x: box.x,
@@ -3163,6 +6017,9 @@ function coordinateFrame(frame, contentBounds) {
3163
6017
  }
3164
6018
  };
3165
6019
  }
6020
+ function framePadding(value) {
6021
+ return normalizeInsets(value ?? 32);
6022
+ }
3166
6023
  function expand(box, padding, titleSize) {
3167
6024
  return {
3168
6025
  x: box.x - padding,
@@ -3201,25 +6058,388 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
3201
6058
  if (childBoxes.length === 0) {
3202
6059
  diagnostics.push(groupReferenceMissing(group.id, "child", void 0));
3203
6060
  }
3204
- continue;
6061
+ continue;
6062
+ }
6063
+ const geometry = computeContainerGeometry({
6064
+ id: group.id,
6065
+ childBoxes,
6066
+ padding: group.padding,
6067
+ ...group.labelLayout === void 0 ? {} : { labelLayout: group.labelLayout },
6068
+ obstacleMargin: options.obstacleMargin ?? 0
6069
+ });
6070
+ groupBoxes.set(group.id, geometry.box);
6071
+ diagnostics.push(...geometry.diagnostics);
6072
+ coordinated.push({
6073
+ ...group,
6074
+ box: geometry.box
6075
+ });
6076
+ }
6077
+ return coordinated;
6078
+ }
6079
+ function coordinateMatrices(matrices) {
6080
+ return matrices.map((block) => ({
6081
+ ...block,
6082
+ box: blockBox(block, {
6083
+ width: defaultMatrixRowHeaderWidth2(block) + Math.max(1, block.cols.length) * DEFAULT_MATRIX_CELL_SIZE2.width,
6084
+ height: Math.max(1, block.rows.length + 1) * DEFAULT_MATRIX_CELL_SIZE2.height
6085
+ })
6086
+ }));
6087
+ }
6088
+ function defaultMatrixRowHeaderWidth2(block) {
6089
+ return block.rows.length === 0 ? 0 : Math.min(96, DEFAULT_MATRIX_CELL_SIZE2.width);
6090
+ }
6091
+ function coordinateTables(tables) {
6092
+ return tables.map((table) => {
6093
+ const box = blockBox(table, {
6094
+ width: Math.max(1, table.columns.length) * DEFAULT_TABLE_CELL_SIZE2.width,
6095
+ height: Math.max(1, table.rows.length + 1) * DEFAULT_TABLE_CELL_SIZE2.height
6096
+ });
6097
+ return {
6098
+ ...table,
6099
+ box,
6100
+ columnXOffsets: columnXOffsets(table, box)
6101
+ };
6102
+ });
6103
+ }
6104
+ function coordinateEvidencePanels(panels) {
6105
+ return panels.map((block) => ({
6106
+ ...block,
6107
+ box: blockBox(block, {
6108
+ width: DEFAULT_PANEL_WIDTH,
6109
+ height: Math.max(1, block.items.length) * DEFAULT_PANEL_ITEM_HEIGHT2
6110
+ })
6111
+ }));
6112
+ }
6113
+ function edgeBounds(edges) {
6114
+ return edges.flatMap((edge) => {
6115
+ if (edge.points.length === 0) {
6116
+ return [];
6117
+ }
6118
+ const extraPoints = [];
6119
+ if (edge.points.length >= 2) {
6120
+ const arrowhead = computeArrowhead(edge.points);
6121
+ extraPoints.push(arrowhead.tip, arrowhead.left, arrowhead.right);
6122
+ }
6123
+ const allPoints = [...edge.points, ...extraPoints];
6124
+ const minX = Math.min(...allPoints.map((point2) => point2.x));
6125
+ const minY = Math.min(...allPoints.map((point2) => point2.y));
6126
+ const maxX = Math.max(...allPoints.map((point2) => point2.x));
6127
+ const maxY = Math.max(...allPoints.map((point2) => point2.y));
6128
+ return [
6129
+ {
6130
+ x: minX,
6131
+ y: minY,
6132
+ width: maxX - minX,
6133
+ height: maxY - minY
6134
+ }
6135
+ ];
6136
+ });
6137
+ }
6138
+ function blockBox(block, defaultSize) {
6139
+ return {
6140
+ x: block.position?.x ?? 0,
6141
+ y: block.position?.y ?? 0,
6142
+ width: block.size?.width ?? defaultSize.width,
6143
+ height: block.size?.height ?? defaultSize.height
6144
+ };
6145
+ }
6146
+ function placeEvidenceBlocks(blocks, contentBounds) {
6147
+ let nextY = contentBounds.y;
6148
+ const x = contentBounds.x + contentBounds.width + DEFAULT_EVIDENCE_BLOCK_GAP;
6149
+ for (const block of blocks) {
6150
+ if (block.position !== void 0) {
6151
+ continue;
6152
+ }
6153
+ block.box.x = x;
6154
+ block.box.y = nextY;
6155
+ nextY += block.box.height + DEFAULT_EVIDENCE_BLOCK_GAP;
6156
+ }
6157
+ }
6158
+ function columnXOffsets(table, box) {
6159
+ if (table.columns.length === 0) {
6160
+ return [];
6161
+ }
6162
+ const columnWidth = box.width / table.columns.length;
6163
+ return table.columns.map((_, index) => box.x + index * columnWidth);
6164
+ }
6165
+ function tableCellBox2(table, columnIndex, rowIndex, rowHeight, columnCount) {
6166
+ const x = table.columnXOffsets[columnIndex] ?? table.box.x + table.box.width / columnCount * columnIndex;
6167
+ const nextX = table.columnXOffsets[columnIndex + 1] ?? table.box.x + table.box.width;
6168
+ return {
6169
+ x,
6170
+ y: table.box.y + rowIndex * rowHeight,
6171
+ width: nextX - x,
6172
+ height: rowHeight
6173
+ };
6174
+ }
6175
+ function refreshTableColumnXOffsets(tables) {
6176
+ for (const table of tables) {
6177
+ table.columnXOffsets = columnXOffsets(table, table.box);
6178
+ }
6179
+ }
6180
+ function measureEvidenceTextBlocks(matrices, tables, panels, textMeasurer) {
6181
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
6182
+ for (const matrix of matrices) {
6183
+ const geometry = matrixGeometry(matrix);
6184
+ matrix.columnLabelLayouts = matrix.cols.map(
6185
+ (column) => measureEvidenceTextLayout(column, geometry.columnHeaderBox, measurer)
6186
+ );
6187
+ matrix.rowLabelLayouts = matrix.rows.map(
6188
+ (row, index) => measureEvidenceTextLayout(row, geometry.rowHeaderBox(index), measurer)
6189
+ );
6190
+ matrix.cellLabelLayouts = matrix.rows.map(
6191
+ (_, rowIndex) => matrix.cols.map((_2, columnIndex) => {
6192
+ const cell2 = matrix.cells[rowIndex]?.[columnIndex] ?? { text: "" };
6193
+ return measureEvidenceTextLayout(
6194
+ cell2.text,
6195
+ geometry.cellBox(rowIndex, columnIndex),
6196
+ measurer
6197
+ );
6198
+ })
6199
+ );
6200
+ }
6201
+ for (const table of tables) {
6202
+ const rowHeight = table.box.height / Math.max(1, table.rows.length + 1);
6203
+ const columnCount = Math.max(1, table.columns.length);
6204
+ table.columnLabelLayouts = table.columns.map(
6205
+ (column, columnIndex) => measureEvidenceTextLayout(
6206
+ column.label.text,
6207
+ tableCellBox2(table, columnIndex, 0, rowHeight, columnCount),
6208
+ measurer
6209
+ )
6210
+ );
6211
+ table.cellLabelLayouts = table.rows.map(
6212
+ (row, rowIndex) => table.columns.map((column, columnIndex) => {
6213
+ const cell2 = row.cells[column.id] ?? { text: "" };
6214
+ return measureEvidenceTextLayout(
6215
+ cell2.text,
6216
+ tableCellBox2(
6217
+ table,
6218
+ columnIndex,
6219
+ rowIndex + 1,
6220
+ rowHeight,
6221
+ columnCount
6222
+ ),
6223
+ measurer
6224
+ );
6225
+ })
6226
+ );
6227
+ }
6228
+ for (const panel of panels) {
6229
+ const geometry = panelGeometry(panel);
6230
+ panel.titleLayout = measureEvidenceTextLayout(
6231
+ `${panel.kind}: ${panel.id}`,
6232
+ geometry.titleBox,
6233
+ measurer
6234
+ );
6235
+ panel.itemLayouts = panel.items.map(
6236
+ (item, index) => measureEvidenceTextLayout(
6237
+ panelItemText2(item.label.text, item.detail?.text),
6238
+ geometry.itemRowBox(index),
6239
+ measurer
6240
+ )
6241
+ );
6242
+ }
6243
+ }
6244
+ function measureEvidenceTextLayout(text, box, textMeasurer) {
6245
+ const lineHeight = EVIDENCE_TEXT_FONT.lineHeight;
6246
+ return {
6247
+ lines: wrapEvidenceText(text, {
6248
+ maxWidth: Math.max(0, box.width - 8),
6249
+ maxLines: Math.max(1, Math.floor((box.height - 4) / lineHeight)),
6250
+ textMeasurer
6251
+ })
6252
+ };
6253
+ }
6254
+ function wrapEvidenceText(text, options) {
6255
+ const normalized = text.trim().replace(/\s+/g, " ");
6256
+ if (normalized.length === 0) {
6257
+ return [""];
6258
+ }
6259
+ const lines = [];
6260
+ let current = "";
6261
+ let overflow = false;
6262
+ for (const word of normalized.split(" ")) {
6263
+ const chunks = chunkEvidenceWord(
6264
+ word,
6265
+ options.maxWidth,
6266
+ options.textMeasurer
6267
+ );
6268
+ for (const chunk of chunks) {
6269
+ const candidate = current.length === 0 ? chunk : `${current} ${chunk}`;
6270
+ if (measureEvidenceText(candidate, options.textMeasurer) <= options.maxWidth) {
6271
+ current = candidate;
6272
+ continue;
6273
+ }
6274
+ if (current.length > 0) {
6275
+ lines.push(current);
6276
+ current = chunk;
6277
+ } else {
6278
+ lines.push(chunk);
6279
+ current = "";
6280
+ }
6281
+ if (lines.length >= options.maxLines) {
6282
+ overflow = true;
6283
+ break;
6284
+ }
6285
+ }
6286
+ if (overflow) {
6287
+ break;
6288
+ }
6289
+ }
6290
+ if (!overflow && current.length > 0) {
6291
+ lines.push(current);
6292
+ }
6293
+ if (lines.length > options.maxLines) {
6294
+ overflow = true;
6295
+ lines.length = options.maxLines;
6296
+ }
6297
+ if (overflow || lines.length === options.maxLines) {
6298
+ const rendered = lines.join(" ");
6299
+ if (rendered.length < normalized.length) {
6300
+ lines[lines.length - 1] = ellipsizeMeasuredEvidenceLine(
6301
+ lines[lines.length - 1] ?? "",
6302
+ options.maxWidth,
6303
+ options.textMeasurer
6304
+ );
6305
+ }
6306
+ }
6307
+ return lines.length === 0 ? [""] : lines;
6308
+ }
6309
+ function chunkEvidenceWord(word, maxWidth, textMeasurer) {
6310
+ if (measureEvidenceText(word, textMeasurer) <= maxWidth) {
6311
+ return [word];
6312
+ }
6313
+ const chunks = [];
6314
+ let current = "";
6315
+ for (const char of Array.from(word)) {
6316
+ const candidate = `${current}${char}`;
6317
+ if (current.length > 0 && measureEvidenceText(candidate, textMeasurer) > maxWidth) {
6318
+ chunks.push(current);
6319
+ current = char;
6320
+ continue;
6321
+ }
6322
+ current = candidate;
6323
+ }
6324
+ if (current.length > 0) {
6325
+ chunks.push(current);
6326
+ }
6327
+ return chunks.length === 0 ? [word] : chunks;
6328
+ }
6329
+ function ellipsizeMeasuredEvidenceLine(line, maxWidth, textMeasurer) {
6330
+ const ellipsis = "...";
6331
+ if (measureEvidenceText(ellipsis, textMeasurer) > maxWidth) {
6332
+ return "";
6333
+ }
6334
+ let candidate = line.trimEnd();
6335
+ while (candidate.length > 0 && measureEvidenceText(`${candidate}${ellipsis}`, textMeasurer) > maxWidth) {
6336
+ candidate = Array.from(candidate).slice(0, -1).join("").trimEnd();
6337
+ }
6338
+ return `${candidate}${ellipsis}`;
6339
+ }
6340
+ function measureEvidenceText(text, textMeasurer) {
6341
+ return textMeasurer.naturalWidth(
6342
+ textMeasurer.prepare(text, EVIDENCE_TEXT_FONT)
6343
+ );
6344
+ }
6345
+ function matrixGeometry(matrix) {
6346
+ const columnCount = Math.max(1, matrix.cols.length);
6347
+ const rowCount = matrix.rows.length;
6348
+ const rowHeaderWidth = rowCount > 0 ? Math.min(96, matrix.box.width * 0.28) : 0;
6349
+ const dataWidth = Math.max(0, matrix.box.width - rowHeaderWidth);
6350
+ const cellWidth = dataWidth / columnCount;
6351
+ const rowHeight = matrix.box.height / Math.max(1, rowCount + 1);
6352
+ return {
6353
+ rowHeaderWidth,
6354
+ cellWidth,
6355
+ rowHeight,
6356
+ columnHeaderBox: {
6357
+ x: matrix.box.x + rowHeaderWidth,
6358
+ y: matrix.box.y,
6359
+ width: cellWidth,
6360
+ height: rowHeight
6361
+ },
6362
+ rowHeaderBox: (rowIndex) => ({
6363
+ x: matrix.box.x,
6364
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
6365
+ width: rowHeaderWidth,
6366
+ height: rowHeight
6367
+ }),
6368
+ cellBox: (rowIndex, columnIndex) => ({
6369
+ x: matrix.box.x + rowHeaderWidth + columnIndex * cellWidth,
6370
+ y: matrix.box.y + (rowIndex + 1) * rowHeight,
6371
+ width: cellWidth,
6372
+ height: rowHeight
6373
+ })
6374
+ };
6375
+ }
6376
+ function panelGeometry(panel) {
6377
+ const titleWidth = Math.min(panel.box.width * 0.36, 140);
6378
+ const itemBox = {
6379
+ x: panel.box.x + titleWidth,
6380
+ y: panel.box.y,
6381
+ width: panel.box.width - titleWidth,
6382
+ height: panel.box.height
6383
+ };
6384
+ const itemHeight = panel.box.height / Math.max(1, panel.items.length);
6385
+ return {
6386
+ titleBox: {
6387
+ x: panel.box.x,
6388
+ y: panel.box.y,
6389
+ width: titleWidth,
6390
+ height: panel.box.height
6391
+ },
6392
+ itemRowBox: (index) => ({
6393
+ x: itemBox.x,
6394
+ y: itemBox.y + index * itemHeight,
6395
+ width: itemBox.width,
6396
+ height: itemHeight
6397
+ })
6398
+ };
6399
+ }
6400
+ function panelItemText2(label, detail) {
6401
+ return detail === void 0 ? label : `${label}: ${detail}`;
6402
+ }
6403
+ function reportEvidenceBlockOverlaps(evidenceBlocks, contentBlocks) {
6404
+ const diagnostics = [];
6405
+ for (let index = 0; index < evidenceBlocks.length; index += 1) {
6406
+ const block = evidenceBlocks[index];
6407
+ if (block === void 0 || block.position === void 0) {
6408
+ continue;
6409
+ }
6410
+ for (const content of contentBlocks) {
6411
+ if (intersectsAabb(block.box, content.box)) {
6412
+ diagnostics.push(evidenceOverlapDiagnostic(block, content));
6413
+ }
6414
+ }
6415
+ for (let otherIndex = 0; otherIndex < evidenceBlocks.length; otherIndex += 1) {
6416
+ if (otherIndex === index) {
6417
+ continue;
6418
+ }
6419
+ const other = evidenceBlocks[otherIndex];
6420
+ if (other === void 0 || other.position !== void 0 && otherIndex < index || !intersectsAabb(block.box, other.box)) {
6421
+ continue;
6422
+ }
6423
+ diagnostics.push(evidenceOverlapDiagnostic(block, other));
3205
6424
  }
3206
- const geometry = computeContainerGeometry({
3207
- id: group.id,
3208
- childBoxes,
3209
- padding: group.padding,
3210
- ...group.labelLayout === void 0 ? {} : { labelLayout: group.labelLayout },
3211
- obstacleMargin: options.obstacleMargin ?? 0
3212
- });
3213
- groupBoxes.set(group.id, geometry.box);
3214
- diagnostics.push(...geometry.diagnostics);
3215
- coordinated.push({
3216
- ...group,
3217
- box: geometry.box
3218
- });
3219
6425
  }
3220
- return coordinated;
6426
+ return diagnostics;
6427
+ }
6428
+ function evidenceOverlapDiagnostic(block, conflict) {
6429
+ return {
6430
+ severity: "warning",
6431
+ code: "constraints.overlap.unresolved",
6432
+ message: `Evidence block ${block.id} overlaps ${conflict.kind} ${conflict.id}.`,
6433
+ path: ["evidence", block.id],
6434
+ detail: {
6435
+ evidenceBlockId: block.id,
6436
+ evidenceBlockKind: block.kind,
6437
+ conflictingObjectId: conflict.id,
6438
+ conflictingObjectKind: conflict.kind
6439
+ }
6440
+ };
3221
6441
  }
3222
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
6442
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
3223
6443
  const coordinated = [];
3224
6444
  const coordinatedNodeById = new Map(
3225
6445
  coordinatedNodes.map((node) => [node.id, node])
@@ -3243,6 +6463,8 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, o
3243
6463
  }
3244
6464
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
3245
6465
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6466
+ const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6467
+ const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
3246
6468
  const route = routeEdge({
3247
6469
  kind: options.routeKind ?? "orthogonal",
3248
6470
  direction,
@@ -3250,9 +6472,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, o
3250
6472
  target: portGeometry(target, targetPort),
3251
6473
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
3252
6474
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
3253
- obstacles: obstacles.filter(
3254
- (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
3255
- )
6475
+ obstacles: [
6476
+ ...obstacles.filter(
6477
+ (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6478
+ ),
6479
+ ...softObstacles,
6480
+ ...routeTextObstacles
6481
+ ],
6482
+ hardObstacles
3256
6483
  });
3257
6484
  diagnostics.push(
3258
6485
  ...route.diagnostics.map((diagnostic) => ({
@@ -3267,6 +6494,610 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, o
3267
6494
  }
3268
6495
  return coordinated;
3269
6496
  }
6497
+ function edgeConnectedTextOwnerIds(edge) {
6498
+ const owners = /* @__PURE__ */ new Set();
6499
+ if (edge.source.portId !== void 0) {
6500
+ owners.add(`${edge.source.nodeId}.${edge.source.portId}`);
6501
+ }
6502
+ if (edge.target.portId !== void 0) {
6503
+ owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
6504
+ }
6505
+ return owners;
6506
+ }
6507
+ function coordinateBaseTextAnnotations(input) {
6508
+ const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
6509
+ const annotations = [];
6510
+ for (const node of input.nodes) {
6511
+ if (node.compartments !== void 0) {
6512
+ continue;
6513
+ }
6514
+ if (node.labelLayout === void 0 && node.label === void 0) {
6515
+ continue;
6516
+ }
6517
+ const layout2 = node.labelLayout ?? fallbackLabelLayout(node.label?.text ?? "");
6518
+ const buildAnnotation = node.labelLayout === void 0 ? buildAnchorCenteredTextAnnotation : buildTextAnnotation;
6519
+ annotations.push(
6520
+ buildAnnotation({
6521
+ ownerId: node.id,
6522
+ surfaceKind: "node-label",
6523
+ layout: layout2,
6524
+ typography: typographyForLabel(node.label),
6525
+ anchor: node.box
6526
+ })
6527
+ );
6528
+ }
6529
+ for (const group of input.groups) {
6530
+ if (group.labelLayout === void 0 && group.label === void 0) {
6531
+ continue;
6532
+ }
6533
+ const layout2 = group.labelLayout ?? fallbackLabelLayout(group.label?.text ?? "");
6534
+ const buildAnnotation = group.labelLayout === void 0 ? buildAnchorCenteredTextAnnotation : buildTextAnnotation;
6535
+ annotations.push(
6536
+ buildAnnotation({
6537
+ ownerId: group.id,
6538
+ surfaceKind: "group-label",
6539
+ layout: layout2,
6540
+ typography: typographyForLabel(group.label),
6541
+ anchor: group.box
6542
+ })
6543
+ );
6544
+ }
6545
+ for (const node of input.nodes) {
6546
+ for (const port of node.ports ?? []) {
6547
+ if (port.label?.text === void 0) {
6548
+ continue;
6549
+ }
6550
+ const layout2 = fitLabel(
6551
+ port.label.text,
6552
+ {
6553
+ font: typographyTextStyle(port.label, {
6554
+ fontFamily: "Arial",
6555
+ fontSize: 10,
6556
+ lineHeight: 12
6557
+ }),
6558
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6559
+ minSize: { width: 0, height: 0 },
6560
+ maxWidth: 160
6561
+ },
6562
+ measurer
6563
+ );
6564
+ annotations.push(
6565
+ buildTextAnnotation({
6566
+ ownerId: `${node.id}.${port.id}`,
6567
+ surfaceKind: "port-label",
6568
+ layout: layout2,
6569
+ typography: typographyForLabel(port.label),
6570
+ anchor: portLabelBox(port)
6571
+ })
6572
+ );
6573
+ }
6574
+ }
6575
+ for (const node of input.nodes) {
6576
+ if (node.compartments === void 0) {
6577
+ continue;
6578
+ }
6579
+ const rows = compartmentRows2(node);
6580
+ for (let index = 0; index < rows.length; index += 1) {
6581
+ const row = rows[index];
6582
+ if (row === void 0) {
6583
+ continue;
6584
+ }
6585
+ const layout2 = fitLabel(
6586
+ row,
6587
+ {
6588
+ font: { fontFamily: "Arial", fontSize: 11, lineHeight: 13 },
6589
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6590
+ minSize: { width: 0, height: 0 },
6591
+ maxWidth: node.box.width
6592
+ },
6593
+ measurer
6594
+ );
6595
+ annotations.push(
6596
+ buildAnchorCenteredTextAnnotation({
6597
+ ownerId: node.id,
6598
+ surfaceKind: "compartment-row",
6599
+ surfaceIndex: index,
6600
+ layout: layout2,
6601
+ anchor: {
6602
+ x: node.box.x,
6603
+ y: node.box.y + 18 + index * 16,
6604
+ width: node.box.width,
6605
+ height: 16
6606
+ }
6607
+ })
6608
+ );
6609
+ }
6610
+ }
6611
+ for (const swimlane of input.swimlanes) {
6612
+ for (const lane of swimlane.lanes) {
6613
+ if (lane.label?.text === void 0 || lane.box === void 0) {
6614
+ continue;
6615
+ }
6616
+ const labelBox = lane.headerBox ?? lane.box;
6617
+ const layout2 = fitLabel(
6618
+ lane.label.text,
6619
+ {
6620
+ font: typographyTextStyle(lane.label, {
6621
+ fontFamily: "Arial",
6622
+ fontSize: 12,
6623
+ lineHeight: 14
6624
+ }),
6625
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6626
+ minSize: { width: 0, height: 0 },
6627
+ maxWidth: swimlane.orientation === "horizontal" ? labelBox.height : labelBox.width
6628
+ },
6629
+ measurer
6630
+ );
6631
+ annotations.push(
6632
+ buildAnchorCenteredTextAnnotation({
6633
+ ownerId: `${swimlane.id}.${lane.id}`,
6634
+ surfaceKind: "swimlane-label",
6635
+ layout: layout2,
6636
+ typography: typographyForLabel(lane.label),
6637
+ anchor: labelBox
6638
+ })
6639
+ );
6640
+ }
6641
+ }
6642
+ return annotations;
6643
+ }
6644
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6645
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
6646
+ const annotations = [];
6647
+ const placedLabelBoxes = [];
6648
+ for (const edge of edges) {
6649
+ if (edge.label?.text === void 0) {
6650
+ continue;
6651
+ }
6652
+ const layout2 = fitLabel(
6653
+ edge.label.text,
6654
+ {
6655
+ font: typographyTextStyle(edge.label, {
6656
+ fontFamily: "Arial",
6657
+ fontSize: 12,
6658
+ lineHeight: 14
6659
+ }),
6660
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6661
+ minSize: { width: 0, height: 0 },
6662
+ maxWidth: 200
6663
+ },
6664
+ measurer
6665
+ );
6666
+ const center = edgeLabelAnchor(
6667
+ edge,
6668
+ layout2,
6669
+ edges,
6670
+ obstacleBoxes,
6671
+ placedLabelBoxes
6672
+ );
6673
+ placedLabelBoxes.push({
6674
+ x: center.x - layout2.box.width / 2,
6675
+ y: center.y - layout2.box.height / 2,
6676
+ width: layout2.box.width,
6677
+ height: layout2.box.height
6678
+ });
6679
+ annotations.push(
6680
+ buildCenteredTextAnnotation({
6681
+ ownerId: edge.id,
6682
+ surfaceKind: "edge-label",
6683
+ layout: layout2,
6684
+ typography: typographyForLabel(edge.label),
6685
+ center
6686
+ })
6687
+ );
6688
+ }
6689
+ return annotations;
6690
+ }
6691
+ function coordinateFrameTextAnnotation(frame, textMeasurer) {
6692
+ const layout2 = fitLabel(
6693
+ frame.titleTab,
6694
+ {
6695
+ font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
6696
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6697
+ minSize: { width: 0, height: 0 },
6698
+ maxWidth: frame.titleBox.width
6699
+ },
6700
+ textMeasurer ?? createDefaultTextMeasurer()
6701
+ );
6702
+ return buildAnchorCenteredTextAnnotation({
6703
+ ownerId: frame.kind,
6704
+ surfaceKind: "frame-title",
6705
+ layout: layout2,
6706
+ anchor: frame.titleBox
6707
+ });
6708
+ }
6709
+ function buildTextAnnotation(input) {
6710
+ return {
6711
+ text: input.layout.text,
6712
+ ownerId: input.ownerId,
6713
+ surfaceKind: input.surfaceKind,
6714
+ ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
6715
+ box: {
6716
+ x: input.anchor.x + input.layout.box.x,
6717
+ y: input.anchor.y + input.layout.box.y,
6718
+ width: input.layout.box.width,
6719
+ height: input.layout.box.height
6720
+ },
6721
+ anchor: input.anchor,
6722
+ paddings: input.layout.padding,
6723
+ lines: input.layout.lines,
6724
+ fontFamily: input.typography?.fontFamily ?? normalizeOutputFontFamily(input.layout.font),
6725
+ fontSize: input.typography?.fontSize ?? input.layout.font.fontSize,
6726
+ textBackend: input.layout.textBackend
6727
+ };
6728
+ }
6729
+ function buildAnchorCenteredTextAnnotation(input) {
6730
+ return buildCenteredTextAnnotation({
6731
+ ownerId: input.ownerId,
6732
+ surfaceKind: input.surfaceKind,
6733
+ ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
6734
+ layout: input.layout,
6735
+ ...input.typography === void 0 ? {} : { typography: input.typography },
6736
+ center: {
6737
+ x: input.anchor.x + input.anchor.width / 2,
6738
+ y: input.anchor.y + input.anchor.height / 2
6739
+ },
6740
+ anchor: input.anchor
6741
+ });
6742
+ }
6743
+ function buildCenteredTextAnnotation(input) {
6744
+ return {
6745
+ text: input.layout.text,
6746
+ ownerId: input.ownerId,
6747
+ surfaceKind: input.surfaceKind,
6748
+ ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
6749
+ box: {
6750
+ x: input.center.x - input.layout.box.width / 2,
6751
+ y: input.center.y - input.layout.box.height / 2,
6752
+ width: input.layout.box.width,
6753
+ height: input.layout.box.height
6754
+ },
6755
+ anchor: input.anchor ?? input.center,
6756
+ paddings: input.layout.padding,
6757
+ lines: input.layout.lines,
6758
+ fontFamily: input.typography?.fontFamily ?? normalizeOutputFontFamily(input.layout.font),
6759
+ fontSize: input.typography?.fontSize ?? input.layout.font.fontSize,
6760
+ textBackend: input.layout.textBackend
6761
+ };
6762
+ }
6763
+ function normalizeOutputFontFamily(font) {
6764
+ return font.fontFamily === "Arial" ? "Arial, sans-serif" : font.fontFamily;
6765
+ }
6766
+ function reportTextAnnotationCollisions(annotations) {
6767
+ const diagnostics = [];
6768
+ const relevantAnnotations = annotations.filter(
6769
+ (annotation) => isExternallyPlacedText(annotation.surfaceKind)
6770
+ );
6771
+ for (let annotationIndex = 0; annotationIndex < relevantAnnotations.length; annotationIndex += 1) {
6772
+ const annotation = relevantAnnotations[annotationIndex];
6773
+ if (annotation === void 0) {
6774
+ continue;
6775
+ }
6776
+ for (let otherIndex = annotationIndex + 1; otherIndex < relevantAnnotations.length; otherIndex += 1) {
6777
+ const other = relevantAnnotations[otherIndex];
6778
+ if (other === void 0) {
6779
+ continue;
6780
+ }
6781
+ if (!intersectsAabb(annotation.box, other.box)) {
6782
+ continue;
6783
+ }
6784
+ if (annotation.ownerId === other.ownerId && annotation.surfaceKind === other.surfaceKind) {
6785
+ continue;
6786
+ }
6787
+ diagnostics.push({
6788
+ severity: "warning",
6789
+ code: "constraints.overlap.unresolved",
6790
+ message: `Text surface ${annotation.surfaceKind} for ${annotation.ownerId} overlaps text surface ${other.surfaceKind} for ${other.ownerId}.`,
6791
+ path: ["textAnnotations", annotation.surfaceKind, annotation.ownerId],
6792
+ detail: compactDetail({
6793
+ textSurfaceKind: annotation.surfaceKind,
6794
+ ownerId: annotation.ownerId,
6795
+ conflictingObjectId: other.ownerId,
6796
+ conflictingObjectKind: other.surfaceKind,
6797
+ surfaceIndex: annotation.surfaceIndex,
6798
+ otherSurfaceKind: other.surfaceKind,
6799
+ otherSurfaceIndex: other.surfaceIndex,
6800
+ textBackend: annotation.textBackend
6801
+ })
6802
+ });
6803
+ }
6804
+ }
6805
+ return diagnostics;
6806
+ }
6807
+ function reportRouteTextClearance(edges, annotations) {
6808
+ const diagnostics = [];
6809
+ const relevantAnnotations = annotations.filter(isRouteClearanceText);
6810
+ for (const edge of edges) {
6811
+ const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6812
+ for (const annotation of relevantAnnotations) {
6813
+ if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
6814
+ continue;
6815
+ }
6816
+ if (!routeIntersectsTextBox(edge.points, annotation.box)) {
6817
+ continue;
6818
+ }
6819
+ diagnostics.push({
6820
+ severity: "warning",
6821
+ code: "routing.text-clearance.unresolved",
6822
+ message: `Edge ${edge.id} intersects solved text surface ${annotation.surfaceKind} for ${annotation.ownerId}.`,
6823
+ path: ["edges", edge.id],
6824
+ detail: compactDetail({
6825
+ edgeId: edge.id,
6826
+ textSurfaceKind: annotation.surfaceKind,
6827
+ conflictingObjectId: annotation.ownerId,
6828
+ surfaceIndex: annotation.surfaceIndex,
6829
+ textBackend: annotation.textBackend
6830
+ })
6831
+ });
6832
+ }
6833
+ }
6834
+ return diagnostics;
6835
+ }
6836
+ function isPreRouteTextObstacle(annotation) {
6837
+ if (annotation.surfaceKind === "edge-label") {
6838
+ return false;
6839
+ }
6840
+ return isRouteClearanceText(annotation);
6841
+ }
6842
+ function isRouteClearanceText(annotation) {
6843
+ switch (annotation.surfaceKind) {
6844
+ case "port-label":
6845
+ case "edge-label":
6846
+ case "swimlane-label":
6847
+ case "frame-title":
6848
+ return true;
6849
+ case "node-label":
6850
+ case "group-label":
6851
+ case "compartment-row":
6852
+ return textExtendsOutsideAnchor(annotation);
6853
+ }
6854
+ }
6855
+ function textExtendsOutsideAnchor(annotation) {
6856
+ if (!("width" in annotation.anchor)) {
6857
+ return true;
6858
+ }
6859
+ const epsilon = 1e-3;
6860
+ 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;
6861
+ }
6862
+ function routeIntersectsTextBox(points, box) {
6863
+ for (let index = 0; index < points.length - 1; index += 1) {
6864
+ const start = points[index];
6865
+ const end = points[index + 1];
6866
+ if (start === void 0 || end === void 0) {
6867
+ continue;
6868
+ }
6869
+ if (segmentIntersectsBox2(start, end, box)) {
6870
+ return true;
6871
+ }
6872
+ }
6873
+ return false;
6874
+ }
6875
+ function segmentIntersectsBox2(start, end, box) {
6876
+ const left = box.x;
6877
+ const right = box.x + box.width;
6878
+ const top = box.y;
6879
+ const bottom = box.y + box.height;
6880
+ if (pointInsideBox2(start, box) || pointInsideBox2(end, box)) {
6881
+ return true;
6882
+ }
6883
+ if (start.x === end.x) {
6884
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
6885
+ }
6886
+ if (start.y === end.y) {
6887
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
6888
+ }
6889
+ 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);
6890
+ }
6891
+ function pointInsideBox2(point2, box) {
6892
+ return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
6893
+ }
6894
+ function rangesOverlap2(a, b, min, max) {
6895
+ const low = Math.min(a, b);
6896
+ const high = Math.max(a, b);
6897
+ return high > min && low < max;
6898
+ }
6899
+ function segmentIntersectsBoxEdge2(start, end, x1, y1, x2, y2) {
6900
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
6901
+ if (denominator === 0) {
6902
+ return false;
6903
+ }
6904
+ const t = ((x1 - start.x) * (y2 - y1) - (y1 - start.y) * (x2 - x1)) / denominator;
6905
+ const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
6906
+ return t > 0 && t < 1 && u > 0 && u < 1;
6907
+ }
6908
+ function compactDetail(detail) {
6909
+ return Object.fromEntries(
6910
+ Object.entries(detail).filter(
6911
+ (entry) => entry[1] !== void 0
6912
+ )
6913
+ );
6914
+ }
6915
+ function isExternallyPlacedText(surfaceKind) {
6916
+ switch (surfaceKind) {
6917
+ case "port-label":
6918
+ return true;
6919
+ case "edge-label":
6920
+ return false;
6921
+ case "swimlane-label":
6922
+ return true;
6923
+ case "frame-title":
6924
+ return true;
6925
+ case "node-label":
6926
+ case "group-label":
6927
+ case "compartment-row":
6928
+ return false;
6929
+ }
6930
+ }
6931
+ function fallbackLabelLayout(text) {
6932
+ const width = Math.max(0, text.length * 7);
6933
+ return {
6934
+ text,
6935
+ box: { x: 0, y: 0, width, height: 14 },
6936
+ contentBox: { x: 0, y: 0, width, height: 14 },
6937
+ naturalSize: { width, height: 14 },
6938
+ fittedSize: { width, height: 14 },
6939
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6940
+ font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
6941
+ lineHeight: 14,
6942
+ lines: [
6943
+ {
6944
+ text,
6945
+ box: { x: 0, y: 0, width, height: 14 },
6946
+ baselineY: 11.2,
6947
+ width,
6948
+ lineIndex: 0
6949
+ }
6950
+ ],
6951
+ overflow: { horizontal: false, vertical: false, truncated: false },
6952
+ diagnostics: []
6953
+ };
6954
+ }
6955
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
6956
+ const placement = labelPlacementOnPolyline2(edge.points);
6957
+ if (placement === void 0) {
6958
+ return { x: 0, y: 0 };
6959
+ }
6960
+ for (const candidate of edgeLabelAnchorCandidates(
6961
+ edge.points,
6962
+ placement,
6963
+ layout2
6964
+ )) {
6965
+ const labelBox = {
6966
+ x: candidate.x - layout2.box.width / 2,
6967
+ y: candidate.y - layout2.box.height / 2,
6968
+ width: layout2.box.width,
6969
+ height: layout2.box.height
6970
+ };
6971
+ if (routeIntersectsTextBox(edge.points, labelBox)) {
6972
+ continue;
6973
+ }
6974
+ const crossesOtherRoute = edges.some(
6975
+ (other) => other.id !== edge.id && routeIntersectsTextBox(other.points, labelBox)
6976
+ );
6977
+ if (crossesOtherRoute) {
6978
+ continue;
6979
+ }
6980
+ const overlapsNode = obstacleBoxes.some(
6981
+ (box) => intersectsAabb(labelBox, box)
6982
+ );
6983
+ if (overlapsNode) {
6984
+ continue;
6985
+ }
6986
+ const overlapsPlacedLabel = placedLabelBoxes.some(
6987
+ (box) => intersectsAabb(labelBox, box)
6988
+ );
6989
+ if (!overlapsPlacedLabel) {
6990
+ return candidate;
6991
+ }
6992
+ }
6993
+ return placement;
6994
+ }
6995
+ function edgeLabelAnchorCandidates(points, placement, layout2) {
6996
+ const segment = labelSegmentOnPolyline(points);
6997
+ if (segment === void 0) {
6998
+ return [placement];
6999
+ }
7000
+ const candidates = [placement];
7001
+ if (segment.start.y === segment.end.y) {
7002
+ const needed = layout2.box.height / 2 + EDGE_LABEL_CLEARANCE;
7003
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7004
+ for (let step = 1; step <= maxSteps; step += 1) {
7005
+ const offset = EDGE_LABEL_CLEARANCE * step;
7006
+ candidates.push(
7007
+ { x: placement.x, y: placement.y - offset },
7008
+ { x: placement.x, y: placement.y + offset }
7009
+ );
7010
+ }
7011
+ return candidates;
7012
+ }
7013
+ if (segment.start.x === segment.end.x) {
7014
+ const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
7015
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7016
+ for (let step = 1; step <= maxSteps; step += 1) {
7017
+ const offset = EDGE_LABEL_CLEARANCE * step;
7018
+ candidates.push(
7019
+ { x: placement.x + offset, y: placement.y },
7020
+ { x: placement.x - offset, y: placement.y }
7021
+ );
7022
+ }
7023
+ return candidates;
7024
+ }
7025
+ return candidates;
7026
+ }
7027
+ function labelPlacementOnPolyline2(points) {
7028
+ return labelSegmentOnPolyline(points)?.placement;
7029
+ }
7030
+ function labelSegmentOnPolyline(points) {
7031
+ const segments = nonZeroSegments2(points);
7032
+ const totalLength = segments.reduce(
7033
+ (sum, segment) => sum + segment.length,
7034
+ 0
7035
+ );
7036
+ if (totalLength <= 0) {
7037
+ return void 0;
7038
+ }
7039
+ let remaining = totalLength / 2;
7040
+ for (const segment of segments) {
7041
+ if (remaining <= segment.length) {
7042
+ const ratio = remaining / segment.length;
7043
+ const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
7044
+ const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
7045
+ const offset2 = labelOffset2(segment);
7046
+ return {
7047
+ start: segment.start,
7048
+ end: segment.end,
7049
+ placement: { x: x + offset2.x, y: y + offset2.y }
7050
+ };
7051
+ }
7052
+ remaining -= segment.length;
7053
+ }
7054
+ const last = segments.at(-1);
7055
+ if (last === void 0) {
7056
+ return void 0;
7057
+ }
7058
+ const offset = labelOffset2(last);
7059
+ return {
7060
+ start: last.start,
7061
+ end: last.end,
7062
+ placement: { x: last.end.x + offset.x, y: last.end.y + offset.y }
7063
+ };
7064
+ }
7065
+ function nonZeroSegments2(points) {
7066
+ const segments = [];
7067
+ for (let index = 0; index < points.length - 1; index += 1) {
7068
+ const start = points[index];
7069
+ const end = points[index + 1];
7070
+ if (start === void 0 || end === void 0) {
7071
+ continue;
7072
+ }
7073
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
7074
+ if (length > 0) {
7075
+ segments.push({ start, end, length });
7076
+ }
7077
+ }
7078
+ return segments;
7079
+ }
7080
+ function labelOffset2(segment) {
7081
+ const offset = 10;
7082
+ const dx = segment.end.x - segment.start.x;
7083
+ const dy = segment.end.y - segment.start.y;
7084
+ return {
7085
+ x: -dy / segment.length * offset,
7086
+ y: dx / segment.length * offset
7087
+ };
7088
+ }
7089
+ function compartmentRows2(node) {
7090
+ const compartments2 = node.compartments;
7091
+ if (compartments2 === void 0) {
7092
+ return [];
7093
+ }
7094
+ return [
7095
+ ...compartments2.stereotype === void 0 ? [] : [compartments2.stereotype],
7096
+ ...compartments2.name === void 0 ? [node.label?.text ?? node.id] : [compartments2.name],
7097
+ ...compartments2.properties ?? [],
7098
+ ...compartments2.constraints ?? []
7099
+ ];
7100
+ }
3270
7101
  function portGeometry(nodeGeometry, port) {
3271
7102
  if (port === void 0) {
3272
7103
  return nodeGeometry;
@@ -3282,8 +7113,26 @@ function portGeometry(nodeGeometry, port) {
3282
7113
  obstacleBox: port.box
3283
7114
  };
3284
7115
  }
3285
- function stableById(items) {
3286
- return [...items].sort((a, b) => a.id.localeCompare(b.id));
7116
+ function stableUniqueById(items, diagnostics, pathRoot, code) {
7117
+ const firstById = /* @__PURE__ */ new Map();
7118
+ for (let index = 0; index < items.length; index += 1) {
7119
+ const item = items[index];
7120
+ if (item === void 0) {
7121
+ continue;
7122
+ }
7123
+ if (firstById.has(item.id)) {
7124
+ diagnostics.push({
7125
+ severity: "error",
7126
+ code,
7127
+ message: `Duplicate ${pathRoot.slice(0, -1)} id ${item.id} was ignored; first occurrence was kept.`,
7128
+ path: [pathRoot, index, "id"],
7129
+ detail: { id: item.id, duplicateIndex: index }
7130
+ });
7131
+ continue;
7132
+ }
7133
+ firstById.set(item.id, item);
7134
+ }
7135
+ return [...firstById.values()].sort((a, b) => a.id.localeCompare(b.id));
3287
7136
  }
3288
7137
  function stableByConstraintId(items) {
3289
7138
  return [...items].sort(
@@ -3343,7 +7192,8 @@ function renderDiagramDsl(source, options = {}) {
3343
7192
  }
3344
7193
  const solved = solveDiagram(normalized.diagram, {
3345
7194
  routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3346
- ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
7195
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
7196
+ ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
3347
7197
  });
3348
7198
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
3349
7199
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -3564,6 +7414,7 @@ exports.routeEdge = routeEdge;
3564
7414
  exports.runDagreInitialLayout = runDagreInitialLayout;
3565
7415
  exports.simplifyRoute = simplifyRoute;
3566
7416
  exports.solveDiagram = solveDiagram;
7417
+ exports.solveDiagramSafe = solveDiagramSafe;
3567
7418
  exports.sortDslDiagnostics = sortDslDiagnostics;
3568
7419
  exports.stringifyCanonical = stringifyCanonical;
3569
7420
  exports.toCanvasFont = toCanvasFont;