@crazyhappyone/auto-graph 0.0.1 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { CommanderError, Command } from 'commander';
3
3
  import { Graph, layout } from '@dagrejs/dagre';
4
+ import { createRequire } from 'module';
5
+ import { prepareWithSegments, layoutWithLines, measureNaturalWidth } from '@chenglou/pretext';
4
6
  import { Buffer as Buffer$1 } from 'buffer';
5
7
  import { parseDocument } from 'yaml';
6
8
  import { z } from 'zod';
@@ -194,11 +196,12 @@ function renderArrow(edge) {
194
196
  height: box.height
195
197
  }),
196
198
  backgroundColor: "transparent",
199
+ strokeStyle: edge.style ?? "solid",
197
200
  points: relativePoints,
198
201
  startBinding: { elementId: `node:${edge.source.nodeId}`, focus: 0, gap: 0 },
199
202
  endBinding: { elementId: `node:${edge.target.nodeId}`, focus: 0, gap: 0 },
200
203
  startArrowhead: null,
201
- endArrowhead: "arrow"
204
+ endArrowhead: mapArrowhead(edge.arrowhead)
202
205
  };
203
206
  }
204
207
  function renderText(id, label, box, containerId, groupIds) {
@@ -276,6 +279,16 @@ function mapShape(shape) {
276
279
  return "cylinder";
277
280
  }
278
281
  }
282
+ function mapArrowhead(arrowhead) {
283
+ switch (arrowhead) {
284
+ case void 0:
285
+ return "arrow";
286
+ case "triangle":
287
+ return "triangle";
288
+ case "hollowTriangle":
289
+ return "triangle_outline";
290
+ }
291
+ }
279
292
  function createGroupMembership(groups) {
280
293
  const membership = /* @__PURE__ */ new Map();
281
294
  for (const group of groups) {
@@ -342,19 +355,28 @@ function exportSvg(diagram, options = {}) {
342
355
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
343
356
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
344
357
  ` <rect class="background" x="${formatNumber(diagram.bounds.x)}" y="${formatNumber(diagram.bounds.y)}" width="${formatNumber(diagram.bounds.width)}" height="${formatNumber(diagram.bounds.height)}" fill="#ffffff"/>`,
358
+ ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame))],
359
+ ...(diagram.swimlanes ?? []).flatMap(
360
+ (swimlane) => renderSwimlane(swimlane)
361
+ ),
345
362
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
346
363
  ...diagram.edges.flatMap((edge) => {
347
- const path = renderEdgePath(edge.points, edge.id);
364
+ const path = renderEdgePath(edge);
348
365
  if (path === void 0) {
349
366
  return [];
350
367
  }
351
- return [indent(path), indent(renderArrowhead(edge.points, edge.id))];
368
+ return [indent(path), indent(renderArrowhead(edge))];
352
369
  }),
353
370
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
371
+ ...diagram.nodes.flatMap((node) => renderCompartments(node)),
372
+ ...diagram.nodes.flatMap((node) => renderPorts(node)),
354
373
  ...diagram.groups.flatMap(
355
374
  (group) => renderLabel(group.label, group.box, group)
356
375
  ),
357
- ...diagram.nodes.flatMap((node) => renderLabel(node.label, node.box, node)),
376
+ ...diagram.nodes.flatMap(
377
+ (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node) : []
378
+ ),
379
+ ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge)),
358
380
  "</svg>"
359
381
  ];
360
382
  return `${lines.join("\n")}
@@ -364,7 +386,9 @@ function renderGroup2(group) {
364
386
  return `<rect class="group" data-id="${escapeAttribute(group.id)}" x="${formatNumber(group.box.x)}" y="${formatNumber(group.box.y)}" width="${formatNumber(group.box.width)}" height="${formatNumber(group.box.height)}" fill="${GROUP_FILL}" stroke="${STROKE}" stroke-dasharray="6 4"/>`;
365
387
  }
366
388
  function renderNode2(node) {
367
- const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${NODE_FILL}" stroke="${STROKE}"`;
389
+ const fill = node.style?.fill ?? NODE_FILL;
390
+ const stroke = node.style?.stroke ?? STROKE;
391
+ const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(stroke)}"`;
368
392
  switch (node.shape) {
369
393
  case "rectangle":
370
394
  return renderRect(node.box, common);
@@ -380,16 +404,111 @@ function renderNode2(node) {
380
404
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
381
405
  }
382
406
  }
407
+ function renderFrame(frame) {
408
+ const stroke = frame.style?.stroke ?? "#6b7280";
409
+ const fill = frame.style?.fill ?? "transparent";
410
+ return [
411
+ `<g class="sysml-frame" data-kind="${escapeAttribute(frame.kind)}">`,
412
+ ` <rect class="sysml-frame-border" x="${formatNumber(frame.box.x)}" y="${formatNumber(frame.box.y)}" width="${formatNumber(frame.box.width)}" height="${formatNumber(frame.box.height)}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(stroke)}"/>`,
413
+ ` <path class="sysml-title-tab" d="M ${formatNumber(frame.titleBox.x)} ${formatNumber(frame.titleBox.y + frame.titleBox.height)} L ${formatNumber(frame.titleBox.x)} ${formatNumber(frame.titleBox.y)} L ${formatNumber(frame.titleBox.x + frame.titleBox.width - 16)} ${formatNumber(frame.titleBox.y)} L ${formatNumber(frame.titleBox.x + frame.titleBox.width)} ${formatNumber(frame.titleBox.y + frame.titleBox.height)} Z" fill="#f3f4f6" stroke="${escapeAttribute(stroke)}"/>`,
414
+ ` <text class="sysml-title-tab-label" x="${formatNumber(frame.titleBox.x + 8)}" y="${formatNumber(frame.titleBox.y + frame.titleBox.height / 2)}" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(frame.titleTab)}</text>`,
415
+ "</g>"
416
+ ].join("\n");
417
+ }
418
+ function renderSwimlane(swimlane) {
419
+ if (swimlane.box === void 0) {
420
+ return [];
421
+ }
422
+ const lines = [
423
+ ` <g class="swimlane" data-id="${escapeAttribute(swimlane.id)}">`,
424
+ ` <rect class="swimlane-frame" x="${formatNumber(swimlane.box.x)}" y="${formatNumber(swimlane.box.y)}" width="${formatNumber(swimlane.box.width)}" height="${formatNumber(swimlane.box.height)}" fill="#ffffff" stroke="${STROKE}"/>`
425
+ ];
426
+ for (const lane of swimlane.lanes) {
427
+ if (lane.box === void 0) {
428
+ continue;
429
+ }
430
+ lines.push(
431
+ ` <rect class="swimlane-lane" data-lane="${escapeAttribute(`${swimlane.id}.${lane.id}`)}" x="${formatNumber(lane.box.x)}" y="${formatNumber(lane.box.y)}" width="${formatNumber(lane.box.width)}" height="${formatNumber(lane.box.height)}" fill="none" stroke="${STROKE}"/>`
432
+ );
433
+ if (lane.label?.text !== void 0) {
434
+ lines.push(
435
+ ` <text class="swimlane-label" x="${formatNumber(lane.box.x + lane.box.width / 2)}" y="${formatNumber(lane.box.y + 16)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(lane.label.text)}</text>`
436
+ );
437
+ }
438
+ }
439
+ lines.push(" </g>");
440
+ return lines;
441
+ }
442
+ function renderPorts(node) {
443
+ return (node.ports ?? []).flatMap((port) => [
444
+ ` <rect class="port" data-kind="${escapeAttribute(port.kind)}" data-port="${escapeAttribute(`${node.id}.${port.id}`)}" x="${formatNumber(port.box.x)}" y="${formatNumber(port.box.y)}" width="${formatNumber(port.box.width)}" height="${formatNumber(port.box.height)}" fill="${escapeAttribute(port.style?.fill ?? "#d9ead3")}" stroke="${escapeAttribute(port.style?.stroke ?? STROKE)}"/>`,
445
+ ...port.label?.text === void 0 ? [] : [
446
+ ` <text class="port-label" data-for="${escapeAttribute(`${node.id}.${port.id}`)}" x="${formatNumber(portLabelX(port.anchor.x, port.side))}" y="${formatNumber(port.anchor.y - 8)}" text-anchor="${port.side === "left" ? "end" : "start"}" font-family="${FONT_FAMILY}" font-size="10" fill="#111827">${escapeXml(port.label.text)}</text>`
447
+ ]
448
+ ]);
449
+ }
450
+ function renderCompartments(node) {
451
+ const compartments2 = node.compartments;
452
+ if (compartments2 === void 0) {
453
+ return [];
454
+ }
455
+ const rows = [
456
+ ...compartments2.stereotype === void 0 ? [] : [{ className: "stereotype", text: compartments2.stereotype }],
457
+ {
458
+ className: "name",
459
+ text: compartments2.name ?? node.label?.text ?? node.id
460
+ },
461
+ ...(compartments2.properties ?? []).map((text) => ({
462
+ className: "properties",
463
+ text
464
+ })),
465
+ ...(compartments2.constraints ?? []).map((text) => ({
466
+ className: "constraints",
467
+ text
468
+ }))
469
+ ];
470
+ const lineHeight = 16;
471
+ const lines = [
472
+ ` <g class="compartment" data-for="${escapeAttribute(node.id)}">`
473
+ ];
474
+ for (let index = 0; index < rows.length; index += 1) {
475
+ const row = rows[index];
476
+ if (row === void 0) {
477
+ continue;
478
+ }
479
+ const y = node.box.y + 18 + index * lineHeight;
480
+ if (index > 1) {
481
+ lines.push(
482
+ ` <line class="compartment-separator" x1="${formatNumber(node.box.x)}" y1="${formatNumber(y - 12)}" x2="${formatNumber(node.box.x + node.box.width)}" y2="${formatNumber(y - 12)}" stroke="${STROKE}"/>`
483
+ );
484
+ }
485
+ lines.push(
486
+ ` <text class="compartment-${row.className}" x="${formatNumber(node.box.x + node.box.width / 2)}" y="${formatNumber(y)}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="11" fill="#111827">${escapeXml(row.text)}</text>`
487
+ );
488
+ }
489
+ lines.push(" </g>");
490
+ return lines;
491
+ }
492
+ function portLabelX(x, side) {
493
+ if (side === "left") {
494
+ return x - 8;
495
+ }
496
+ if (side === "right") {
497
+ return x + 8;
498
+ }
499
+ return x + 8;
500
+ }
383
501
  function renderRect(box, attributes) {
384
502
  return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
385
503
  }
386
504
  function renderLabel(label, box, item) {
387
505
  const labelLayout = item.labelLayout;
388
506
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
507
+ const offset = { x: box.x, y: box.y };
389
508
  return [
390
509
  ` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
391
510
  ...labelLayout.lines.map(
392
- (line) => ` <tspan x="${formatNumber(line.box.x)}" y="${formatNumber(line.baselineY)}">${escapeXml(line.text)}</tspan>`
511
+ (line) => ` <tspan x="${formatNumber(offset.x + line.box.x)}" y="${formatNumber(offset.y + line.baselineY)}">${escapeXml(line.text)}</tspan>`
393
512
  ),
394
513
  " </text>"
395
514
  ];
@@ -401,15 +520,88 @@ function renderLabel(label, box, item) {
401
520
  ` <text class="label" data-for="${escapeAttribute(item.id)}" x="${formatNumber(box.x + box.width / 2)}" y="${formatNumber(box.y + box.height / 2)}" text-anchor="middle" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="14" fill="#111827">${escapeXml(label.text)}</text>`
402
521
  ];
403
522
  }
404
- function renderEdgePath(points, id) {
405
- if (points.length < 2) {
523
+ function renderEdgePath(edge) {
524
+ if (edge.points.length < 2) {
525
+ return void 0;
526
+ }
527
+ const dash = edge.style === "dashed" ? ' stroke-dasharray="6 4"' : "";
528
+ return `<path class="edge" data-id="${escapeAttribute(edge.id)}" d="${formatPath(pathPointsBeforeArrowhead(edge.points))}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"${dash}/>`;
529
+ }
530
+ function renderEdgeLabel(edge) {
531
+ if (edge.label?.text === void 0 || edge.points.length < 2) {
532
+ return [];
533
+ }
534
+ const placement = labelPlacementOnPolyline(edge.points);
535
+ if (placement === void 0) {
536
+ return [];
537
+ }
538
+ return [
539
+ ` <text class="edge-label" data-for="${escapeAttribute(edge.id)}" x="${formatNumber(placement.x)}" y="${formatNumber(placement.y)}" text-anchor="middle" dominant-baseline="middle" font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(edge.label.text)}</text>`
540
+ ];
541
+ }
542
+ function renderArrowhead(edge) {
543
+ const arrowhead = computeArrowhead(edge.points);
544
+ const fill = edge.arrowhead === "hollowTriangle" ? "none" : EDGE_STROKE;
545
+ return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(edge.id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${fill}" stroke="${EDGE_STROKE}"/>`;
546
+ }
547
+ function labelPlacementOnPolyline(points) {
548
+ const segments = nonZeroSegments(points);
549
+ const totalLength = segments.reduce(
550
+ (sum, segment) => sum + segment.length,
551
+ 0
552
+ );
553
+ if (totalLength <= 0) {
406
554
  return void 0;
407
555
  }
408
- return `<path class="edge" data-id="${escapeAttribute(id)}" d="${formatPath(points)}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"/>`;
556
+ let remaining = totalLength / 2;
557
+ for (const segment of segments) {
558
+ if (remaining <= segment.length) {
559
+ const ratio = remaining / segment.length;
560
+ const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
561
+ const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
562
+ const offset2 = labelOffset(segment);
563
+ return { x: x + offset2.x, y: y + offset2.y };
564
+ }
565
+ remaining -= segment.length;
566
+ }
567
+ const last = segments.at(-1);
568
+ if (last === void 0) {
569
+ return void 0;
570
+ }
571
+ const offset = labelOffset(last);
572
+ return { x: last.end.x + offset.x, y: last.end.y + offset.y };
573
+ }
574
+ function nonZeroSegments(points) {
575
+ const segments = [];
576
+ for (let index = 0; index < points.length - 1; index += 1) {
577
+ const start = points[index];
578
+ const end = points[index + 1];
579
+ if (start === void 0 || end === void 0) {
580
+ continue;
581
+ }
582
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
583
+ if (length > 0) {
584
+ segments.push({ start, end, length });
585
+ }
586
+ }
587
+ return segments;
409
588
  }
410
- function renderArrowhead(points, id) {
589
+ function labelOffset(segment) {
590
+ const offset = 10;
591
+ const dx = segment.end.x - segment.start.x;
592
+ const dy = segment.end.y - segment.start.y;
593
+ return {
594
+ x: -dy / segment.length * offset,
595
+ y: dx / segment.length * offset
596
+ };
597
+ }
598
+ function pathPointsBeforeArrowhead(points) {
411
599
  const arrowhead = computeArrowhead(points);
412
- return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${EDGE_STROKE}" stroke="${EDGE_STROKE}"/>`;
600
+ const base = {
601
+ x: (arrowhead.left.x + arrowhead.right.x) / 2,
602
+ y: (arrowhead.left.y + arrowhead.right.y) / 2
603
+ };
604
+ return [...points.slice(0, -1), base];
413
605
  }
414
606
  function shapePoints(shape, box) {
415
607
  const left = box.x;
@@ -1294,20 +1486,33 @@ function isValidDimension(value) {
1294
1486
  // src/routing/routes.ts
1295
1487
  function routeEdge(input) {
1296
1488
  const diagnostics = [];
1489
+ const defaultAnchors = defaultAnchorsForGeometry(
1490
+ input.source.box,
1491
+ input.target.box,
1492
+ input.direction
1493
+ );
1297
1494
  const source = getEdgePort(
1298
1495
  input.source,
1299
1496
  input.target.center,
1300
- input.sourceAnchor
1497
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
1301
1498
  );
1302
1499
  const target = getEdgePort(
1303
1500
  input.target,
1304
1501
  input.source.center,
1305
- input.targetAnchor
1502
+ input.targetAnchor ?? defaultAnchors.targetAnchor
1306
1503
  );
1307
1504
  if ((input.kind ?? "orthogonal") === "straight") {
1308
1505
  return { points: simplifyRoute([source, target]), diagnostics };
1309
1506
  }
1310
1507
  const candidates = orthogonalCandidates(source, target, input.direction);
1508
+ candidates.push(
1509
+ ...expandedObstacleCandidates(
1510
+ source,
1511
+ target,
1512
+ input.direction,
1513
+ input.obstacles ?? []
1514
+ )
1515
+ );
1311
1516
  for (const candidate of candidates) {
1312
1517
  if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
1313
1518
  return { points: simplifyRoute(candidate), diagnostics };
@@ -1346,27 +1551,113 @@ function simplifyRoute(points) {
1346
1551
  function orthogonalCandidates(source, target, direction) {
1347
1552
  const midpointX = (source.x + target.x) / 2;
1348
1553
  const midpointY = (source.y + target.y) / 2;
1349
- const candidates = [
1350
- [source, { x: target.x, y: source.y }, target],
1351
- [source, { x: source.x, y: target.y }, target]
1352
- ];
1554
+ const candidates = [];
1353
1555
  if (direction === "TB" || direction === "BT") {
1354
1556
  candidates.push([
1355
1557
  source,
1356
- { x: midpointX, y: source.y },
1357
- { x: midpointX, y: target.y },
1558
+ { x: source.x, y: midpointY },
1559
+ { x: target.x, y: midpointY },
1358
1560
  target
1359
1561
  ]);
1360
1562
  } else {
1361
1563
  candidates.push([
1362
1564
  source,
1363
- { x: source.x, y: midpointY },
1364
- { x: target.x, y: midpointY },
1565
+ { x: midpointX, y: source.y },
1566
+ { x: midpointX, y: target.y },
1365
1567
  target
1366
1568
  ]);
1367
1569
  }
1570
+ candidates.push(
1571
+ [source, { x: target.x, y: source.y }, target],
1572
+ [source, { x: source.x, y: target.y }, target]
1573
+ );
1574
+ return candidates;
1575
+ }
1576
+ function defaultSourceAnchor(direction) {
1577
+ switch (direction) {
1578
+ case "LR":
1579
+ return "right";
1580
+ case "RL":
1581
+ return "left";
1582
+ case "TB":
1583
+ return "bottom";
1584
+ case "BT":
1585
+ return "top";
1586
+ }
1587
+ }
1588
+ function defaultAnchorsForGeometry(source, target, direction) {
1589
+ const dx = target.x + target.width / 2 - (source.x + source.width / 2);
1590
+ const dy = target.y + target.height / 2 - (source.y + source.height / 2);
1591
+ if (Math.abs(dy) > Math.abs(dx)) {
1592
+ return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
1593
+ }
1594
+ if (Math.abs(dx) > 0) {
1595
+ return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
1596
+ }
1597
+ return {
1598
+ sourceAnchor: defaultSourceAnchor(direction),
1599
+ targetAnchor: defaultTargetAnchor(direction)
1600
+ };
1601
+ }
1602
+ function defaultTargetAnchor(direction) {
1603
+ switch (direction) {
1604
+ case "LR":
1605
+ return "left";
1606
+ case "RL":
1607
+ return "right";
1608
+ case "TB":
1609
+ return "top";
1610
+ case "BT":
1611
+ return "bottom";
1612
+ }
1613
+ }
1614
+ function expandedObstacleCandidates(source, target, direction, obstacles) {
1615
+ if (obstacles.length === 0) {
1616
+ return [];
1617
+ }
1618
+ const margin = 16;
1619
+ const candidates = [];
1620
+ if (direction === "TB" || direction === "BT") {
1621
+ const lanes = sortedUniqueLanes(
1622
+ obstacles.flatMap((obstacle) => [
1623
+ obstacle.x - margin,
1624
+ obstacle.x + obstacle.width + margin
1625
+ ]),
1626
+ (source.x + target.x) / 2
1627
+ );
1628
+ for (const laneX of lanes) {
1629
+ candidates.push([
1630
+ source,
1631
+ { x: laneX, y: source.y },
1632
+ { x: laneX, y: target.y },
1633
+ target
1634
+ ]);
1635
+ }
1636
+ } else {
1637
+ const lanes = sortedUniqueLanes(
1638
+ obstacles.flatMap((obstacle) => [
1639
+ obstacle.y - margin,
1640
+ obstacle.y + obstacle.height + margin
1641
+ ]),
1642
+ (source.y + target.y) / 2
1643
+ );
1644
+ for (const laneY of lanes) {
1645
+ candidates.push([
1646
+ source,
1647
+ { x: source.x, y: laneY },
1648
+ { x: target.x, y: laneY },
1649
+ target
1650
+ ]);
1651
+ }
1652
+ }
1368
1653
  return candidates;
1369
1654
  }
1655
+ function sortedUniqueLanes(lanes, midpoint) {
1656
+ return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
1657
+ const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
1658
+ return distance === 0 ? left - right : distance;
1659
+ });
1660
+ }
1370
1661
  function routeIntersectsObstacles(points, obstacles) {
1371
1662
  for (let index = 0; index < points.length - 1; index += 1) {
1372
1663
  const a = points[index];
@@ -1445,12 +1736,17 @@ function solveDiagram(diagram, options = {}) {
1445
1736
  options,
1446
1737
  diagnostics
1447
1738
  );
1739
+ const coordinatedSwimlanes = coordinateSwimlanes(
1740
+ diagram.swimlanes ?? [],
1741
+ constrained.boxes
1742
+ );
1448
1743
  const groupBoxes = new Map(
1449
1744
  coordinatedGroups.map((group) => [group.id, group.box])
1450
1745
  );
1451
1746
  const coordinatedEdges = coordinateEdges(
1452
1747
  edges,
1453
1748
  nodeGeometryById,
1749
+ coordinatedNodes,
1454
1750
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
1455
1751
  diagram.direction,
1456
1752
  options,
@@ -1458,8 +1754,18 @@ function solveDiagram(diagram, options = {}) {
1458
1754
  );
1459
1755
  const allBoxes = [
1460
1756
  ...coordinatedNodes.map((node) => node.box),
1461
- ...groupBoxes.values()
1757
+ ...coordinatedNodes.flatMap(
1758
+ (node) => (node.ports ?? []).flatMap(
1759
+ (port) => port.label === void 0 ? [port.box] : [port.box, portLabelBox(port)]
1760
+ )
1761
+ ),
1762
+ ...groupBoxes.values(),
1763
+ ...coordinatedSwimlanes.flatMap(
1764
+ (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
1765
+ )
1462
1766
  ];
1767
+ const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
1768
+ const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
1463
1769
  return {
1464
1770
  id: diagram.id,
1465
1771
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -1467,8 +1773,10 @@ function solveDiagram(diagram, options = {}) {
1467
1773
  nodes: coordinatedNodes,
1468
1774
  edges: coordinatedEdges,
1469
1775
  groups: coordinatedGroups,
1776
+ ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
1470
1777
  diagnostics,
1471
- bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
1778
+ bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
1779
+ ...frame === void 0 ? {} : { frame },
1472
1780
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
1473
1781
  };
1474
1782
  }
@@ -1494,6 +1802,9 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
1494
1802
  coordinated.push({
1495
1803
  id: node.id,
1496
1804
  ...node.label === void 0 ? {} : { label: node.label },
1805
+ ...node.style === void 0 ? {} : { style: node.style },
1806
+ ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
1807
+ ...node.compartments === void 0 ? {} : { compartments: node.compartments },
1497
1808
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
1498
1809
  shape: node.shape,
1499
1810
  ...node.metadata === void 0 ? {} : { metadata: node.metadata },
@@ -1504,6 +1815,142 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
1504
1815
  }
1505
1816
  return coordinated;
1506
1817
  }
1818
+ function coordinatePorts(node, nodeBox, portShifting) {
1819
+ const portsBySide = /* @__PURE__ */ new Map();
1820
+ for (const port of node.ports ?? []) {
1821
+ const ports = portsBySide.get(port.side) ?? [];
1822
+ ports.push(port);
1823
+ portsBySide.set(port.side, ports);
1824
+ }
1825
+ const coordinated = [];
1826
+ for (const [side, ports] of portsBySide) {
1827
+ const sorted = [...ports ?? []].sort((a, b) => {
1828
+ const order = (a.order ?? 0) - (b.order ?? 0);
1829
+ return order === 0 ? a.id.localeCompare(b.id) : order;
1830
+ });
1831
+ for (let index = 0; index < sorted.length; index += 1) {
1832
+ const port = sorted[index];
1833
+ if (port === void 0) {
1834
+ continue;
1835
+ }
1836
+ const anchor = portAnchor(
1837
+ nodeBox,
1838
+ side,
1839
+ index,
1840
+ sorted.length,
1841
+ portShifting
1842
+ );
1843
+ const box = portBox(anchor);
1844
+ coordinated.push({ ...port, box, anchor });
1845
+ }
1846
+ }
1847
+ return coordinated.sort((a, b) => a.id.localeCompare(b.id));
1848
+ }
1849
+ function portAnchor(nodeBox, side, index, count, portShifting) {
1850
+ const shiftingEnabled = portShifting?.enabled ?? true;
1851
+ const spacing = portShifting?.spacing ?? 24;
1852
+ const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
1853
+ switch (side) {
1854
+ case "left":
1855
+ return {
1856
+ x: nodeBox.x,
1857
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
1858
+ };
1859
+ case "right":
1860
+ return {
1861
+ x: nodeBox.x + nodeBox.width,
1862
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
1863
+ };
1864
+ case "top":
1865
+ return {
1866
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
1867
+ y: nodeBox.y
1868
+ };
1869
+ case "bottom":
1870
+ return {
1871
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
1872
+ y: nodeBox.y + nodeBox.height
1873
+ };
1874
+ }
1875
+ }
1876
+ function portBox(anchor) {
1877
+ const size = 10;
1878
+ return {
1879
+ x: anchor.x - size / 2,
1880
+ y: anchor.y - size / 2,
1881
+ width: size,
1882
+ height: size
1883
+ };
1884
+ }
1885
+ function portLabelBox(port) {
1886
+ const textWidth = Math.max(0, (port.label?.text.length ?? 0) * 6);
1887
+ const height = 12;
1888
+ const gap = 8;
1889
+ const x = port.side === "left" ? port.anchor.x - gap - textWidth : port.anchor.x + gap;
1890
+ return {
1891
+ x,
1892
+ y: port.anchor.y - 8 - height,
1893
+ width: textWidth,
1894
+ height
1895
+ };
1896
+ }
1897
+ function coordinateSwimlanes(swimlanes, nodeBoxes) {
1898
+ const titleSize = 28;
1899
+ const padding = 16;
1900
+ return swimlanes.map((swimlane) => {
1901
+ const laneBoxes = swimlane.lanes.flatMap((lane) => {
1902
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
1903
+ return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
1904
+ });
1905
+ const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
1906
+ const outer = expand(laneUnion, padding, titleSize);
1907
+ const laneCount = Math.max(1, swimlane.lanes.length);
1908
+ const lanes = swimlane.lanes.map((lane, index) => {
1909
+ const box = swimlane.orientation === "vertical" ? {
1910
+ x: outer.x + outer.width / laneCount * index,
1911
+ y: outer.y,
1912
+ width: outer.width / laneCount,
1913
+ height: outer.height
1914
+ } : {
1915
+ x: outer.x,
1916
+ y: outer.y + outer.height / laneCount * index,
1917
+ width: outer.width,
1918
+ height: outer.height / laneCount
1919
+ };
1920
+ return { ...lane, box };
1921
+ });
1922
+ return { ...swimlane, lanes, box: outer };
1923
+ });
1924
+ }
1925
+ function coordinateFrame(frame, contentBounds) {
1926
+ const padding = 32;
1927
+ const titleHeight = 28;
1928
+ const titleWidth = Math.max(180, frame.titleTab.length * 7);
1929
+ const box = {
1930
+ x: contentBounds.x - padding,
1931
+ y: contentBounds.y - padding - titleHeight,
1932
+ width: contentBounds.width + padding * 2,
1933
+ height: contentBounds.height + padding * 2 + titleHeight
1934
+ };
1935
+ return {
1936
+ ...frame,
1937
+ box,
1938
+ titleBox: {
1939
+ x: box.x,
1940
+ y: box.y,
1941
+ width: Math.min(titleWidth, box.width * 0.8),
1942
+ height: titleHeight
1943
+ }
1944
+ };
1945
+ }
1946
+ function expand(box, padding, titleSize) {
1947
+ return {
1948
+ x: box.x - padding,
1949
+ y: box.y - padding - titleSize,
1950
+ width: box.width + padding * 2,
1951
+ height: box.height + padding * 2 + titleSize
1952
+ };
1953
+ }
1507
1954
  function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
1508
1955
  const coordinated = [];
1509
1956
  const groupBoxes = /* @__PURE__ */ new Map();
@@ -1552,8 +1999,11 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
1552
1999
  }
1553
2000
  return coordinated;
1554
2001
  }
1555
- function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
2002
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
1556
2003
  const coordinated = [];
2004
+ const coordinatedNodeById = new Map(
2005
+ coordinatedNodes.map((node) => [node.id, node])
2006
+ );
1557
2007
  for (const edge of edges) {
1558
2008
  const source = nodes.get(edge.source.nodeId);
1559
2009
  const target = nodes.get(edge.target.nodeId);
@@ -1571,11 +2021,13 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
1571
2021
  });
1572
2022
  continue;
1573
2023
  }
2024
+ const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
2025
+ const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
1574
2026
  const route = routeEdge({
1575
2027
  kind: options.routeKind ?? "orthogonal",
1576
2028
  direction,
1577
- source,
1578
- target,
2029
+ source: portGeometry(source, sourcePort),
2030
+ target: portGeometry(target, targetPort),
1579
2031
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
1580
2032
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
1581
2033
  obstacles: obstacles.filter(
@@ -1595,6 +2047,21 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
1595
2047
  }
1596
2048
  return coordinated;
1597
2049
  }
2050
+ function portGeometry(nodeGeometry, port) {
2051
+ if (port === void 0) {
2052
+ return nodeGeometry;
2053
+ }
2054
+ return {
2055
+ ...nodeGeometry,
2056
+ box: port.box,
2057
+ center: port.anchor,
2058
+ anchors: nodeGeometry.anchors.map((anchor) => ({
2059
+ name: anchor.name,
2060
+ point: port.anchor
2061
+ })),
2062
+ obstacleBox: port.box
2063
+ };
2064
+ }
1598
2065
  function stableById(items) {
1599
2066
  return [...items].sort((a, b) => a.id.localeCompare(b.id));
1600
2067
  }
@@ -1624,34 +2091,34 @@ function assertFiniteNonNegative(value, label) {
1624
2091
  throw new TypeError(`${label} must be a finite non-negative width`);
1625
2092
  }
1626
2093
  }
1627
- function validateTextStyle(style) {
1628
- assertFinitePositive(style.fontSize, "fontSize");
1629
- if (style.lineHeight !== void 0) {
1630
- assertFinitePositive(style.lineHeight, "lineHeight");
2094
+ function validateTextStyle(style2) {
2095
+ assertFinitePositive(style2.fontSize, "fontSize");
2096
+ if (style2.lineHeight !== void 0) {
2097
+ assertFinitePositive(style2.lineHeight, "lineHeight");
1631
2098
  }
1632
- if (style.letterSpacing !== void 0 && !Number.isFinite(style.letterSpacing)) {
2099
+ if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
1633
2100
  throw new TypeError("letterSpacing must be finite");
1634
2101
  }
1635
2102
  }
1636
- function resolveLineHeight(style) {
1637
- validateTextStyle(style);
1638
- return style.lineHeight ?? style.fontSize * 1.2;
2103
+ function resolveLineHeight(style2) {
2104
+ validateTextStyle(style2);
2105
+ return style2.lineHeight ?? style2.fontSize * 1.2;
1639
2106
  }
1640
- function toCanvasFont(style) {
1641
- validateTextStyle(style);
1642
- const fontStyle = style.fontStyle === "italic" ? "italic " : "";
1643
- const fontWeight = style.fontWeight ?? 400;
1644
- return `${fontStyle}${fontWeight} ${style.fontSize}px ${style.fontFamily}`;
2107
+ function toCanvasFont(style2) {
2108
+ validateTextStyle(style2);
2109
+ const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
2110
+ const fontWeight = style2.fontWeight ?? 400;
2111
+ return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
1645
2112
  }
1646
2113
 
1647
2114
  // src/text/fallback.ts
1648
2115
  var DeterministicTextMeasurer = class {
1649
- prepare(text, style) {
1650
- validateTextStyle(style);
2116
+ prepare(text, style2) {
2117
+ validateTextStyle(style2);
1651
2118
  return {
1652
2119
  text,
1653
- font: toCanvasFont(style),
1654
- style: { ...style },
2120
+ font: toCanvasFont(style2),
2121
+ style: { ...style2 },
1655
2122
  backend: "deterministic"
1656
2123
  };
1657
2124
  }
@@ -1710,9 +2177,9 @@ var DeterministicTextMeasurer = class {
1710
2177
  return output;
1711
2178
  }
1712
2179
  };
1713
- function getCharacterWidth(style) {
1714
- const letterSpacing = style.letterSpacing ?? 0;
1715
- return Math.max(0, style.fontSize * 0.6 + letterSpacing);
2180
+ function getCharacterWidth(style2) {
2181
+ const letterSpacing = style2.letterSpacing ?? 0;
2182
+ return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
1716
2183
  }
1717
2184
  function createLine(text, width, segmentIndex, start, end) {
1718
2185
  return {
@@ -1733,6 +2200,108 @@ function assertFinitePositiveLineHeight(lineHeight) {
1733
2200
  throw new TypeError("lineHeight must be finite and positive");
1734
2201
  }
1735
2202
  }
2203
+ var require2 = createRequire(import.meta.url);
2204
+ function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
2205
+ if (typeof globalThis.OffscreenCanvas === "function") {
2206
+ return true;
2207
+ }
2208
+ try {
2209
+ const canvasModule = loadNodeCanvasModule();
2210
+ const { createCanvas } = canvasModule;
2211
+ const NodeOffscreenCanvas = class {
2212
+ canvas;
2213
+ constructor(width, height) {
2214
+ this.canvas = createCanvas(width, height);
2215
+ }
2216
+ getContext(contextId) {
2217
+ return contextId === "2d" ? this.canvas.getContext("2d") : null;
2218
+ }
2219
+ };
2220
+ globalThis.OffscreenCanvas = NodeOffscreenCanvas;
2221
+ return true;
2222
+ } catch {
2223
+ return false;
2224
+ }
2225
+ }
2226
+ function loadDefaultNodeCanvasModule() {
2227
+ return require2("@napi-rs/canvas");
2228
+ }
2229
+ var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
2230
+ function isPretextRuntimeAvailable() {
2231
+ return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
2232
+ }
2233
+ var PretextTextMeasurer = class {
2234
+ prepare(text, style2) {
2235
+ if (!isPretextRuntimeAvailable()) {
2236
+ throw new TypeError(RUNTIME_UNAVAILABLE);
2237
+ }
2238
+ validateTextStyle(style2);
2239
+ const font = toCanvasFont(style2);
2240
+ const options = {
2241
+ ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
2242
+ ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
2243
+ ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
2244
+ };
2245
+ const prepared = prepareWithSegments(text, font, options);
2246
+ return {
2247
+ text,
2248
+ font,
2249
+ style: { ...style2 },
2250
+ backend: "pretext",
2251
+ pretextPrepared: prepared
2252
+ };
2253
+ }
2254
+ layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
2255
+ assertFiniteNonNegative(maxWidth, "maxWidth");
2256
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2257
+ throw new TypeError("lineHeight must be finite and positive");
2258
+ }
2259
+ const result = layoutWithLines(
2260
+ toInternalPrepared(prepared),
2261
+ maxWidth,
2262
+ lineHeight
2263
+ );
2264
+ const width = result.lines.reduce(
2265
+ (current, line) => Math.max(current, line.width),
2266
+ 0
2267
+ );
2268
+ return {
2269
+ width,
2270
+ height: result.height,
2271
+ lineHeight,
2272
+ lineCount: result.lineCount,
2273
+ lines: result.lines.map((line) => ({
2274
+ text: line.text,
2275
+ width: line.width,
2276
+ start: {
2277
+ segmentIndex: line.start.segmentIndex,
2278
+ graphemeIndex: line.start.graphemeIndex
2279
+ },
2280
+ end: {
2281
+ segmentIndex: line.end.segmentIndex,
2282
+ graphemeIndex: line.end.graphemeIndex
2283
+ }
2284
+ })),
2285
+ diagnostics: []
2286
+ };
2287
+ }
2288
+ naturalWidth(prepared) {
2289
+ return measureNaturalWidth(toInternalPrepared(prepared));
2290
+ }
2291
+ };
2292
+ function toInternalPrepared(prepared) {
2293
+ if (prepared.backend !== "pretext" || !("pretextPrepared" in prepared)) {
2294
+ throw new TypeError("prepared text was not created by PretextTextMeasurer");
2295
+ }
2296
+ return prepared.pretextPrepared;
2297
+ }
2298
+
2299
+ // src/text/default.ts
2300
+ function createDefaultTextMeasurer(options = {}) {
2301
+ const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
2302
+ installRuntime();
2303
+ return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
2304
+ }
1736
2305
 
1737
2306
  // src/labels/fit.ts
1738
2307
  function fitLabel(text, options, measurer) {
@@ -1791,6 +2360,7 @@ function computeLabelLayout(text, options, measurer) {
1791
2360
  fittedSize,
1792
2361
  padding,
1793
2362
  font: { ...options.font },
2363
+ textBackend: prepared.backend,
1794
2364
  lineHeight,
1795
2365
  lines: buildLines(textLayout, contentBox2, lineHeight),
1796
2366
  overflow,
@@ -1885,8 +2455,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1885
2455
  ...outputResult(dsl)
1886
2456
  };
1887
2457
  }
1888
- const measurer = options.textMeasurer ?? new DeterministicTextMeasurer();
2458
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1889
2459
  const routeKind = dsl.routing?.kind ?? "orthogonal";
2460
+ const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1890
2461
  const diagram = {
1891
2462
  id: options.id ?? dsl.id ?? "diagram",
1892
2463
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1894,9 +2465,14 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1894
2465
  nodes: normalizeNodes(dsl, measurer),
1895
2466
  edges: normalizeEdges(dsl),
1896
2467
  groups: normalizeGroups(dsl, measurer),
2468
+ swimlanes: normalizeSwimlanes(dsl),
1897
2469
  constraints: normalizeConstraints(dsl),
1898
2470
  diagnostics: [],
1899
- metadata: { routeKind }
2471
+ ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
2472
+ metadata: {
2473
+ routeKind,
2474
+ ...portShifting === void 0 ? {} : { portShifting }
2475
+ }
1900
2476
  };
1901
2477
  return {
1902
2478
  diagram,
@@ -1904,6 +2480,15 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1904
2480
  ...outputResult(dsl)
1905
2481
  };
1906
2482
  }
2483
+ function normalizePortShifting(portShifting) {
2484
+ if (portShifting === void 0) {
2485
+ return void 0;
2486
+ }
2487
+ return {
2488
+ ...portShifting.enabled === void 0 ? {} : { enabled: portShifting.enabled },
2489
+ ...portShifting.spacing === void 0 ? {} : { spacing: portShifting.spacing }
2490
+ };
2491
+ }
1907
2492
  function outputResult(dsl) {
1908
2493
  return dsl.output?.format === void 0 ? {} : { output: { format: dsl.output.format } };
1909
2494
  }
@@ -1913,15 +2498,24 @@ function normalizeNodes(dsl, measurer) {
1913
2498
  const label = toLabel(node?.label);
1914
2499
  const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
1915
2500
  const fittedSize = labelLayout?.fittedSize;
2501
+ const nodeCompartments = node?.compartments === void 0 ? void 0 : compartments(node.compartments);
2502
+ const compartmentWidth = nodeCompartments === void 0 ? 0 : compartmentNaturalWidth(id, label, nodeCompartments, measurer);
1916
2503
  return {
1917
2504
  id,
1918
2505
  ...label === void 0 ? {} : { label },
1919
2506
  shape: node?.shape ?? "rectangle",
1920
2507
  ...node?.position === void 0 ? {} : { position: point(node.position) },
2508
+ ...node?.style === void 0 ? {} : { style: style(node.style) },
2509
+ ...node?.ports === void 0 ? {} : { ports: normalizePorts(node.ports) },
2510
+ ...nodeCompartments === void 0 ? {} : { compartments: nodeCompartments },
1921
2511
  size: {
1922
- width: Math.max(DEFAULT_NODE_MIN_SIZE.width, fittedSize?.width ?? 0),
2512
+ width: Math.max(
2513
+ DEFAULT_NODE_MIN_SIZE.width,
2514
+ fittedSize?.width ?? 0,
2515
+ compartmentWidth
2516
+ ),
1923
2517
  height: Math.max(
1924
- DEFAULT_NODE_MIN_SIZE.height,
2518
+ nodeCompartments === void 0 ? DEFAULT_NODE_MIN_SIZE.height : compartmentHeight(nodeCompartments),
1925
2519
  fittedSize?.height ?? 0
1926
2520
  )
1927
2521
  },
@@ -1930,11 +2524,42 @@ function normalizeNodes(dsl, measurer) {
1930
2524
  };
1931
2525
  });
1932
2526
  }
2527
+ function compartmentHeight(value) {
2528
+ const rowCount = (value.stereotype === void 0 ? 0 : 1) + 1 + (value.properties?.length ?? 0) + (value.constraints?.length ?? 0);
2529
+ const rowHeight = 16;
2530
+ const verticalPadding = 20;
2531
+ return Math.max(
2532
+ DEFAULT_NODE_MIN_SIZE.height,
2533
+ rowCount * rowHeight + verticalPadding
2534
+ );
2535
+ }
2536
+ function compartmentNaturalWidth(id, label, value, measurer) {
2537
+ const rows = compartmentRows(id, label, value);
2538
+ const maxRowWidth = rows.reduce((width, row) => {
2539
+ const prepared = measurer.prepare(row, DEFAULT_FONT);
2540
+ return Math.max(width, measurer.naturalWidth(prepared));
2541
+ }, 0);
2542
+ return Math.ceil(
2543
+ maxRowWidth + DEFAULT_NODE_PADDING.left + DEFAULT_NODE_PADDING.right
2544
+ );
2545
+ }
2546
+ function compartmentRows(id, label, value) {
2547
+ return [
2548
+ ...value.stereotype === void 0 ? [] : [value.stereotype],
2549
+ value.name ?? label?.text ?? id,
2550
+ ...value.properties ?? [],
2551
+ ...value.constraints ?? []
2552
+ ];
2553
+ }
1933
2554
  function normalizeEdges(dsl) {
1934
2555
  const counts = /* @__PURE__ */ new Map();
1935
2556
  return (dsl.edges ?? []).map((edge) => {
1936
- const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? edge.source ?? "";
1937
- const targetId = typeof edge === "string" ? "" : edge.targetId ?? edge.target ?? "";
2557
+ const source = typeof edge === "string" ? void 0 : edge.source;
2558
+ const target = typeof edge === "string" ? void 0 : edge.target;
2559
+ const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? endpointNodeId(source) ?? "";
2560
+ const targetId = typeof edge === "string" ? "" : edge.targetId ?? endpointNodeId(target) ?? "";
2561
+ const sourceEndpoint = typeof edge === "string" ? { nodeId: sourceId } : endpoint(source, edge.sourceId);
2562
+ const targetEndpoint = typeof edge === "string" ? { nodeId: targetId } : endpoint(target, edge.targetId);
1938
2563
  const baseId = `${sourceId}-${targetId}`;
1939
2564
  const count = counts.get(baseId) ?? 0;
1940
2565
  counts.set(baseId, count + 1);
@@ -1942,9 +2567,96 @@ function normalizeEdges(dsl) {
1942
2567
  const label = typeof edge === "string" ? void 0 : toLabel(edge.label);
1943
2568
  return {
1944
2569
  id,
1945
- source: { nodeId: sourceId },
1946
- target: { nodeId: targetId },
1947
- ...label === void 0 ? {} : { label }
2570
+ source: sourceEndpoint,
2571
+ target: targetEndpoint,
2572
+ ...label === void 0 ? {} : { label },
2573
+ ...typeof edge === "string" || edge.style === void 0 ? {} : { style: edge.style },
2574
+ ...typeof edge === "string" || edge.arrowhead === void 0 ? {} : { arrowhead: edge.arrowhead }
2575
+ };
2576
+ });
2577
+ }
2578
+ function normalizePorts(ports) {
2579
+ return Object.keys(ports ?? {}).sort().map((id) => {
2580
+ const port = ports?.[id];
2581
+ const label = toLabel(port?.label);
2582
+ return {
2583
+ id,
2584
+ ...label === void 0 ? {} : { label },
2585
+ side: port?.side ?? "right",
2586
+ kind: port?.kind ?? "proxy",
2587
+ ...port?.order === void 0 ? {} : { order: port.order },
2588
+ ...port?.style === void 0 ? {} : { style: style(port.style) }
2589
+ };
2590
+ });
2591
+ }
2592
+ function endpoint(value, nodeIdOverride) {
2593
+ if (nodeIdOverride !== void 0) {
2594
+ return {
2595
+ nodeId: nodeIdOverride,
2596
+ ...typeof value === "object" && value.node === nodeIdOverride && value.port !== void 0 ? { portId: value.port } : {}
2597
+ };
2598
+ }
2599
+ if (value === void 0) {
2600
+ return { nodeId: "" };
2601
+ }
2602
+ if (typeof value === "string") {
2603
+ return { nodeId: value };
2604
+ }
2605
+ return {
2606
+ nodeId: value.node,
2607
+ ...value.port === void 0 ? {} : { portId: value.port }
2608
+ };
2609
+ }
2610
+ function style(value) {
2611
+ return {
2612
+ ...value.fill === void 0 ? {} : { fill: value.fill },
2613
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
2614
+ };
2615
+ }
2616
+ function compartments(value) {
2617
+ return {
2618
+ ...value.stereotype === void 0 ? {} : { stereotype: value.stereotype },
2619
+ ...value.name === void 0 ? {} : { name: value.name },
2620
+ ...value.properties === void 0 ? {} : { properties: value.properties.map(formatCompartmentEntry) },
2621
+ ...value.constraints === void 0 ? {} : { constraints: [...value.constraints] }
2622
+ };
2623
+ }
2624
+ function normalizeFrame(frame) {
2625
+ return {
2626
+ kind: frame.kind,
2627
+ ...frame.context === void 0 ? {} : { context: frame.context },
2628
+ ...frame.name === void 0 ? {} : { name: frame.name },
2629
+ titleTab: frame.titleTab,
2630
+ ...frame.style === void 0 ? {} : { style: style(frame.style) }
2631
+ };
2632
+ }
2633
+ function formatCompartmentEntry(value) {
2634
+ if (typeof value === "string") {
2635
+ return value;
2636
+ }
2637
+ const [entry] = Object.entries(value);
2638
+ if (entry === void 0) {
2639
+ return "";
2640
+ }
2641
+ return `${entry[0]}: ${entry[1]}`;
2642
+ }
2643
+ function normalizeSwimlanes(dsl) {
2644
+ return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
2645
+ const swimlane = dsl.swimlanes?.[id];
2646
+ const label = toLabel(swimlane?.label);
2647
+ return {
2648
+ id,
2649
+ ...label === void 0 ? {} : { label },
2650
+ orientation: swimlane?.orientation ?? "vertical",
2651
+ lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
2652
+ const lane = swimlane?.lanes[laneId];
2653
+ const laneLabel = toLabel(lane?.label);
2654
+ return {
2655
+ id: laneId,
2656
+ ...laneLabel === void 0 ? {} : { label: laneLabel },
2657
+ children: [...lane?.children ?? []]
2658
+ };
2659
+ })
1948
2660
  };
1949
2661
  });
1950
2662
  }
@@ -2018,14 +2730,28 @@ function validateReferences(dsl) {
2018
2730
  if (typeof edge === "string") {
2019
2731
  return;
2020
2732
  }
2021
- const sourceId = edge.sourceId ?? edge.source;
2022
- const targetId = edge.targetId ?? edge.target;
2733
+ const sourceId = edge.sourceId ?? endpointNodeId(edge.source);
2734
+ const targetId = edge.targetId ?? endpointNodeId(edge.target);
2735
+ const sourceEndpoint = endpoint(edge.source, edge.sourceId);
2736
+ const targetEndpoint = endpoint(edge.target, edge.targetId);
2023
2737
  if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
2024
2738
  diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
2025
2739
  }
2026
2740
  if (targetId !== void 0 && !nodeIds.has(targetId)) {
2027
2741
  diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
2028
2742
  }
2743
+ validateEndpointPort(
2744
+ dsl,
2745
+ sourceEndpoint,
2746
+ ["edges", index, "source"],
2747
+ diagnostics
2748
+ );
2749
+ validateEndpointPort(
2750
+ dsl,
2751
+ targetEndpoint,
2752
+ ["edges", index, "target"],
2753
+ diagnostics
2754
+ );
2029
2755
  });
2030
2756
  for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
2031
2757
  (group.nodes ?? []).forEach((nodeId, index) => {
@@ -2043,6 +2769,27 @@ function validateReferences(dsl) {
2043
2769
  }
2044
2770
  });
2045
2771
  }
2772
+ for (const [swimlaneId, swimlane] of Object.entries(dsl.swimlanes ?? {})) {
2773
+ for (const [laneId, lane] of Object.entries(swimlane.lanes)) {
2774
+ (lane.children ?? []).forEach((child, childIndex) => {
2775
+ if (!nodeIds.has(child)) {
2776
+ diagnostics.push(
2777
+ referenceMissing(
2778
+ [
2779
+ "swimlanes",
2780
+ swimlaneId,
2781
+ "lanes",
2782
+ laneId,
2783
+ "children",
2784
+ childIndex
2785
+ ],
2786
+ child
2787
+ )
2788
+ );
2789
+ }
2790
+ });
2791
+ }
2792
+ }
2046
2793
  (dsl.constraints ?? []).forEach((constraint, index) => {
2047
2794
  switch (constraint.kind) {
2048
2795
  case "exact-position": {
@@ -2086,10 +2833,12 @@ function validateReferences(dsl) {
2086
2833
  break;
2087
2834
  case "containment": {
2088
2835
  const container = constraint.containerId ?? constraint.container;
2089
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
2090
- diagnostics.push(
2091
- referenceMissing(["constraints", index, "container"], container)
2092
- );
2836
+ if (container !== void 0) {
2837
+ if (!nodeIds.has(container)) {
2838
+ diagnostics.push(
2839
+ referenceMissing(["constraints", index, "container"], container)
2840
+ );
2841
+ }
2093
2842
  }
2094
2843
  (constraint.childIds ?? constraint.children ?? []).forEach(
2095
2844
  (child, childIndex) => {
@@ -2119,8 +2868,23 @@ function referenceMissing(path, id) {
2119
2868
  hint: "Define the referenced node or group id, or update this reference."
2120
2869
  };
2121
2870
  }
2122
- function hasNodeOrGroup(id, nodeIds, groupIds) {
2123
- return nodeIds.has(id) || groupIds.has(id);
2871
+ function hasNodeOrGroup(id, nodeIds, groupIds, swimlaneLaneIds = /* @__PURE__ */ new Set()) {
2872
+ return nodeIds.has(id) || groupIds.has(id) || swimlaneLaneIds.has(id);
2873
+ }
2874
+ function endpointNodeId(endpointValue) {
2875
+ if (typeof endpointValue === "string" || endpointValue === void 0) {
2876
+ return endpointValue;
2877
+ }
2878
+ return endpointValue.node;
2879
+ }
2880
+ function validateEndpointPort(dsl, endpointValue, path, diagnostics) {
2881
+ if (endpointValue.portId === void 0) {
2882
+ return;
2883
+ }
2884
+ const node = dsl.nodes[endpointValue.nodeId];
2885
+ if (node !== void 0 && node.ports?.[endpointValue.portId] === void 0) {
2886
+ diagnostics.push(referenceMissing([...path, "port"], endpointValue.portId));
2887
+ }
2124
2888
  }
2125
2889
  function toLabel(value) {
2126
2890
  if (value === void 0) {
@@ -2190,6 +2954,8 @@ function isValidEdgeId(value) {
2190
2954
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
2191
2955
  var routeKindSchema = z.enum(["orthogonal", "straight"]);
2192
2956
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
2957
+ var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
2958
+ var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
2193
2959
  var nodeShapeSchema = z.enum([
2194
2960
  "rectangle",
2195
2961
  "rounded-rectangle",
@@ -2217,18 +2983,49 @@ var labelSchema = z.union([
2217
2983
  maxWidth: finiteNumberSchema.optional()
2218
2984
  })
2219
2985
  ]);
2986
+ var styleSchema = z.object({
2987
+ fill: z.string().optional(),
2988
+ stroke: z.string().optional()
2989
+ });
2990
+ var portSideSchema = z.enum(["top", "right", "bottom", "left"]);
2991
+ var portKindSchema = z.enum(["proxy", "flow"]);
2992
+ var portSchema = z.object({
2993
+ label: labelSchema.optional(),
2994
+ side: portSideSchema,
2995
+ kind: portKindSchema.optional(),
2996
+ order: finiteNumberSchema.optional(),
2997
+ style: styleSchema.optional()
2998
+ });
2999
+ var compartmentsSchema = z.object({
3000
+ stereotype: z.string().optional(),
3001
+ name: z.string().optional(),
3002
+ properties: z.array(z.record(z.string(), z.string()).or(z.string())).optional(),
3003
+ constraints: z.array(z.string()).optional()
3004
+ });
2220
3005
  var nodeSchema = z.object({
2221
3006
  label: labelSchema.optional(),
2222
3007
  shape: nodeShapeSchema.optional(),
2223
- position: pointSchema.optional()
3008
+ position: pointSchema.optional(),
3009
+ style: styleSchema.optional(),
3010
+ ports: z.record(z.string(), portSchema).optional(),
3011
+ compartments: compartmentsSchema.optional()
2224
3012
  });
3013
+ var endpointSchema = z.union([
3014
+ z.string(),
3015
+ z.object({
3016
+ node: z.string(),
3017
+ port: z.string().optional()
3018
+ })
3019
+ ]);
2225
3020
  var structuredEdgeSchema = z.object({
2226
3021
  id: z.string().optional(),
2227
- source: z.string().optional(),
2228
- target: z.string().optional(),
3022
+ source: endpointSchema.optional(),
3023
+ target: endpointSchema.optional(),
2229
3024
  sourceId: z.string().optional(),
2230
3025
  targetId: z.string().optional(),
2231
- label: labelSchema.optional()
3026
+ label: labelSchema.optional(),
3027
+ style: edgeStrokeStyleSchema.optional(),
3028
+ arrowhead: edgeArrowheadSchema.optional()
2232
3029
  }).superRefine((edge, context) => {
2233
3030
  if (edge.source === void 0 && edge.sourceId === void 0) {
2234
3031
  context.addIssue({
@@ -2252,6 +3049,17 @@ var groupSchema = z.object({
2252
3049
  groups: z.array(z.string()).optional(),
2253
3050
  padding: insetsSchema.optional()
2254
3051
  });
3052
+ var swimlaneSchema = z.object({
3053
+ label: labelSchema.optional(),
3054
+ orientation: z.enum(["vertical", "horizontal"]).optional(),
3055
+ lanes: z.record(
3056
+ z.string(),
3057
+ z.object({
3058
+ label: labelSchema.optional(),
3059
+ children: z.array(z.string()).optional()
3060
+ })
3061
+ )
3062
+ });
2255
3063
  var exactPositionConstraintSchema = z.object({
2256
3064
  kind: z.literal("exact-position"),
2257
3065
  target: z.string().optional(),
@@ -2312,12 +3120,24 @@ var diagramDslSchema = z.object({
2312
3120
  direction: directionSchema.optional()
2313
3121
  }).optional(),
2314
3122
  routing: z.object({
2315
- kind: routeKindSchema.optional()
3123
+ kind: routeKindSchema.optional(),
3124
+ portShifting: z.object({
3125
+ enabled: z.boolean().optional(),
3126
+ spacing: finiteNumberSchema.optional()
3127
+ }).optional()
2316
3128
  }).optional(),
2317
3129
  nodes: z.record(z.string(), nodeSchema),
2318
3130
  edges: z.array(edgeSchema).optional(),
2319
3131
  groups: z.record(z.string(), groupSchema).optional(),
3132
+ swimlanes: z.record(z.string(), swimlaneSchema).optional(),
2320
3133
  constraints: z.array(constraintSchema).optional(),
3134
+ frame: z.object({
3135
+ kind: z.string(),
3136
+ context: z.string().optional(),
3137
+ name: z.string().optional(),
3138
+ titleTab: z.string(),
3139
+ style: styleSchema.optional()
3140
+ }).optional(),
2321
3141
  output: z.object({
2322
3142
  format: outputFormatSchema.optional()
2323
3143
  }).optional()
@@ -2514,7 +3334,8 @@ function renderDiagramDsl(source, options = {}) {
2514
3334
  return { diagnostics };
2515
3335
  }
2516
3336
  const solved = solveDiagram(normalized.diagram, {
2517
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
3337
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3338
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
2518
3339
  });
2519
3340
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
2520
3341
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -2553,6 +3374,22 @@ function renderDiagramDsl(source, options = {}) {
2553
3374
  function toSolveDiagnostic(diagnostic) {
2554
3375
  return { ...diagnostic, layer: "solve" };
2555
3376
  }
3377
+ function solvePortShiftingOption(value) {
3378
+ if (!isJsonObject(value)) {
3379
+ return {};
3380
+ }
3381
+ const portShifting = {};
3382
+ if (value.enabled === false) {
3383
+ portShifting.enabled = false;
3384
+ }
3385
+ if (typeof value.spacing === "number") {
3386
+ portShifting.spacing = value.spacing;
3387
+ }
3388
+ return { portShifting };
3389
+ }
3390
+ function isJsonObject(value) {
3391
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3392
+ }
2556
3393
  function toExportDiagnostic(diagnostic) {
2557
3394
  return { ...diagnostic, layer: "export" };
2558
3395
  }