@crazyhappyone/auto-graph 0.0.6 → 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.js CHANGED
@@ -96,11 +96,21 @@ function applyLayoutConstraints(input) {
96
96
  const nodeById = new Map(input.nodes.map((node) => [node.id, node]));
97
97
  applyFixedPositionLocks(input.nodes, boxes, locks, diagnostics);
98
98
  applyExactPositions(input.constraints, boxes, locks, diagnostics, nodeById);
99
- applyContainment(input.constraints, boxes, locks, diagnostics);
99
+ applyContainment(input.constraints, boxes, locks, diagnostics, false);
100
100
  applyRelative(input.constraints, boxes, locks, diagnostics);
101
101
  applyAlign(input.constraints, boxes, locks, diagnostics);
102
102
  applyDistribute(input.constraints, boxes, locks, diagnostics);
103
- repairOverlaps(input, boxes, locks, diagnostics);
103
+ repairOverlaps(
104
+ input,
105
+ boxes,
106
+ locks,
107
+ diagnostics,
108
+ siblingOverlapKeys(input.constraints)
109
+ );
110
+ applyContainment(input.constraints, boxes, locks, diagnostics, true);
111
+ applyDistributeContained(input, boxes, locks, diagnostics);
112
+ reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
113
+ reportIntraContainerOverflow(input, boxes, diagnostics);
104
114
  return { boxes, locks, diagnostics };
105
115
  }
106
116
  function cloneValidBoxes(input, diagnostics) {
@@ -188,7 +198,7 @@ function applyExactPositions(constraints, boxes, locks, diagnostics, nodeById) {
188
198
  locks.set(targetId, { nodeId: targetId, source: "exact-position" });
189
199
  }
190
200
  }
191
- function applyContainment(constraints, boxes, locks, diagnostics) {
201
+ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow) {
192
202
  for (const constraint of constraints) {
193
203
  if (constraint.kind !== "containment") {
194
204
  continue;
@@ -210,21 +220,23 @@ function applyContainment(constraints, boxes, locks, diagnostics) {
210
220
  continue;
211
221
  }
212
222
  if (locks.has(childId)) {
213
- diagnostics.push({
214
- severity: "warning",
215
- code: "constraints.locked-target-not-moved",
216
- message: `Locked child ${childId} was not moved into containment.`,
217
- path: ["constraints", constraint.id ?? constraint.containerId],
218
- detail: { nodeId: childId }
219
- });
220
- if (!isInside(child, content)) {
223
+ if (!reportOverflow) {
221
224
  diagnostics.push({
222
- severity: "error",
223
- code: "constraints.containment.impossible",
224
- message: `Locked child ${childId} cannot fit inside ${constraint.containerId}.`,
225
+ severity: "warning",
226
+ code: "constraints.locked-target-not-moved",
227
+ message: `Locked child ${childId} was not moved into containment.`,
225
228
  path: ["constraints", constraint.id ?? constraint.containerId],
226
- detail: { nodeId: childId, containerId: constraint.containerId }
229
+ detail: { nodeId: childId }
227
230
  });
231
+ if (!isInside(child, content)) {
232
+ diagnostics.push({
233
+ severity: "error",
234
+ code: "constraints.containment.impossible",
235
+ message: `Locked child ${childId} cannot fit inside ${constraint.containerId}.`,
236
+ path: ["constraints", constraint.id ?? constraint.containerId],
237
+ detail: { nodeId: childId, containerId: constraint.containerId }
238
+ });
239
+ }
228
240
  }
229
241
  continue;
230
242
  }
@@ -239,6 +251,15 @@ function applyContainment(constraints, boxes, locks, diagnostics) {
239
251
  continue;
240
252
  }
241
253
  boxes.set(childId, next);
254
+ if (reportOverflow) {
255
+ diagnostics.push({
256
+ severity: "warning",
257
+ code: "containment_overflow",
258
+ message: `Child ${childId} was clamped back inside ${constraint.containerId} after constraint solving.`,
259
+ path: ["constraints", constraint.id ?? constraint.containerId],
260
+ detail: { nodeId: childId, containerId: constraint.containerId }
261
+ });
262
+ }
242
263
  }
243
264
  }
244
265
  }
@@ -314,10 +335,11 @@ function applyDistribute(constraints, boxes, locks, diagnostics) {
314
335
  }
315
336
  }
316
337
  }
317
- function repairOverlaps(input, boxes, locks, diagnostics) {
338
+ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
318
339
  const spacing = input.overlapSpacing ?? 40;
319
340
  const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
320
341
  const secondaryAxis = axis === "x" ? "y" : "x";
342
+ const ignoredPairs = containmentOverlapKeys(input.constraints);
321
343
  const ids = [...boxes.keys()].sort();
322
344
  for (let pass = 0; pass < 2; pass += 1) {
323
345
  for (const firstId of ids) {
@@ -325,6 +347,9 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
325
347
  if (firstId >= secondId) {
326
348
  continue;
327
349
  }
350
+ if (ignoredPairs.has(overlapKey(firstId, secondId))) {
351
+ continue;
352
+ }
328
353
  const first = boxes.get(firstId);
329
354
  const second = boxes.get(secondId);
330
355
  if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
@@ -339,16 +364,40 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
339
364
  const moving = movingId === firstId ? first : second;
340
365
  const fixed = movingId === firstId ? second : first;
341
366
  const repairAxis = firstLocked === secondLocked && pass === 0 ? secondaryAxis : axis;
342
- const moved = movePastOverlap(moving, fixed, repairAxis, spacing);
367
+ const pairKey = overlapKey(firstId, secondId);
368
+ const effectiveSpacing = siblingPairs.has(pairKey) ? Math.max(spacing, input.minSiblingGap ?? 0) : spacing;
369
+ const moved = movePastOverlap(
370
+ moving,
371
+ fixed,
372
+ repairAxis,
373
+ effectiveSpacing
374
+ );
343
375
  boxes.set(movingId, moved);
344
376
  }
345
377
  }
346
378
  }
379
+ reportOverlaps(boxes, diagnostics, ignoredPairs);
380
+ }
381
+ function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
382
+ const ids = [...boxes.keys()].sort();
383
+ const reported = new Set(
384
+ diagnostics.filter(
385
+ (diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
386
+ ).map((diagnostic) => {
387
+ const firstId = diagnostic.detail?.firstId;
388
+ const secondId = diagnostic.detail?.secondId;
389
+ return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
390
+ }).filter((key) => key !== void 0)
391
+ );
347
392
  for (const firstId of ids) {
348
393
  for (const secondId of ids) {
349
394
  if (firstId >= secondId) {
350
395
  continue;
351
396
  }
397
+ const key = overlapKey(firstId, secondId);
398
+ if (reported.has(key) || ignoredPairs.has(key)) {
399
+ continue;
400
+ }
352
401
  const first = boxes.get(firstId);
353
402
  const second = boxes.get(secondId);
354
403
  if (first !== void 0 && second !== void 0 && intersectsAabb(first, second)) {
@@ -359,9 +408,136 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
359
408
  path: ["boxes"],
360
409
  detail: { firstId, secondId }
361
410
  });
411
+ reported.add(key);
412
+ }
413
+ }
414
+ }
415
+ }
416
+ function reportIntraContainerOverflow(input, boxes, diagnostics) {
417
+ if (input.minSiblingGap === void 0) {
418
+ return;
419
+ }
420
+ const minGap = input.minSiblingGap;
421
+ const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
422
+ for (const constraint of input.constraints) {
423
+ if (constraint.kind !== "containment") {
424
+ continue;
425
+ }
426
+ const container = boxes.get(constraint.containerId);
427
+ if (container === void 0) {
428
+ continue;
429
+ }
430
+ const children = [];
431
+ for (const childId of constraint.childIds) {
432
+ const child = boxes.get(childId);
433
+ if (child !== void 0) {
434
+ children.push(child);
435
+ }
436
+ }
437
+ if (children.length < 2) {
438
+ continue;
439
+ }
440
+ const sorted = [...children].sort((a, b) => a[axis] - b[axis]);
441
+ const mainDim = axis === "x" ? "width" : "height";
442
+ let overlapPairs = 0;
443
+ for (let i = 0; i < sorted.length; i += 1) {
444
+ const first = sorted[i];
445
+ if (first === void 0) {
446
+ continue;
447
+ }
448
+ for (let j = i + 1; j < sorted.length; j += 1) {
449
+ const second = sorted[j];
450
+ if (second === void 0) {
451
+ continue;
452
+ }
453
+ if (second[axis] >= first[axis] + first[mainDim]) {
454
+ break;
455
+ }
456
+ if (intersectsAabb(first, second)) {
457
+ overlapPairs += 1;
458
+ }
362
459
  }
363
460
  }
461
+ if (overlapPairs > 0) {
462
+ diagnostics.push({
463
+ severity: "warning",
464
+ code: "intra_container_overflow",
465
+ message: `${overlapPairs} sibling pair(s) overlap inside ${constraint.containerId}.`,
466
+ path: ["constraints", constraint.id ?? constraint.containerId],
467
+ detail: {
468
+ containerId: constraint.containerId,
469
+ overlapPairs,
470
+ minGap
471
+ }
472
+ });
473
+ }
474
+ const pad = constraint.padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
475
+ const contentMain = mainDim === "width" ? Math.max(0, container.width - pad.left - pad.right) : Math.max(0, container.height - pad.top - pad.bottom);
476
+ let childStart = Infinity;
477
+ let childEnd = -Infinity;
478
+ for (const child of sorted) {
479
+ const start = child[axis];
480
+ const end = start + child[mainDim];
481
+ if (start < childStart) childStart = start;
482
+ if (end > childEnd) childEnd = end;
483
+ }
484
+ if (sorted.length === 0) {
485
+ childStart = 0;
486
+ childEnd = 0;
487
+ }
488
+ const actualExtent = childEnd - childStart;
489
+ if (actualExtent > contentMain) {
490
+ diagnostics.push({
491
+ severity: "error",
492
+ code: "intra_container_overflow_total",
493
+ message: `Container ${constraint.containerId} cannot fit ${sorted.length} siblings along ${axis} (extent ${actualExtent}, available ${contentMain}).`,
494
+ path: ["constraints", constraint.id ?? constraint.containerId],
495
+ detail: {
496
+ containerId: constraint.containerId,
497
+ axis,
498
+ needed: actualExtent,
499
+ available: contentMain,
500
+ siblingCount: sorted.length,
501
+ minGap
502
+ }
503
+ });
504
+ }
505
+ }
506
+ }
507
+ function overlapKey(firstId, secondId) {
508
+ return firstId < secondId ? `${firstId}\0${secondId}` : `${secondId}\0${firstId}`;
509
+ }
510
+ function containmentOverlapKeys(constraints) {
511
+ const keys = /* @__PURE__ */ new Set();
512
+ for (const constraint of constraints) {
513
+ if (constraint.kind !== "containment") {
514
+ continue;
515
+ }
516
+ for (const childId of constraint.childIds) {
517
+ keys.add(overlapKey(constraint.containerId, childId));
518
+ }
364
519
  }
520
+ return keys;
521
+ }
522
+ function siblingOverlapKeys(constraints) {
523
+ const keys = /* @__PURE__ */ new Set();
524
+ for (const constraint of constraints) {
525
+ if (constraint.kind !== "containment") {
526
+ continue;
527
+ }
528
+ const { childIds } = constraint;
529
+ for (let i = 0; i < childIds.length; i += 1) {
530
+ for (let j = i + 1; j < childIds.length; j += 1) {
531
+ const a = childIds[i];
532
+ const b = childIds[j];
533
+ if (a === void 0 || b === void 0) {
534
+ continue;
535
+ }
536
+ keys.add(overlapKey(a, b));
537
+ }
538
+ }
539
+ }
540
+ return keys;
365
541
  }
366
542
  function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
367
543
  const current = boxes.get(id);
@@ -479,10 +655,125 @@ function contentBox(container, padding) {
479
655
  return {
480
656
  x: container.x + margin.left,
481
657
  y: container.y + margin.top,
482
- width: container.width - margin.left - margin.right,
483
- height: container.height - margin.top - margin.bottom
658
+ width: Math.max(0, container.width - margin.left - margin.right),
659
+ height: Math.max(0, container.height - margin.top - margin.bottom)
484
660
  };
485
661
  }
662
+ function applyDistributeContained(input, boxes, locks, diagnostics) {
663
+ if (!input.distributeContainedChildren) {
664
+ return;
665
+ }
666
+ const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
667
+ const crossAxis = axis === "x" ? "y" : "x";
668
+ const mainSize = axis === "x" ? "width" : "height";
669
+ const crossSize = axis === "x" ? "height" : "width";
670
+ const minGap = input.minSiblingGap ?? 8;
671
+ for (const constraint of input.constraints) {
672
+ if (constraint.kind !== "containment") {
673
+ continue;
674
+ }
675
+ const container = boxes.get(constraint.containerId);
676
+ if (container === void 0) {
677
+ continue;
678
+ }
679
+ const content = contentBox(container, constraint.padding);
680
+ const unlocked = [];
681
+ const reserved = [];
682
+ for (const childId of constraint.childIds) {
683
+ const box = boxes.get(childId);
684
+ if (box === void 0) {
685
+ continue;
686
+ }
687
+ if (locks.has(childId)) {
688
+ diagnostics.push({
689
+ severity: "warning",
690
+ code: "constraints.locked-target-not-moved",
691
+ message: `Locked child ${childId} skipped during containment distribution.`,
692
+ path: ["constraints", constraint.id ?? constraint.containerId],
693
+ detail: { nodeId: childId }
694
+ });
695
+ reserved.push(intervalForBox(box, axis, mainSize));
696
+ continue;
697
+ }
698
+ unlocked.push({ id: childId, box });
699
+ }
700
+ if (unlocked.length < 2) {
701
+ continue;
702
+ }
703
+ const oversized = unlocked.filter(
704
+ (child) => child.box[mainSize] > content[mainSize] || child.box[crossSize] > content[crossSize]
705
+ );
706
+ if (oversized.length > 0) {
707
+ diagnostics.push({
708
+ severity: "warning",
709
+ code: "constraints.containment.impossible",
710
+ message: `Skipped ${oversized.length} oversized child(ren) during distribution in ${constraint.containerId}.`,
711
+ path: ["constraints", constraint.id ?? constraint.containerId],
712
+ detail: {
713
+ containerId: constraint.containerId,
714
+ oversized: oversized.map((c) => c.id)
715
+ }
716
+ });
717
+ }
718
+ for (const child of oversized) {
719
+ reserved.push(intervalForBox(child.box, axis, mainSize));
720
+ }
721
+ reserved.sort((a, b) => a.start - b.start || a.end - b.end);
722
+ const distributable = unlocked.filter(
723
+ (child) => child.box[mainSize] <= content[mainSize] && child.box[crossSize] <= content[crossSize]
724
+ );
725
+ if (distributable.length < 2) {
726
+ continue;
727
+ }
728
+ let pos = content[axis];
729
+ for (const child of distributable) {
730
+ pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
731
+ const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
732
+ const next = { ...child.box };
733
+ next[axis] = pos;
734
+ next[crossAxis] = crossPos;
735
+ const clamped = moveInside(next, content);
736
+ if (clamped[axis] !== next[axis]) {
737
+ diagnostics.push({
738
+ severity: "warning",
739
+ code: "intra_container_distributed_clamped",
740
+ message: `Distribution gap clamped for ${child.id} in ${constraint.containerId}.`,
741
+ path: ["constraints", constraint.id ?? constraint.containerId],
742
+ detail: { nodeId: child.id, containerId: constraint.containerId }
743
+ });
744
+ }
745
+ boxes.set(child.id, clamped);
746
+ pos = clamped[axis] + clamped[mainSize] + minGap;
747
+ }
748
+ diagnostics.push({
749
+ severity: "info",
750
+ code: "intra_container_distributed",
751
+ message: `Distributed ${distributable.length} children in ${constraint.containerId} along ${axis}.`,
752
+ path: ["constraints", constraint.id ?? constraint.containerId],
753
+ detail: {
754
+ containerId: constraint.containerId,
755
+ count: distributable.length,
756
+ axis
757
+ }
758
+ });
759
+ }
760
+ }
761
+ function intervalForBox(box, axis, mainSize) {
762
+ return { start: box[axis], end: box[axis] + box[mainSize] };
763
+ }
764
+ function advancePastReserved(pos, size, reserved, minGap) {
765
+ let next = pos;
766
+ for (const interval of reserved) {
767
+ if (next + size + minGap <= interval.start) {
768
+ break;
769
+ }
770
+ if (next >= interval.end + minGap) {
771
+ continue;
772
+ }
773
+ next = interval.end + minGap;
774
+ }
775
+ return next;
776
+ }
486
777
  function moveInside(child, container) {
487
778
  return {
488
779
  ...child,
@@ -1188,7 +1479,11 @@ var DEFAULT_GROUP_PADDING = {
1188
1479
  left: 16
1189
1480
  };
1190
1481
  var DEFAULT_LABEL_MAX_WIDTH = 160;
1191
- var DEFAULT_FONT = { fontFamily: "Arial", fontSize: 14, lineHeight: 18 };
1482
+ var DEFAULT_FONT = {
1483
+ fontFamily: "Arial",
1484
+ fontSize: 14,
1485
+ lineHeight: 18
1486
+ };
1192
1487
  var DEFAULT_MATRIX_CELL_SIZE = { width: 120, height: 36 };
1193
1488
  var DEFAULT_TABLE_CELL_SIZE = { width: 128, height: 34 };
1194
1489
  var DEFAULT_PANEL_ITEM_HEIGHT = 28;
@@ -1364,7 +1659,9 @@ function endpoint(value, nodeIdOverride) {
1364
1659
  function style(value) {
1365
1660
  return {
1366
1661
  ...value.fill === void 0 ? {} : { fill: value.fill },
1367
- ...value.stroke === void 0 ? {} : { stroke: value.stroke }
1662
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke },
1663
+ ...value.fontFamily === void 0 ? {} : { fontFamily: value.fontFamily },
1664
+ ...value.fontSize === void 0 ? {} : { fontSize: value.fontSize }
1368
1665
  };
1369
1666
  }
1370
1667
  function compartments(value) {
@@ -1381,6 +1678,10 @@ function normalizeFrame(frame) {
1381
1678
  ...frame.context === void 0 ? {} : { context: frame.context },
1382
1679
  ...frame.name === void 0 ? {} : { name: frame.name },
1383
1680
  titleTab: frame.titleTab,
1681
+ ...frame.headerHeight === void 0 ? {} : { headerHeight: frame.headerHeight },
1682
+ ...frame.padding === void 0 ? {} : { padding: frame.padding },
1683
+ ...frame.labelPosition === void 0 ? {} : { labelPosition: frame.labelPosition },
1684
+ ...frame.direction === void 0 ? {} : { direction: frame.direction },
1384
1685
  ...frame.style === void 0 ? {} : { style: style(frame.style) }
1385
1686
  };
1386
1687
  }
@@ -1524,6 +1825,9 @@ function normalizeGroups(dsl, measurer) {
1524
1825
  nodeIds: [...group?.nodes ?? []],
1525
1826
  groupIds: [...group?.groups ?? []],
1526
1827
  padding: group?.padding ?? { ...DEFAULT_GROUP_PADDING },
1828
+ ...group?.headerHeight === void 0 ? {} : { headerHeight: group.headerHeight },
1829
+ ...group?.labelPosition === void 0 ? {} : { labelPosition: group.labelPosition },
1830
+ ...group?.direction === void 0 ? {} : { direction: group.direction },
1527
1831
  ...labelLayout === void 0 ? {} : { labelLayout }
1528
1832
  };
1529
1833
  });
@@ -1793,6 +2097,12 @@ var insetsSchema = z.object({
1793
2097
  bottom: finiteNumberSchema,
1794
2098
  left: finiteNumberSchema
1795
2099
  });
2100
+ var nonNegativeInsetsSchema = z.object({
2101
+ top: nonNegativeNumberSchema,
2102
+ right: nonNegativeNumberSchema,
2103
+ bottom: nonNegativeNumberSchema,
2104
+ left: nonNegativeNumberSchema
2105
+ });
1796
2106
  var labelSchema = z.union([
1797
2107
  z.string(),
1798
2108
  z.object({
@@ -1802,7 +2112,9 @@ var labelSchema = z.union([
1802
2112
  ]);
1803
2113
  var styleSchema = z.object({
1804
2114
  fill: z.string().optional(),
1805
- stroke: z.string().optional()
2115
+ stroke: z.string().optional(),
2116
+ fontFamily: z.string().optional(),
2117
+ fontSize: finiteNumberSchema.optional()
1806
2118
  });
1807
2119
  var blockCellSchema = z.union([
1808
2120
  z.string(),
@@ -1873,7 +2185,10 @@ var groupSchema = z.object({
1873
2185
  label: labelSchema.optional(),
1874
2186
  nodes: z.array(z.string()).optional(),
1875
2187
  groups: z.array(z.string()).optional(),
1876
- padding: insetsSchema.optional()
2188
+ padding: insetsSchema.optional(),
2189
+ headerHeight: nonNegativeNumberSchema.optional(),
2190
+ labelPosition: z.enum(["top", "inside", "outside"]).optional(),
2191
+ direction: z.enum(["horizontal", "vertical"]).optional()
1877
2192
  });
1878
2193
  var swimlaneSchema = z.object({
1879
2194
  label: labelSchema.optional(),
@@ -2085,6 +2400,10 @@ var diagramDslSchema = z.object({
2085
2400
  context: z.string().optional(),
2086
2401
  name: z.string().optional(),
2087
2402
  titleTab: z.string(),
2403
+ headerHeight: nonNegativeNumberSchema.optional(),
2404
+ padding: z.union([nonNegativeNumberSchema, nonNegativeInsetsSchema]).optional(),
2405
+ labelPosition: z.enum(["top", "inside", "outside"]).optional(),
2406
+ direction: z.enum(["horizontal", "vertical"]).optional(),
2088
2407
  style: styleSchema.optional()
2089
2408
  }).optional(),
2090
2409
  output: z.object({
@@ -3219,7 +3538,7 @@ function renderSolvedTextAnnotation(annotation, className, options) {
3219
3538
  `data-text-surface="${escapeAttribute(annotation.surfaceKind)}"`,
3220
3539
  `data-owner-id="${escapeAttribute(annotation.ownerId)}"`,
3221
3540
  `data-text-backend="${escapeAttribute(annotation.textBackend ?? "deterministic")}"`,
3222
- `font-family="${FONT_FAMILY}"`,
3541
+ `font-family="${escapeAttribute(annotation.fontFamily)}"`,
3223
3542
  `font-size="${formatNumber(annotation.fontSize)}"`,
3224
3543
  `fill="#111827"`
3225
3544
  ];
@@ -3547,7 +3866,12 @@ function routeEdge(input) {
3547
3866
  input.source.center,
3548
3867
  input.targetAnchor ?? defaultAnchors.targetAnchor
3549
3868
  );
3550
- const points = simplifyRoute([source, target]);
3869
+ const points = finalizeRoute(
3870
+ [source, target],
3871
+ softObstacles,
3872
+ hardObstacles,
3873
+ diagnostics
3874
+ );
3551
3875
  if (routeCrossesBoxes(points, hardObstacles)) {
3552
3876
  diagnostics.push({
3553
3877
  severity: "error",
@@ -3603,7 +3927,15 @@ function routeEdge(input) {
3603
3927
  candidate.points,
3604
3928
  candidate.endpointObstacles
3605
3929
  )) {
3606
- return { points: simplifyRoute(candidate.points), diagnostics };
3930
+ return {
3931
+ points: finalizeRoute(
3932
+ candidate.points,
3933
+ softObstacles,
3934
+ hardObstacles,
3935
+ diagnostics
3936
+ ),
3937
+ diagnostics
3938
+ };
3607
3939
  }
3608
3940
  }
3609
3941
  const hardClearCandidate = candidateRoutes.find(
@@ -3619,7 +3951,12 @@ function routeEdge(input) {
3619
3951
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
3620
3952
  });
3621
3953
  return {
3622
- points: simplifyRoute(hardClearCandidate.points),
3954
+ points: finalizeRoute(
3955
+ hardClearCandidate.points,
3956
+ softObstacles,
3957
+ hardObstacles,
3958
+ diagnostics
3959
+ ),
3623
3960
  diagnostics
3624
3961
  };
3625
3962
  }
@@ -3630,8 +3967,11 @@ function routeEdge(input) {
3630
3967
  message: "No bounded orthogonal route candidate avoided hard evidence block obstacles."
3631
3968
  });
3632
3969
  return {
3633
- points: simplifyRoute(
3634
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors)
3970
+ points: finalizeRoute(
3971
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3972
+ softObstacles,
3973
+ hardObstacles,
3974
+ diagnostics
3635
3975
  ),
3636
3976
  diagnostics
3637
3977
  };
@@ -3642,12 +3982,133 @@ function routeEdge(input) {
3642
3982
  message: "No bounded orthogonal route candidate avoided all obstacles."
3643
3983
  });
3644
3984
  return {
3645
- points: simplifyRoute(
3646
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors)
3985
+ points: finalizeRoute(
3986
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3987
+ softObstacles,
3988
+ hardObstacles,
3989
+ diagnostics
3647
3990
  ),
3648
3991
  diagnostics
3649
3992
  };
3650
3993
  }
3994
+ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3995
+ const simplified = simplifyRoute(points);
3996
+ const crossesHardObstacles = routeCrossesBoxes(simplified, hardObstacles);
3997
+ const crossesSoftObstacles = routeCrossesBoxes(simplified, softObstacles);
3998
+ if (simplified.length < 3 && (crossesHardObstacles || crossesSoftObstacles)) {
3999
+ diagnostics.push({
4000
+ severity: crossesHardObstacles ? "error" : "warning",
4001
+ code: "route_obstacle_fallback",
4002
+ message: "Obstacle-aware routing fell back to fewer than three route points.",
4003
+ detail: { pointCount: simplified.length }
4004
+ });
4005
+ return expandFallbackRoute(simplified, [
4006
+ ...softObstacles,
4007
+ ...hardObstacles
4008
+ ]);
4009
+ }
4010
+ return simplified;
4011
+ }
4012
+ function expandFallbackRoute(points, obstacles) {
4013
+ if (points.length !== 2) {
4014
+ return points.map((point2) => ({ ...point2 }));
4015
+ }
4016
+ const [source, target] = points;
4017
+ if (source === void 0 || target === void 0) {
4018
+ return points.map((point2) => ({ ...point2 }));
4019
+ }
4020
+ if (source.y === target.y) {
4021
+ const detourY = horizontalDetourLane(source, target, obstacles);
4022
+ return [
4023
+ { ...source },
4024
+ { x: source.x, y: detourY },
4025
+ { x: target.x, y: detourY },
4026
+ { ...target }
4027
+ ];
4028
+ }
4029
+ if (source.x === target.x) {
4030
+ const detourX = verticalDetourLane(source, target, obstacles);
4031
+ return [
4032
+ { ...source },
4033
+ { x: detourX, y: source.y },
4034
+ { x: detourX, y: target.y },
4035
+ { ...target }
4036
+ ];
4037
+ }
4038
+ const hv = diagonalDetourHV(source, target, obstacles);
4039
+ const vh = diagonalDetourVH(source, target, obstacles);
4040
+ const viable = [hv, vh].filter((c) => !routeCrossesBoxes(c, obstacles));
4041
+ if (viable.length > 0) {
4042
+ const directLen = Math.hypot(target.x - source.x, target.y - source.y);
4043
+ let best = viable[0];
4044
+ for (let i = 1; i < viable.length; i += 1) {
4045
+ const cand = viable[i];
4046
+ if (cand !== void 0 && pathLength(cand) - directLen < pathLength(best) - directLen) {
4047
+ best = cand;
4048
+ }
4049
+ }
4050
+ return best;
4051
+ }
4052
+ return [
4053
+ { ...source },
4054
+ { x: (source.x + target.x) / 2, y: source.y },
4055
+ { x: (source.x + target.x) / 2, y: target.y },
4056
+ { ...target }
4057
+ ];
4058
+ }
4059
+ function horizontalDetourLane(source, target, obstacles) {
4060
+ const crossing = obstacles.filter(
4061
+ (obstacle) => segmentIntersectsBox(source, target, obstacle)
4062
+ );
4063
+ if (crossing.length === 0) {
4064
+ return source.y + (source.x <= target.x ? 1 : -1) * 24;
4065
+ }
4066
+ const margin = 24;
4067
+ const above = Math.min(...crossing.map((obstacle) => obstacle.y)) - margin;
4068
+ const below = Math.max(...crossing.map((obstacle) => obstacle.y + obstacle.height)) + margin;
4069
+ return Math.abs(above - source.y) <= Math.abs(below - source.y) ? above : below;
4070
+ }
4071
+ function verticalDetourLane(source, target, obstacles) {
4072
+ const crossing = obstacles.filter(
4073
+ (obstacle) => segmentIntersectsBox(source, target, obstacle)
4074
+ );
4075
+ if (crossing.length === 0) {
4076
+ return source.x + (source.y <= target.y ? 1 : -1) * 24;
4077
+ }
4078
+ const margin = 24;
4079
+ const left = Math.min(...crossing.map((obstacle) => obstacle.x)) - margin;
4080
+ const right = Math.max(...crossing.map((obstacle) => obstacle.x + obstacle.width)) + margin;
4081
+ return Math.abs(left - source.x) <= Math.abs(right - source.x) ? left : right;
4082
+ }
4083
+ function diagonalDetourHV(source, target, obstacles) {
4084
+ const detourY = horizontalDetourLane(source, target, obstacles);
4085
+ return [
4086
+ { ...source },
4087
+ { x: source.x, y: detourY },
4088
+ { x: target.x, y: detourY },
4089
+ { ...target }
4090
+ ];
4091
+ }
4092
+ function diagonalDetourVH(source, target, obstacles) {
4093
+ const detourX = verticalDetourLane(source, target, obstacles);
4094
+ return [
4095
+ { ...source },
4096
+ { x: detourX, y: source.y },
4097
+ { x: detourX, y: target.y },
4098
+ { ...target }
4099
+ ];
4100
+ }
4101
+ function pathLength(points) {
4102
+ let len = 0;
4103
+ for (let i = 1; i < points.length; i += 1) {
4104
+ const a = points[i - 1];
4105
+ const b = points[i];
4106
+ if (a !== void 0 && b !== void 0) {
4107
+ len += Math.hypot(b.x - a.x, b.y - a.y);
4108
+ }
4109
+ }
4110
+ return len;
4111
+ }
3651
4112
  function endpointObstaclesForAutoAnchors(input) {
3652
4113
  const boxes = [];
3653
4114
  if (input.sourceAnchor === void 0 && hasDistinctAnchors(input.source)) {
@@ -4019,6 +4480,15 @@ var DEFAULT_PANEL_WIDTH = 320;
4019
4480
  var DEFAULT_PANEL_ITEM_HEIGHT2 = 28;
4020
4481
  var DEFAULT_EVIDENCE_BLOCK_GAP = 24;
4021
4482
  var EDGE_LABEL_CLEARANCE = 8;
4483
+ var DEFAULT_CJK_FONT_FAMILY = "YaHei,SimSun,sans-serif";
4484
+ var DEFAULT_MIN_CJK_FONT_SIZE = 14;
4485
+ function prefitLabelFont(node, _options) {
4486
+ const cjk = labelCjkTypography(node.label?.metadata);
4487
+ const fontFamily = cjk.fontFamily ?? DEFAULT_FONT.fontFamily;
4488
+ const fontSize = cjk.fontSize ?? DEFAULT_FONT.fontSize;
4489
+ const lineHeight = fontSize !== DEFAULT_FONT.fontSize ? Math.max(DEFAULT_FONT.lineHeight ?? 18, fontSize * 1.2) : DEFAULT_FONT.lineHeight ?? 18;
4490
+ return { fontFamily, fontSize, lineHeight };
4491
+ }
4022
4492
  var EVIDENCE_TEXT_FONT = {
4023
4493
  fontFamily: "Arial, sans-serif",
4024
4494
  fontSize: 10,
@@ -4026,43 +4496,85 @@ var EVIDENCE_TEXT_FONT = {
4026
4496
  };
4027
4497
  function solveDiagram(diagram, options = {}) {
4028
4498
  const diagnostics = [...diagram.diagnostics];
4029
- const nodes = stableById(diagram.nodes);
4030
- const edges = stableById(diagram.edges);
4031
- const groups = stableById(diagram.groups);
4499
+ const nodes = stableUniqueById(
4500
+ diagram.nodes,
4501
+ diagnostics,
4502
+ "nodes",
4503
+ "duplicate_node_id"
4504
+ );
4505
+ const edges = stableUniqueById(
4506
+ diagram.edges,
4507
+ diagnostics,
4508
+ "edges",
4509
+ "duplicate_edge_id"
4510
+ );
4511
+ const groups = stableUniqueById(
4512
+ diagram.groups,
4513
+ diagnostics,
4514
+ "groups",
4515
+ "duplicate_group_id"
4516
+ );
4517
+ const cjkTypography = createCjkTypographyOptions(options);
4518
+ const cjkStyledNodes = nodes.map(
4519
+ (node) => enhanceNodeCjkTypography(node, cjkTypography, diagnostics)
4520
+ );
4521
+ const styledNodes = options.prefitLabelSize === true ? cjkStyledNodes.map(
4522
+ (node) => prefitNodeLabelSize(node, options, diagnostics)
4523
+ ) : cjkStyledNodes;
4524
+ const styledEdges = edges.map(
4525
+ (edge) => enhanceEdgeCjkTypography(edge, cjkTypography, diagnostics)
4526
+ );
4527
+ const styledGroups = groups.map(
4528
+ (group) => enhanceGroupCjkTypography(group, cjkTypography, diagnostics)
4529
+ );
4530
+ const styledSwimlanes = (diagram.swimlanes ?? []).map(
4531
+ (swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
4532
+ );
4032
4533
  const constraints = stableByConstraintId(diagram.constraints);
4033
4534
  const layout2 = runDagreInitialLayout({
4034
4535
  direction: diagram.direction,
4035
- nodes: nodes.map((node) => ({ id: node.id, size: node.size })),
4036
- edges: edges.map((edge) => ({
4536
+ nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4537
+ edges: styledEdges.map((edge) => ({
4037
4538
  id: edge.id,
4038
4539
  sourceId: edge.source.nodeId,
4039
4540
  targetId: edge.target.nodeId
4040
4541
  }))
4041
4542
  });
4042
4543
  diagnostics.push(...layout2.diagnostics);
4544
+ const initialNodeBoxes = wrapVerticalStackIfNeeded(
4545
+ layout2.boxes,
4546
+ styledNodes,
4547
+ styledEdges,
4548
+ diagram.direction,
4549
+ options,
4550
+ diagnostics
4551
+ );
4043
4552
  const constrained = applyLayoutConstraints({
4044
4553
  direction: diagram.direction,
4045
4554
  overlapSpacing: options?.overlapSpacing ?? 40,
4046
- boxes: layout2.boxes,
4047
- nodes,
4555
+ ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4556
+ ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
4557
+ boxes: initialNodeBoxes,
4558
+ nodes: styledNodes,
4048
4559
  constraints
4049
4560
  });
4050
4561
  diagnostics.push(...constrained.diagnostics);
4051
4562
  const swimlaneContracts = applySwimlaneLayoutContracts(
4052
- diagram.swimlanes ?? [],
4563
+ styledSwimlanes,
4053
4564
  constraints,
4054
- edges,
4565
+ styledEdges,
4055
4566
  isTopToBottomReadingDirection(diagram.metadata?.primaryReadingDirection),
4056
4567
  constrained.boxes,
4057
4568
  constrained.locks,
4058
- options?.overlapSpacing ?? 40
4569
+ options?.overlapSpacing ?? 40,
4570
+ Math.max(0, options?.minLaneGutter ?? 0)
4059
4571
  );
4060
4572
  if (swimlaneContracts.layouts.size > 0) {
4061
4573
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4062
4574
  }
4063
4575
  diagnostics.push(...swimlaneContracts.diagnostics);
4064
4576
  const coordinatedNodes = coordinateNodes(
4065
- nodes,
4577
+ styledNodes,
4066
4578
  constrained.boxes,
4067
4579
  options,
4068
4580
  diagnostics
@@ -4078,13 +4590,13 @@ function solveDiagram(diagram, options = {}) {
4078
4590
  ])
4079
4591
  );
4080
4592
  const coordinatedGroups = coordinateGroups(
4081
- groups,
4593
+ styledGroups,
4082
4594
  constrained.boxes,
4083
4595
  options,
4084
4596
  diagnostics
4085
4597
  );
4086
4598
  const coordinatedSwimlanes = coordinateSwimlanes(
4087
- diagram.swimlanes ?? [],
4599
+ styledSwimlanes,
4088
4600
  constrained.boxes,
4089
4601
  swimlaneContracts.layouts
4090
4602
  );
@@ -4184,7 +4696,7 @@ function solveDiagram(diagram, options = {}) {
4184
4696
  ...frameTextAnnotation.filter(isPreRouteTextObstacle)
4185
4697
  ];
4186
4698
  const coordinatedEdges = coordinateEdges(
4187
- edges,
4699
+ styledEdges,
4188
4700
  nodeGeometryById,
4189
4701
  coordinatedNodes,
4190
4702
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
@@ -4200,6 +4712,11 @@ function solveDiagram(diagram, options = {}) {
4200
4712
  );
4201
4713
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
4202
4714
  coordinatedEdges,
4715
+ [
4716
+ ...coordinatedNodes.map((node) => node.box),
4717
+ ...baseTextAnnotations.map((annotation) => annotation.box),
4718
+ ...frameTextAnnotation.map((annotation) => annotation.box)
4719
+ ],
4203
4720
  options.textMeasurer
4204
4721
  );
4205
4722
  const textAnnotations = [
@@ -4217,6 +4734,12 @@ function solveDiagram(diagram, options = {}) {
4217
4734
  ...edgePointBounds,
4218
4735
  ...edgeTextAnnotations.map((annotation) => annotation.box)
4219
4736
  ];
4737
+ diagnostics.push(
4738
+ ...reportPageOverflow(
4739
+ frame === void 0 ? unionBoxes(boundsBase) : unionBoxes([...boundsBase, frame.box, frame.titleBox]),
4740
+ options.pageBounds
4741
+ )
4742
+ );
4220
4743
  return {
4221
4744
  id: diagram.id,
4222
4745
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -4235,7 +4758,303 @@ function solveDiagram(diagram, options = {}) {
4235
4758
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
4236
4759
  };
4237
4760
  }
4238
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing) {
4761
+ function solveDiagramSafe(diagram, options = {}) {
4762
+ return solveDiagram(diagram, { ...options, prefitLabelSize: true });
4763
+ }
4764
+ function prefitNodeLabelSize(node, options, diagnostics) {
4765
+ if (node.label === void 0) {
4766
+ return node;
4767
+ }
4768
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
4769
+ const layout2 = fitLabel(
4770
+ node.label.text,
4771
+ {
4772
+ font: prefitLabelFont(node),
4773
+ padding: DEFAULT_NODE_PADDING,
4774
+ minSize: DEFAULT_NODE_MIN_SIZE,
4775
+ maxWidth: node.label.maxWidth ?? Math.max(node.size.width, DEFAULT_LABEL_MAX_WIDTH)
4776
+ },
4777
+ measurer
4778
+ );
4779
+ const width = Math.max(node.size.width, layout2.fittedSize.width);
4780
+ const height = Math.max(node.size.height, layout2.fittedSize.height);
4781
+ const resized = width !== node.size.width || height !== node.size.height;
4782
+ if (resized) {
4783
+ diagnostics.push({
4784
+ severity: "info",
4785
+ code: "prefit_label_resized",
4786
+ message: `Node ${node.id} size expanded to fit its label.`,
4787
+ path: ["nodes", node.id],
4788
+ detail: {
4789
+ nodeId: node.id,
4790
+ from: { width: node.size.width, height: node.size.height },
4791
+ to: { width, height }
4792
+ }
4793
+ });
4794
+ }
4795
+ const centeredLayout = expandLabelLayoutToNode(layout2, { width, height });
4796
+ return { ...node, size: { width, height }, labelLayout: centeredLayout };
4797
+ }
4798
+ function expandLabelLayoutToNode(layout2, nodeSize) {
4799
+ if (layout2.box.width >= nodeSize.width && layout2.box.height >= nodeSize.height) {
4800
+ return layout2;
4801
+ }
4802
+ const offsetX = Math.max(0, (nodeSize.width - layout2.box.width) / 2);
4803
+ const offsetY = Math.max(0, (nodeSize.height - layout2.box.height) / 2);
4804
+ if (offsetX === 0 && offsetY === 0) {
4805
+ return layout2;
4806
+ }
4807
+ return {
4808
+ ...layout2,
4809
+ box: {
4810
+ x: layout2.box.x + offsetX,
4811
+ y: layout2.box.y + offsetY,
4812
+ width: layout2.box.width,
4813
+ height: layout2.box.height
4814
+ },
4815
+ contentBox: {
4816
+ x: layout2.contentBox.x + offsetX,
4817
+ y: layout2.contentBox.y + offsetY,
4818
+ width: layout2.contentBox.width,
4819
+ height: layout2.contentBox.height
4820
+ },
4821
+ lines: layout2.lines.map((line) => ({
4822
+ ...line,
4823
+ box: {
4824
+ x: line.box.x + offsetX,
4825
+ y: line.box.y + offsetY,
4826
+ width: line.box.width,
4827
+ height: line.box.height
4828
+ }
4829
+ }))
4830
+ };
4831
+ }
4832
+ function reportPageOverflow(contentBounds, pageBounds) {
4833
+ if (pageBounds === void 0) {
4834
+ return [];
4835
+ }
4836
+ const overflowRight = Math.max(
4837
+ 0,
4838
+ contentBounds.x + contentBounds.width - pageBounds.width
4839
+ );
4840
+ const overflowBottom = Math.max(
4841
+ 0,
4842
+ contentBounds.y + contentBounds.height - pageBounds.height
4843
+ );
4844
+ const overflowLeft = Math.max(0, -contentBounds.x);
4845
+ const overflowTop = Math.max(0, -contentBounds.y);
4846
+ if (overflowRight === 0 && overflowBottom === 0 && overflowLeft === 0 && overflowTop === 0) {
4847
+ return [];
4848
+ }
4849
+ return [
4850
+ {
4851
+ severity: "warning",
4852
+ code: "page_overflow",
4853
+ message: `Content ${contentBounds.width}x${contentBounds.height} exceeds page ${pageBounds.width}x${pageBounds.height}.`,
4854
+ path: ["bounds"],
4855
+ detail: {
4856
+ page: { width: pageBounds.width, height: pageBounds.height },
4857
+ content: {
4858
+ width: contentBounds.width,
4859
+ height: contentBounds.height
4860
+ },
4861
+ overflow: {
4862
+ right: overflowRight,
4863
+ bottom: overflowBottom,
4864
+ left: overflowLeft,
4865
+ top: overflowTop
4866
+ }
4867
+ }
4868
+ }
4869
+ ];
4870
+ }
4871
+ function createCjkTypographyOptions(options) {
4872
+ const fontFamily = options.cjkFontFamily === false ? void 0 : options.cjkFontFamily ?? DEFAULT_CJK_FONT_FAMILY;
4873
+ const minFontSize = options.minCjkFontSize === false ? void 0 : options.minCjkFontSize ?? DEFAULT_MIN_CJK_FONT_SIZE;
4874
+ return {
4875
+ ...fontFamily === void 0 ? {} : { fontFamily },
4876
+ ...minFontSize === void 0 ? {} : { minFontSize }
4877
+ };
4878
+ }
4879
+ function enhanceNodeCjkTypography(node, options, diagnostics) {
4880
+ const nodeWithStyle = enhanceStyledLabelOwner(
4881
+ node,
4882
+ ["nodes", node.id],
4883
+ options,
4884
+ diagnostics
4885
+ );
4886
+ const ports = nodeWithStyle.ports === void 0 ? void 0 : nodeWithStyle.ports.map(
4887
+ (port) => enhanceStyledLabelOwner(
4888
+ port,
4889
+ ["nodes", node.id, "ports", port.id],
4890
+ options,
4891
+ diagnostics
4892
+ )
4893
+ );
4894
+ return ports === void 0 ? nodeWithStyle : { ...nodeWithStyle, ports };
4895
+ }
4896
+ function enhanceEdgeCjkTypography(edge, options, diagnostics) {
4897
+ return enhanceStyledLabelOwner(
4898
+ edge,
4899
+ ["edges", edge.id],
4900
+ options,
4901
+ diagnostics
4902
+ );
4903
+ }
4904
+ function enhanceGroupCjkTypography(group, options, diagnostics) {
4905
+ return enhanceStyledLabelOwner(
4906
+ group,
4907
+ ["groups", group.id],
4908
+ options,
4909
+ diagnostics
4910
+ );
4911
+ }
4912
+ function enhanceSwimlaneCjkTypography(swimlane, options, diagnostics) {
4913
+ const root = enhanceStyledLabelOwner(
4914
+ swimlane,
4915
+ ["swimlanes", swimlane.id],
4916
+ options,
4917
+ diagnostics
4918
+ );
4919
+ const lanes = root.lanes.map(
4920
+ (lane) => enhanceSwimlaneLaneCjkTypography(swimlane.id, lane, options, diagnostics)
4921
+ );
4922
+ return { ...root, lanes };
4923
+ }
4924
+ function enhanceSwimlaneLaneCjkTypography(swimlaneId, lane, options, diagnostics) {
4925
+ return enhanceStyledLabelOwner(
4926
+ lane,
4927
+ ["swimlanes", swimlaneId, "lanes", lane.id],
4928
+ options,
4929
+ diagnostics
4930
+ );
4931
+ }
4932
+ function enhanceStyledLabelOwner(owner, path, options, diagnostics) {
4933
+ const text = owner.label?.text;
4934
+ if (text === void 0 || !containsCjk(text)) {
4935
+ return owner;
4936
+ }
4937
+ const typography = cjkTypographyForOwner(owner, options);
4938
+ if (typography.fontFamily === void 0 && typography.fontSize === void 0) {
4939
+ return owner;
4940
+ }
4941
+ const label = owner.label;
4942
+ if (label === void 0) {
4943
+ return owner;
4944
+ }
4945
+ const nextLabel = {
4946
+ ...label,
4947
+ metadata: {
4948
+ ...metadataObject(label.metadata),
4949
+ cjkTypography: typography
4950
+ }
4951
+ };
4952
+ const nextOwner = { ...owner, label: nextLabel };
4953
+ const maybeStyled = nextOwner;
4954
+ const nextStyle = enhanceCjkStyle(maybeStyled.style, typography);
4955
+ reportCjkTypographyDiagnostics(
4956
+ path,
4957
+ typography,
4958
+ maybeStyled.style,
4959
+ diagnostics
4960
+ );
4961
+ return nextStyle === maybeStyled.style ? nextOwner : { ...nextOwner, style: nextStyle };
4962
+ }
4963
+ function cjkTypographyForOwner(owner, options) {
4964
+ const metadataTypography = labelCjkTypography(owner.label?.metadata);
4965
+ const fontFamily = metadataTypography.fontFamily ?? owner.style?.fontFamily ?? options.fontFamily;
4966
+ const fontSize = boostedCjkFontSize(
4967
+ metadataTypography.fontSize ?? owner.style?.fontSize,
4968
+ options.minFontSize
4969
+ );
4970
+ return {
4971
+ ...fontFamily === void 0 ? {} : { fontFamily },
4972
+ ...fontSize === void 0 ? {} : { fontSize }
4973
+ };
4974
+ }
4975
+ function labelCjkTypography(metadata) {
4976
+ const metadataRecord = metadataObject(metadata);
4977
+ if (metadataRecord === void 0) {
4978
+ return {};
4979
+ }
4980
+ const value = metadataRecord.cjkTypography;
4981
+ if (value === void 0 || value === null || typeof value !== "object") {
4982
+ return {};
4983
+ }
4984
+ const typography = value;
4985
+ const fontFamily = typeof typography.fontFamily === "string" ? typography.fontFamily : void 0;
4986
+ const fontSize = typeof typography.fontSize === "number" && Number.isFinite(typography.fontSize) && typography.fontSize > 0 ? typography.fontSize : void 0;
4987
+ return {
4988
+ ...fontFamily === void 0 ? {} : { fontFamily },
4989
+ ...fontSize === void 0 ? {} : { fontSize }
4990
+ };
4991
+ }
4992
+ function metadataObject(metadata) {
4993
+ if (metadata === void 0 || metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
4994
+ return void 0;
4995
+ }
4996
+ return metadata;
4997
+ }
4998
+ function typographyForLabel(label) {
4999
+ return labelCjkTypography(label?.metadata);
5000
+ }
5001
+ function typographyTextStyle(label, base) {
5002
+ const typography = typographyForLabel(label);
5003
+ return {
5004
+ ...base,
5005
+ ...typography.fontFamily === void 0 ? {} : { fontFamily: typography.fontFamily },
5006
+ ...typography.fontSize === void 0 ? {} : {
5007
+ fontSize: typography.fontSize,
5008
+ lineHeight: Math.max(base.lineHeight ?? 0, typography.fontSize * 1.2)
5009
+ }
5010
+ };
5011
+ }
5012
+ function boostedCjkFontSize(current, minFontSize) {
5013
+ if (minFontSize === void 0) {
5014
+ return current;
5015
+ }
5016
+ if (current === void 0 || current < minFontSize) {
5017
+ return minFontSize;
5018
+ }
5019
+ return current;
5020
+ }
5021
+ function enhanceCjkStyle(style2, typography) {
5022
+ let next = style2;
5023
+ if (typography.fontFamily !== void 0 && next?.fontFamily === void 0) {
5024
+ next = { ...next, fontFamily: typography.fontFamily };
5025
+ }
5026
+ if (typography.fontSize !== void 0 && (next?.fontSize === void 0 || next.fontSize < typography.fontSize)) {
5027
+ next = { ...next, fontSize: typography.fontSize };
5028
+ }
5029
+ return next;
5030
+ }
5031
+ function reportCjkTypographyDiagnostics(path, typography, previousStyle, diagnostics) {
5032
+ if (typography.fontFamily !== void 0 && previousStyle?.fontFamily === void 0) {
5033
+ diagnostics.push({
5034
+ severity: "info",
5035
+ code: "cjk_font_family_applied",
5036
+ message: `Applied CJK font family ${typography.fontFamily}.`,
5037
+ path: [...path, "label", "metadata", "cjkTypography", "fontFamily"],
5038
+ detail: { fontFamily: typography.fontFamily }
5039
+ });
5040
+ }
5041
+ if (typography.fontSize !== void 0 && (previousStyle?.fontSize === void 0 || previousStyle.fontSize < typography.fontSize)) {
5042
+ diagnostics.push({
5043
+ severity: "info",
5044
+ code: "cjk_font_size_boosted",
5045
+ message: `Raised CJK font size to ${typography.fontSize}.`,
5046
+ path: [...path, "label", "metadata", "cjkTypography", "fontSize"],
5047
+ detail: {
5048
+ minFontSize: typography.fontSize,
5049
+ ...previousStyle?.fontSize === void 0 ? {} : { previousFontSize: previousStyle.fontSize }
5050
+ }
5051
+ });
5052
+ }
5053
+ }
5054
+ function containsCjk(value) {
5055
+ return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u.test(value);
5056
+ }
5057
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing, laneGutter) {
4239
5058
  const layouts = /* @__PURE__ */ new Map();
4240
5059
  const diagnostics = [];
4241
5060
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -4253,7 +5072,8 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
4253
5072
  nodeBoxes,
4254
5073
  locks,
4255
5074
  diagnostics,
4256
- movedChildIds
5075
+ movedChildIds,
5076
+ laneGutter
4257
5077
  );
4258
5078
  if (layout2 !== void 0) {
4259
5079
  layouts.set(swimlane.id, layout2);
@@ -4268,10 +5088,123 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
4268
5088
  movedChildIds
4269
5089
  )
4270
5090
  );
5091
+ if (laneGutter > 0) {
5092
+ diagnostics.push({
5093
+ severity: "info",
5094
+ code: "lane_gutter_applied",
5095
+ message: `Applied ${laneGutter}px gutter between ${layouts.size} contract swimlane lane(s).`,
5096
+ path: ["swimlanes"],
5097
+ detail: { laneGutter, swimlaneCount: layouts.size }
5098
+ });
5099
+ }
4271
5100
  }
4272
5101
  return { layouts, diagnostics, movedChildIds };
4273
5102
  }
4274
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds) {
5103
+ function wrapVerticalStackIfNeeded(boxes, nodes, edges, direction, options, diagnostics) {
5104
+ const wrapped = new Map([...boxes].map(([id, box]) => [id, { ...box }]));
5105
+ const maxStackDepth = options.maxStackDepth;
5106
+ if (maxStackDepth === void 0 || maxStackDepth <= 0 || nodes.length <= maxStackDepth) {
5107
+ reportVerticalRunaway(
5108
+ wrapped,
5109
+ nodes,
5110
+ edges,
5111
+ direction,
5112
+ options,
5113
+ diagnostics
5114
+ );
5115
+ return wrapped;
5116
+ }
5117
+ if (edges.length > 0 || !isVerticalRunaway(wrapped, nodes, direction, options)) {
5118
+ reportVerticalRunaway(
5119
+ wrapped,
5120
+ nodes,
5121
+ edges,
5122
+ direction,
5123
+ options,
5124
+ diagnostics
5125
+ );
5126
+ return wrapped;
5127
+ }
5128
+ const ordered = nodes.map((node) => ({ node, box: wrapped.get(node.id) })).filter(
5129
+ (item) => item.box !== void 0
5130
+ ).sort((a, b) => {
5131
+ const delta = a.box.y - b.box.y;
5132
+ return delta === 0 ? a.node.id.localeCompare(b.node.id) : delta;
5133
+ });
5134
+ const columns = Math.ceil(ordered.length / maxStackDepth);
5135
+ const horizontalGap = options.overlapSpacing ?? 40;
5136
+ const verticalGap = Math.max(24, horizontalGap / 2);
5137
+ const columnWidths = Array.from(
5138
+ { length: columns },
5139
+ (_, column) => Math.max(
5140
+ 0,
5141
+ ...ordered.slice(column * maxStackDepth, (column + 1) * maxStackDepth).map((item) => item.box.width)
5142
+ )
5143
+ );
5144
+ const startX = Math.min(...ordered.map((item) => item.box.x));
5145
+ const startY = Math.min(...ordered.map((item) => item.box.y));
5146
+ let columnX = startX;
5147
+ for (let column = 0; column < columns; column += 1) {
5148
+ let y = startY;
5149
+ const items = ordered.slice(
5150
+ column * maxStackDepth,
5151
+ (column + 1) * maxStackDepth
5152
+ );
5153
+ for (const item of items) {
5154
+ wrapped.set(item.node.id, { ...item.box, x: columnX, y });
5155
+ y += item.box.height + verticalGap;
5156
+ }
5157
+ columnX += (columnWidths[column] ?? 0) + horizontalGap;
5158
+ }
5159
+ diagnostics.push({
5160
+ severity: "warning",
5161
+ code: "vertical_runaway",
5162
+ message: `Single-column layout exceeded maxStackDepth ${maxStackDepth}; wrapped into ${columns} columns.`,
5163
+ path: ["nodes"],
5164
+ detail: { nodeCount: ordered.length, maxStackDepth, columns }
5165
+ });
5166
+ return wrapped;
5167
+ }
5168
+ function reportVerticalRunaway(boxes, nodes, edges, direction, options, diagnostics) {
5169
+ if (!isVerticalRunaway(boxes, nodes, direction, options)) {
5170
+ return;
5171
+ }
5172
+ diagnostics.push({
5173
+ severity: "warning",
5174
+ code: "vertical_runaway",
5175
+ message: "Layout produced a tall vertical stack beyond the preferred aspect ratio.",
5176
+ path: ["nodes"],
5177
+ detail: {
5178
+ nodeCount: nodes.length,
5179
+ edgeCount: edges.length,
5180
+ ...options.preferredAspectRatio === void 0 ? {} : { preferredAspectRatio: options.preferredAspectRatio },
5181
+ ...options.maxStackDepth === void 0 ? {} : { maxStackDepth: options.maxStackDepth }
5182
+ }
5183
+ });
5184
+ }
5185
+ function isVerticalRunaway(boxes, nodes, direction, options) {
5186
+ if (options.maxStackDepth === void 0 && options.preferredAspectRatio === void 0) {
5187
+ return false;
5188
+ }
5189
+ if (nodes.length < 2 || direction !== "LR" && direction !== "RL") {
5190
+ return false;
5191
+ }
5192
+ const nodeBoxes = nodes.map((node) => boxes.get(node.id)).filter((box) => box !== void 0);
5193
+ if (nodeBoxes.length < 2) {
5194
+ return false;
5195
+ }
5196
+ const bounds = unionBoxes(nodeBoxes);
5197
+ const aspectRatio = bounds.width <= 0 ? Number.POSITIVE_INFINITY : bounds.height / bounds.width;
5198
+ const preferred = options.preferredAspectRatio ?? 3;
5199
+ if (aspectRatio < preferred) {
5200
+ return false;
5201
+ }
5202
+ const xCenters = nodeBoxes.map((box) => box.x + box.width / 2);
5203
+ const xSpread = Math.max(...xCenters) - Math.min(...xCenters);
5204
+ const maxWidth = Math.max(...nodeBoxes.map((box) => box.width));
5205
+ return xSpread <= Math.max(maxWidth, options.overlapSpacing ?? 40);
5206
+ }
5207
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds, laneGutter) {
4275
5208
  const headerHeight = swimlane.headerHeight ?? 28;
4276
5209
  const padding = swimlane.padding ?? 16;
4277
5210
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -4295,7 +5228,8 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
4295
5228
  padding,
4296
5229
  locks,
4297
5230
  diagnostics,
4298
- movedChildIds
5231
+ movedChildIds,
5232
+ laneGutter
4299
5233
  );
4300
5234
  }
4301
5235
  return applyHorizontalSwimlaneContract(
@@ -4306,10 +5240,11 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
4306
5240
  padding,
4307
5241
  locks,
4308
5242
  diagnostics,
4309
- movedChildIds
5243
+ movedChildIds,
5244
+ laneGutter
4310
5245
  );
4311
5246
  }
4312
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
5247
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
4313
5248
  const populatedBounds = laneBounds.filter(
4314
5249
  (box) => box !== void 0
4315
5250
  );
@@ -4328,6 +5263,7 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
4328
5263
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
4329
5264
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
4330
5265
  const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
5266
+ const laneStep = slotWidth + laneGutter;
4331
5267
  const laneContentTop = top + headerHeight + padding;
4332
5268
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
4333
5269
  const lane = swimlane.lanes[index];
@@ -4336,7 +5272,7 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
4336
5272
  continue;
4337
5273
  }
4338
5274
  const target = {
4339
- x: left + slotWidth * index + padding,
5275
+ x: left + laneStep * index + padding,
4340
5276
  y: laneContentTop
4341
5277
  };
4342
5278
  if (maxRank === 0) {
@@ -4372,11 +5308,12 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
4372
5308
  box: {
4373
5309
  x: left,
4374
5310
  y: top,
4375
- width: slotWidth * swimlane.lanes.length,
5311
+ width: laneStep * (swimlane.lanes.length - 1) + slotWidth,
4376
5312
  height: contentHeight + padding * 2 + headerHeight
4377
5313
  },
4378
5314
  slotWidth,
4379
- slotHeight: contentHeight + padding * 2 + headerHeight
5315
+ slotHeight: contentHeight + padding * 2 + headerHeight,
5316
+ laneStep
4380
5317
  };
4381
5318
  }
4382
5319
  function isTopToBottomReadingDirection(value) {
@@ -4521,7 +5458,7 @@ function rankStacks(childIds, nodeBoxes, flowRanks) {
4521
5458
  }
4522
5459
  return stacks;
4523
5460
  }
4524
- function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
5461
+ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
4525
5462
  const populatedBounds = laneBounds.filter(
4526
5463
  (box) => box !== void 0
4527
5464
  );
@@ -4529,6 +5466,7 @@ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, header
4529
5466
  const left = Math.min(...populatedBounds.map((box) => box.x));
4530
5467
  const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + headerHeight + padding * 2;
4531
5468
  const slotHeight = Math.max(...populatedBounds.map((box) => box.height)) + padding * 2;
5469
+ const laneStep = slotHeight + laneGutter;
4532
5470
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
4533
5471
  const lane = swimlane.lanes[index];
4534
5472
  const bounds = laneBounds[index];
@@ -4537,7 +5475,7 @@ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, header
4537
5475
  }
4538
5476
  const target = {
4539
5477
  x: left + headerHeight + padding,
4540
- y: top + slotHeight * index + padding
5478
+ y: top + laneStep * index + padding
4541
5479
  };
4542
5480
  moveLaneChildren(
4543
5481
  lane.children,
@@ -4556,10 +5494,11 @@ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, header
4556
5494
  x: left,
4557
5495
  y: top,
4558
5496
  width: slotWidth,
4559
- height: slotHeight * swimlane.lanes.length
5497
+ height: laneStep * (swimlane.lanes.length - 1) + slotHeight
4560
5498
  },
4561
5499
  slotWidth,
4562
- slotHeight
5500
+ slotHeight,
5501
+ laneStep
4563
5502
  };
4564
5503
  }
4565
5504
  function moveLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, offset) {
@@ -4888,7 +5827,10 @@ function coordinatePorts(node, nodeBox, portShifting) {
4888
5827
  }
4889
5828
  function portAnchor(nodeBox, side, index, count, portShifting) {
4890
5829
  const shiftingEnabled = portShifting?.enabled ?? true;
4891
- const spacing = portShifting?.spacing ?? 24;
5830
+ const requestedSpacing = portShifting?.spacing ?? 24;
5831
+ const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
5832
+ const availableSpan = 2 * maxOffset;
5833
+ const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
4892
5834
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
4893
5835
  switch (side) {
4894
5836
  case "left":
@@ -4943,13 +5885,13 @@ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
4943
5885
  if (layout2 === "contract" && contractLayout !== void 0) {
4944
5886
  const lanes2 = swimlane.lanes.map((lane, index) => {
4945
5887
  const box = swimlane.orientation === "vertical" ? {
4946
- x: contractLayout.box.x + contractLayout.slotWidth * index,
5888
+ x: contractLayout.box.x + contractLayout.laneStep * index,
4947
5889
  y: contractLayout.box.y,
4948
5890
  width: contractLayout.slotWidth,
4949
5891
  height: contractLayout.box.height
4950
5892
  } : {
4951
5893
  x: contractLayout.box.x,
4952
- y: contractLayout.box.y + contractLayout.slotHeight * index,
5894
+ y: contractLayout.box.y + contractLayout.laneStep * index,
4953
5895
  width: contractLayout.box.width,
4954
5896
  height: contractLayout.slotHeight
4955
5897
  };
@@ -5050,17 +5992,19 @@ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
5050
5992
  });
5051
5993
  }
5052
5994
  function coordinateFrame(frame, contentBounds) {
5053
- const padding = 32;
5054
- const titleHeight = 28;
5995
+ const padding = framePadding(frame.padding);
5996
+ const titleHeight = frame.headerHeight ?? 28;
5055
5997
  const titleWidth = Math.max(180, frame.titleTab.length * 7);
5056
5998
  const box = {
5057
- x: contentBounds.x - padding,
5058
- y: contentBounds.y - padding - titleHeight,
5059
- width: contentBounds.width + padding * 2,
5060
- height: contentBounds.height + padding * 2 + titleHeight
5999
+ x: contentBounds.x - padding.left,
6000
+ y: contentBounds.y - padding.top - titleHeight,
6001
+ width: contentBounds.width + padding.left + padding.right,
6002
+ height: contentBounds.height + padding.top + padding.bottom + titleHeight
5061
6003
  };
5062
6004
  return {
5063
6005
  ...frame,
6006
+ headerHeight: titleHeight,
6007
+ padding: frame.padding ?? 32,
5064
6008
  box,
5065
6009
  titleBox: {
5066
6010
  x: box.x,
@@ -5070,6 +6014,9 @@ function coordinateFrame(frame, contentBounds) {
5070
6014
  }
5071
6015
  };
5072
6016
  }
6017
+ function framePadding(value) {
6018
+ return normalizeInsets(value ?? 32);
6019
+ }
5073
6020
  function expand(box, padding, titleSize) {
5074
6021
  return {
5075
6022
  x: box.x - padding,
@@ -5165,10 +6112,16 @@ function edgeBounds(edges) {
5165
6112
  if (edge.points.length === 0) {
5166
6113
  return [];
5167
6114
  }
5168
- const minX = Math.min(...edge.points.map((point2) => point2.x));
5169
- const minY = Math.min(...edge.points.map((point2) => point2.y));
5170
- const maxX = Math.max(...edge.points.map((point2) => point2.x));
5171
- const maxY = Math.max(...edge.points.map((point2) => point2.y));
6115
+ const extraPoints = [];
6116
+ if (edge.points.length >= 2) {
6117
+ const arrowhead = computeArrowhead(edge.points);
6118
+ extraPoints.push(arrowhead.tip, arrowhead.left, arrowhead.right);
6119
+ }
6120
+ const allPoints = [...edge.points, ...extraPoints];
6121
+ const minX = Math.min(...allPoints.map((point2) => point2.x));
6122
+ const minY = Math.min(...allPoints.map((point2) => point2.y));
6123
+ const maxX = Math.max(...allPoints.map((point2) => point2.x));
6124
+ const maxY = Math.max(...allPoints.map((point2) => point2.y));
5172
6125
  return [
5173
6126
  {
5174
6127
  x: minX,
@@ -5565,6 +6518,7 @@ function coordinateBaseTextAnnotations(input) {
5565
6518
  ownerId: node.id,
5566
6519
  surfaceKind: "node-label",
5567
6520
  layout: layout2,
6521
+ typography: typographyForLabel(node.label),
5568
6522
  anchor: node.box
5569
6523
  })
5570
6524
  );
@@ -5580,6 +6534,7 @@ function coordinateBaseTextAnnotations(input) {
5580
6534
  ownerId: group.id,
5581
6535
  surfaceKind: "group-label",
5582
6536
  layout: layout2,
6537
+ typography: typographyForLabel(group.label),
5583
6538
  anchor: group.box
5584
6539
  })
5585
6540
  );
@@ -5592,7 +6547,11 @@ function coordinateBaseTextAnnotations(input) {
5592
6547
  const layout2 = fitLabel(
5593
6548
  port.label.text,
5594
6549
  {
5595
- font: { fontFamily: "Arial", fontSize: 10, lineHeight: 12 },
6550
+ font: typographyTextStyle(port.label, {
6551
+ fontFamily: "Arial",
6552
+ fontSize: 10,
6553
+ lineHeight: 12
6554
+ }),
5596
6555
  padding: { top: 0, right: 0, bottom: 0, left: 0 },
5597
6556
  minSize: { width: 0, height: 0 },
5598
6557
  maxWidth: 160
@@ -5604,6 +6563,7 @@ function coordinateBaseTextAnnotations(input) {
5604
6563
  ownerId: `${node.id}.${port.id}`,
5605
6564
  surfaceKind: "port-label",
5606
6565
  layout: layout2,
6566
+ typography: typographyForLabel(port.label),
5607
6567
  anchor: portLabelBox(port)
5608
6568
  })
5609
6569
  );
@@ -5654,7 +6614,11 @@ function coordinateBaseTextAnnotations(input) {
5654
6614
  const layout2 = fitLabel(
5655
6615
  lane.label.text,
5656
6616
  {
5657
- font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
6617
+ font: typographyTextStyle(lane.label, {
6618
+ fontFamily: "Arial",
6619
+ fontSize: 12,
6620
+ lineHeight: 14
6621
+ }),
5658
6622
  padding: { top: 0, right: 0, bottom: 0, left: 0 },
5659
6623
  minSize: { width: 0, height: 0 },
5660
6624
  maxWidth: swimlane.orientation === "horizontal" ? labelBox.height : labelBox.width
@@ -5666,6 +6630,7 @@ function coordinateBaseTextAnnotations(input) {
5666
6630
  ownerId: `${swimlane.id}.${lane.id}`,
5667
6631
  surfaceKind: "swimlane-label",
5668
6632
  layout: layout2,
6633
+ typography: typographyForLabel(lane.label),
5669
6634
  anchor: labelBox
5670
6635
  })
5671
6636
  );
@@ -5673,9 +6638,10 @@ function coordinateBaseTextAnnotations(input) {
5673
6638
  }
5674
6639
  return annotations;
5675
6640
  }
5676
- function coordinateEdgeTextAnnotations(edges, textMeasurer) {
6641
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
5677
6642
  const measurer = textMeasurer ?? createDefaultTextMeasurer();
5678
6643
  const annotations = [];
6644
+ const placedLabelBoxes = [];
5679
6645
  for (const edge of edges) {
5680
6646
  if (edge.label?.text === void 0) {
5681
6647
  continue;
@@ -5683,19 +6649,37 @@ function coordinateEdgeTextAnnotations(edges, textMeasurer) {
5683
6649
  const layout2 = fitLabel(
5684
6650
  edge.label.text,
5685
6651
  {
5686
- font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
6652
+ font: typographyTextStyle(edge.label, {
6653
+ fontFamily: "Arial",
6654
+ fontSize: 12,
6655
+ lineHeight: 14
6656
+ }),
5687
6657
  padding: { top: 0, right: 0, bottom: 0, left: 0 },
5688
6658
  minSize: { width: 0, height: 0 },
5689
6659
  maxWidth: 200
5690
6660
  },
5691
6661
  measurer
5692
6662
  );
6663
+ const center = edgeLabelAnchor(
6664
+ edge,
6665
+ layout2,
6666
+ edges,
6667
+ obstacleBoxes,
6668
+ placedLabelBoxes
6669
+ );
6670
+ placedLabelBoxes.push({
6671
+ x: center.x - layout2.box.width / 2,
6672
+ y: center.y - layout2.box.height / 2,
6673
+ width: layout2.box.width,
6674
+ height: layout2.box.height
6675
+ });
5693
6676
  annotations.push(
5694
6677
  buildCenteredTextAnnotation({
5695
6678
  ownerId: edge.id,
5696
6679
  surfaceKind: "edge-label",
5697
6680
  layout: layout2,
5698
- center: edgeLabelAnchor(edge, layout2, edges)
6681
+ typography: typographyForLabel(edge.label),
6682
+ center
5699
6683
  })
5700
6684
  );
5701
6685
  }
@@ -5734,7 +6718,8 @@ function buildTextAnnotation(input) {
5734
6718
  anchor: input.anchor,
5735
6719
  paddings: input.layout.padding,
5736
6720
  lines: input.layout.lines,
5737
- fontSize: input.layout.font.fontSize,
6721
+ fontFamily: input.typography?.fontFamily ?? normalizeOutputFontFamily(input.layout.font),
6722
+ fontSize: input.typography?.fontSize ?? input.layout.font.fontSize,
5738
6723
  textBackend: input.layout.textBackend
5739
6724
  };
5740
6725
  }
@@ -5744,6 +6729,7 @@ function buildAnchorCenteredTextAnnotation(input) {
5744
6729
  surfaceKind: input.surfaceKind,
5745
6730
  ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
5746
6731
  layout: input.layout,
6732
+ ...input.typography === void 0 ? {} : { typography: input.typography },
5747
6733
  center: {
5748
6734
  x: input.anchor.x + input.anchor.width / 2,
5749
6735
  y: input.anchor.y + input.anchor.height / 2
@@ -5766,10 +6752,14 @@ function buildCenteredTextAnnotation(input) {
5766
6752
  anchor: input.anchor ?? input.center,
5767
6753
  paddings: input.layout.padding,
5768
6754
  lines: input.layout.lines,
5769
- fontSize: input.layout.font.fontSize,
6755
+ fontFamily: input.typography?.fontFamily ?? normalizeOutputFontFamily(input.layout.font),
6756
+ fontSize: input.typography?.fontSize ?? input.layout.font.fontSize,
5770
6757
  textBackend: input.layout.textBackend
5771
6758
  };
5772
6759
  }
6760
+ function normalizeOutputFontFamily(font) {
6761
+ return font.fontFamily === "Arial" ? "Arial, sans-serif" : font.fontFamily;
6762
+ }
5773
6763
  function reportTextAnnotationCollisions(annotations) {
5774
6764
  const diagnostics = [];
5775
6765
  const relevantAnnotations = annotations.filter(
@@ -5959,12 +6949,16 @@ function fallbackLabelLayout(text) {
5959
6949
  diagnostics: []
5960
6950
  };
5961
6951
  }
5962
- function edgeLabelAnchor(edge, layout2, edges) {
6952
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
5963
6953
  const placement = labelPlacementOnPolyline2(edge.points);
5964
6954
  if (placement === void 0) {
5965
6955
  return { x: 0, y: 0 };
5966
6956
  }
5967
- for (const candidate of edgeLabelAnchorCandidates(edge.points, placement)) {
6957
+ for (const candidate of edgeLabelAnchorCandidates(
6958
+ edge.points,
6959
+ placement,
6960
+ layout2
6961
+ )) {
5968
6962
  const labelBox = {
5969
6963
  x: candidate.x - layout2.box.width / 2,
5970
6964
  y: candidate.y - layout2.box.height / 2,
@@ -5977,36 +6971,55 @@ function edgeLabelAnchor(edge, layout2, edges) {
5977
6971
  const crossesOtherRoute = edges.some(
5978
6972
  (other) => other.id !== edge.id && routeIntersectsTextBox(other.points, labelBox)
5979
6973
  );
5980
- if (!crossesOtherRoute) {
6974
+ if (crossesOtherRoute) {
6975
+ continue;
6976
+ }
6977
+ const overlapsNode = obstacleBoxes.some(
6978
+ (box) => intersectsAabb(labelBox, box)
6979
+ );
6980
+ if (overlapsNode) {
6981
+ continue;
6982
+ }
6983
+ const overlapsPlacedLabel = placedLabelBoxes.some(
6984
+ (box) => intersectsAabb(labelBox, box)
6985
+ );
6986
+ if (!overlapsPlacedLabel) {
5981
6987
  return candidate;
5982
6988
  }
5983
6989
  }
5984
6990
  return placement;
5985
6991
  }
5986
- function edgeLabelAnchorCandidates(points, placement) {
6992
+ function edgeLabelAnchorCandidates(points, placement, layout2) {
5987
6993
  const segment = labelSegmentOnPolyline(points);
5988
6994
  if (segment === void 0) {
5989
6995
  return [placement];
5990
6996
  }
6997
+ const candidates = [placement];
5991
6998
  if (segment.start.y === segment.end.y) {
5992
- return [
5993
- placement,
5994
- { x: placement.x, y: placement.y - EDGE_LABEL_CLEARANCE },
5995
- { x: placement.x, y: placement.y + EDGE_LABEL_CLEARANCE },
5996
- { x: placement.x, y: placement.y - EDGE_LABEL_CLEARANCE * 2 },
5997
- { x: placement.x, y: placement.y + EDGE_LABEL_CLEARANCE * 2 }
5998
- ];
6999
+ const needed = layout2.box.height / 2 + EDGE_LABEL_CLEARANCE;
7000
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7001
+ for (let step = 1; step <= maxSteps; step += 1) {
7002
+ const offset = EDGE_LABEL_CLEARANCE * step;
7003
+ candidates.push(
7004
+ { x: placement.x, y: placement.y - offset },
7005
+ { x: placement.x, y: placement.y + offset }
7006
+ );
7007
+ }
7008
+ return candidates;
5999
7009
  }
6000
7010
  if (segment.start.x === segment.end.x) {
6001
- return [
6002
- placement,
6003
- { x: placement.x + EDGE_LABEL_CLEARANCE, y: placement.y },
6004
- { x: placement.x - EDGE_LABEL_CLEARANCE, y: placement.y },
6005
- { x: placement.x + EDGE_LABEL_CLEARANCE * 2, y: placement.y },
6006
- { x: placement.x - EDGE_LABEL_CLEARANCE * 2, y: placement.y }
6007
- ];
7011
+ const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
7012
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7013
+ for (let step = 1; step <= maxSteps; step += 1) {
7014
+ const offset = EDGE_LABEL_CLEARANCE * step;
7015
+ candidates.push(
7016
+ { x: placement.x + offset, y: placement.y },
7017
+ { x: placement.x - offset, y: placement.y }
7018
+ );
7019
+ }
7020
+ return candidates;
6008
7021
  }
6009
- return [placement];
7022
+ return candidates;
6010
7023
  }
6011
7024
  function labelPlacementOnPolyline2(points) {
6012
7025
  return labelSegmentOnPolyline(points)?.placement;
@@ -6097,8 +7110,26 @@ function portGeometry(nodeGeometry, port) {
6097
7110
  obstacleBox: port.box
6098
7111
  };
6099
7112
  }
6100
- function stableById(items) {
6101
- return [...items].sort((a, b) => a.id.localeCompare(b.id));
7113
+ function stableUniqueById(items, diagnostics, pathRoot, code) {
7114
+ const firstById = /* @__PURE__ */ new Map();
7115
+ for (let index = 0; index < items.length; index += 1) {
7116
+ const item = items[index];
7117
+ if (item === void 0) {
7118
+ continue;
7119
+ }
7120
+ if (firstById.has(item.id)) {
7121
+ diagnostics.push({
7122
+ severity: "error",
7123
+ code,
7124
+ message: `Duplicate ${pathRoot.slice(0, -1)} id ${item.id} was ignored; first occurrence was kept.`,
7125
+ path: [pathRoot, index, "id"],
7126
+ detail: { id: item.id, duplicateIndex: index }
7127
+ });
7128
+ continue;
7129
+ }
7130
+ firstById.set(item.id, item);
7131
+ }
7132
+ return [...firstById.values()].sort((a, b) => a.id.localeCompare(b.id));
6102
7133
  }
6103
7134
  function stableByConstraintId(items) {
6104
7135
  return [...items].sort(
@@ -6347,6 +7378,6 @@ function isPointLikeRecord(value) {
6347
7378
  return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
6348
7379
  }
6349
7380
 
6350
- export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createDefaultTextMeasurer, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute, solveDiagram, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
7381
+ export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createDefaultTextMeasurer, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute, solveDiagram, solveDiagramSafe, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
6351
7382
  //# sourceMappingURL=index.js.map
6352
7383
  //# sourceMappingURL=index.js.map