@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.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,10 +338,11 @@ 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";
323
344
  const secondaryAxis = axis === "x" ? "y" : "x";
345
+ const ignoredPairs = containmentOverlapKeys(input.constraints);
324
346
  const ids = [...boxes.keys()].sort();
325
347
  for (let pass = 0; pass < 2; pass += 1) {
326
348
  for (const firstId of ids) {
@@ -328,6 +350,9 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
328
350
  if (firstId >= secondId) {
329
351
  continue;
330
352
  }
353
+ if (ignoredPairs.has(overlapKey(firstId, secondId))) {
354
+ continue;
355
+ }
331
356
  const first = boxes.get(firstId);
332
357
  const second = boxes.get(secondId);
333
358
  if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
@@ -342,16 +367,40 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
342
367
  const moving = movingId === firstId ? first : second;
343
368
  const fixed = movingId === firstId ? second : first;
344
369
  const repairAxis = firstLocked === secondLocked && pass === 0 ? secondaryAxis : axis;
345
- const moved = movePastOverlap(moving, fixed, repairAxis, spacing);
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
+ );
346
378
  boxes.set(movingId, moved);
347
379
  }
348
380
  }
349
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
+ );
350
395
  for (const firstId of ids) {
351
396
  for (const secondId of ids) {
352
397
  if (firstId >= secondId) {
353
398
  continue;
354
399
  }
400
+ const key = overlapKey(firstId, secondId);
401
+ if (reported.has(key) || ignoredPairs.has(key)) {
402
+ continue;
403
+ }
355
404
  const first = boxes.get(firstId);
356
405
  const second = boxes.get(secondId);
357
406
  if (first !== void 0 && second !== void 0 && intersectsAabb(first, second)) {
@@ -362,9 +411,136 @@ function repairOverlaps(input, boxes, locks, diagnostics) {
362
411
  path: ["boxes"],
363
412
  detail: { firstId, secondId }
364
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
+ }
365
462
  }
366
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
+ }
367
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));
540
+ }
541
+ }
542
+ }
543
+ return keys;
368
544
  }
369
545
  function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
370
546
  const current = boxes.get(id);
@@ -482,10 +658,125 @@ function contentBox(container, padding) {
482
658
  return {
483
659
  x: container.x + margin.left,
484
660
  y: container.y + margin.top,
485
- width: container.width - margin.left - margin.right,
486
- 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)
487
663
  };
488
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
+ }
489
780
  function moveInside(child, container) {
490
781
  return {
491
782
  ...child,
@@ -1191,7 +1482,11 @@ var DEFAULT_GROUP_PADDING = {
1191
1482
  left: 16
1192
1483
  };
1193
1484
  var DEFAULT_LABEL_MAX_WIDTH = 160;
1194
- var DEFAULT_FONT = { fontFamily: "Arial", fontSize: 14, lineHeight: 18 };
1485
+ var DEFAULT_FONT = {
1486
+ fontFamily: "Arial",
1487
+ fontSize: 14,
1488
+ lineHeight: 18
1489
+ };
1195
1490
  var DEFAULT_MATRIX_CELL_SIZE = { width: 120, height: 36 };
1196
1491
  var DEFAULT_TABLE_CELL_SIZE = { width: 128, height: 34 };
1197
1492
  var DEFAULT_PANEL_ITEM_HEIGHT = 28;
@@ -1367,7 +1662,9 @@ function endpoint(value, nodeIdOverride) {
1367
1662
  function style(value) {
1368
1663
  return {
1369
1664
  ...value.fill === void 0 ? {} : { fill: value.fill },
1370
- ...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 }
1371
1668
  };
1372
1669
  }
1373
1670
  function compartments(value) {
@@ -1384,6 +1681,10 @@ function normalizeFrame(frame) {
1384
1681
  ...frame.context === void 0 ? {} : { context: frame.context },
1385
1682
  ...frame.name === void 0 ? {} : { name: frame.name },
1386
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 },
1387
1688
  ...frame.style === void 0 ? {} : { style: style(frame.style) }
1388
1689
  };
1389
1690
  }
@@ -1527,6 +1828,9 @@ function normalizeGroups(dsl, measurer) {
1527
1828
  nodeIds: [...group?.nodes ?? []],
1528
1829
  groupIds: [...group?.groups ?? []],
1529
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 },
1530
1834
  ...labelLayout === void 0 ? {} : { labelLayout }
1531
1835
  };
1532
1836
  });
@@ -1796,6 +2100,12 @@ var insetsSchema = zod.z.object({
1796
2100
  bottom: finiteNumberSchema,
1797
2101
  left: finiteNumberSchema
1798
2102
  });
2103
+ var nonNegativeInsetsSchema = zod.z.object({
2104
+ top: nonNegativeNumberSchema,
2105
+ right: nonNegativeNumberSchema,
2106
+ bottom: nonNegativeNumberSchema,
2107
+ left: nonNegativeNumberSchema
2108
+ });
1799
2109
  var labelSchema = zod.z.union([
1800
2110
  zod.z.string(),
1801
2111
  zod.z.object({
@@ -1805,7 +2115,9 @@ var labelSchema = zod.z.union([
1805
2115
  ]);
1806
2116
  var styleSchema = zod.z.object({
1807
2117
  fill: zod.z.string().optional(),
1808
- stroke: zod.z.string().optional()
2118
+ stroke: zod.z.string().optional(),
2119
+ fontFamily: zod.z.string().optional(),
2120
+ fontSize: finiteNumberSchema.optional()
1809
2121
  });
1810
2122
  var blockCellSchema = zod.z.union([
1811
2123
  zod.z.string(),
@@ -1876,7 +2188,10 @@ var groupSchema = zod.z.object({
1876
2188
  label: labelSchema.optional(),
1877
2189
  nodes: zod.z.array(zod.z.string()).optional(),
1878
2190
  groups: zod.z.array(zod.z.string()).optional(),
1879
- 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()
1880
2195
  });
1881
2196
  var swimlaneSchema = zod.z.object({
1882
2197
  label: labelSchema.optional(),
@@ -2088,6 +2403,10 @@ var diagramDslSchema = zod.z.object({
2088
2403
  context: zod.z.string().optional(),
2089
2404
  name: zod.z.string().optional(),
2090
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(),
2091
2410
  style: styleSchema.optional()
2092
2411
  }).optional(),
2093
2412
  output: zod.z.object({
@@ -3222,7 +3541,7 @@ function renderSolvedTextAnnotation(annotation, className, options) {
3222
3541
  `data-text-surface="${escapeAttribute(annotation.surfaceKind)}"`,
3223
3542
  `data-owner-id="${escapeAttribute(annotation.ownerId)}"`,
3224
3543
  `data-text-backend="${escapeAttribute(annotation.textBackend ?? "deterministic")}"`,
3225
- `font-family="${FONT_FAMILY}"`,
3544
+ `font-family="${escapeAttribute(annotation.fontFamily)}"`,
3226
3545
  `font-size="${formatNumber(annotation.fontSize)}"`,
3227
3546
  `fill="#111827"`
3228
3547
  ];
@@ -3550,7 +3869,12 @@ function routeEdge(input) {
3550
3869
  input.source.center,
3551
3870
  input.targetAnchor ?? defaultAnchors.targetAnchor
3552
3871
  );
3553
- const points = simplifyRoute([source, target]);
3872
+ const points = finalizeRoute(
3873
+ [source, target],
3874
+ softObstacles,
3875
+ hardObstacles,
3876
+ diagnostics
3877
+ );
3554
3878
  if (routeCrossesBoxes(points, hardObstacles)) {
3555
3879
  diagnostics.push({
3556
3880
  severity: "error",
@@ -3606,7 +3930,15 @@ function routeEdge(input) {
3606
3930
  candidate.points,
3607
3931
  candidate.endpointObstacles
3608
3932
  )) {
3609
- return { points: simplifyRoute(candidate.points), diagnostics };
3933
+ return {
3934
+ points: finalizeRoute(
3935
+ candidate.points,
3936
+ softObstacles,
3937
+ hardObstacles,
3938
+ diagnostics
3939
+ ),
3940
+ diagnostics
3941
+ };
3610
3942
  }
3611
3943
  }
3612
3944
  const hardClearCandidate = candidateRoutes.find(
@@ -3622,7 +3954,12 @@ function routeEdge(input) {
3622
3954
  message: "No bounded orthogonal route candidate avoided all soft obstacles."
3623
3955
  });
3624
3956
  return {
3625
- points: simplifyRoute(hardClearCandidate.points),
3957
+ points: finalizeRoute(
3958
+ hardClearCandidate.points,
3959
+ softObstacles,
3960
+ hardObstacles,
3961
+ diagnostics
3962
+ ),
3626
3963
  diagnostics
3627
3964
  };
3628
3965
  }
@@ -3633,8 +3970,11 @@ function routeEdge(input) {
3633
3970
  message: "No bounded orthogonal route candidate avoided hard evidence block obstacles."
3634
3971
  });
3635
3972
  return {
3636
- points: simplifyRoute(
3637
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors)
3973
+ points: finalizeRoute(
3974
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3975
+ softObstacles,
3976
+ hardObstacles,
3977
+ diagnostics
3638
3978
  ),
3639
3979
  diagnostics
3640
3980
  };
@@ -3645,12 +3985,133 @@ function routeEdge(input) {
3645
3985
  message: "No bounded orthogonal route candidate avoided all obstacles."
3646
3986
  });
3647
3987
  return {
3648
- points: simplifyRoute(
3649
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors)
3988
+ points: finalizeRoute(
3989
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3990
+ softObstacles,
3991
+ hardObstacles,
3992
+ diagnostics
3650
3993
  ),
3651
3994
  diagnostics
3652
3995
  };
3653
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
+ }
3654
4115
  function endpointObstaclesForAutoAnchors(input) {
3655
4116
  const boxes = [];
3656
4117
  if (input.sourceAnchor === void 0 && hasDistinctAnchors(input.source)) {
@@ -4022,6 +4483,15 @@ var DEFAULT_PANEL_WIDTH = 320;
4022
4483
  var DEFAULT_PANEL_ITEM_HEIGHT2 = 28;
4023
4484
  var DEFAULT_EVIDENCE_BLOCK_GAP = 24;
4024
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
+ }
4025
4495
  var EVIDENCE_TEXT_FONT = {
4026
4496
  fontFamily: "Arial, sans-serif",
4027
4497
  fontSize: 10,
@@ -4029,43 +4499,85 @@ var EVIDENCE_TEXT_FONT = {
4029
4499
  };
4030
4500
  function solveDiagram(diagram, options = {}) {
4031
4501
  const diagnostics = [...diagram.diagnostics];
4032
- const nodes = stableById(diagram.nodes);
4033
- const edges = stableById(diagram.edges);
4034
- 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
+ );
4035
4536
  const constraints = stableByConstraintId(diagram.constraints);
4036
4537
  const layout2 = runDagreInitialLayout({
4037
4538
  direction: diagram.direction,
4038
- nodes: nodes.map((node) => ({ id: node.id, size: node.size })),
4039
- edges: edges.map((edge) => ({
4539
+ nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4540
+ edges: styledEdges.map((edge) => ({
4040
4541
  id: edge.id,
4041
4542
  sourceId: edge.source.nodeId,
4042
4543
  targetId: edge.target.nodeId
4043
4544
  }))
4044
4545
  });
4045
4546
  diagnostics.push(...layout2.diagnostics);
4547
+ const initialNodeBoxes = wrapVerticalStackIfNeeded(
4548
+ layout2.boxes,
4549
+ styledNodes,
4550
+ styledEdges,
4551
+ diagram.direction,
4552
+ options,
4553
+ diagnostics
4554
+ );
4046
4555
  const constrained = applyLayoutConstraints({
4047
4556
  direction: diagram.direction,
4048
4557
  overlapSpacing: options?.overlapSpacing ?? 40,
4049
- boxes: layout2.boxes,
4050
- nodes,
4558
+ ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4559
+ ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
4560
+ boxes: initialNodeBoxes,
4561
+ nodes: styledNodes,
4051
4562
  constraints
4052
4563
  });
4053
4564
  diagnostics.push(...constrained.diagnostics);
4054
4565
  const swimlaneContracts = applySwimlaneLayoutContracts(
4055
- diagram.swimlanes ?? [],
4566
+ styledSwimlanes,
4056
4567
  constraints,
4057
- edges,
4568
+ styledEdges,
4058
4569
  isTopToBottomReadingDirection(diagram.metadata?.primaryReadingDirection),
4059
4570
  constrained.boxes,
4060
4571
  constrained.locks,
4061
- options?.overlapSpacing ?? 40
4572
+ options?.overlapSpacing ?? 40,
4573
+ Math.max(0, options?.minLaneGutter ?? 0)
4062
4574
  );
4063
4575
  if (swimlaneContracts.layouts.size > 0) {
4064
4576
  removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4065
4577
  }
4066
4578
  diagnostics.push(...swimlaneContracts.diagnostics);
4067
4579
  const coordinatedNodes = coordinateNodes(
4068
- nodes,
4580
+ styledNodes,
4069
4581
  constrained.boxes,
4070
4582
  options,
4071
4583
  diagnostics
@@ -4081,13 +4593,13 @@ function solveDiagram(diagram, options = {}) {
4081
4593
  ])
4082
4594
  );
4083
4595
  const coordinatedGroups = coordinateGroups(
4084
- groups,
4596
+ styledGroups,
4085
4597
  constrained.boxes,
4086
4598
  options,
4087
4599
  diagnostics
4088
4600
  );
4089
4601
  const coordinatedSwimlanes = coordinateSwimlanes(
4090
- diagram.swimlanes ?? [],
4602
+ styledSwimlanes,
4091
4603
  constrained.boxes,
4092
4604
  swimlaneContracts.layouts
4093
4605
  );
@@ -4187,7 +4699,7 @@ function solveDiagram(diagram, options = {}) {
4187
4699
  ...frameTextAnnotation.filter(isPreRouteTextObstacle)
4188
4700
  ];
4189
4701
  const coordinatedEdges = coordinateEdges(
4190
- edges,
4702
+ styledEdges,
4191
4703
  nodeGeometryById,
4192
4704
  coordinatedNodes,
4193
4705
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
@@ -4203,6 +4715,11 @@ function solveDiagram(diagram, options = {}) {
4203
4715
  );
4204
4716
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
4205
4717
  coordinatedEdges,
4718
+ [
4719
+ ...coordinatedNodes.map((node) => node.box),
4720
+ ...baseTextAnnotations.map((annotation) => annotation.box),
4721
+ ...frameTextAnnotation.map((annotation) => annotation.box)
4722
+ ],
4206
4723
  options.textMeasurer
4207
4724
  );
4208
4725
  const textAnnotations = [
@@ -4220,6 +4737,12 @@ function solveDiagram(diagram, options = {}) {
4220
4737
  ...edgePointBounds,
4221
4738
  ...edgeTextAnnotations.map((annotation) => annotation.box)
4222
4739
  ];
4740
+ diagnostics.push(
4741
+ ...reportPageOverflow(
4742
+ frame === void 0 ? unionBoxes(boundsBase) : unionBoxes([...boundsBase, frame.box, frame.titleBox]),
4743
+ options.pageBounds
4744
+ )
4745
+ );
4223
4746
  return {
4224
4747
  id: diagram.id,
4225
4748
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -4238,7 +4761,303 @@ function solveDiagram(diagram, options = {}) {
4238
4761
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
4239
4762
  };
4240
4763
  }
4241
- function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing) {
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
+ }
4796
+ });
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) {
4242
5061
  const layouts = /* @__PURE__ */ new Map();
4243
5062
  const diagnostics = [];
4244
5063
  const movedChildIds = /* @__PURE__ */ new Set();
@@ -4256,7 +5075,8 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
4256
5075
  nodeBoxes,
4257
5076
  locks,
4258
5077
  diagnostics,
4259
- movedChildIds
5078
+ movedChildIds,
5079
+ laneGutter
4260
5080
  );
4261
5081
  if (layout2 !== void 0) {
4262
5082
  layouts.set(swimlane.id, layout2);
@@ -4271,10 +5091,123 @@ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottom
4271
5091
  movedChildIds
4272
5092
  )
4273
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
+ }
4274
5103
  }
4275
5104
  return { layouts, diagnostics, movedChildIds };
4276
5105
  }
4277
- function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds) {
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) {
4278
5211
  const headerHeight = swimlane.headerHeight ?? 28;
4279
5212
  const padding = swimlane.padding ?? 16;
4280
5213
  const laneBounds = swimlane.lanes.map((lane) => {
@@ -4298,7 +5231,8 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
4298
5231
  padding,
4299
5232
  locks,
4300
5233
  diagnostics,
4301
- movedChildIds
5234
+ movedChildIds,
5235
+ laneGutter
4302
5236
  );
4303
5237
  }
4304
5238
  return applyHorizontalSwimlaneContract(
@@ -4309,10 +5243,11 @@ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes
4309
5243
  padding,
4310
5244
  locks,
4311
5245
  diagnostics,
4312
- movedChildIds
5246
+ movedChildIds,
5247
+ laneGutter
4313
5248
  );
4314
5249
  }
4315
- function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
5250
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
4316
5251
  const populatedBounds = laneBounds.filter(
4317
5252
  (box) => box !== void 0
4318
5253
  );
@@ -4331,6 +5266,7 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
4331
5266
  const rankSpacing = Math.max(96, maxRankStackHeight + padding);
4332
5267
  const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
4333
5268
  const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
5269
+ const laneStep = slotWidth + laneGutter;
4334
5270
  const laneContentTop = top + headerHeight + padding;
4335
5271
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
4336
5272
  const lane = swimlane.lanes[index];
@@ -4339,7 +5275,7 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
4339
5275
  continue;
4340
5276
  }
4341
5277
  const target = {
4342
- x: left + slotWidth * index + padding,
5278
+ x: left + laneStep * index + padding,
4343
5279
  y: laneContentTop
4344
5280
  };
4345
5281
  if (maxRank === 0) {
@@ -4375,11 +5311,12 @@ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBox
4375
5311
  box: {
4376
5312
  x: left,
4377
5313
  y: top,
4378
- width: slotWidth * swimlane.lanes.length,
5314
+ width: laneStep * (swimlane.lanes.length - 1) + slotWidth,
4379
5315
  height: contentHeight + padding * 2 + headerHeight
4380
5316
  },
4381
5317
  slotWidth,
4382
- slotHeight: contentHeight + padding * 2 + headerHeight
5318
+ slotHeight: contentHeight + padding * 2 + headerHeight,
5319
+ laneStep
4383
5320
  };
4384
5321
  }
4385
5322
  function isTopToBottomReadingDirection(value) {
@@ -4524,7 +5461,7 @@ function rankStacks(childIds, nodeBoxes, flowRanks) {
4524
5461
  }
4525
5462
  return stacks;
4526
5463
  }
4527
- function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
5464
+ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds, laneGutter) {
4528
5465
  const populatedBounds = laneBounds.filter(
4529
5466
  (box) => box !== void 0
4530
5467
  );
@@ -4532,6 +5469,7 @@ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, header
4532
5469
  const left = Math.min(...populatedBounds.map((box) => box.x));
4533
5470
  const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + headerHeight + padding * 2;
4534
5471
  const slotHeight = Math.max(...populatedBounds.map((box) => box.height)) + padding * 2;
5472
+ const laneStep = slotHeight + laneGutter;
4535
5473
  for (let index = 0; index < swimlane.lanes.length; index += 1) {
4536
5474
  const lane = swimlane.lanes[index];
4537
5475
  const bounds = laneBounds[index];
@@ -4540,7 +5478,7 @@ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, header
4540
5478
  }
4541
5479
  const target = {
4542
5480
  x: left + headerHeight + padding,
4543
- y: top + slotHeight * index + padding
5481
+ y: top + laneStep * index + padding
4544
5482
  };
4545
5483
  moveLaneChildren(
4546
5484
  lane.children,
@@ -4559,10 +5497,11 @@ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, header
4559
5497
  x: left,
4560
5498
  y: top,
4561
5499
  width: slotWidth,
4562
- height: slotHeight * swimlane.lanes.length
5500
+ height: laneStep * (swimlane.lanes.length - 1) + slotHeight
4563
5501
  },
4564
5502
  slotWidth,
4565
- slotHeight
5503
+ slotHeight,
5504
+ laneStep
4566
5505
  };
4567
5506
  }
4568
5507
  function moveLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, offset) {
@@ -4891,7 +5830,10 @@ function coordinatePorts(node, nodeBox, portShifting) {
4891
5830
  }
4892
5831
  function portAnchor(nodeBox, side, index, count, portShifting) {
4893
5832
  const shiftingEnabled = portShifting?.enabled ?? true;
4894
- 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;
4895
5837
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
4896
5838
  switch (side) {
4897
5839
  case "left":
@@ -4946,13 +5888,13 @@ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
4946
5888
  if (layout2 === "contract" && contractLayout !== void 0) {
4947
5889
  const lanes2 = swimlane.lanes.map((lane, index) => {
4948
5890
  const box = swimlane.orientation === "vertical" ? {
4949
- x: contractLayout.box.x + contractLayout.slotWidth * index,
5891
+ x: contractLayout.box.x + contractLayout.laneStep * index,
4950
5892
  y: contractLayout.box.y,
4951
5893
  width: contractLayout.slotWidth,
4952
5894
  height: contractLayout.box.height
4953
5895
  } : {
4954
5896
  x: contractLayout.box.x,
4955
- y: contractLayout.box.y + contractLayout.slotHeight * index,
5897
+ y: contractLayout.box.y + contractLayout.laneStep * index,
4956
5898
  width: contractLayout.box.width,
4957
5899
  height: contractLayout.slotHeight
4958
5900
  };
@@ -5053,17 +5995,19 @@ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
5053
5995
  });
5054
5996
  }
5055
5997
  function coordinateFrame(frame, contentBounds) {
5056
- const padding = 32;
5057
- const titleHeight = 28;
5998
+ const padding = framePadding(frame.padding);
5999
+ const titleHeight = frame.headerHeight ?? 28;
5058
6000
  const titleWidth = Math.max(180, frame.titleTab.length * 7);
5059
6001
  const box = {
5060
- x: contentBounds.x - padding,
5061
- y: contentBounds.y - padding - titleHeight,
5062
- width: contentBounds.width + padding * 2,
5063
- 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
5064
6006
  };
5065
6007
  return {
5066
6008
  ...frame,
6009
+ headerHeight: titleHeight,
6010
+ padding: frame.padding ?? 32,
5067
6011
  box,
5068
6012
  titleBox: {
5069
6013
  x: box.x,
@@ -5073,6 +6017,9 @@ function coordinateFrame(frame, contentBounds) {
5073
6017
  }
5074
6018
  };
5075
6019
  }
6020
+ function framePadding(value) {
6021
+ return normalizeInsets(value ?? 32);
6022
+ }
5076
6023
  function expand(box, padding, titleSize) {
5077
6024
  return {
5078
6025
  x: box.x - padding,
@@ -5168,10 +6115,16 @@ function edgeBounds(edges) {
5168
6115
  if (edge.points.length === 0) {
5169
6116
  return [];
5170
6117
  }
5171
- const minX = Math.min(...edge.points.map((point2) => point2.x));
5172
- const minY = Math.min(...edge.points.map((point2) => point2.y));
5173
- const maxX = Math.max(...edge.points.map((point2) => point2.x));
5174
- const maxY = Math.max(...edge.points.map((point2) => point2.y));
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));
5175
6128
  return [
5176
6129
  {
5177
6130
  x: minX,
@@ -5568,6 +6521,7 @@ function coordinateBaseTextAnnotations(input) {
5568
6521
  ownerId: node.id,
5569
6522
  surfaceKind: "node-label",
5570
6523
  layout: layout2,
6524
+ typography: typographyForLabel(node.label),
5571
6525
  anchor: node.box
5572
6526
  })
5573
6527
  );
@@ -5583,6 +6537,7 @@ function coordinateBaseTextAnnotations(input) {
5583
6537
  ownerId: group.id,
5584
6538
  surfaceKind: "group-label",
5585
6539
  layout: layout2,
6540
+ typography: typographyForLabel(group.label),
5586
6541
  anchor: group.box
5587
6542
  })
5588
6543
  );
@@ -5595,7 +6550,11 @@ function coordinateBaseTextAnnotations(input) {
5595
6550
  const layout2 = fitLabel(
5596
6551
  port.label.text,
5597
6552
  {
5598
- font: { fontFamily: "Arial", fontSize: 10, lineHeight: 12 },
6553
+ font: typographyTextStyle(port.label, {
6554
+ fontFamily: "Arial",
6555
+ fontSize: 10,
6556
+ lineHeight: 12
6557
+ }),
5599
6558
  padding: { top: 0, right: 0, bottom: 0, left: 0 },
5600
6559
  minSize: { width: 0, height: 0 },
5601
6560
  maxWidth: 160
@@ -5607,6 +6566,7 @@ function coordinateBaseTextAnnotations(input) {
5607
6566
  ownerId: `${node.id}.${port.id}`,
5608
6567
  surfaceKind: "port-label",
5609
6568
  layout: layout2,
6569
+ typography: typographyForLabel(port.label),
5610
6570
  anchor: portLabelBox(port)
5611
6571
  })
5612
6572
  );
@@ -5657,7 +6617,11 @@ function coordinateBaseTextAnnotations(input) {
5657
6617
  const layout2 = fitLabel(
5658
6618
  lane.label.text,
5659
6619
  {
5660
- font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
6620
+ font: typographyTextStyle(lane.label, {
6621
+ fontFamily: "Arial",
6622
+ fontSize: 12,
6623
+ lineHeight: 14
6624
+ }),
5661
6625
  padding: { top: 0, right: 0, bottom: 0, left: 0 },
5662
6626
  minSize: { width: 0, height: 0 },
5663
6627
  maxWidth: swimlane.orientation === "horizontal" ? labelBox.height : labelBox.width
@@ -5669,6 +6633,7 @@ function coordinateBaseTextAnnotations(input) {
5669
6633
  ownerId: `${swimlane.id}.${lane.id}`,
5670
6634
  surfaceKind: "swimlane-label",
5671
6635
  layout: layout2,
6636
+ typography: typographyForLabel(lane.label),
5672
6637
  anchor: labelBox
5673
6638
  })
5674
6639
  );
@@ -5676,9 +6641,10 @@ function coordinateBaseTextAnnotations(input) {
5676
6641
  }
5677
6642
  return annotations;
5678
6643
  }
5679
- function coordinateEdgeTextAnnotations(edges, textMeasurer) {
6644
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
5680
6645
  const measurer = textMeasurer ?? createDefaultTextMeasurer();
5681
6646
  const annotations = [];
6647
+ const placedLabelBoxes = [];
5682
6648
  for (const edge of edges) {
5683
6649
  if (edge.label?.text === void 0) {
5684
6650
  continue;
@@ -5686,19 +6652,37 @@ function coordinateEdgeTextAnnotations(edges, textMeasurer) {
5686
6652
  const layout2 = fitLabel(
5687
6653
  edge.label.text,
5688
6654
  {
5689
- font: { fontFamily: "Arial", fontSize: 12, lineHeight: 14 },
6655
+ font: typographyTextStyle(edge.label, {
6656
+ fontFamily: "Arial",
6657
+ fontSize: 12,
6658
+ lineHeight: 14
6659
+ }),
5690
6660
  padding: { top: 0, right: 0, bottom: 0, left: 0 },
5691
6661
  minSize: { width: 0, height: 0 },
5692
6662
  maxWidth: 200
5693
6663
  },
5694
6664
  measurer
5695
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
+ });
5696
6679
  annotations.push(
5697
6680
  buildCenteredTextAnnotation({
5698
6681
  ownerId: edge.id,
5699
6682
  surfaceKind: "edge-label",
5700
6683
  layout: layout2,
5701
- center: edgeLabelAnchor(edge, layout2, edges)
6684
+ typography: typographyForLabel(edge.label),
6685
+ center
5702
6686
  })
5703
6687
  );
5704
6688
  }
@@ -5737,7 +6721,8 @@ function buildTextAnnotation(input) {
5737
6721
  anchor: input.anchor,
5738
6722
  paddings: input.layout.padding,
5739
6723
  lines: input.layout.lines,
5740
- fontSize: input.layout.font.fontSize,
6724
+ fontFamily: input.typography?.fontFamily ?? normalizeOutputFontFamily(input.layout.font),
6725
+ fontSize: input.typography?.fontSize ?? input.layout.font.fontSize,
5741
6726
  textBackend: input.layout.textBackend
5742
6727
  };
5743
6728
  }
@@ -5747,6 +6732,7 @@ function buildAnchorCenteredTextAnnotation(input) {
5747
6732
  surfaceKind: input.surfaceKind,
5748
6733
  ...input.surfaceIndex === void 0 ? {} : { surfaceIndex: input.surfaceIndex },
5749
6734
  layout: input.layout,
6735
+ ...input.typography === void 0 ? {} : { typography: input.typography },
5750
6736
  center: {
5751
6737
  x: input.anchor.x + input.anchor.width / 2,
5752
6738
  y: input.anchor.y + input.anchor.height / 2
@@ -5769,10 +6755,14 @@ function buildCenteredTextAnnotation(input) {
5769
6755
  anchor: input.anchor ?? input.center,
5770
6756
  paddings: input.layout.padding,
5771
6757
  lines: input.layout.lines,
5772
- fontSize: input.layout.font.fontSize,
6758
+ fontFamily: input.typography?.fontFamily ?? normalizeOutputFontFamily(input.layout.font),
6759
+ fontSize: input.typography?.fontSize ?? input.layout.font.fontSize,
5773
6760
  textBackend: input.layout.textBackend
5774
6761
  };
5775
6762
  }
6763
+ function normalizeOutputFontFamily(font) {
6764
+ return font.fontFamily === "Arial" ? "Arial, sans-serif" : font.fontFamily;
6765
+ }
5776
6766
  function reportTextAnnotationCollisions(annotations) {
5777
6767
  const diagnostics = [];
5778
6768
  const relevantAnnotations = annotations.filter(
@@ -5962,12 +6952,16 @@ function fallbackLabelLayout(text) {
5962
6952
  diagnostics: []
5963
6953
  };
5964
6954
  }
5965
- function edgeLabelAnchor(edge, layout2, edges) {
6955
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
5966
6956
  const placement = labelPlacementOnPolyline2(edge.points);
5967
6957
  if (placement === void 0) {
5968
6958
  return { x: 0, y: 0 };
5969
6959
  }
5970
- for (const candidate of edgeLabelAnchorCandidates(edge.points, placement)) {
6960
+ for (const candidate of edgeLabelAnchorCandidates(
6961
+ edge.points,
6962
+ placement,
6963
+ layout2
6964
+ )) {
5971
6965
  const labelBox = {
5972
6966
  x: candidate.x - layout2.box.width / 2,
5973
6967
  y: candidate.y - layout2.box.height / 2,
@@ -5980,36 +6974,55 @@ function edgeLabelAnchor(edge, layout2, edges) {
5980
6974
  const crossesOtherRoute = edges.some(
5981
6975
  (other) => other.id !== edge.id && routeIntersectsTextBox(other.points, labelBox)
5982
6976
  );
5983
- if (!crossesOtherRoute) {
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) {
5984
6990
  return candidate;
5985
6991
  }
5986
6992
  }
5987
6993
  return placement;
5988
6994
  }
5989
- function edgeLabelAnchorCandidates(points, placement) {
6995
+ function edgeLabelAnchorCandidates(points, placement, layout2) {
5990
6996
  const segment = labelSegmentOnPolyline(points);
5991
6997
  if (segment === void 0) {
5992
6998
  return [placement];
5993
6999
  }
7000
+ const candidates = [placement];
5994
7001
  if (segment.start.y === segment.end.y) {
5995
- return [
5996
- placement,
5997
- { x: placement.x, y: placement.y - EDGE_LABEL_CLEARANCE },
5998
- { x: placement.x, y: placement.y + EDGE_LABEL_CLEARANCE },
5999
- { x: placement.x, y: placement.y - EDGE_LABEL_CLEARANCE * 2 },
6000
- { x: placement.x, y: placement.y + EDGE_LABEL_CLEARANCE * 2 }
6001
- ];
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;
6002
7012
  }
6003
7013
  if (segment.start.x === segment.end.x) {
6004
- return [
6005
- placement,
6006
- { x: placement.x + EDGE_LABEL_CLEARANCE, y: placement.y },
6007
- { x: placement.x - EDGE_LABEL_CLEARANCE, y: placement.y },
6008
- { x: placement.x + EDGE_LABEL_CLEARANCE * 2, y: placement.y },
6009
- { x: placement.x - EDGE_LABEL_CLEARANCE * 2, y: placement.y }
6010
- ];
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;
6011
7024
  }
6012
- return [placement];
7025
+ return candidates;
6013
7026
  }
6014
7027
  function labelPlacementOnPolyline2(points) {
6015
7028
  return labelSegmentOnPolyline(points)?.placement;
@@ -6100,8 +7113,26 @@ function portGeometry(nodeGeometry, port) {
6100
7113
  obstacleBox: port.box
6101
7114
  };
6102
7115
  }
6103
- function stableById(items) {
6104
- 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));
6105
7136
  }
6106
7137
  function stableByConstraintId(items) {
6107
7138
  return [...items].sort(
@@ -6383,6 +7414,7 @@ exports.routeEdge = routeEdge;
6383
7414
  exports.runDagreInitialLayout = runDagreInitialLayout;
6384
7415
  exports.simplifyRoute = simplifyRoute;
6385
7416
  exports.solveDiagram = solveDiagram;
7417
+ exports.solveDiagramSafe = solveDiagramSafe;
6386
7418
  exports.sortDslDiagnostics = sortDslDiagnostics;
6387
7419
  exports.stringifyCanonical = stringifyCanonical;
6388
7420
  exports.toCanvasFont = toCanvasFont;