@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.
@@ -3,12 +3,15 @@
3
3
 
4
4
  var commander = require('commander');
5
5
  var dagre = require('@dagrejs/dagre');
6
+ var module$1 = require('module');
7
+ var pretext = require('@chenglou/pretext');
6
8
  var buffer = require('buffer');
7
9
  var yaml = require('yaml');
8
10
  var zod = require('zod');
9
11
  var promises = require('fs/promises');
10
12
  var path = require('path');
11
13
 
14
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
15
  // src/dsl/diagnostics.ts
13
16
  var SEVERITY_RANK = /* @__PURE__ */ new Map([
14
17
  ["error", 0],
@@ -196,11 +199,12 @@ function renderArrow(edge) {
196
199
  height: box.height
197
200
  }),
198
201
  backgroundColor: "transparent",
202
+ strokeStyle: edge.style ?? "solid",
199
203
  points: relativePoints,
200
204
  startBinding: { elementId: `node:${edge.source.nodeId}`, focus: 0, gap: 0 },
201
205
  endBinding: { elementId: `node:${edge.target.nodeId}`, focus: 0, gap: 0 },
202
206
  startArrowhead: null,
203
- endArrowhead: "arrow"
207
+ endArrowhead: mapArrowhead(edge.arrowhead)
204
208
  };
205
209
  }
206
210
  function renderText(id, label, box, containerId, groupIds) {
@@ -278,6 +282,16 @@ function mapShape(shape) {
278
282
  return "cylinder";
279
283
  }
280
284
  }
285
+ function mapArrowhead(arrowhead) {
286
+ switch (arrowhead) {
287
+ case void 0:
288
+ return "arrow";
289
+ case "triangle":
290
+ return "triangle";
291
+ case "hollowTriangle":
292
+ return "triangle_outline";
293
+ }
294
+ }
281
295
  function createGroupMembership(groups) {
282
296
  const membership = /* @__PURE__ */ new Map();
283
297
  for (const group of groups) {
@@ -344,19 +358,28 @@ function exportSvg(diagram, options = {}) {
344
358
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
345
359
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
346
360
  ` <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"/>`,
361
+ ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame))],
362
+ ...(diagram.swimlanes ?? []).flatMap(
363
+ (swimlane) => renderSwimlane(swimlane)
364
+ ),
347
365
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
348
366
  ...diagram.edges.flatMap((edge) => {
349
- const path = renderEdgePath(edge.points, edge.id);
367
+ const path = renderEdgePath(edge);
350
368
  if (path === void 0) {
351
369
  return [];
352
370
  }
353
- return [indent(path), indent(renderArrowhead(edge.points, edge.id))];
371
+ return [indent(path), indent(renderArrowhead(edge))];
354
372
  }),
355
373
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
374
+ ...diagram.nodes.flatMap((node) => renderCompartments(node)),
375
+ ...diagram.nodes.flatMap((node) => renderPorts(node)),
356
376
  ...diagram.groups.flatMap(
357
377
  (group) => renderLabel(group.label, group.box, group)
358
378
  ),
359
- ...diagram.nodes.flatMap((node) => renderLabel(node.label, node.box, node)),
379
+ ...diagram.nodes.flatMap(
380
+ (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node) : []
381
+ ),
382
+ ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge)),
360
383
  "</svg>"
361
384
  ];
362
385
  return `${lines.join("\n")}
@@ -366,7 +389,9 @@ function renderGroup2(group) {
366
389
  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"/>`;
367
390
  }
368
391
  function renderNode2(node) {
369
- const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${NODE_FILL}" stroke="${STROKE}"`;
392
+ const fill = node.style?.fill ?? NODE_FILL;
393
+ const stroke = node.style?.stroke ?? STROKE;
394
+ const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(stroke)}"`;
370
395
  switch (node.shape) {
371
396
  case "rectangle":
372
397
  return renderRect(node.box, common);
@@ -382,16 +407,111 @@ function renderNode2(node) {
382
407
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
383
408
  }
384
409
  }
410
+ function renderFrame(frame) {
411
+ const stroke = frame.style?.stroke ?? "#6b7280";
412
+ const fill = frame.style?.fill ?? "transparent";
413
+ return [
414
+ `<g class="sysml-frame" data-kind="${escapeAttribute(frame.kind)}">`,
415
+ ` <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)}"/>`,
416
+ ` <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)}"/>`,
417
+ ` <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>`,
418
+ "</g>"
419
+ ].join("\n");
420
+ }
421
+ function renderSwimlane(swimlane) {
422
+ if (swimlane.box === void 0) {
423
+ return [];
424
+ }
425
+ const lines = [
426
+ ` <g class="swimlane" data-id="${escapeAttribute(swimlane.id)}">`,
427
+ ` <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}"/>`
428
+ ];
429
+ for (const lane of swimlane.lanes) {
430
+ if (lane.box === void 0) {
431
+ continue;
432
+ }
433
+ lines.push(
434
+ ` <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}"/>`
435
+ );
436
+ if (lane.label?.text !== void 0) {
437
+ lines.push(
438
+ ` <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>`
439
+ );
440
+ }
441
+ }
442
+ lines.push(" </g>");
443
+ return lines;
444
+ }
445
+ function renderPorts(node) {
446
+ return (node.ports ?? []).flatMap((port) => [
447
+ ` <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)}"/>`,
448
+ ...port.label?.text === void 0 ? [] : [
449
+ ` <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>`
450
+ ]
451
+ ]);
452
+ }
453
+ function renderCompartments(node) {
454
+ const compartments2 = node.compartments;
455
+ if (compartments2 === void 0) {
456
+ return [];
457
+ }
458
+ const rows = [
459
+ ...compartments2.stereotype === void 0 ? [] : [{ className: "stereotype", text: compartments2.stereotype }],
460
+ {
461
+ className: "name",
462
+ text: compartments2.name ?? node.label?.text ?? node.id
463
+ },
464
+ ...(compartments2.properties ?? []).map((text) => ({
465
+ className: "properties",
466
+ text
467
+ })),
468
+ ...(compartments2.constraints ?? []).map((text) => ({
469
+ className: "constraints",
470
+ text
471
+ }))
472
+ ];
473
+ const lineHeight = 16;
474
+ const lines = [
475
+ ` <g class="compartment" data-for="${escapeAttribute(node.id)}">`
476
+ ];
477
+ for (let index = 0; index < rows.length; index += 1) {
478
+ const row = rows[index];
479
+ if (row === void 0) {
480
+ continue;
481
+ }
482
+ const y = node.box.y + 18 + index * lineHeight;
483
+ if (index > 1) {
484
+ lines.push(
485
+ ` <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}"/>`
486
+ );
487
+ }
488
+ lines.push(
489
+ ` <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>`
490
+ );
491
+ }
492
+ lines.push(" </g>");
493
+ return lines;
494
+ }
495
+ function portLabelX(x, side) {
496
+ if (side === "left") {
497
+ return x - 8;
498
+ }
499
+ if (side === "right") {
500
+ return x + 8;
501
+ }
502
+ return x + 8;
503
+ }
385
504
  function renderRect(box, attributes) {
386
505
  return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
387
506
  }
388
507
  function renderLabel(label, box, item) {
389
508
  const labelLayout = item.labelLayout;
390
509
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
510
+ const offset = isAbsoluteLabelLayout(labelLayout.box, box) ? { x: 0, y: 0 } : { x: box.x, y: box.y };
391
511
  return [
392
512
  ` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
393
513
  ...labelLayout.lines.map(
394
- (line) => ` <tspan x="${formatNumber(line.box.x)}" y="${formatNumber(line.baselineY)}">${escapeXml(line.text)}</tspan>`
514
+ (line) => ` <tspan x="${formatNumber(offset.x + line.box.x)}" y="${formatNumber(offset.y + line.baselineY)}">${escapeXml(line.text)}</tspan>`
395
515
  ),
396
516
  " </text>"
397
517
  ];
@@ -403,15 +523,91 @@ function renderLabel(label, box, item) {
403
523
  ` <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>`
404
524
  ];
405
525
  }
406
- function renderEdgePath(points, id) {
407
- if (points.length < 2) {
526
+ function renderEdgePath(edge) {
527
+ if (edge.points.length < 2) {
528
+ return void 0;
529
+ }
530
+ const dash = edge.style === "dashed" ? ' stroke-dasharray="6 4"' : "";
531
+ return `<path class="edge" data-id="${escapeAttribute(edge.id)}" d="${formatPath(pathPointsBeforeArrowhead(edge.points))}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"${dash}/>`;
532
+ }
533
+ function renderEdgeLabel(edge) {
534
+ if (edge.label?.text === void 0 || edge.points.length < 2) {
535
+ return [];
536
+ }
537
+ const placement = labelPlacementOnPolyline(edge.points);
538
+ if (placement === void 0) {
539
+ return [];
540
+ }
541
+ return [
542
+ ` <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>`
543
+ ];
544
+ }
545
+ function renderArrowhead(edge) {
546
+ const arrowhead = computeArrowhead(edge.points);
547
+ const fill = edge.arrowhead === "hollowTriangle" ? "none" : EDGE_STROKE;
548
+ return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(edge.id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${fill}" stroke="${EDGE_STROKE}"/>`;
549
+ }
550
+ function labelPlacementOnPolyline(points) {
551
+ const segments = nonZeroSegments(points);
552
+ const totalLength = segments.reduce(
553
+ (sum, segment) => sum + segment.length,
554
+ 0
555
+ );
556
+ if (totalLength <= 0) {
408
557
  return void 0;
409
558
  }
410
- return `<path class="edge" data-id="${escapeAttribute(id)}" d="${formatPath(points)}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"/>`;
559
+ let remaining = totalLength / 2;
560
+ for (const segment of segments) {
561
+ if (remaining <= segment.length) {
562
+ const ratio = remaining / segment.length;
563
+ const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
564
+ const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
565
+ const offset2 = labelOffset(segment);
566
+ return { x: x + offset2.x, y: y + offset2.y };
567
+ }
568
+ remaining -= segment.length;
569
+ }
570
+ const last = segments.at(-1);
571
+ if (last === void 0) {
572
+ return void 0;
573
+ }
574
+ const offset = labelOffset(last);
575
+ return { x: last.end.x + offset.x, y: last.end.y + offset.y };
576
+ }
577
+ function nonZeroSegments(points) {
578
+ const segments = [];
579
+ for (let index = 0; index < points.length - 1; index += 1) {
580
+ const start = points[index];
581
+ const end = points[index + 1];
582
+ if (start === void 0 || end === void 0) {
583
+ continue;
584
+ }
585
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
586
+ if (length > 0) {
587
+ segments.push({ start, end, length });
588
+ }
589
+ }
590
+ return segments;
411
591
  }
412
- function renderArrowhead(points, id) {
592
+ function labelOffset(segment) {
593
+ const offset = 10;
594
+ const dx = segment.end.x - segment.start.x;
595
+ const dy = segment.end.y - segment.start.y;
596
+ return {
597
+ x: -dy / segment.length * offset,
598
+ y: dx / segment.length * offset
599
+ };
600
+ }
601
+ function pathPointsBeforeArrowhead(points) {
413
602
  const arrowhead = computeArrowhead(points);
414
- return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${EDGE_STROKE}" stroke="${EDGE_STROKE}"/>`;
603
+ const base = {
604
+ x: (arrowhead.left.x + arrowhead.right.x) / 2,
605
+ y: (arrowhead.left.y + arrowhead.right.y) / 2
606
+ };
607
+ return [...points.slice(0, -1), base];
608
+ }
609
+ function isAbsoluteLabelLayout(labelBox, itemBox) {
610
+ 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;
415
611
  }
416
612
  function shapePoints(shape, box) {
417
613
  const left = box.x;
@@ -1296,20 +1492,33 @@ function isValidDimension(value) {
1296
1492
  // src/routing/routes.ts
1297
1493
  function routeEdge(input) {
1298
1494
  const diagnostics = [];
1495
+ const defaultAnchors = defaultAnchorsForGeometry(
1496
+ input.source.box,
1497
+ input.target.box,
1498
+ input.direction
1499
+ );
1299
1500
  const source = getEdgePort(
1300
1501
  input.source,
1301
1502
  input.target.center,
1302
- input.sourceAnchor
1503
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
1303
1504
  );
1304
1505
  const target = getEdgePort(
1305
1506
  input.target,
1306
1507
  input.source.center,
1307
- input.targetAnchor
1508
+ input.targetAnchor ?? defaultAnchors.targetAnchor
1308
1509
  );
1309
1510
  if ((input.kind ?? "orthogonal") === "straight") {
1310
1511
  return { points: simplifyRoute([source, target]), diagnostics };
1311
1512
  }
1312
1513
  const candidates = orthogonalCandidates(source, target, input.direction);
1514
+ candidates.push(
1515
+ ...expandedObstacleCandidates(
1516
+ source,
1517
+ target,
1518
+ input.direction,
1519
+ input.obstacles ?? []
1520
+ )
1521
+ );
1313
1522
  for (const candidate of candidates) {
1314
1523
  if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
1315
1524
  return { points: simplifyRoute(candidate), diagnostics };
@@ -1348,27 +1557,113 @@ function simplifyRoute(points) {
1348
1557
  function orthogonalCandidates(source, target, direction) {
1349
1558
  const midpointX = (source.x + target.x) / 2;
1350
1559
  const midpointY = (source.y + target.y) / 2;
1351
- const candidates = [
1352
- [source, { x: target.x, y: source.y }, target],
1353
- [source, { x: source.x, y: target.y }, target]
1354
- ];
1560
+ const candidates = [];
1355
1561
  if (direction === "TB" || direction === "BT") {
1356
1562
  candidates.push([
1357
1563
  source,
1358
- { x: midpointX, y: source.y },
1359
- { x: midpointX, y: target.y },
1564
+ { x: source.x, y: midpointY },
1565
+ { x: target.x, y: midpointY },
1360
1566
  target
1361
1567
  ]);
1362
1568
  } else {
1363
1569
  candidates.push([
1364
1570
  source,
1365
- { x: source.x, y: midpointY },
1366
- { x: target.x, y: midpointY },
1571
+ { x: midpointX, y: source.y },
1572
+ { x: midpointX, y: target.y },
1367
1573
  target
1368
1574
  ]);
1369
1575
  }
1576
+ candidates.push(
1577
+ [source, { x: target.x, y: source.y }, target],
1578
+ [source, { x: source.x, y: target.y }, target]
1579
+ );
1580
+ return candidates;
1581
+ }
1582
+ function defaultSourceAnchor(direction) {
1583
+ switch (direction) {
1584
+ case "LR":
1585
+ return "right";
1586
+ case "RL":
1587
+ return "left";
1588
+ case "TB":
1589
+ return "bottom";
1590
+ case "BT":
1591
+ return "top";
1592
+ }
1593
+ }
1594
+ function defaultAnchorsForGeometry(source, target, direction) {
1595
+ const dx = target.x + target.width / 2 - (source.x + source.width / 2);
1596
+ const dy = target.y + target.height / 2 - (source.y + source.height / 2);
1597
+ if (Math.abs(dy) > Math.abs(dx)) {
1598
+ return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
1599
+ }
1600
+ if (Math.abs(dx) > 0) {
1601
+ return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
1602
+ }
1603
+ return {
1604
+ sourceAnchor: defaultSourceAnchor(direction),
1605
+ targetAnchor: defaultTargetAnchor(direction)
1606
+ };
1607
+ }
1608
+ function defaultTargetAnchor(direction) {
1609
+ switch (direction) {
1610
+ case "LR":
1611
+ return "left";
1612
+ case "RL":
1613
+ return "right";
1614
+ case "TB":
1615
+ return "top";
1616
+ case "BT":
1617
+ return "bottom";
1618
+ }
1619
+ }
1620
+ function expandedObstacleCandidates(source, target, direction, obstacles) {
1621
+ if (obstacles.length === 0) {
1622
+ return [];
1623
+ }
1624
+ const margin = 16;
1625
+ const candidates = [];
1626
+ if (direction === "TB" || direction === "BT") {
1627
+ const lanes = sortedUniqueLanes(
1628
+ obstacles.flatMap((obstacle) => [
1629
+ obstacle.x - margin,
1630
+ obstacle.x + obstacle.width + margin
1631
+ ]),
1632
+ (source.x + target.x) / 2
1633
+ );
1634
+ for (const laneX of lanes) {
1635
+ candidates.push([
1636
+ source,
1637
+ { x: laneX, y: source.y },
1638
+ { x: laneX, y: target.y },
1639
+ target
1640
+ ]);
1641
+ }
1642
+ } else {
1643
+ const lanes = sortedUniqueLanes(
1644
+ obstacles.flatMap((obstacle) => [
1645
+ obstacle.y - margin,
1646
+ obstacle.y + obstacle.height + margin
1647
+ ]),
1648
+ (source.y + target.y) / 2
1649
+ );
1650
+ for (const laneY of lanes) {
1651
+ candidates.push([
1652
+ source,
1653
+ { x: source.x, y: laneY },
1654
+ { x: target.x, y: laneY },
1655
+ target
1656
+ ]);
1657
+ }
1658
+ }
1370
1659
  return candidates;
1371
1660
  }
1661
+ function sortedUniqueLanes(lanes, midpoint) {
1662
+ return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
1663
+ const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
1664
+ return distance === 0 ? left - right : distance;
1665
+ });
1666
+ }
1372
1667
  function routeIntersectsObstacles(points, obstacles) {
1373
1668
  for (let index = 0; index < points.length - 1; index += 1) {
1374
1669
  const a = points[index];
@@ -1447,12 +1742,17 @@ function solveDiagram(diagram, options = {}) {
1447
1742
  options,
1448
1743
  diagnostics
1449
1744
  );
1745
+ const coordinatedSwimlanes = coordinateSwimlanes(
1746
+ diagram.swimlanes ?? [],
1747
+ constrained.boxes
1748
+ );
1450
1749
  const groupBoxes = new Map(
1451
1750
  coordinatedGroups.map((group) => [group.id, group.box])
1452
1751
  );
1453
1752
  const coordinatedEdges = coordinateEdges(
1454
1753
  edges,
1455
1754
  nodeGeometryById,
1755
+ coordinatedNodes,
1456
1756
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
1457
1757
  diagram.direction,
1458
1758
  options,
@@ -1460,8 +1760,18 @@ function solveDiagram(diagram, options = {}) {
1460
1760
  );
1461
1761
  const allBoxes = [
1462
1762
  ...coordinatedNodes.map((node) => node.box),
1463
- ...groupBoxes.values()
1763
+ ...coordinatedNodes.flatMap(
1764
+ (node) => (node.ports ?? []).flatMap(
1765
+ (port) => port.label === void 0 ? [port.box] : [port.box, portLabelBox(port)]
1766
+ )
1767
+ ),
1768
+ ...groupBoxes.values(),
1769
+ ...coordinatedSwimlanes.flatMap(
1770
+ (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
1771
+ )
1464
1772
  ];
1773
+ const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
1774
+ const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
1465
1775
  return {
1466
1776
  id: diagram.id,
1467
1777
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -1469,8 +1779,10 @@ function solveDiagram(diagram, options = {}) {
1469
1779
  nodes: coordinatedNodes,
1470
1780
  edges: coordinatedEdges,
1471
1781
  groups: coordinatedGroups,
1782
+ ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
1472
1783
  diagnostics,
1473
- bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
1784
+ bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
1785
+ ...frame === void 0 ? {} : { frame },
1474
1786
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
1475
1787
  };
1476
1788
  }
@@ -1496,6 +1808,9 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
1496
1808
  coordinated.push({
1497
1809
  id: node.id,
1498
1810
  ...node.label === void 0 ? {} : { label: node.label },
1811
+ ...node.style === void 0 ? {} : { style: node.style },
1812
+ ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
1813
+ ...node.compartments === void 0 ? {} : { compartments: node.compartments },
1499
1814
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
1500
1815
  shape: node.shape,
1501
1816
  ...node.metadata === void 0 ? {} : { metadata: node.metadata },
@@ -1506,6 +1821,142 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
1506
1821
  }
1507
1822
  return coordinated;
1508
1823
  }
1824
+ function coordinatePorts(node, nodeBox, portShifting) {
1825
+ const portsBySide = /* @__PURE__ */ new Map();
1826
+ for (const port of node.ports ?? []) {
1827
+ const ports = portsBySide.get(port.side) ?? [];
1828
+ ports.push(port);
1829
+ portsBySide.set(port.side, ports);
1830
+ }
1831
+ const coordinated = [];
1832
+ for (const [side, ports] of portsBySide) {
1833
+ const sorted = [...ports ?? []].sort((a, b) => {
1834
+ const order = (a.order ?? 0) - (b.order ?? 0);
1835
+ return order === 0 ? a.id.localeCompare(b.id) : order;
1836
+ });
1837
+ for (let index = 0; index < sorted.length; index += 1) {
1838
+ const port = sorted[index];
1839
+ if (port === void 0) {
1840
+ continue;
1841
+ }
1842
+ const anchor = portAnchor(
1843
+ nodeBox,
1844
+ side,
1845
+ index,
1846
+ sorted.length,
1847
+ portShifting
1848
+ );
1849
+ const box = portBox(anchor);
1850
+ coordinated.push({ ...port, box, anchor });
1851
+ }
1852
+ }
1853
+ return coordinated.sort((a, b) => a.id.localeCompare(b.id));
1854
+ }
1855
+ function portAnchor(nodeBox, side, index, count, portShifting) {
1856
+ const shiftingEnabled = portShifting?.enabled ?? true;
1857
+ const spacing = portShifting?.spacing ?? 24;
1858
+ const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
1859
+ switch (side) {
1860
+ case "left":
1861
+ return {
1862
+ x: nodeBox.x,
1863
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
1864
+ };
1865
+ case "right":
1866
+ return {
1867
+ x: nodeBox.x + nodeBox.width,
1868
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
1869
+ };
1870
+ case "top":
1871
+ return {
1872
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
1873
+ y: nodeBox.y
1874
+ };
1875
+ case "bottom":
1876
+ return {
1877
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
1878
+ y: nodeBox.y + nodeBox.height
1879
+ };
1880
+ }
1881
+ }
1882
+ function portBox(anchor) {
1883
+ const size = 10;
1884
+ return {
1885
+ x: anchor.x - size / 2,
1886
+ y: anchor.y - size / 2,
1887
+ width: size,
1888
+ height: size
1889
+ };
1890
+ }
1891
+ function portLabelBox(port) {
1892
+ const textWidth = Math.max(0, (port.label?.text.length ?? 0) * 6);
1893
+ const height = 12;
1894
+ const gap = 8;
1895
+ const x = port.side === "left" ? port.anchor.x - gap - textWidth : port.anchor.x + gap;
1896
+ return {
1897
+ x,
1898
+ y: port.anchor.y - 8 - height,
1899
+ width: textWidth,
1900
+ height
1901
+ };
1902
+ }
1903
+ function coordinateSwimlanes(swimlanes, nodeBoxes) {
1904
+ const titleSize = 28;
1905
+ const padding = 16;
1906
+ return swimlanes.map((swimlane) => {
1907
+ const laneBoxes = swimlane.lanes.flatMap((lane) => {
1908
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
1909
+ return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
1910
+ });
1911
+ const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
1912
+ const outer = expand(laneUnion, padding, titleSize);
1913
+ const laneCount = Math.max(1, swimlane.lanes.length);
1914
+ const lanes = swimlane.lanes.map((lane, index) => {
1915
+ const box = swimlane.orientation === "vertical" ? {
1916
+ x: outer.x + outer.width / laneCount * index,
1917
+ y: outer.y,
1918
+ width: outer.width / laneCount,
1919
+ height: outer.height
1920
+ } : {
1921
+ x: outer.x,
1922
+ y: outer.y + outer.height / laneCount * index,
1923
+ width: outer.width,
1924
+ height: outer.height / laneCount
1925
+ };
1926
+ return { ...lane, box };
1927
+ });
1928
+ return { ...swimlane, lanes, box: outer };
1929
+ });
1930
+ }
1931
+ function coordinateFrame(frame, contentBounds) {
1932
+ const padding = 32;
1933
+ const titleHeight = 28;
1934
+ const titleWidth = Math.max(180, frame.titleTab.length * 7);
1935
+ const box = {
1936
+ x: contentBounds.x - padding,
1937
+ y: contentBounds.y - padding - titleHeight,
1938
+ width: contentBounds.width + padding * 2,
1939
+ height: contentBounds.height + padding * 2 + titleHeight
1940
+ };
1941
+ return {
1942
+ ...frame,
1943
+ box,
1944
+ titleBox: {
1945
+ x: box.x,
1946
+ y: box.y,
1947
+ width: Math.min(titleWidth, box.width * 0.8),
1948
+ height: titleHeight
1949
+ }
1950
+ };
1951
+ }
1952
+ function expand(box, padding, titleSize) {
1953
+ return {
1954
+ x: box.x - padding,
1955
+ y: box.y - padding - titleSize,
1956
+ width: box.width + padding * 2,
1957
+ height: box.height + padding * 2 + titleSize
1958
+ };
1959
+ }
1509
1960
  function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
1510
1961
  const coordinated = [];
1511
1962
  const groupBoxes = /* @__PURE__ */ new Map();
@@ -1554,8 +2005,11 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
1554
2005
  }
1555
2006
  return coordinated;
1556
2007
  }
1557
- function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
2008
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
1558
2009
  const coordinated = [];
2010
+ const coordinatedNodeById = new Map(
2011
+ coordinatedNodes.map((node) => [node.id, node])
2012
+ );
1559
2013
  for (const edge of edges) {
1560
2014
  const source = nodes.get(edge.source.nodeId);
1561
2015
  const target = nodes.get(edge.target.nodeId);
@@ -1573,11 +2027,13 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
1573
2027
  });
1574
2028
  continue;
1575
2029
  }
2030
+ const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
2031
+ const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
1576
2032
  const route = routeEdge({
1577
2033
  kind: options.routeKind ?? "orthogonal",
1578
2034
  direction,
1579
- source,
1580
- target,
2035
+ source: portGeometry(source, sourcePort),
2036
+ target: portGeometry(target, targetPort),
1581
2037
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
1582
2038
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
1583
2039
  obstacles: obstacles.filter(
@@ -1597,6 +2053,21 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
1597
2053
  }
1598
2054
  return coordinated;
1599
2055
  }
2056
+ function portGeometry(nodeGeometry, port) {
2057
+ if (port === void 0) {
2058
+ return nodeGeometry;
2059
+ }
2060
+ return {
2061
+ ...nodeGeometry,
2062
+ box: port.box,
2063
+ center: port.anchor,
2064
+ anchors: nodeGeometry.anchors.map((anchor) => ({
2065
+ name: anchor.name,
2066
+ point: port.anchor
2067
+ })),
2068
+ obstacleBox: port.box
2069
+ };
2070
+ }
1600
2071
  function stableById(items) {
1601
2072
  return [...items].sort((a, b) => a.id.localeCompare(b.id));
1602
2073
  }
@@ -1626,34 +2097,34 @@ function assertFiniteNonNegative(value, label) {
1626
2097
  throw new TypeError(`${label} must be a finite non-negative width`);
1627
2098
  }
1628
2099
  }
1629
- function validateTextStyle(style) {
1630
- assertFinitePositive(style.fontSize, "fontSize");
1631
- if (style.lineHeight !== void 0) {
1632
- assertFinitePositive(style.lineHeight, "lineHeight");
2100
+ function validateTextStyle(style2) {
2101
+ assertFinitePositive(style2.fontSize, "fontSize");
2102
+ if (style2.lineHeight !== void 0) {
2103
+ assertFinitePositive(style2.lineHeight, "lineHeight");
1633
2104
  }
1634
- if (style.letterSpacing !== void 0 && !Number.isFinite(style.letterSpacing)) {
2105
+ if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
1635
2106
  throw new TypeError("letterSpacing must be finite");
1636
2107
  }
1637
2108
  }
1638
- function resolveLineHeight(style) {
1639
- validateTextStyle(style);
1640
- return style.lineHeight ?? style.fontSize * 1.2;
2109
+ function resolveLineHeight(style2) {
2110
+ validateTextStyle(style2);
2111
+ return style2.lineHeight ?? style2.fontSize * 1.2;
1641
2112
  }
1642
- function toCanvasFont(style) {
1643
- validateTextStyle(style);
1644
- const fontStyle = style.fontStyle === "italic" ? "italic " : "";
1645
- const fontWeight = style.fontWeight ?? 400;
1646
- return `${fontStyle}${fontWeight} ${style.fontSize}px ${style.fontFamily}`;
2113
+ function toCanvasFont(style2) {
2114
+ validateTextStyle(style2);
2115
+ const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
2116
+ const fontWeight = style2.fontWeight ?? 400;
2117
+ return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
1647
2118
  }
1648
2119
 
1649
2120
  // src/text/fallback.ts
1650
2121
  var DeterministicTextMeasurer = class {
1651
- prepare(text, style) {
1652
- validateTextStyle(style);
2122
+ prepare(text, style2) {
2123
+ validateTextStyle(style2);
1653
2124
  return {
1654
2125
  text,
1655
- font: toCanvasFont(style),
1656
- style: { ...style },
2126
+ font: toCanvasFont(style2),
2127
+ style: { ...style2 },
1657
2128
  backend: "deterministic"
1658
2129
  };
1659
2130
  }
@@ -1712,9 +2183,9 @@ var DeterministicTextMeasurer = class {
1712
2183
  return output;
1713
2184
  }
1714
2185
  };
1715
- function getCharacterWidth(style) {
1716
- const letterSpacing = style.letterSpacing ?? 0;
1717
- return Math.max(0, style.fontSize * 0.6 + letterSpacing);
2186
+ function getCharacterWidth(style2) {
2187
+ const letterSpacing = style2.letterSpacing ?? 0;
2188
+ return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
1718
2189
  }
1719
2190
  function createLine(text, width, segmentIndex, start, end) {
1720
2191
  return {
@@ -1735,6 +2206,108 @@ function assertFinitePositiveLineHeight(lineHeight) {
1735
2206
  throw new TypeError("lineHeight must be finite and positive");
1736
2207
  }
1737
2208
  }
2209
+ var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
2210
+ function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
2211
+ if (typeof globalThis.OffscreenCanvas === "function") {
2212
+ return true;
2213
+ }
2214
+ try {
2215
+ const canvasModule = loadNodeCanvasModule();
2216
+ const { createCanvas } = canvasModule;
2217
+ const NodeOffscreenCanvas = class {
2218
+ canvas;
2219
+ constructor(width, height) {
2220
+ this.canvas = createCanvas(width, height);
2221
+ }
2222
+ getContext(contextId) {
2223
+ return contextId === "2d" ? this.canvas.getContext("2d") : null;
2224
+ }
2225
+ };
2226
+ globalThis.OffscreenCanvas = NodeOffscreenCanvas;
2227
+ return true;
2228
+ } catch {
2229
+ return false;
2230
+ }
2231
+ }
2232
+ function loadDefaultNodeCanvasModule() {
2233
+ return require2("@napi-rs/canvas");
2234
+ }
2235
+ var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
2236
+ function isPretextRuntimeAvailable() {
2237
+ return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
2238
+ }
2239
+ var PretextTextMeasurer = class {
2240
+ prepare(text, style2) {
2241
+ if (!isPretextRuntimeAvailable()) {
2242
+ throw new TypeError(RUNTIME_UNAVAILABLE);
2243
+ }
2244
+ validateTextStyle(style2);
2245
+ const font = toCanvasFont(style2);
2246
+ const options = {
2247
+ ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
2248
+ ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
2249
+ ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
2250
+ };
2251
+ const prepared = pretext.prepareWithSegments(text, font, options);
2252
+ return {
2253
+ text,
2254
+ font,
2255
+ style: { ...style2 },
2256
+ backend: "pretext",
2257
+ pretextPrepared: prepared
2258
+ };
2259
+ }
2260
+ layout(prepared, maxWidth, lineHeight = resolveLineHeight(prepared.style)) {
2261
+ assertFiniteNonNegative(maxWidth, "maxWidth");
2262
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
2263
+ throw new TypeError("lineHeight must be finite and positive");
2264
+ }
2265
+ const result = pretext.layoutWithLines(
2266
+ toInternalPrepared(prepared),
2267
+ maxWidth,
2268
+ lineHeight
2269
+ );
2270
+ const width = result.lines.reduce(
2271
+ (current, line) => Math.max(current, line.width),
2272
+ 0
2273
+ );
2274
+ return {
2275
+ width,
2276
+ height: result.height,
2277
+ lineHeight,
2278
+ lineCount: result.lineCount,
2279
+ lines: result.lines.map((line) => ({
2280
+ text: line.text,
2281
+ width: line.width,
2282
+ start: {
2283
+ segmentIndex: line.start.segmentIndex,
2284
+ graphemeIndex: line.start.graphemeIndex
2285
+ },
2286
+ end: {
2287
+ segmentIndex: line.end.segmentIndex,
2288
+ graphemeIndex: line.end.graphemeIndex
2289
+ }
2290
+ })),
2291
+ diagnostics: []
2292
+ };
2293
+ }
2294
+ naturalWidth(prepared) {
2295
+ return pretext.measureNaturalWidth(toInternalPrepared(prepared));
2296
+ }
2297
+ };
2298
+ function toInternalPrepared(prepared) {
2299
+ if (prepared.backend !== "pretext" || !("pretextPrepared" in prepared)) {
2300
+ throw new TypeError("prepared text was not created by PretextTextMeasurer");
2301
+ }
2302
+ return prepared.pretextPrepared;
2303
+ }
2304
+
2305
+ // src/text/default.ts
2306
+ function createDefaultTextMeasurer(options = {}) {
2307
+ const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
2308
+ installRuntime();
2309
+ return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
2310
+ }
1738
2311
 
1739
2312
  // src/labels/fit.ts
1740
2313
  function fitLabel(text, options, measurer) {
@@ -1793,6 +2366,7 @@ function computeLabelLayout(text, options, measurer) {
1793
2366
  fittedSize,
1794
2367
  padding,
1795
2368
  font: { ...options.font },
2369
+ textBackend: prepared.backend,
1796
2370
  lineHeight,
1797
2371
  lines: buildLines(textLayout, contentBox2, lineHeight),
1798
2372
  overflow,
@@ -1887,8 +2461,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1887
2461
  ...outputResult(dsl)
1888
2462
  };
1889
2463
  }
1890
- const measurer = options.textMeasurer ?? new DeterministicTextMeasurer();
2464
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1891
2465
  const routeKind = dsl.routing?.kind ?? "orthogonal";
2466
+ const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1892
2467
  const diagram = {
1893
2468
  id: options.id ?? dsl.id ?? "diagram",
1894
2469
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1896,9 +2471,14 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1896
2471
  nodes: normalizeNodes(dsl, measurer),
1897
2472
  edges: normalizeEdges(dsl),
1898
2473
  groups: normalizeGroups(dsl, measurer),
2474
+ swimlanes: normalizeSwimlanes(dsl),
1899
2475
  constraints: normalizeConstraints(dsl),
1900
2476
  diagnostics: [],
1901
- metadata: { routeKind }
2477
+ ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
2478
+ metadata: {
2479
+ routeKind,
2480
+ ...portShifting === void 0 ? {} : { portShifting }
2481
+ }
1902
2482
  };
1903
2483
  return {
1904
2484
  diagram,
@@ -1906,6 +2486,15 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1906
2486
  ...outputResult(dsl)
1907
2487
  };
1908
2488
  }
2489
+ function normalizePortShifting(portShifting) {
2490
+ if (portShifting === void 0) {
2491
+ return void 0;
2492
+ }
2493
+ return {
2494
+ ...portShifting.enabled === void 0 ? {} : { enabled: portShifting.enabled },
2495
+ ...portShifting.spacing === void 0 ? {} : { spacing: portShifting.spacing }
2496
+ };
2497
+ }
1909
2498
  function outputResult(dsl) {
1910
2499
  return dsl.output?.format === void 0 ? {} : { output: { format: dsl.output.format } };
1911
2500
  }
@@ -1915,15 +2504,24 @@ function normalizeNodes(dsl, measurer) {
1915
2504
  const label = toLabel(node?.label);
1916
2505
  const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
1917
2506
  const fittedSize = labelLayout?.fittedSize;
2507
+ const nodeCompartments = node?.compartments === void 0 ? void 0 : compartments(node.compartments);
2508
+ const compartmentWidth = nodeCompartments === void 0 ? 0 : compartmentNaturalWidth(id, label, nodeCompartments, measurer);
1918
2509
  return {
1919
2510
  id,
1920
2511
  ...label === void 0 ? {} : { label },
1921
2512
  shape: node?.shape ?? "rectangle",
1922
2513
  ...node?.position === void 0 ? {} : { position: point(node.position) },
2514
+ ...node?.style === void 0 ? {} : { style: style(node.style) },
2515
+ ...node?.ports === void 0 ? {} : { ports: normalizePorts(node.ports) },
2516
+ ...nodeCompartments === void 0 ? {} : { compartments: nodeCompartments },
1923
2517
  size: {
1924
- width: Math.max(DEFAULT_NODE_MIN_SIZE.width, fittedSize?.width ?? 0),
2518
+ width: Math.max(
2519
+ DEFAULT_NODE_MIN_SIZE.width,
2520
+ fittedSize?.width ?? 0,
2521
+ compartmentWidth
2522
+ ),
1925
2523
  height: Math.max(
1926
- DEFAULT_NODE_MIN_SIZE.height,
2524
+ nodeCompartments === void 0 ? DEFAULT_NODE_MIN_SIZE.height : compartmentHeight(nodeCompartments),
1927
2525
  fittedSize?.height ?? 0
1928
2526
  )
1929
2527
  },
@@ -1932,11 +2530,42 @@ function normalizeNodes(dsl, measurer) {
1932
2530
  };
1933
2531
  });
1934
2532
  }
2533
+ function compartmentHeight(value) {
2534
+ const rowCount = (value.stereotype === void 0 ? 0 : 1) + 1 + (value.properties?.length ?? 0) + (value.constraints?.length ?? 0);
2535
+ const rowHeight = 16;
2536
+ const verticalPadding = 20;
2537
+ return Math.max(
2538
+ DEFAULT_NODE_MIN_SIZE.height,
2539
+ rowCount * rowHeight + verticalPadding
2540
+ );
2541
+ }
2542
+ function compartmentNaturalWidth(id, label, value, measurer) {
2543
+ const rows = compartmentRows(id, label, value);
2544
+ const maxRowWidth = rows.reduce((width, row) => {
2545
+ const prepared = measurer.prepare(row, DEFAULT_FONT);
2546
+ return Math.max(width, measurer.naturalWidth(prepared));
2547
+ }, 0);
2548
+ return Math.ceil(
2549
+ maxRowWidth + DEFAULT_NODE_PADDING.left + DEFAULT_NODE_PADDING.right
2550
+ );
2551
+ }
2552
+ function compartmentRows(id, label, value) {
2553
+ return [
2554
+ ...value.stereotype === void 0 ? [] : [value.stereotype],
2555
+ value.name ?? label?.text ?? id,
2556
+ ...value.properties ?? [],
2557
+ ...value.constraints ?? []
2558
+ ];
2559
+ }
1935
2560
  function normalizeEdges(dsl) {
1936
2561
  const counts = /* @__PURE__ */ new Map();
1937
2562
  return (dsl.edges ?? []).map((edge) => {
1938
- const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? edge.source ?? "";
1939
- const targetId = typeof edge === "string" ? "" : edge.targetId ?? edge.target ?? "";
2563
+ const source = typeof edge === "string" ? void 0 : edge.source;
2564
+ const target = typeof edge === "string" ? void 0 : edge.target;
2565
+ const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? endpointNodeId(source) ?? "";
2566
+ const targetId = typeof edge === "string" ? "" : edge.targetId ?? endpointNodeId(target) ?? "";
2567
+ const sourceEndpoint = typeof edge === "string" ? { nodeId: sourceId } : endpoint(source, edge.sourceId);
2568
+ const targetEndpoint = typeof edge === "string" ? { nodeId: targetId } : endpoint(target, edge.targetId);
1940
2569
  const baseId = `${sourceId}-${targetId}`;
1941
2570
  const count = counts.get(baseId) ?? 0;
1942
2571
  counts.set(baseId, count + 1);
@@ -1944,9 +2573,96 @@ function normalizeEdges(dsl) {
1944
2573
  const label = typeof edge === "string" ? void 0 : toLabel(edge.label);
1945
2574
  return {
1946
2575
  id,
1947
- source: { nodeId: sourceId },
1948
- target: { nodeId: targetId },
1949
- ...label === void 0 ? {} : { label }
2576
+ source: sourceEndpoint,
2577
+ target: targetEndpoint,
2578
+ ...label === void 0 ? {} : { label },
2579
+ ...typeof edge === "string" || edge.style === void 0 ? {} : { style: edge.style },
2580
+ ...typeof edge === "string" || edge.arrowhead === void 0 ? {} : { arrowhead: edge.arrowhead }
2581
+ };
2582
+ });
2583
+ }
2584
+ function normalizePorts(ports) {
2585
+ return Object.keys(ports ?? {}).sort().map((id) => {
2586
+ const port = ports?.[id];
2587
+ const label = toLabel(port?.label);
2588
+ return {
2589
+ id,
2590
+ ...label === void 0 ? {} : { label },
2591
+ side: port?.side ?? "right",
2592
+ kind: port?.kind ?? "proxy",
2593
+ ...port?.order === void 0 ? {} : { order: port.order },
2594
+ ...port?.style === void 0 ? {} : { style: style(port.style) }
2595
+ };
2596
+ });
2597
+ }
2598
+ function endpoint(value, nodeIdOverride) {
2599
+ if (nodeIdOverride !== void 0) {
2600
+ return {
2601
+ nodeId: nodeIdOverride,
2602
+ ...typeof value === "object" && value.node === nodeIdOverride && value.port !== void 0 ? { portId: value.port } : {}
2603
+ };
2604
+ }
2605
+ if (value === void 0) {
2606
+ return { nodeId: "" };
2607
+ }
2608
+ if (typeof value === "string") {
2609
+ return { nodeId: value };
2610
+ }
2611
+ return {
2612
+ nodeId: value.node,
2613
+ ...value.port === void 0 ? {} : { portId: value.port }
2614
+ };
2615
+ }
2616
+ function style(value) {
2617
+ return {
2618
+ ...value.fill === void 0 ? {} : { fill: value.fill },
2619
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
2620
+ };
2621
+ }
2622
+ function compartments(value) {
2623
+ return {
2624
+ ...value.stereotype === void 0 ? {} : { stereotype: value.stereotype },
2625
+ ...value.name === void 0 ? {} : { name: value.name },
2626
+ ...value.properties === void 0 ? {} : { properties: value.properties.map(formatCompartmentEntry) },
2627
+ ...value.constraints === void 0 ? {} : { constraints: [...value.constraints] }
2628
+ };
2629
+ }
2630
+ function normalizeFrame(frame) {
2631
+ return {
2632
+ kind: frame.kind,
2633
+ ...frame.context === void 0 ? {} : { context: frame.context },
2634
+ ...frame.name === void 0 ? {} : { name: frame.name },
2635
+ titleTab: frame.titleTab,
2636
+ ...frame.style === void 0 ? {} : { style: style(frame.style) }
2637
+ };
2638
+ }
2639
+ function formatCompartmentEntry(value) {
2640
+ if (typeof value === "string") {
2641
+ return value;
2642
+ }
2643
+ const [entry] = Object.entries(value);
2644
+ if (entry === void 0) {
2645
+ return "";
2646
+ }
2647
+ return `${entry[0]}: ${entry[1]}`;
2648
+ }
2649
+ function normalizeSwimlanes(dsl) {
2650
+ return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
2651
+ const swimlane = dsl.swimlanes?.[id];
2652
+ const label = toLabel(swimlane?.label);
2653
+ return {
2654
+ id,
2655
+ ...label === void 0 ? {} : { label },
2656
+ orientation: swimlane?.orientation ?? "vertical",
2657
+ lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
2658
+ const lane = swimlane?.lanes[laneId];
2659
+ const laneLabel = toLabel(lane?.label);
2660
+ return {
2661
+ id: laneId,
2662
+ ...laneLabel === void 0 ? {} : { label: laneLabel },
2663
+ children: [...lane?.children ?? []]
2664
+ };
2665
+ })
1950
2666
  };
1951
2667
  });
1952
2668
  }
@@ -2016,18 +2732,37 @@ function validateReferences(dsl) {
2016
2732
  const diagnostics = [];
2017
2733
  const nodeIds = new Set(Object.keys(dsl.nodes));
2018
2734
  const groupIds = new Set(Object.keys(dsl.groups ?? {}));
2735
+ const swimlaneLaneIds = new Set(
2736
+ Object.entries(dsl.swimlanes ?? {}).flatMap(
2737
+ ([swimlaneId, swimlane]) => Object.keys(swimlane.lanes).map((laneId) => `${swimlaneId}.${laneId}`)
2738
+ )
2739
+ );
2019
2740
  (dsl.edges ?? []).forEach((edge, index) => {
2020
2741
  if (typeof edge === "string") {
2021
2742
  return;
2022
2743
  }
2023
- const sourceId = edge.sourceId ?? edge.source;
2024
- const targetId = edge.targetId ?? edge.target;
2744
+ const sourceId = edge.sourceId ?? endpointNodeId(edge.source);
2745
+ const targetId = edge.targetId ?? endpointNodeId(edge.target);
2746
+ const sourceEndpoint = endpoint(edge.source, edge.sourceId);
2747
+ const targetEndpoint = endpoint(edge.target, edge.targetId);
2025
2748
  if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
2026
2749
  diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
2027
2750
  }
2028
2751
  if (targetId !== void 0 && !nodeIds.has(targetId)) {
2029
2752
  diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
2030
2753
  }
2754
+ validateEndpointPort(
2755
+ dsl,
2756
+ sourceEndpoint,
2757
+ ["edges", index, "source"],
2758
+ diagnostics
2759
+ );
2760
+ validateEndpointPort(
2761
+ dsl,
2762
+ targetEndpoint,
2763
+ ["edges", index, "target"],
2764
+ diagnostics
2765
+ );
2031
2766
  });
2032
2767
  for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
2033
2768
  (group.nodes ?? []).forEach((nodeId, index) => {
@@ -2045,6 +2780,27 @@ function validateReferences(dsl) {
2045
2780
  }
2046
2781
  });
2047
2782
  }
2783
+ for (const [swimlaneId, swimlane] of Object.entries(dsl.swimlanes ?? {})) {
2784
+ for (const [laneId, lane] of Object.entries(swimlane.lanes)) {
2785
+ (lane.children ?? []).forEach((child, childIndex) => {
2786
+ if (!nodeIds.has(child)) {
2787
+ diagnostics.push(
2788
+ referenceMissing(
2789
+ [
2790
+ "swimlanes",
2791
+ swimlaneId,
2792
+ "lanes",
2793
+ laneId,
2794
+ "children",
2795
+ childIndex
2796
+ ],
2797
+ child
2798
+ )
2799
+ );
2800
+ }
2801
+ });
2802
+ }
2803
+ }
2048
2804
  (dsl.constraints ?? []).forEach((constraint, index) => {
2049
2805
  switch (constraint.kind) {
2050
2806
  case "exact-position": {
@@ -2088,7 +2844,7 @@ function validateReferences(dsl) {
2088
2844
  break;
2089
2845
  case "containment": {
2090
2846
  const container = constraint.containerId ?? constraint.container;
2091
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
2847
+ if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds, swimlaneLaneIds)) {
2092
2848
  diagnostics.push(
2093
2849
  referenceMissing(["constraints", index, "container"], container)
2094
2850
  );
@@ -2121,8 +2877,23 @@ function referenceMissing(path, id) {
2121
2877
  hint: "Define the referenced node or group id, or update this reference."
2122
2878
  };
2123
2879
  }
2124
- function hasNodeOrGroup(id, nodeIds, groupIds) {
2125
- return nodeIds.has(id) || groupIds.has(id);
2880
+ function hasNodeOrGroup(id, nodeIds, groupIds, swimlaneLaneIds = /* @__PURE__ */ new Set()) {
2881
+ return nodeIds.has(id) || groupIds.has(id) || swimlaneLaneIds.has(id);
2882
+ }
2883
+ function endpointNodeId(endpointValue) {
2884
+ if (typeof endpointValue === "string" || endpointValue === void 0) {
2885
+ return endpointValue;
2886
+ }
2887
+ return endpointValue.node;
2888
+ }
2889
+ function validateEndpointPort(dsl, endpointValue, path, diagnostics) {
2890
+ if (endpointValue.portId === void 0) {
2891
+ return;
2892
+ }
2893
+ const node = dsl.nodes[endpointValue.nodeId];
2894
+ if (node !== void 0 && node.ports?.[endpointValue.portId] === void 0) {
2895
+ diagnostics.push(referenceMissing([...path, "port"], endpointValue.portId));
2896
+ }
2126
2897
  }
2127
2898
  function toLabel(value) {
2128
2899
  if (value === void 0) {
@@ -2192,6 +2963,8 @@ function isValidEdgeId(value) {
2192
2963
  var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
2193
2964
  var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
2194
2965
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
2966
+ var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
2967
+ var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
2195
2968
  var nodeShapeSchema = zod.z.enum([
2196
2969
  "rectangle",
2197
2970
  "rounded-rectangle",
@@ -2219,18 +2992,49 @@ var labelSchema = zod.z.union([
2219
2992
  maxWidth: finiteNumberSchema.optional()
2220
2993
  })
2221
2994
  ]);
2995
+ var styleSchema = zod.z.object({
2996
+ fill: zod.z.string().optional(),
2997
+ stroke: zod.z.string().optional()
2998
+ });
2999
+ var portSideSchema = zod.z.enum(["top", "right", "bottom", "left"]);
3000
+ var portKindSchema = zod.z.enum(["proxy", "flow"]);
3001
+ var portSchema = zod.z.object({
3002
+ label: labelSchema.optional(),
3003
+ side: portSideSchema,
3004
+ kind: portKindSchema.optional(),
3005
+ order: finiteNumberSchema.optional(),
3006
+ style: styleSchema.optional()
3007
+ });
3008
+ var compartmentsSchema = zod.z.object({
3009
+ stereotype: zod.z.string().optional(),
3010
+ name: zod.z.string().optional(),
3011
+ properties: zod.z.array(zod.z.record(zod.z.string(), zod.z.string()).or(zod.z.string())).optional(),
3012
+ constraints: zod.z.array(zod.z.string()).optional()
3013
+ });
2222
3014
  var nodeSchema = zod.z.object({
2223
3015
  label: labelSchema.optional(),
2224
3016
  shape: nodeShapeSchema.optional(),
2225
- position: pointSchema.optional()
3017
+ position: pointSchema.optional(),
3018
+ style: styleSchema.optional(),
3019
+ ports: zod.z.record(zod.z.string(), portSchema).optional(),
3020
+ compartments: compartmentsSchema.optional()
2226
3021
  });
3022
+ var endpointSchema = zod.z.union([
3023
+ zod.z.string(),
3024
+ zod.z.object({
3025
+ node: zod.z.string(),
3026
+ port: zod.z.string().optional()
3027
+ })
3028
+ ]);
2227
3029
  var structuredEdgeSchema = zod.z.object({
2228
3030
  id: zod.z.string().optional(),
2229
- source: zod.z.string().optional(),
2230
- target: zod.z.string().optional(),
3031
+ source: endpointSchema.optional(),
3032
+ target: endpointSchema.optional(),
2231
3033
  sourceId: zod.z.string().optional(),
2232
3034
  targetId: zod.z.string().optional(),
2233
- label: labelSchema.optional()
3035
+ label: labelSchema.optional(),
3036
+ style: edgeStrokeStyleSchema.optional(),
3037
+ arrowhead: edgeArrowheadSchema.optional()
2234
3038
  }).superRefine((edge, context) => {
2235
3039
  if (edge.source === void 0 && edge.sourceId === void 0) {
2236
3040
  context.addIssue({
@@ -2254,6 +3058,17 @@ var groupSchema = zod.z.object({
2254
3058
  groups: zod.z.array(zod.z.string()).optional(),
2255
3059
  padding: insetsSchema.optional()
2256
3060
  });
3061
+ var swimlaneSchema = zod.z.object({
3062
+ label: labelSchema.optional(),
3063
+ orientation: zod.z.enum(["vertical", "horizontal"]).optional(),
3064
+ lanes: zod.z.record(
3065
+ zod.z.string(),
3066
+ zod.z.object({
3067
+ label: labelSchema.optional(),
3068
+ children: zod.z.array(zod.z.string()).optional()
3069
+ })
3070
+ )
3071
+ });
2257
3072
  var exactPositionConstraintSchema = zod.z.object({
2258
3073
  kind: zod.z.literal("exact-position"),
2259
3074
  target: zod.z.string().optional(),
@@ -2314,12 +3129,24 @@ var diagramDslSchema = zod.z.object({
2314
3129
  direction: directionSchema.optional()
2315
3130
  }).optional(),
2316
3131
  routing: zod.z.object({
2317
- kind: routeKindSchema.optional()
3132
+ kind: routeKindSchema.optional(),
3133
+ portShifting: zod.z.object({
3134
+ enabled: zod.z.boolean().optional(),
3135
+ spacing: finiteNumberSchema.optional()
3136
+ }).optional()
2318
3137
  }).optional(),
2319
3138
  nodes: zod.z.record(zod.z.string(), nodeSchema),
2320
3139
  edges: zod.z.array(edgeSchema).optional(),
2321
3140
  groups: zod.z.record(zod.z.string(), groupSchema).optional(),
3141
+ swimlanes: zod.z.record(zod.z.string(), swimlaneSchema).optional(),
2322
3142
  constraints: zod.z.array(constraintSchema).optional(),
3143
+ frame: zod.z.object({
3144
+ kind: zod.z.string(),
3145
+ context: zod.z.string().optional(),
3146
+ name: zod.z.string().optional(),
3147
+ titleTab: zod.z.string(),
3148
+ style: styleSchema.optional()
3149
+ }).optional(),
2323
3150
  output: zod.z.object({
2324
3151
  format: outputFormatSchema.optional()
2325
3152
  }).optional()
@@ -2516,7 +3343,8 @@ function renderDiagramDsl(source, options = {}) {
2516
3343
  return { diagnostics };
2517
3344
  }
2518
3345
  const solved = solveDiagram(normalized.diagram, {
2519
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
3346
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3347
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
2520
3348
  });
2521
3349
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
2522
3350
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -2555,6 +3383,22 @@ function renderDiagramDsl(source, options = {}) {
2555
3383
  function toSolveDiagnostic(diagnostic) {
2556
3384
  return { ...diagnostic, layer: "solve" };
2557
3385
  }
3386
+ function solvePortShiftingOption(value) {
3387
+ if (!isJsonObject(value)) {
3388
+ return {};
3389
+ }
3390
+ const portShifting = {};
3391
+ if (value.enabled === false) {
3392
+ portShifting.enabled = false;
3393
+ }
3394
+ if (typeof value.spacing === "number") {
3395
+ portShifting.spacing = value.spacing;
3396
+ }
3397
+ return { portShifting };
3398
+ }
3399
+ function isJsonObject(value) {
3400
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3401
+ }
2558
3402
  function toExportDiagnostic(diagnostic) {
2559
3403
  return { ...diagnostic, layer: "export" };
2560
3404
  }