@crazyhappyone/auto-graph 0.0.1 → 0.0.2

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 = isAbsoluteLabelLayout(labelLayout.box, box) ? { x: 0, y: 0 } : { 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,91 @@ 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];
605
+ }
606
+ function isAbsoluteLabelLayout(labelBox, itemBox) {
607
+ return labelBox.x >= itemBox.x && labelBox.y >= itemBox.y && labelBox.x + labelBox.width <= itemBox.x + itemBox.width && labelBox.y + labelBox.height <= itemBox.y + itemBox.height;
413
608
  }
414
609
  function shapePoints(shape, box) {
415
610
  const left = box.x;
@@ -1294,20 +1489,33 @@ function isValidDimension(value) {
1294
1489
  // src/routing/routes.ts
1295
1490
  function routeEdge(input) {
1296
1491
  const diagnostics = [];
1492
+ const defaultAnchors = defaultAnchorsForGeometry(
1493
+ input.source.box,
1494
+ input.target.box,
1495
+ input.direction
1496
+ );
1297
1497
  const source = getEdgePort(
1298
1498
  input.source,
1299
1499
  input.target.center,
1300
- input.sourceAnchor
1500
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
1301
1501
  );
1302
1502
  const target = getEdgePort(
1303
1503
  input.target,
1304
1504
  input.source.center,
1305
- input.targetAnchor
1505
+ input.targetAnchor ?? defaultAnchors.targetAnchor
1306
1506
  );
1307
1507
  if ((input.kind ?? "orthogonal") === "straight") {
1308
1508
  return { points: simplifyRoute([source, target]), diagnostics };
1309
1509
  }
1310
1510
  const candidates = orthogonalCandidates(source, target, input.direction);
1511
+ candidates.push(
1512
+ ...expandedObstacleCandidates(
1513
+ source,
1514
+ target,
1515
+ input.direction,
1516
+ input.obstacles ?? []
1517
+ )
1518
+ );
1311
1519
  for (const candidate of candidates) {
1312
1520
  if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
1313
1521
  return { points: simplifyRoute(candidate), diagnostics };
@@ -1346,27 +1554,113 @@ function simplifyRoute(points) {
1346
1554
  function orthogonalCandidates(source, target, direction) {
1347
1555
  const midpointX = (source.x + target.x) / 2;
1348
1556
  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
- ];
1557
+ const candidates = [];
1353
1558
  if (direction === "TB" || direction === "BT") {
1354
1559
  candidates.push([
1355
1560
  source,
1356
- { x: midpointX, y: source.y },
1357
- { x: midpointX, y: target.y },
1561
+ { x: source.x, y: midpointY },
1562
+ { x: target.x, y: midpointY },
1358
1563
  target
1359
1564
  ]);
1360
1565
  } else {
1361
1566
  candidates.push([
1362
1567
  source,
1363
- { x: source.x, y: midpointY },
1364
- { x: target.x, y: midpointY },
1568
+ { x: midpointX, y: source.y },
1569
+ { x: midpointX, y: target.y },
1365
1570
  target
1366
1571
  ]);
1367
1572
  }
1573
+ candidates.push(
1574
+ [source, { x: target.x, y: source.y }, target],
1575
+ [source, { x: source.x, y: target.y }, target]
1576
+ );
1577
+ return candidates;
1578
+ }
1579
+ function defaultSourceAnchor(direction) {
1580
+ switch (direction) {
1581
+ case "LR":
1582
+ return "right";
1583
+ case "RL":
1584
+ return "left";
1585
+ case "TB":
1586
+ return "bottom";
1587
+ case "BT":
1588
+ return "top";
1589
+ }
1590
+ }
1591
+ function defaultAnchorsForGeometry(source, target, direction) {
1592
+ const dx = target.x + target.width / 2 - (source.x + source.width / 2);
1593
+ const dy = target.y + target.height / 2 - (source.y + source.height / 2);
1594
+ if (Math.abs(dy) > Math.abs(dx)) {
1595
+ return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
1596
+ }
1597
+ if (Math.abs(dx) > 0) {
1598
+ return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
1599
+ }
1600
+ return {
1601
+ sourceAnchor: defaultSourceAnchor(direction),
1602
+ targetAnchor: defaultTargetAnchor(direction)
1603
+ };
1604
+ }
1605
+ function defaultTargetAnchor(direction) {
1606
+ switch (direction) {
1607
+ case "LR":
1608
+ return "left";
1609
+ case "RL":
1610
+ return "right";
1611
+ case "TB":
1612
+ return "top";
1613
+ case "BT":
1614
+ return "bottom";
1615
+ }
1616
+ }
1617
+ function expandedObstacleCandidates(source, target, direction, obstacles) {
1618
+ if (obstacles.length === 0) {
1619
+ return [];
1620
+ }
1621
+ const margin = 16;
1622
+ const candidates = [];
1623
+ if (direction === "TB" || direction === "BT") {
1624
+ const lanes = sortedUniqueLanes(
1625
+ obstacles.flatMap((obstacle) => [
1626
+ obstacle.x - margin,
1627
+ obstacle.x + obstacle.width + margin
1628
+ ]),
1629
+ (source.x + target.x) / 2
1630
+ );
1631
+ for (const laneX of lanes) {
1632
+ candidates.push([
1633
+ source,
1634
+ { x: laneX, y: source.y },
1635
+ { x: laneX, y: target.y },
1636
+ target
1637
+ ]);
1638
+ }
1639
+ } else {
1640
+ const lanes = sortedUniqueLanes(
1641
+ obstacles.flatMap((obstacle) => [
1642
+ obstacle.y - margin,
1643
+ obstacle.y + obstacle.height + margin
1644
+ ]),
1645
+ (source.y + target.y) / 2
1646
+ );
1647
+ for (const laneY of lanes) {
1648
+ candidates.push([
1649
+ source,
1650
+ { x: source.x, y: laneY },
1651
+ { x: target.x, y: laneY },
1652
+ target
1653
+ ]);
1654
+ }
1655
+ }
1368
1656
  return candidates;
1369
1657
  }
1658
+ function sortedUniqueLanes(lanes, midpoint) {
1659
+ return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
1660
+ const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
1661
+ return distance === 0 ? left - right : distance;
1662
+ });
1663
+ }
1370
1664
  function routeIntersectsObstacles(points, obstacles) {
1371
1665
  for (let index = 0; index < points.length - 1; index += 1) {
1372
1666
  const a = points[index];
@@ -1445,12 +1739,17 @@ function solveDiagram(diagram, options = {}) {
1445
1739
  options,
1446
1740
  diagnostics
1447
1741
  );
1742
+ const coordinatedSwimlanes = coordinateSwimlanes(
1743
+ diagram.swimlanes ?? [],
1744
+ constrained.boxes
1745
+ );
1448
1746
  const groupBoxes = new Map(
1449
1747
  coordinatedGroups.map((group) => [group.id, group.box])
1450
1748
  );
1451
1749
  const coordinatedEdges = coordinateEdges(
1452
1750
  edges,
1453
1751
  nodeGeometryById,
1752
+ coordinatedNodes,
1454
1753
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
1455
1754
  diagram.direction,
1456
1755
  options,
@@ -1458,8 +1757,18 @@ function solveDiagram(diagram, options = {}) {
1458
1757
  );
1459
1758
  const allBoxes = [
1460
1759
  ...coordinatedNodes.map((node) => node.box),
1461
- ...groupBoxes.values()
1760
+ ...coordinatedNodes.flatMap(
1761
+ (node) => (node.ports ?? []).flatMap(
1762
+ (port) => port.label === void 0 ? [port.box] : [port.box, portLabelBox(port)]
1763
+ )
1764
+ ),
1765
+ ...groupBoxes.values(),
1766
+ ...coordinatedSwimlanes.flatMap(
1767
+ (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
1768
+ )
1462
1769
  ];
1770
+ const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
1771
+ const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
1463
1772
  return {
1464
1773
  id: diagram.id,
1465
1774
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -1467,8 +1776,10 @@ function solveDiagram(diagram, options = {}) {
1467
1776
  nodes: coordinatedNodes,
1468
1777
  edges: coordinatedEdges,
1469
1778
  groups: coordinatedGroups,
1779
+ ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
1470
1780
  diagnostics,
1471
- bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
1781
+ bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
1782
+ ...frame === void 0 ? {} : { frame },
1472
1783
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
1473
1784
  };
1474
1785
  }
@@ -1494,6 +1805,9 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
1494
1805
  coordinated.push({
1495
1806
  id: node.id,
1496
1807
  ...node.label === void 0 ? {} : { label: node.label },
1808
+ ...node.style === void 0 ? {} : { style: node.style },
1809
+ ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
1810
+ ...node.compartments === void 0 ? {} : { compartments: node.compartments },
1497
1811
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
1498
1812
  shape: node.shape,
1499
1813
  ...node.metadata === void 0 ? {} : { metadata: node.metadata },
@@ -1504,6 +1818,142 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
1504
1818
  }
1505
1819
  return coordinated;
1506
1820
  }
1821
+ function coordinatePorts(node, nodeBox, portShifting) {
1822
+ const portsBySide = /* @__PURE__ */ new Map();
1823
+ for (const port of node.ports ?? []) {
1824
+ const ports = portsBySide.get(port.side) ?? [];
1825
+ ports.push(port);
1826
+ portsBySide.set(port.side, ports);
1827
+ }
1828
+ const coordinated = [];
1829
+ for (const [side, ports] of portsBySide) {
1830
+ const sorted = [...ports ?? []].sort((a, b) => {
1831
+ const order = (a.order ?? 0) - (b.order ?? 0);
1832
+ return order === 0 ? a.id.localeCompare(b.id) : order;
1833
+ });
1834
+ for (let index = 0; index < sorted.length; index += 1) {
1835
+ const port = sorted[index];
1836
+ if (port === void 0) {
1837
+ continue;
1838
+ }
1839
+ const anchor = portAnchor(
1840
+ nodeBox,
1841
+ side,
1842
+ index,
1843
+ sorted.length,
1844
+ portShifting
1845
+ );
1846
+ const box = portBox(anchor);
1847
+ coordinated.push({ ...port, box, anchor });
1848
+ }
1849
+ }
1850
+ return coordinated.sort((a, b) => a.id.localeCompare(b.id));
1851
+ }
1852
+ function portAnchor(nodeBox, side, index, count, portShifting) {
1853
+ const shiftingEnabled = portShifting?.enabled ?? true;
1854
+ const spacing = portShifting?.spacing ?? 24;
1855
+ const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
1856
+ switch (side) {
1857
+ case "left":
1858
+ return {
1859
+ x: nodeBox.x,
1860
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
1861
+ };
1862
+ case "right":
1863
+ return {
1864
+ x: nodeBox.x + nodeBox.width,
1865
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
1866
+ };
1867
+ case "top":
1868
+ return {
1869
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
1870
+ y: nodeBox.y
1871
+ };
1872
+ case "bottom":
1873
+ return {
1874
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
1875
+ y: nodeBox.y + nodeBox.height
1876
+ };
1877
+ }
1878
+ }
1879
+ function portBox(anchor) {
1880
+ const size = 10;
1881
+ return {
1882
+ x: anchor.x - size / 2,
1883
+ y: anchor.y - size / 2,
1884
+ width: size,
1885
+ height: size
1886
+ };
1887
+ }
1888
+ function portLabelBox(port) {
1889
+ const textWidth = Math.max(0, (port.label?.text.length ?? 0) * 6);
1890
+ const height = 12;
1891
+ const gap = 8;
1892
+ const x = port.side === "left" ? port.anchor.x - gap - textWidth : port.anchor.x + gap;
1893
+ return {
1894
+ x,
1895
+ y: port.anchor.y - 8 - height,
1896
+ width: textWidth,
1897
+ height
1898
+ };
1899
+ }
1900
+ function coordinateSwimlanes(swimlanes, nodeBoxes) {
1901
+ const titleSize = 28;
1902
+ const padding = 16;
1903
+ return swimlanes.map((swimlane) => {
1904
+ const laneBoxes = swimlane.lanes.flatMap((lane) => {
1905
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
1906
+ return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
1907
+ });
1908
+ const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
1909
+ const outer = expand(laneUnion, padding, titleSize);
1910
+ const laneCount = Math.max(1, swimlane.lanes.length);
1911
+ const lanes = swimlane.lanes.map((lane, index) => {
1912
+ const box = swimlane.orientation === "vertical" ? {
1913
+ x: outer.x + outer.width / laneCount * index,
1914
+ y: outer.y,
1915
+ width: outer.width / laneCount,
1916
+ height: outer.height
1917
+ } : {
1918
+ x: outer.x,
1919
+ y: outer.y + outer.height / laneCount * index,
1920
+ width: outer.width,
1921
+ height: outer.height / laneCount
1922
+ };
1923
+ return { ...lane, box };
1924
+ });
1925
+ return { ...swimlane, lanes, box: outer };
1926
+ });
1927
+ }
1928
+ function coordinateFrame(frame, contentBounds) {
1929
+ const padding = 32;
1930
+ const titleHeight = 28;
1931
+ const titleWidth = Math.max(180, frame.titleTab.length * 7);
1932
+ const box = {
1933
+ x: contentBounds.x - padding,
1934
+ y: contentBounds.y - padding - titleHeight,
1935
+ width: contentBounds.width + padding * 2,
1936
+ height: contentBounds.height + padding * 2 + titleHeight
1937
+ };
1938
+ return {
1939
+ ...frame,
1940
+ box,
1941
+ titleBox: {
1942
+ x: box.x,
1943
+ y: box.y,
1944
+ width: Math.min(titleWidth, box.width * 0.8),
1945
+ height: titleHeight
1946
+ }
1947
+ };
1948
+ }
1949
+ function expand(box, padding, titleSize) {
1950
+ return {
1951
+ x: box.x - padding,
1952
+ y: box.y - padding - titleSize,
1953
+ width: box.width + padding * 2,
1954
+ height: box.height + padding * 2 + titleSize
1955
+ };
1956
+ }
1507
1957
  function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
1508
1958
  const coordinated = [];
1509
1959
  const groupBoxes = /* @__PURE__ */ new Map();
@@ -1552,8 +2002,11 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
1552
2002
  }
1553
2003
  return coordinated;
1554
2004
  }
1555
- function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
2005
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
1556
2006
  const coordinated = [];
2007
+ const coordinatedNodeById = new Map(
2008
+ coordinatedNodes.map((node) => [node.id, node])
2009
+ );
1557
2010
  for (const edge of edges) {
1558
2011
  const source = nodes.get(edge.source.nodeId);
1559
2012
  const target = nodes.get(edge.target.nodeId);
@@ -1571,11 +2024,13 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
1571
2024
  });
1572
2025
  continue;
1573
2026
  }
2027
+ const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
2028
+ const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
1574
2029
  const route = routeEdge({
1575
2030
  kind: options.routeKind ?? "orthogonal",
1576
2031
  direction,
1577
- source,
1578
- target,
2032
+ source: portGeometry(source, sourcePort),
2033
+ target: portGeometry(target, targetPort),
1579
2034
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
1580
2035
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
1581
2036
  obstacles: obstacles.filter(
@@ -1595,6 +2050,21 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
1595
2050
  }
1596
2051
  return coordinated;
1597
2052
  }
2053
+ function portGeometry(nodeGeometry, port) {
2054
+ if (port === void 0) {
2055
+ return nodeGeometry;
2056
+ }
2057
+ return {
2058
+ ...nodeGeometry,
2059
+ box: port.box,
2060
+ center: port.anchor,
2061
+ anchors: nodeGeometry.anchors.map((anchor) => ({
2062
+ name: anchor.name,
2063
+ point: port.anchor
2064
+ })),
2065
+ obstacleBox: port.box
2066
+ };
2067
+ }
1598
2068
  function stableById(items) {
1599
2069
  return [...items].sort((a, b) => a.id.localeCompare(b.id));
1600
2070
  }
@@ -1624,34 +2094,34 @@ function assertFiniteNonNegative(value, label) {
1624
2094
  throw new TypeError(`${label} must be a finite non-negative width`);
1625
2095
  }
1626
2096
  }
1627
- function validateTextStyle(style) {
1628
- assertFinitePositive(style.fontSize, "fontSize");
1629
- if (style.lineHeight !== void 0) {
1630
- assertFinitePositive(style.lineHeight, "lineHeight");
2097
+ function validateTextStyle(style2) {
2098
+ assertFinitePositive(style2.fontSize, "fontSize");
2099
+ if (style2.lineHeight !== void 0) {
2100
+ assertFinitePositive(style2.lineHeight, "lineHeight");
1631
2101
  }
1632
- if (style.letterSpacing !== void 0 && !Number.isFinite(style.letterSpacing)) {
2102
+ if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
1633
2103
  throw new TypeError("letterSpacing must be finite");
1634
2104
  }
1635
2105
  }
1636
- function resolveLineHeight(style) {
1637
- validateTextStyle(style);
1638
- return style.lineHeight ?? style.fontSize * 1.2;
2106
+ function resolveLineHeight(style2) {
2107
+ validateTextStyle(style2);
2108
+ return style2.lineHeight ?? style2.fontSize * 1.2;
1639
2109
  }
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}`;
2110
+ function toCanvasFont(style2) {
2111
+ validateTextStyle(style2);
2112
+ const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
2113
+ const fontWeight = style2.fontWeight ?? 400;
2114
+ return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
1645
2115
  }
1646
2116
 
1647
2117
  // src/text/fallback.ts
1648
2118
  var DeterministicTextMeasurer = class {
1649
- prepare(text, style) {
1650
- validateTextStyle(style);
2119
+ prepare(text, style2) {
2120
+ validateTextStyle(style2);
1651
2121
  return {
1652
2122
  text,
1653
- font: toCanvasFont(style),
1654
- style: { ...style },
2123
+ font: toCanvasFont(style2),
2124
+ style: { ...style2 },
1655
2125
  backend: "deterministic"
1656
2126
  };
1657
2127
  }
@@ -1710,9 +2180,9 @@ var DeterministicTextMeasurer = class {
1710
2180
  return output;
1711
2181
  }
1712
2182
  };
1713
- function getCharacterWidth(style) {
1714
- const letterSpacing = style.letterSpacing ?? 0;
1715
- return Math.max(0, style.fontSize * 0.6 + letterSpacing);
2183
+ function getCharacterWidth(style2) {
2184
+ const letterSpacing = style2.letterSpacing ?? 0;
2185
+ return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
1716
2186
  }
1717
2187
  function createLine(text, width, segmentIndex, start, end) {
1718
2188
  return {
@@ -1733,6 +2203,108 @@ function assertFinitePositiveLineHeight(lineHeight) {
1733
2203
  throw new TypeError("lineHeight must be finite and positive");
1734
2204
  }
1735
2205
  }
2206
+ var require2 = createRequire(import.meta.url);
2207
+ function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
2208
+ if (typeof globalThis.OffscreenCanvas === "function") {
2209
+ return true;
2210
+ }
2211
+ try {
2212
+ const canvasModule = loadNodeCanvasModule();
2213
+ const { createCanvas } = canvasModule;
2214
+ const NodeOffscreenCanvas = class {
2215
+ canvas;
2216
+ constructor(width, height) {
2217
+ this.canvas = createCanvas(width, height);
2218
+ }
2219
+ getContext(contextId) {
2220
+ return contextId === "2d" ? this.canvas.getContext("2d") : null;
2221
+ }
2222
+ };
2223
+ globalThis.OffscreenCanvas = NodeOffscreenCanvas;
2224
+ return true;
2225
+ } catch {
2226
+ return false;
2227
+ }
2228
+ }
2229
+ function loadDefaultNodeCanvasModule() {
2230
+ return require2("@napi-rs/canvas");
2231
+ }
2232
+ var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
2233
+ function isPretextRuntimeAvailable() {
2234
+ return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
2235
+ }
2236
+ var PretextTextMeasurer = class {
2237
+ prepare(text, style2) {
2238
+ if (!isPretextRuntimeAvailable()) {
2239
+ throw new TypeError(RUNTIME_UNAVAILABLE);
2240
+ }
2241
+ validateTextStyle(style2);
2242
+ const font = toCanvasFont(style2);
2243
+ const options = {
2244
+ ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
2245
+ ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
2246
+ ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
2247
+ };
2248
+ const prepared = prepareWithSegments(text, font, options);
2249
+ return {
2250
+ text,
2251
+ font,
2252
+ style: { ...style2 },
2253
+ backend: "pretext",
2254
+ pretextPrepared: prepared
2255
+ };
2256
+ }
2257
+ layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
2258
+ assertFiniteNonNegative(maxWidth, "maxWidth");
2259
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2260
+ throw new TypeError("lineHeight must be finite and positive");
2261
+ }
2262
+ const result = layoutWithLines(
2263
+ toInternalPrepared(prepared),
2264
+ maxWidth,
2265
+ lineHeight
2266
+ );
2267
+ const width = result.lines.reduce(
2268
+ (current, line) => Math.max(current, line.width),
2269
+ 0
2270
+ );
2271
+ return {
2272
+ width,
2273
+ height: result.height,
2274
+ lineHeight,
2275
+ lineCount: result.lineCount,
2276
+ lines: result.lines.map((line) => ({
2277
+ text: line.text,
2278
+ width: line.width,
2279
+ start: {
2280
+ segmentIndex: line.start.segmentIndex,
2281
+ graphemeIndex: line.start.graphemeIndex
2282
+ },
2283
+ end: {
2284
+ segmentIndex: line.end.segmentIndex,
2285
+ graphemeIndex: line.end.graphemeIndex
2286
+ }
2287
+ })),
2288
+ diagnostics: []
2289
+ };
2290
+ }
2291
+ naturalWidth(prepared) {
2292
+ return measureNaturalWidth(toInternalPrepared(prepared));
2293
+ }
2294
+ };
2295
+ function toInternalPrepared(prepared) {
2296
+ if (prepared.backend !== "pretext" || !("pretextPrepared" in prepared)) {
2297
+ throw new TypeError("prepared text was not created by PretextTextMeasurer");
2298
+ }
2299
+ return prepared.pretextPrepared;
2300
+ }
2301
+
2302
+ // src/text/default.ts
2303
+ function createDefaultTextMeasurer(options = {}) {
2304
+ const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
2305
+ installRuntime();
2306
+ return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
2307
+ }
1736
2308
 
1737
2309
  // src/labels/fit.ts
1738
2310
  function fitLabel(text, options, measurer) {
@@ -1791,6 +2363,7 @@ function computeLabelLayout(text, options, measurer) {
1791
2363
  fittedSize,
1792
2364
  padding,
1793
2365
  font: { ...options.font },
2366
+ textBackend: prepared.backend,
1794
2367
  lineHeight,
1795
2368
  lines: buildLines(textLayout, contentBox2, lineHeight),
1796
2369
  overflow,
@@ -1885,8 +2458,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1885
2458
  ...outputResult(dsl)
1886
2459
  };
1887
2460
  }
1888
- const measurer = options.textMeasurer ?? new DeterministicTextMeasurer();
2461
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1889
2462
  const routeKind = dsl.routing?.kind ?? "orthogonal";
2463
+ const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1890
2464
  const diagram = {
1891
2465
  id: options.id ?? dsl.id ?? "diagram",
1892
2466
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1894,9 +2468,14 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1894
2468
  nodes: normalizeNodes(dsl, measurer),
1895
2469
  edges: normalizeEdges(dsl),
1896
2470
  groups: normalizeGroups(dsl, measurer),
2471
+ swimlanes: normalizeSwimlanes(dsl),
1897
2472
  constraints: normalizeConstraints(dsl),
1898
2473
  diagnostics: [],
1899
- metadata: { routeKind }
2474
+ ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
2475
+ metadata: {
2476
+ routeKind,
2477
+ ...portShifting === void 0 ? {} : { portShifting }
2478
+ }
1900
2479
  };
1901
2480
  return {
1902
2481
  diagram,
@@ -1904,6 +2483,15 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1904
2483
  ...outputResult(dsl)
1905
2484
  };
1906
2485
  }
2486
+ function normalizePortShifting(portShifting) {
2487
+ if (portShifting === void 0) {
2488
+ return void 0;
2489
+ }
2490
+ return {
2491
+ ...portShifting.enabled === void 0 ? {} : { enabled: portShifting.enabled },
2492
+ ...portShifting.spacing === void 0 ? {} : { spacing: portShifting.spacing }
2493
+ };
2494
+ }
1907
2495
  function outputResult(dsl) {
1908
2496
  return dsl.output?.format === void 0 ? {} : { output: { format: dsl.output.format } };
1909
2497
  }
@@ -1913,15 +2501,24 @@ function normalizeNodes(dsl, measurer) {
1913
2501
  const label = toLabel(node?.label);
1914
2502
  const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
1915
2503
  const fittedSize = labelLayout?.fittedSize;
2504
+ const nodeCompartments = node?.compartments === void 0 ? void 0 : compartments(node.compartments);
2505
+ const compartmentWidth = nodeCompartments === void 0 ? 0 : compartmentNaturalWidth(id, label, nodeCompartments, measurer);
1916
2506
  return {
1917
2507
  id,
1918
2508
  ...label === void 0 ? {} : { label },
1919
2509
  shape: node?.shape ?? "rectangle",
1920
2510
  ...node?.position === void 0 ? {} : { position: point(node.position) },
2511
+ ...node?.style === void 0 ? {} : { style: style(node.style) },
2512
+ ...node?.ports === void 0 ? {} : { ports: normalizePorts(node.ports) },
2513
+ ...nodeCompartments === void 0 ? {} : { compartments: nodeCompartments },
1921
2514
  size: {
1922
- width: Math.max(DEFAULT_NODE_MIN_SIZE.width, fittedSize?.width ?? 0),
2515
+ width: Math.max(
2516
+ DEFAULT_NODE_MIN_SIZE.width,
2517
+ fittedSize?.width ?? 0,
2518
+ compartmentWidth
2519
+ ),
1923
2520
  height: Math.max(
1924
- DEFAULT_NODE_MIN_SIZE.height,
2521
+ nodeCompartments === void 0 ? DEFAULT_NODE_MIN_SIZE.height : compartmentHeight(nodeCompartments),
1925
2522
  fittedSize?.height ?? 0
1926
2523
  )
1927
2524
  },
@@ -1930,11 +2527,42 @@ function normalizeNodes(dsl, measurer) {
1930
2527
  };
1931
2528
  });
1932
2529
  }
2530
+ function compartmentHeight(value) {
2531
+ const rowCount = (value.stereotype === void 0 ? 0 : 1) + 1 + (value.properties?.length ?? 0) + (value.constraints?.length ?? 0);
2532
+ const rowHeight = 16;
2533
+ const verticalPadding = 20;
2534
+ return Math.max(
2535
+ DEFAULT_NODE_MIN_SIZE.height,
2536
+ rowCount * rowHeight + verticalPadding
2537
+ );
2538
+ }
2539
+ function compartmentNaturalWidth(id, label, value, measurer) {
2540
+ const rows = compartmentRows(id, label, value);
2541
+ const maxRowWidth = rows.reduce((width, row) => {
2542
+ const prepared = measurer.prepare(row, DEFAULT_FONT);
2543
+ return Math.max(width, measurer.naturalWidth(prepared));
2544
+ }, 0);
2545
+ return Math.ceil(
2546
+ maxRowWidth + DEFAULT_NODE_PADDING.left + DEFAULT_NODE_PADDING.right
2547
+ );
2548
+ }
2549
+ function compartmentRows(id, label, value) {
2550
+ return [
2551
+ ...value.stereotype === void 0 ? [] : [value.stereotype],
2552
+ value.name ?? label?.text ?? id,
2553
+ ...value.properties ?? [],
2554
+ ...value.constraints ?? []
2555
+ ];
2556
+ }
1933
2557
  function normalizeEdges(dsl) {
1934
2558
  const counts = /* @__PURE__ */ new Map();
1935
2559
  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 ?? "";
2560
+ const source = typeof edge === "string" ? void 0 : edge.source;
2561
+ const target = typeof edge === "string" ? void 0 : edge.target;
2562
+ const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? endpointNodeId(source) ?? "";
2563
+ const targetId = typeof edge === "string" ? "" : edge.targetId ?? endpointNodeId(target) ?? "";
2564
+ const sourceEndpoint = typeof edge === "string" ? { nodeId: sourceId } : endpoint(source, edge.sourceId);
2565
+ const targetEndpoint = typeof edge === "string" ? { nodeId: targetId } : endpoint(target, edge.targetId);
1938
2566
  const baseId = `${sourceId}-${targetId}`;
1939
2567
  const count = counts.get(baseId) ?? 0;
1940
2568
  counts.set(baseId, count + 1);
@@ -1942,9 +2570,96 @@ function normalizeEdges(dsl) {
1942
2570
  const label = typeof edge === "string" ? void 0 : toLabel(edge.label);
1943
2571
  return {
1944
2572
  id,
1945
- source: { nodeId: sourceId },
1946
- target: { nodeId: targetId },
1947
- ...label === void 0 ? {} : { label }
2573
+ source: sourceEndpoint,
2574
+ target: targetEndpoint,
2575
+ ...label === void 0 ? {} : { label },
2576
+ ...typeof edge === "string" || edge.style === void 0 ? {} : { style: edge.style },
2577
+ ...typeof edge === "string" || edge.arrowhead === void 0 ? {} : { arrowhead: edge.arrowhead }
2578
+ };
2579
+ });
2580
+ }
2581
+ function normalizePorts(ports) {
2582
+ return Object.keys(ports ?? {}).sort().map((id) => {
2583
+ const port = ports?.[id];
2584
+ const label = toLabel(port?.label);
2585
+ return {
2586
+ id,
2587
+ ...label === void 0 ? {} : { label },
2588
+ side: port?.side ?? "right",
2589
+ kind: port?.kind ?? "proxy",
2590
+ ...port?.order === void 0 ? {} : { order: port.order },
2591
+ ...port?.style === void 0 ? {} : { style: style(port.style) }
2592
+ };
2593
+ });
2594
+ }
2595
+ function endpoint(value, nodeIdOverride) {
2596
+ if (nodeIdOverride !== void 0) {
2597
+ return {
2598
+ nodeId: nodeIdOverride,
2599
+ ...typeof value === "object" && value.node === nodeIdOverride && value.port !== void 0 ? { portId: value.port } : {}
2600
+ };
2601
+ }
2602
+ if (value === void 0) {
2603
+ return { nodeId: "" };
2604
+ }
2605
+ if (typeof value === "string") {
2606
+ return { nodeId: value };
2607
+ }
2608
+ return {
2609
+ nodeId: value.node,
2610
+ ...value.port === void 0 ? {} : { portId: value.port }
2611
+ };
2612
+ }
2613
+ function style(value) {
2614
+ return {
2615
+ ...value.fill === void 0 ? {} : { fill: value.fill },
2616
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
2617
+ };
2618
+ }
2619
+ function compartments(value) {
2620
+ return {
2621
+ ...value.stereotype === void 0 ? {} : { stereotype: value.stereotype },
2622
+ ...value.name === void 0 ? {} : { name: value.name },
2623
+ ...value.properties === void 0 ? {} : { properties: value.properties.map(formatCompartmentEntry) },
2624
+ ...value.constraints === void 0 ? {} : { constraints: [...value.constraints] }
2625
+ };
2626
+ }
2627
+ function normalizeFrame(frame) {
2628
+ return {
2629
+ kind: frame.kind,
2630
+ ...frame.context === void 0 ? {} : { context: frame.context },
2631
+ ...frame.name === void 0 ? {} : { name: frame.name },
2632
+ titleTab: frame.titleTab,
2633
+ ...frame.style === void 0 ? {} : { style: style(frame.style) }
2634
+ };
2635
+ }
2636
+ function formatCompartmentEntry(value) {
2637
+ if (typeof value === "string") {
2638
+ return value;
2639
+ }
2640
+ const [entry] = Object.entries(value);
2641
+ if (entry === void 0) {
2642
+ return "";
2643
+ }
2644
+ return `${entry[0]}: ${entry[1]}`;
2645
+ }
2646
+ function normalizeSwimlanes(dsl) {
2647
+ return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
2648
+ const swimlane = dsl.swimlanes?.[id];
2649
+ const label = toLabel(swimlane?.label);
2650
+ return {
2651
+ id,
2652
+ ...label === void 0 ? {} : { label },
2653
+ orientation: swimlane?.orientation ?? "vertical",
2654
+ lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
2655
+ const lane = swimlane?.lanes[laneId];
2656
+ const laneLabel = toLabel(lane?.label);
2657
+ return {
2658
+ id: laneId,
2659
+ ...laneLabel === void 0 ? {} : { label: laneLabel },
2660
+ children: [...lane?.children ?? []]
2661
+ };
2662
+ })
1948
2663
  };
1949
2664
  });
1950
2665
  }
@@ -2014,18 +2729,37 @@ function validateReferences(dsl) {
2014
2729
  const diagnostics = [];
2015
2730
  const nodeIds = new Set(Object.keys(dsl.nodes));
2016
2731
  const groupIds = new Set(Object.keys(dsl.groups ?? {}));
2732
+ const swimlaneLaneIds = new Set(
2733
+ Object.entries(dsl.swimlanes ?? {}).flatMap(
2734
+ ([swimlaneId, swimlane]) => Object.keys(swimlane.lanes).map((laneId) => `${swimlaneId}.${laneId}`)
2735
+ )
2736
+ );
2017
2737
  (dsl.edges ?? []).forEach((edge, index) => {
2018
2738
  if (typeof edge === "string") {
2019
2739
  return;
2020
2740
  }
2021
- const sourceId = edge.sourceId ?? edge.source;
2022
- const targetId = edge.targetId ?? edge.target;
2741
+ const sourceId = edge.sourceId ?? endpointNodeId(edge.source);
2742
+ const targetId = edge.targetId ?? endpointNodeId(edge.target);
2743
+ const sourceEndpoint = endpoint(edge.source, edge.sourceId);
2744
+ const targetEndpoint = endpoint(edge.target, edge.targetId);
2023
2745
  if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
2024
2746
  diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
2025
2747
  }
2026
2748
  if (targetId !== void 0 && !nodeIds.has(targetId)) {
2027
2749
  diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
2028
2750
  }
2751
+ validateEndpointPort(
2752
+ dsl,
2753
+ sourceEndpoint,
2754
+ ["edges", index, "source"],
2755
+ diagnostics
2756
+ );
2757
+ validateEndpointPort(
2758
+ dsl,
2759
+ targetEndpoint,
2760
+ ["edges", index, "target"],
2761
+ diagnostics
2762
+ );
2029
2763
  });
2030
2764
  for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
2031
2765
  (group.nodes ?? []).forEach((nodeId, index) => {
@@ -2043,6 +2777,27 @@ function validateReferences(dsl) {
2043
2777
  }
2044
2778
  });
2045
2779
  }
2780
+ for (const [swimlaneId, swimlane] of Object.entries(dsl.swimlanes ?? {})) {
2781
+ for (const [laneId, lane] of Object.entries(swimlane.lanes)) {
2782
+ (lane.children ?? []).forEach((child, childIndex) => {
2783
+ if (!nodeIds.has(child)) {
2784
+ diagnostics.push(
2785
+ referenceMissing(
2786
+ [
2787
+ "swimlanes",
2788
+ swimlaneId,
2789
+ "lanes",
2790
+ laneId,
2791
+ "children",
2792
+ childIndex
2793
+ ],
2794
+ child
2795
+ )
2796
+ );
2797
+ }
2798
+ });
2799
+ }
2800
+ }
2046
2801
  (dsl.constraints ?? []).forEach((constraint, index) => {
2047
2802
  switch (constraint.kind) {
2048
2803
  case "exact-position": {
@@ -2086,7 +2841,7 @@ function validateReferences(dsl) {
2086
2841
  break;
2087
2842
  case "containment": {
2088
2843
  const container = constraint.containerId ?? constraint.container;
2089
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
2844
+ if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds, swimlaneLaneIds)) {
2090
2845
  diagnostics.push(
2091
2846
  referenceMissing(["constraints", index, "container"], container)
2092
2847
  );
@@ -2119,8 +2874,23 @@ function referenceMissing(path, id) {
2119
2874
  hint: "Define the referenced node or group id, or update this reference."
2120
2875
  };
2121
2876
  }
2122
- function hasNodeOrGroup(id, nodeIds, groupIds) {
2123
- return nodeIds.has(id) || groupIds.has(id);
2877
+ function hasNodeOrGroup(id, nodeIds, groupIds, swimlaneLaneIds = /* @__PURE__ */ new Set()) {
2878
+ return nodeIds.has(id) || groupIds.has(id) || swimlaneLaneIds.has(id);
2879
+ }
2880
+ function endpointNodeId(endpointValue) {
2881
+ if (typeof endpointValue === "string" || endpointValue === void 0) {
2882
+ return endpointValue;
2883
+ }
2884
+ return endpointValue.node;
2885
+ }
2886
+ function validateEndpointPort(dsl, endpointValue, path, diagnostics) {
2887
+ if (endpointValue.portId === void 0) {
2888
+ return;
2889
+ }
2890
+ const node = dsl.nodes[endpointValue.nodeId];
2891
+ if (node !== void 0 && node.ports?.[endpointValue.portId] === void 0) {
2892
+ diagnostics.push(referenceMissing([...path, "port"], endpointValue.portId));
2893
+ }
2124
2894
  }
2125
2895
  function toLabel(value) {
2126
2896
  if (value === void 0) {
@@ -2190,6 +2960,8 @@ function isValidEdgeId(value) {
2190
2960
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
2191
2961
  var routeKindSchema = z.enum(["orthogonal", "straight"]);
2192
2962
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
2963
+ var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
2964
+ var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
2193
2965
  var nodeShapeSchema = z.enum([
2194
2966
  "rectangle",
2195
2967
  "rounded-rectangle",
@@ -2217,18 +2989,49 @@ var labelSchema = z.union([
2217
2989
  maxWidth: finiteNumberSchema.optional()
2218
2990
  })
2219
2991
  ]);
2992
+ var styleSchema = z.object({
2993
+ fill: z.string().optional(),
2994
+ stroke: z.string().optional()
2995
+ });
2996
+ var portSideSchema = z.enum(["top", "right", "bottom", "left"]);
2997
+ var portKindSchema = z.enum(["proxy", "flow"]);
2998
+ var portSchema = z.object({
2999
+ label: labelSchema.optional(),
3000
+ side: portSideSchema,
3001
+ kind: portKindSchema.optional(),
3002
+ order: finiteNumberSchema.optional(),
3003
+ style: styleSchema.optional()
3004
+ });
3005
+ var compartmentsSchema = z.object({
3006
+ stereotype: z.string().optional(),
3007
+ name: z.string().optional(),
3008
+ properties: z.array(z.record(z.string(), z.string()).or(z.string())).optional(),
3009
+ constraints: z.array(z.string()).optional()
3010
+ });
2220
3011
  var nodeSchema = z.object({
2221
3012
  label: labelSchema.optional(),
2222
3013
  shape: nodeShapeSchema.optional(),
2223
- position: pointSchema.optional()
3014
+ position: pointSchema.optional(),
3015
+ style: styleSchema.optional(),
3016
+ ports: z.record(z.string(), portSchema).optional(),
3017
+ compartments: compartmentsSchema.optional()
2224
3018
  });
3019
+ var endpointSchema = z.union([
3020
+ z.string(),
3021
+ z.object({
3022
+ node: z.string(),
3023
+ port: z.string().optional()
3024
+ })
3025
+ ]);
2225
3026
  var structuredEdgeSchema = z.object({
2226
3027
  id: z.string().optional(),
2227
- source: z.string().optional(),
2228
- target: z.string().optional(),
3028
+ source: endpointSchema.optional(),
3029
+ target: endpointSchema.optional(),
2229
3030
  sourceId: z.string().optional(),
2230
3031
  targetId: z.string().optional(),
2231
- label: labelSchema.optional()
3032
+ label: labelSchema.optional(),
3033
+ style: edgeStrokeStyleSchema.optional(),
3034
+ arrowhead: edgeArrowheadSchema.optional()
2232
3035
  }).superRefine((edge, context) => {
2233
3036
  if (edge.source === void 0 && edge.sourceId === void 0) {
2234
3037
  context.addIssue({
@@ -2252,6 +3055,17 @@ var groupSchema = z.object({
2252
3055
  groups: z.array(z.string()).optional(),
2253
3056
  padding: insetsSchema.optional()
2254
3057
  });
3058
+ var swimlaneSchema = z.object({
3059
+ label: labelSchema.optional(),
3060
+ orientation: z.enum(["vertical", "horizontal"]).optional(),
3061
+ lanes: z.record(
3062
+ z.string(),
3063
+ z.object({
3064
+ label: labelSchema.optional(),
3065
+ children: z.array(z.string()).optional()
3066
+ })
3067
+ )
3068
+ });
2255
3069
  var exactPositionConstraintSchema = z.object({
2256
3070
  kind: z.literal("exact-position"),
2257
3071
  target: z.string().optional(),
@@ -2312,12 +3126,24 @@ var diagramDslSchema = z.object({
2312
3126
  direction: directionSchema.optional()
2313
3127
  }).optional(),
2314
3128
  routing: z.object({
2315
- kind: routeKindSchema.optional()
3129
+ kind: routeKindSchema.optional(),
3130
+ portShifting: z.object({
3131
+ enabled: z.boolean().optional(),
3132
+ spacing: finiteNumberSchema.optional()
3133
+ }).optional()
2316
3134
  }).optional(),
2317
3135
  nodes: z.record(z.string(), nodeSchema),
2318
3136
  edges: z.array(edgeSchema).optional(),
2319
3137
  groups: z.record(z.string(), groupSchema).optional(),
3138
+ swimlanes: z.record(z.string(), swimlaneSchema).optional(),
2320
3139
  constraints: z.array(constraintSchema).optional(),
3140
+ frame: z.object({
3141
+ kind: z.string(),
3142
+ context: z.string().optional(),
3143
+ name: z.string().optional(),
3144
+ titleTab: z.string(),
3145
+ style: styleSchema.optional()
3146
+ }).optional(),
2321
3147
  output: z.object({
2322
3148
  format: outputFormatSchema.optional()
2323
3149
  }).optional()
@@ -2514,7 +3340,8 @@ function renderDiagramDsl(source, options = {}) {
2514
3340
  return { diagnostics };
2515
3341
  }
2516
3342
  const solved = solveDiagram(normalized.diagram, {
2517
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
3343
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3344
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
2518
3345
  });
2519
3346
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
2520
3347
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -2553,6 +3380,22 @@ function renderDiagramDsl(source, options = {}) {
2553
3380
  function toSolveDiagnostic(diagnostic) {
2554
3381
  return { ...diagnostic, layer: "solve" };
2555
3382
  }
3383
+ function solvePortShiftingOption(value) {
3384
+ if (!isJsonObject(value)) {
3385
+ return {};
3386
+ }
3387
+ const portShifting = {};
3388
+ if (value.enabled === false) {
3389
+ portShifting.enabled = false;
3390
+ }
3391
+ if (typeof value.spacing === "number") {
3392
+ portShifting.spacing = value.spacing;
3393
+ }
3394
+ return { portShifting };
3395
+ }
3396
+ function isJsonObject(value) {
3397
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3398
+ }
2556
3399
  function toExportDiagnostic(diagnostic) {
2557
3400
  return { ...diagnostic, layer: "export" };
2558
3401
  }