@crazyhappyone/auto-graph 0.0.1 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from 'module';
1
2
  import { prepareWithSegments, layoutWithLines, measureNaturalWidth } from '@chenglou/pretext';
2
3
  import { Buffer } from 'buffer';
3
4
  import { parseDocument } from 'yaml';
@@ -820,34 +821,34 @@ function assertFiniteNonNegative(value, label) {
820
821
  throw new TypeError(`${label} must be a finite non-negative width`);
821
822
  }
822
823
  }
823
- function validateTextStyle(style) {
824
- assertFinitePositive(style.fontSize, "fontSize");
825
- if (style.lineHeight !== void 0) {
826
- assertFinitePositive(style.lineHeight, "lineHeight");
824
+ function validateTextStyle(style2) {
825
+ assertFinitePositive(style2.fontSize, "fontSize");
826
+ if (style2.lineHeight !== void 0) {
827
+ assertFinitePositive(style2.lineHeight, "lineHeight");
827
828
  }
828
- if (style.letterSpacing !== void 0 && !Number.isFinite(style.letterSpacing)) {
829
+ if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
829
830
  throw new TypeError("letterSpacing must be finite");
830
831
  }
831
832
  }
832
- function resolveLineHeight(style) {
833
- validateTextStyle(style);
834
- return style.lineHeight ?? style.fontSize * 1.2;
833
+ function resolveLineHeight(style2) {
834
+ validateTextStyle(style2);
835
+ return style2.lineHeight ?? style2.fontSize * 1.2;
835
836
  }
836
- function toCanvasFont(style) {
837
- validateTextStyle(style);
838
- const fontStyle = style.fontStyle === "italic" ? "italic " : "";
839
- const fontWeight = style.fontWeight ?? 400;
840
- return `${fontStyle}${fontWeight} ${style.fontSize}px ${style.fontFamily}`;
837
+ function toCanvasFont(style2) {
838
+ validateTextStyle(style2);
839
+ const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
840
+ const fontWeight = style2.fontWeight ?? 400;
841
+ return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
841
842
  }
842
843
 
843
844
  // src/text/fallback.ts
844
845
  var DeterministicTextMeasurer = class {
845
- prepare(text, style) {
846
- validateTextStyle(style);
846
+ prepare(text, style2) {
847
+ validateTextStyle(style2);
847
848
  return {
848
849
  text,
849
- font: toCanvasFont(style),
850
- style: { ...style },
850
+ font: toCanvasFont(style2),
851
+ style: { ...style2 },
851
852
  backend: "deterministic"
852
853
  };
853
854
  }
@@ -906,9 +907,9 @@ var DeterministicTextMeasurer = class {
906
907
  return output;
907
908
  }
908
909
  };
909
- function getCharacterWidth(style) {
910
- const letterSpacing = style.letterSpacing ?? 0;
911
- return Math.max(0, style.fontSize * 0.6 + letterSpacing);
910
+ function getCharacterWidth(style2) {
911
+ const letterSpacing = style2.letterSpacing ?? 0;
912
+ return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
912
913
  }
913
914
  function createLine(text, width, segmentIndex, start, end) {
914
915
  return {
@@ -929,27 +930,53 @@ function assertFinitePositiveLineHeight(lineHeight) {
929
930
  throw new TypeError("lineHeight must be finite and positive");
930
931
  }
931
932
  }
933
+ var require2 = createRequire(import.meta.url);
934
+ function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
935
+ if (typeof globalThis.OffscreenCanvas === "function") {
936
+ return true;
937
+ }
938
+ try {
939
+ const canvasModule = loadNodeCanvasModule();
940
+ const { createCanvas } = canvasModule;
941
+ const NodeOffscreenCanvas = class {
942
+ canvas;
943
+ constructor(width, height) {
944
+ this.canvas = createCanvas(width, height);
945
+ }
946
+ getContext(contextId) {
947
+ return contextId === "2d" ? this.canvas.getContext("2d") : null;
948
+ }
949
+ };
950
+ globalThis.OffscreenCanvas = NodeOffscreenCanvas;
951
+ return true;
952
+ } catch {
953
+ return false;
954
+ }
955
+ }
956
+ function loadDefaultNodeCanvasModule() {
957
+ return require2("@napi-rs/canvas");
958
+ }
932
959
  var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
933
960
  function isPretextRuntimeAvailable() {
934
961
  return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
935
962
  }
936
963
  var PretextTextMeasurer = class {
937
- prepare(text, style) {
964
+ prepare(text, style2) {
938
965
  if (!isPretextRuntimeAvailable()) {
939
966
  throw new TypeError(RUNTIME_UNAVAILABLE);
940
967
  }
941
- validateTextStyle(style);
942
- const font = toCanvasFont(style);
968
+ validateTextStyle(style2);
969
+ const font = toCanvasFont(style2);
943
970
  const options = {
944
- ...style.whiteSpace === void 0 ? {} : { whiteSpace: style.whiteSpace },
945
- ...style.wordBreak === void 0 ? {} : { wordBreak: style.wordBreak },
946
- ...style.letterSpacing === void 0 ? {} : { letterSpacing: style.letterSpacing }
971
+ ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
972
+ ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
973
+ ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
947
974
  };
948
975
  const prepared = prepareWithSegments(text, font, options);
949
976
  return {
950
977
  text,
951
978
  font,
952
- style: { ...style },
979
+ style: { ...style2 },
953
980
  backend: "pretext",
954
981
  pretextPrepared: prepared
955
982
  };
@@ -999,6 +1026,13 @@ function toInternalPrepared(prepared) {
999
1026
  return prepared.pretextPrepared;
1000
1027
  }
1001
1028
 
1029
+ // src/text/default.ts
1030
+ function createDefaultTextMeasurer(options = {}) {
1031
+ const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
1032
+ installRuntime();
1033
+ return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
1034
+ }
1035
+
1002
1036
  // src/labels/fit.ts
1003
1037
  function fitLabel(text, options, measurer) {
1004
1038
  return computeLabelLayout(text, options, measurer);
@@ -1065,6 +1099,7 @@ function computeLabelLayout(text, options, measurer) {
1065
1099
  fittedSize,
1066
1100
  padding,
1067
1101
  font: { ...options.font },
1102
+ textBackend: prepared.backend,
1068
1103
  lineHeight,
1069
1104
  lines: buildLines(textLayout, contentBox2, lineHeight),
1070
1105
  overflow,
@@ -1159,8 +1194,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1159
1194
  ...outputResult(dsl)
1160
1195
  };
1161
1196
  }
1162
- const measurer = options.textMeasurer ?? new DeterministicTextMeasurer();
1197
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1163
1198
  const routeKind = dsl.routing?.kind ?? "orthogonal";
1199
+ const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1164
1200
  const diagram = {
1165
1201
  id: options.id ?? dsl.id ?? "diagram",
1166
1202
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1168,9 +1204,14 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1168
1204
  nodes: normalizeNodes(dsl, measurer),
1169
1205
  edges: normalizeEdges(dsl),
1170
1206
  groups: normalizeGroups(dsl, measurer),
1207
+ swimlanes: normalizeSwimlanes(dsl),
1171
1208
  constraints: normalizeConstraints(dsl),
1172
1209
  diagnostics: [],
1173
- metadata: { routeKind }
1210
+ ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
1211
+ metadata: {
1212
+ routeKind,
1213
+ ...portShifting === void 0 ? {} : { portShifting }
1214
+ }
1174
1215
  };
1175
1216
  return {
1176
1217
  diagram,
@@ -1178,6 +1219,15 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1178
1219
  ...outputResult(dsl)
1179
1220
  };
1180
1221
  }
1222
+ function normalizePortShifting(portShifting) {
1223
+ if (portShifting === void 0) {
1224
+ return void 0;
1225
+ }
1226
+ return {
1227
+ ...portShifting.enabled === void 0 ? {} : { enabled: portShifting.enabled },
1228
+ ...portShifting.spacing === void 0 ? {} : { spacing: portShifting.spacing }
1229
+ };
1230
+ }
1181
1231
  function outputResult(dsl) {
1182
1232
  return dsl.output?.format === void 0 ? {} : { output: { format: dsl.output.format } };
1183
1233
  }
@@ -1187,15 +1237,24 @@ function normalizeNodes(dsl, measurer) {
1187
1237
  const label = toLabel(node?.label);
1188
1238
  const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
1189
1239
  const fittedSize = labelLayout?.fittedSize;
1240
+ const nodeCompartments = node?.compartments === void 0 ? void 0 : compartments(node.compartments);
1241
+ const compartmentWidth = nodeCompartments === void 0 ? 0 : compartmentNaturalWidth(id, label, nodeCompartments, measurer);
1190
1242
  return {
1191
1243
  id,
1192
1244
  ...label === void 0 ? {} : { label },
1193
1245
  shape: node?.shape ?? "rectangle",
1194
1246
  ...node?.position === void 0 ? {} : { position: point(node.position) },
1247
+ ...node?.style === void 0 ? {} : { style: style(node.style) },
1248
+ ...node?.ports === void 0 ? {} : { ports: normalizePorts(node.ports) },
1249
+ ...nodeCompartments === void 0 ? {} : { compartments: nodeCompartments },
1195
1250
  size: {
1196
- width: Math.max(DEFAULT_NODE_MIN_SIZE.width, fittedSize?.width ?? 0),
1251
+ width: Math.max(
1252
+ DEFAULT_NODE_MIN_SIZE.width,
1253
+ fittedSize?.width ?? 0,
1254
+ compartmentWidth
1255
+ ),
1197
1256
  height: Math.max(
1198
- DEFAULT_NODE_MIN_SIZE.height,
1257
+ nodeCompartments === void 0 ? DEFAULT_NODE_MIN_SIZE.height : compartmentHeight(nodeCompartments),
1199
1258
  fittedSize?.height ?? 0
1200
1259
  )
1201
1260
  },
@@ -1204,11 +1263,42 @@ function normalizeNodes(dsl, measurer) {
1204
1263
  };
1205
1264
  });
1206
1265
  }
1266
+ function compartmentHeight(value) {
1267
+ const rowCount = (value.stereotype === void 0 ? 0 : 1) + 1 + (value.properties?.length ?? 0) + (value.constraints?.length ?? 0);
1268
+ const rowHeight = 16;
1269
+ const verticalPadding = 20;
1270
+ return Math.max(
1271
+ DEFAULT_NODE_MIN_SIZE.height,
1272
+ rowCount * rowHeight + verticalPadding
1273
+ );
1274
+ }
1275
+ function compartmentNaturalWidth(id, label, value, measurer) {
1276
+ const rows = compartmentRows(id, label, value);
1277
+ const maxRowWidth = rows.reduce((width, row) => {
1278
+ const prepared = measurer.prepare(row, DEFAULT_FONT);
1279
+ return Math.max(width, measurer.naturalWidth(prepared));
1280
+ }, 0);
1281
+ return Math.ceil(
1282
+ maxRowWidth + DEFAULT_NODE_PADDING.left + DEFAULT_NODE_PADDING.right
1283
+ );
1284
+ }
1285
+ function compartmentRows(id, label, value) {
1286
+ return [
1287
+ ...value.stereotype === void 0 ? [] : [value.stereotype],
1288
+ value.name ?? label?.text ?? id,
1289
+ ...value.properties ?? [],
1290
+ ...value.constraints ?? []
1291
+ ];
1292
+ }
1207
1293
  function normalizeEdges(dsl) {
1208
1294
  const counts = /* @__PURE__ */ new Map();
1209
1295
  return (dsl.edges ?? []).map((edge) => {
1210
- const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? edge.source ?? "";
1211
- const targetId = typeof edge === "string" ? "" : edge.targetId ?? edge.target ?? "";
1296
+ const source = typeof edge === "string" ? void 0 : edge.source;
1297
+ const target = typeof edge === "string" ? void 0 : edge.target;
1298
+ const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? endpointNodeId(source) ?? "";
1299
+ const targetId = typeof edge === "string" ? "" : edge.targetId ?? endpointNodeId(target) ?? "";
1300
+ const sourceEndpoint = typeof edge === "string" ? { nodeId: sourceId } : endpoint(source, edge.sourceId);
1301
+ const targetEndpoint = typeof edge === "string" ? { nodeId: targetId } : endpoint(target, edge.targetId);
1212
1302
  const baseId = `${sourceId}-${targetId}`;
1213
1303
  const count = counts.get(baseId) ?? 0;
1214
1304
  counts.set(baseId, count + 1);
@@ -1216,9 +1306,96 @@ function normalizeEdges(dsl) {
1216
1306
  const label = typeof edge === "string" ? void 0 : toLabel(edge.label);
1217
1307
  return {
1218
1308
  id,
1219
- source: { nodeId: sourceId },
1220
- target: { nodeId: targetId },
1221
- ...label === void 0 ? {} : { label }
1309
+ source: sourceEndpoint,
1310
+ target: targetEndpoint,
1311
+ ...label === void 0 ? {} : { label },
1312
+ ...typeof edge === "string" || edge.style === void 0 ? {} : { style: edge.style },
1313
+ ...typeof edge === "string" || edge.arrowhead === void 0 ? {} : { arrowhead: edge.arrowhead }
1314
+ };
1315
+ });
1316
+ }
1317
+ function normalizePorts(ports) {
1318
+ return Object.keys(ports ?? {}).sort().map((id) => {
1319
+ const port = ports?.[id];
1320
+ const label = toLabel(port?.label);
1321
+ return {
1322
+ id,
1323
+ ...label === void 0 ? {} : { label },
1324
+ side: port?.side ?? "right",
1325
+ kind: port?.kind ?? "proxy",
1326
+ ...port?.order === void 0 ? {} : { order: port.order },
1327
+ ...port?.style === void 0 ? {} : { style: style(port.style) }
1328
+ };
1329
+ });
1330
+ }
1331
+ function endpoint(value, nodeIdOverride) {
1332
+ if (nodeIdOverride !== void 0) {
1333
+ return {
1334
+ nodeId: nodeIdOverride,
1335
+ ...typeof value === "object" && value.node === nodeIdOverride && value.port !== void 0 ? { portId: value.port } : {}
1336
+ };
1337
+ }
1338
+ if (value === void 0) {
1339
+ return { nodeId: "" };
1340
+ }
1341
+ if (typeof value === "string") {
1342
+ return { nodeId: value };
1343
+ }
1344
+ return {
1345
+ nodeId: value.node,
1346
+ ...value.port === void 0 ? {} : { portId: value.port }
1347
+ };
1348
+ }
1349
+ function style(value) {
1350
+ return {
1351
+ ...value.fill === void 0 ? {} : { fill: value.fill },
1352
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
1353
+ };
1354
+ }
1355
+ function compartments(value) {
1356
+ return {
1357
+ ...value.stereotype === void 0 ? {} : { stereotype: value.stereotype },
1358
+ ...value.name === void 0 ? {} : { name: value.name },
1359
+ ...value.properties === void 0 ? {} : { properties: value.properties.map(formatCompartmentEntry) },
1360
+ ...value.constraints === void 0 ? {} : { constraints: [...value.constraints] }
1361
+ };
1362
+ }
1363
+ function normalizeFrame(frame) {
1364
+ return {
1365
+ kind: frame.kind,
1366
+ ...frame.context === void 0 ? {} : { context: frame.context },
1367
+ ...frame.name === void 0 ? {} : { name: frame.name },
1368
+ titleTab: frame.titleTab,
1369
+ ...frame.style === void 0 ? {} : { style: style(frame.style) }
1370
+ };
1371
+ }
1372
+ function formatCompartmentEntry(value) {
1373
+ if (typeof value === "string") {
1374
+ return value;
1375
+ }
1376
+ const [entry] = Object.entries(value);
1377
+ if (entry === void 0) {
1378
+ return "";
1379
+ }
1380
+ return `${entry[0]}: ${entry[1]}`;
1381
+ }
1382
+ function normalizeSwimlanes(dsl) {
1383
+ return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
1384
+ const swimlane = dsl.swimlanes?.[id];
1385
+ const label = toLabel(swimlane?.label);
1386
+ return {
1387
+ id,
1388
+ ...label === void 0 ? {} : { label },
1389
+ orientation: swimlane?.orientation ?? "vertical",
1390
+ lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
1391
+ const lane = swimlane?.lanes[laneId];
1392
+ const laneLabel = toLabel(lane?.label);
1393
+ return {
1394
+ id: laneId,
1395
+ ...laneLabel === void 0 ? {} : { label: laneLabel },
1396
+ children: [...lane?.children ?? []]
1397
+ };
1398
+ })
1222
1399
  };
1223
1400
  });
1224
1401
  }
@@ -1292,14 +1469,28 @@ function validateReferences(dsl) {
1292
1469
  if (typeof edge === "string") {
1293
1470
  return;
1294
1471
  }
1295
- const sourceId = edge.sourceId ?? edge.source;
1296
- const targetId = edge.targetId ?? edge.target;
1472
+ const sourceId = edge.sourceId ?? endpointNodeId(edge.source);
1473
+ const targetId = edge.targetId ?? endpointNodeId(edge.target);
1474
+ const sourceEndpoint = endpoint(edge.source, edge.sourceId);
1475
+ const targetEndpoint = endpoint(edge.target, edge.targetId);
1297
1476
  if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
1298
1477
  diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
1299
1478
  }
1300
1479
  if (targetId !== void 0 && !nodeIds.has(targetId)) {
1301
1480
  diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
1302
1481
  }
1482
+ validateEndpointPort(
1483
+ dsl,
1484
+ sourceEndpoint,
1485
+ ["edges", index, "source"],
1486
+ diagnostics
1487
+ );
1488
+ validateEndpointPort(
1489
+ dsl,
1490
+ targetEndpoint,
1491
+ ["edges", index, "target"],
1492
+ diagnostics
1493
+ );
1303
1494
  });
1304
1495
  for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
1305
1496
  (group.nodes ?? []).forEach((nodeId, index) => {
@@ -1317,6 +1508,27 @@ function validateReferences(dsl) {
1317
1508
  }
1318
1509
  });
1319
1510
  }
1511
+ for (const [swimlaneId, swimlane] of Object.entries(dsl.swimlanes ?? {})) {
1512
+ for (const [laneId, lane] of Object.entries(swimlane.lanes)) {
1513
+ (lane.children ?? []).forEach((child, childIndex) => {
1514
+ if (!nodeIds.has(child)) {
1515
+ diagnostics.push(
1516
+ referenceMissing(
1517
+ [
1518
+ "swimlanes",
1519
+ swimlaneId,
1520
+ "lanes",
1521
+ laneId,
1522
+ "children",
1523
+ childIndex
1524
+ ],
1525
+ child
1526
+ )
1527
+ );
1528
+ }
1529
+ });
1530
+ }
1531
+ }
1320
1532
  (dsl.constraints ?? []).forEach((constraint, index) => {
1321
1533
  switch (constraint.kind) {
1322
1534
  case "exact-position": {
@@ -1360,10 +1572,12 @@ function validateReferences(dsl) {
1360
1572
  break;
1361
1573
  case "containment": {
1362
1574
  const container = constraint.containerId ?? constraint.container;
1363
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
1364
- diagnostics.push(
1365
- referenceMissing(["constraints", index, "container"], container)
1366
- );
1575
+ if (container !== void 0) {
1576
+ if (!nodeIds.has(container)) {
1577
+ diagnostics.push(
1578
+ referenceMissing(["constraints", index, "container"], container)
1579
+ );
1580
+ }
1367
1581
  }
1368
1582
  (constraint.childIds ?? constraint.children ?? []).forEach(
1369
1583
  (child, childIndex) => {
@@ -1393,8 +1607,23 @@ function referenceMissing(path, id) {
1393
1607
  hint: "Define the referenced node or group id, or update this reference."
1394
1608
  };
1395
1609
  }
1396
- function hasNodeOrGroup(id, nodeIds, groupIds) {
1397
- return nodeIds.has(id) || groupIds.has(id);
1610
+ function hasNodeOrGroup(id, nodeIds, groupIds, swimlaneLaneIds = /* @__PURE__ */ new Set()) {
1611
+ return nodeIds.has(id) || groupIds.has(id) || swimlaneLaneIds.has(id);
1612
+ }
1613
+ function endpointNodeId(endpointValue) {
1614
+ if (typeof endpointValue === "string" || endpointValue === void 0) {
1615
+ return endpointValue;
1616
+ }
1617
+ return endpointValue.node;
1618
+ }
1619
+ function validateEndpointPort(dsl, endpointValue, path, diagnostics) {
1620
+ if (endpointValue.portId === void 0) {
1621
+ return;
1622
+ }
1623
+ const node = dsl.nodes[endpointValue.nodeId];
1624
+ if (node !== void 0 && node.ports?.[endpointValue.portId] === void 0) {
1625
+ diagnostics.push(referenceMissing([...path, "port"], endpointValue.portId));
1626
+ }
1398
1627
  }
1399
1628
  function toLabel(value) {
1400
1629
  if (value === void 0) {
@@ -1423,6 +1652,8 @@ function point(value) {
1423
1652
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
1424
1653
  var routeKindSchema = z.enum(["orthogonal", "straight"]);
1425
1654
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
1655
+ var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
1656
+ var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
1426
1657
  var nodeShapeSchema = z.enum([
1427
1658
  "rectangle",
1428
1659
  "rounded-rectangle",
@@ -1450,18 +1681,49 @@ var labelSchema = z.union([
1450
1681
  maxWidth: finiteNumberSchema.optional()
1451
1682
  })
1452
1683
  ]);
1684
+ var styleSchema = z.object({
1685
+ fill: z.string().optional(),
1686
+ stroke: z.string().optional()
1687
+ });
1688
+ var portSideSchema = z.enum(["top", "right", "bottom", "left"]);
1689
+ var portKindSchema = z.enum(["proxy", "flow"]);
1690
+ var portSchema = z.object({
1691
+ label: labelSchema.optional(),
1692
+ side: portSideSchema,
1693
+ kind: portKindSchema.optional(),
1694
+ order: finiteNumberSchema.optional(),
1695
+ style: styleSchema.optional()
1696
+ });
1697
+ var compartmentsSchema = z.object({
1698
+ stereotype: z.string().optional(),
1699
+ name: z.string().optional(),
1700
+ properties: z.array(z.record(z.string(), z.string()).or(z.string())).optional(),
1701
+ constraints: z.array(z.string()).optional()
1702
+ });
1453
1703
  var nodeSchema = z.object({
1454
1704
  label: labelSchema.optional(),
1455
1705
  shape: nodeShapeSchema.optional(),
1456
- position: pointSchema.optional()
1706
+ position: pointSchema.optional(),
1707
+ style: styleSchema.optional(),
1708
+ ports: z.record(z.string(), portSchema).optional(),
1709
+ compartments: compartmentsSchema.optional()
1457
1710
  });
1711
+ var endpointSchema = z.union([
1712
+ z.string(),
1713
+ z.object({
1714
+ node: z.string(),
1715
+ port: z.string().optional()
1716
+ })
1717
+ ]);
1458
1718
  var structuredEdgeSchema = z.object({
1459
1719
  id: z.string().optional(),
1460
- source: z.string().optional(),
1461
- target: z.string().optional(),
1720
+ source: endpointSchema.optional(),
1721
+ target: endpointSchema.optional(),
1462
1722
  sourceId: z.string().optional(),
1463
1723
  targetId: z.string().optional(),
1464
- label: labelSchema.optional()
1724
+ label: labelSchema.optional(),
1725
+ style: edgeStrokeStyleSchema.optional(),
1726
+ arrowhead: edgeArrowheadSchema.optional()
1465
1727
  }).superRefine((edge, context) => {
1466
1728
  if (edge.source === void 0 && edge.sourceId === void 0) {
1467
1729
  context.addIssue({
@@ -1485,6 +1747,17 @@ var groupSchema = z.object({
1485
1747
  groups: z.array(z.string()).optional(),
1486
1748
  padding: insetsSchema.optional()
1487
1749
  });
1750
+ var swimlaneSchema = z.object({
1751
+ label: labelSchema.optional(),
1752
+ orientation: z.enum(["vertical", "horizontal"]).optional(),
1753
+ lanes: z.record(
1754
+ z.string(),
1755
+ z.object({
1756
+ label: labelSchema.optional(),
1757
+ children: z.array(z.string()).optional()
1758
+ })
1759
+ )
1760
+ });
1488
1761
  var exactPositionConstraintSchema = z.object({
1489
1762
  kind: z.literal("exact-position"),
1490
1763
  target: z.string().optional(),
@@ -1545,12 +1818,24 @@ var diagramDslSchema = z.object({
1545
1818
  direction: directionSchema.optional()
1546
1819
  }).optional(),
1547
1820
  routing: z.object({
1548
- kind: routeKindSchema.optional()
1821
+ kind: routeKindSchema.optional(),
1822
+ portShifting: z.object({
1823
+ enabled: z.boolean().optional(),
1824
+ spacing: finiteNumberSchema.optional()
1825
+ }).optional()
1549
1826
  }).optional(),
1550
1827
  nodes: z.record(z.string(), nodeSchema),
1551
1828
  edges: z.array(edgeSchema).optional(),
1552
1829
  groups: z.record(z.string(), groupSchema).optional(),
1830
+ swimlanes: z.record(z.string(), swimlaneSchema).optional(),
1553
1831
  constraints: z.array(constraintSchema).optional(),
1832
+ frame: z.object({
1833
+ kind: z.string(),
1834
+ context: z.string().optional(),
1835
+ name: z.string().optional(),
1836
+ titleTab: z.string(),
1837
+ style: styleSchema.optional()
1838
+ }).optional(),
1554
1839
  output: z.object({
1555
1840
  format: outputFormatSchema.optional()
1556
1841
  }).optional()
@@ -1826,11 +2111,12 @@ function renderArrow(edge) {
1826
2111
  height: box.height
1827
2112
  }),
1828
2113
  backgroundColor: "transparent",
2114
+ strokeStyle: edge.style ?? "solid",
1829
2115
  points: relativePoints,
1830
2116
  startBinding: { elementId: `node:${edge.source.nodeId}`, focus: 0, gap: 0 },
1831
2117
  endBinding: { elementId: `node:${edge.target.nodeId}`, focus: 0, gap: 0 },
1832
2118
  startArrowhead: null,
1833
- endArrowhead: "arrow"
2119
+ endArrowhead: mapArrowhead(edge.arrowhead)
1834
2120
  };
1835
2121
  }
1836
2122
  function renderText(id, label, box, containerId, groupIds) {
@@ -1908,6 +2194,16 @@ function mapShape(shape) {
1908
2194
  return "cylinder";
1909
2195
  }
1910
2196
  }
2197
+ function mapArrowhead(arrowhead) {
2198
+ switch (arrowhead) {
2199
+ case void 0:
2200
+ return "arrow";
2201
+ case "triangle":
2202
+ return "triangle";
2203
+ case "hollowTriangle":
2204
+ return "triangle_outline";
2205
+ }
2206
+ }
1911
2207
  function createGroupMembership(groups) {
1912
2208
  const membership = /* @__PURE__ */ new Map();
1913
2209
  for (const group of groups) {
@@ -1974,19 +2270,28 @@ function exportSvg(diagram, options = {}) {
1974
2270
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
1975
2271
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
1976
2272
  ` <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"/>`,
2273
+ ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame))],
2274
+ ...(diagram.swimlanes ?? []).flatMap(
2275
+ (swimlane) => renderSwimlane(swimlane)
2276
+ ),
1977
2277
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
1978
2278
  ...diagram.edges.flatMap((edge) => {
1979
- const path = renderEdgePath(edge.points, edge.id);
2279
+ const path = renderEdgePath(edge);
1980
2280
  if (path === void 0) {
1981
2281
  return [];
1982
2282
  }
1983
- return [indent(path), indent(renderArrowhead(edge.points, edge.id))];
2283
+ return [indent(path), indent(renderArrowhead(edge))];
1984
2284
  }),
1985
2285
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
2286
+ ...diagram.nodes.flatMap((node) => renderCompartments(node)),
2287
+ ...diagram.nodes.flatMap((node) => renderPorts(node)),
1986
2288
  ...diagram.groups.flatMap(
1987
2289
  (group) => renderLabel(group.label, group.box, group)
1988
2290
  ),
1989
- ...diagram.nodes.flatMap((node) => renderLabel(node.label, node.box, node)),
2291
+ ...diagram.nodes.flatMap(
2292
+ (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node) : []
2293
+ ),
2294
+ ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge)),
1990
2295
  "</svg>"
1991
2296
  ];
1992
2297
  return `${lines.join("\n")}
@@ -1996,7 +2301,9 @@ function renderGroup2(group) {
1996
2301
  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"/>`;
1997
2302
  }
1998
2303
  function renderNode2(node) {
1999
- const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${NODE_FILL}" stroke="${STROKE}"`;
2304
+ const fill = node.style?.fill ?? NODE_FILL;
2305
+ const stroke = node.style?.stroke ?? STROKE;
2306
+ const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(stroke)}"`;
2000
2307
  switch (node.shape) {
2001
2308
  case "rectangle":
2002
2309
  return renderRect(node.box, common);
@@ -2012,16 +2319,111 @@ function renderNode2(node) {
2012
2319
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
2013
2320
  }
2014
2321
  }
2322
+ function renderFrame(frame) {
2323
+ const stroke = frame.style?.stroke ?? "#6b7280";
2324
+ const fill = frame.style?.fill ?? "transparent";
2325
+ return [
2326
+ `<g class="sysml-frame" data-kind="${escapeAttribute(frame.kind)}">`,
2327
+ ` <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)}"/>`,
2328
+ ` <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)}"/>`,
2329
+ ` <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>`,
2330
+ "</g>"
2331
+ ].join("\n");
2332
+ }
2333
+ function renderSwimlane(swimlane) {
2334
+ if (swimlane.box === void 0) {
2335
+ return [];
2336
+ }
2337
+ const lines = [
2338
+ ` <g class="swimlane" data-id="${escapeAttribute(swimlane.id)}">`,
2339
+ ` <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}"/>`
2340
+ ];
2341
+ for (const lane of swimlane.lanes) {
2342
+ if (lane.box === void 0) {
2343
+ continue;
2344
+ }
2345
+ lines.push(
2346
+ ` <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}"/>`
2347
+ );
2348
+ if (lane.label?.text !== void 0) {
2349
+ lines.push(
2350
+ ` <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>`
2351
+ );
2352
+ }
2353
+ }
2354
+ lines.push(" </g>");
2355
+ return lines;
2356
+ }
2357
+ function renderPorts(node) {
2358
+ return (node.ports ?? []).flatMap((port) => [
2359
+ ` <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)}"/>`,
2360
+ ...port.label?.text === void 0 ? [] : [
2361
+ ` <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>`
2362
+ ]
2363
+ ]);
2364
+ }
2365
+ function renderCompartments(node) {
2366
+ const compartments2 = node.compartments;
2367
+ if (compartments2 === void 0) {
2368
+ return [];
2369
+ }
2370
+ const rows = [
2371
+ ...compartments2.stereotype === void 0 ? [] : [{ className: "stereotype", text: compartments2.stereotype }],
2372
+ {
2373
+ className: "name",
2374
+ text: compartments2.name ?? node.label?.text ?? node.id
2375
+ },
2376
+ ...(compartments2.properties ?? []).map((text) => ({
2377
+ className: "properties",
2378
+ text
2379
+ })),
2380
+ ...(compartments2.constraints ?? []).map((text) => ({
2381
+ className: "constraints",
2382
+ text
2383
+ }))
2384
+ ];
2385
+ const lineHeight = 16;
2386
+ const lines = [
2387
+ ` <g class="compartment" data-for="${escapeAttribute(node.id)}">`
2388
+ ];
2389
+ for (let index = 0; index < rows.length; index += 1) {
2390
+ const row = rows[index];
2391
+ if (row === void 0) {
2392
+ continue;
2393
+ }
2394
+ const y = node.box.y + 18 + index * lineHeight;
2395
+ if (index > 1) {
2396
+ lines.push(
2397
+ ` <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}"/>`
2398
+ );
2399
+ }
2400
+ lines.push(
2401
+ ` <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>`
2402
+ );
2403
+ }
2404
+ lines.push(" </g>");
2405
+ return lines;
2406
+ }
2407
+ function portLabelX(x, side) {
2408
+ if (side === "left") {
2409
+ return x - 8;
2410
+ }
2411
+ if (side === "right") {
2412
+ return x + 8;
2413
+ }
2414
+ return x + 8;
2415
+ }
2015
2416
  function renderRect(box, attributes) {
2016
2417
  return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
2017
2418
  }
2018
2419
  function renderLabel(label, box, item) {
2019
2420
  const labelLayout = item.labelLayout;
2020
2421
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
2422
+ const offset = { x: box.x, y: box.y };
2021
2423
  return [
2022
2424
  ` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
2023
2425
  ...labelLayout.lines.map(
2024
- (line) => ` <tspan x="${formatNumber(line.box.x)}" y="${formatNumber(line.baselineY)}">${escapeXml(line.text)}</tspan>`
2426
+ (line) => ` <tspan x="${formatNumber(offset.x + line.box.x)}" y="${formatNumber(offset.y + line.baselineY)}">${escapeXml(line.text)}</tspan>`
2025
2427
  ),
2026
2428
  " </text>"
2027
2429
  ];
@@ -2033,15 +2435,88 @@ function renderLabel(label, box, item) {
2033
2435
  ` <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>`
2034
2436
  ];
2035
2437
  }
2036
- function renderEdgePath(points, id) {
2037
- if (points.length < 2) {
2438
+ function renderEdgePath(edge) {
2439
+ if (edge.points.length < 2) {
2038
2440
  return void 0;
2039
2441
  }
2040
- return `<path class="edge" data-id="${escapeAttribute(id)}" d="${formatPath(points)}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"/>`;
2442
+ const dash = edge.style === "dashed" ? ' stroke-dasharray="6 4"' : "";
2443
+ return `<path class="edge" data-id="${escapeAttribute(edge.id)}" d="${formatPath(pathPointsBeforeArrowhead(edge.points))}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"${dash}/>`;
2041
2444
  }
2042
- function renderArrowhead(points, id) {
2445
+ function renderEdgeLabel(edge) {
2446
+ if (edge.label?.text === void 0 || edge.points.length < 2) {
2447
+ return [];
2448
+ }
2449
+ const placement = labelPlacementOnPolyline(edge.points);
2450
+ if (placement === void 0) {
2451
+ return [];
2452
+ }
2453
+ return [
2454
+ ` <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>`
2455
+ ];
2456
+ }
2457
+ function renderArrowhead(edge) {
2458
+ const arrowhead = computeArrowhead(edge.points);
2459
+ const fill = edge.arrowhead === "hollowTriangle" ? "none" : EDGE_STROKE;
2460
+ return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(edge.id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${fill}" stroke="${EDGE_STROKE}"/>`;
2461
+ }
2462
+ function labelPlacementOnPolyline(points) {
2463
+ const segments = nonZeroSegments(points);
2464
+ const totalLength = segments.reduce(
2465
+ (sum, segment) => sum + segment.length,
2466
+ 0
2467
+ );
2468
+ if (totalLength <= 0) {
2469
+ return void 0;
2470
+ }
2471
+ let remaining = totalLength / 2;
2472
+ for (const segment of segments) {
2473
+ if (remaining <= segment.length) {
2474
+ const ratio = remaining / segment.length;
2475
+ const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
2476
+ const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
2477
+ const offset2 = labelOffset(segment);
2478
+ return { x: x + offset2.x, y: y + offset2.y };
2479
+ }
2480
+ remaining -= segment.length;
2481
+ }
2482
+ const last = segments.at(-1);
2483
+ if (last === void 0) {
2484
+ return void 0;
2485
+ }
2486
+ const offset = labelOffset(last);
2487
+ return { x: last.end.x + offset.x, y: last.end.y + offset.y };
2488
+ }
2489
+ function nonZeroSegments(points) {
2490
+ const segments = [];
2491
+ for (let index = 0; index < points.length - 1; index += 1) {
2492
+ const start = points[index];
2493
+ const end = points[index + 1];
2494
+ if (start === void 0 || end === void 0) {
2495
+ continue;
2496
+ }
2497
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
2498
+ if (length > 0) {
2499
+ segments.push({ start, end, length });
2500
+ }
2501
+ }
2502
+ return segments;
2503
+ }
2504
+ function labelOffset(segment) {
2505
+ const offset = 10;
2506
+ const dx = segment.end.x - segment.start.x;
2507
+ const dy = segment.end.y - segment.start.y;
2508
+ return {
2509
+ x: -dy / segment.length * offset,
2510
+ y: dx / segment.length * offset
2511
+ };
2512
+ }
2513
+ function pathPointsBeforeArrowhead(points) {
2043
2514
  const arrowhead = computeArrowhead(points);
2044
- return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${EDGE_STROKE}" stroke="${EDGE_STROKE}"/>`;
2515
+ const base = {
2516
+ x: (arrowhead.left.x + arrowhead.right.x) / 2,
2517
+ y: (arrowhead.left.y + arrowhead.right.y) / 2
2518
+ };
2519
+ return [...points.slice(0, -1), base];
2045
2520
  }
2046
2521
  function shapePoints(shape, box) {
2047
2522
  const left = box.x;
@@ -2228,20 +2703,33 @@ function isValidDimension(value) {
2228
2703
  // src/routing/routes.ts
2229
2704
  function routeEdge(input) {
2230
2705
  const diagnostics = [];
2706
+ const defaultAnchors = defaultAnchorsForGeometry(
2707
+ input.source.box,
2708
+ input.target.box,
2709
+ input.direction
2710
+ );
2231
2711
  const source = getEdgePort(
2232
2712
  input.source,
2233
2713
  input.target.center,
2234
- input.sourceAnchor
2714
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
2235
2715
  );
2236
2716
  const target = getEdgePort(
2237
2717
  input.target,
2238
2718
  input.source.center,
2239
- input.targetAnchor
2719
+ input.targetAnchor ?? defaultAnchors.targetAnchor
2240
2720
  );
2241
2721
  if ((input.kind ?? "orthogonal") === "straight") {
2242
2722
  return { points: simplifyRoute([source, target]), diagnostics };
2243
2723
  }
2244
2724
  const candidates = orthogonalCandidates(source, target, input.direction);
2725
+ candidates.push(
2726
+ ...expandedObstacleCandidates(
2727
+ source,
2728
+ target,
2729
+ input.direction,
2730
+ input.obstacles ?? []
2731
+ )
2732
+ );
2245
2733
  for (const candidate of candidates) {
2246
2734
  if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
2247
2735
  return { points: simplifyRoute(candidate), diagnostics };
@@ -2280,27 +2768,113 @@ function simplifyRoute(points) {
2280
2768
  function orthogonalCandidates(source, target, direction) {
2281
2769
  const midpointX = (source.x + target.x) / 2;
2282
2770
  const midpointY = (source.y + target.y) / 2;
2283
- const candidates = [
2284
- [source, { x: target.x, y: source.y }, target],
2285
- [source, { x: source.x, y: target.y }, target]
2286
- ];
2771
+ const candidates = [];
2287
2772
  if (direction === "TB" || direction === "BT") {
2288
2773
  candidates.push([
2289
2774
  source,
2290
- { x: midpointX, y: source.y },
2291
- { x: midpointX, y: target.y },
2775
+ { x: source.x, y: midpointY },
2776
+ { x: target.x, y: midpointY },
2292
2777
  target
2293
2778
  ]);
2294
2779
  } else {
2295
2780
  candidates.push([
2296
2781
  source,
2297
- { x: source.x, y: midpointY },
2298
- { x: target.x, y: midpointY },
2782
+ { x: midpointX, y: source.y },
2783
+ { x: midpointX, y: target.y },
2299
2784
  target
2300
2785
  ]);
2301
2786
  }
2787
+ candidates.push(
2788
+ [source, { x: target.x, y: source.y }, target],
2789
+ [source, { x: source.x, y: target.y }, target]
2790
+ );
2302
2791
  return candidates;
2303
2792
  }
2793
+ function defaultSourceAnchor(direction) {
2794
+ switch (direction) {
2795
+ case "LR":
2796
+ return "right";
2797
+ case "RL":
2798
+ return "left";
2799
+ case "TB":
2800
+ return "bottom";
2801
+ case "BT":
2802
+ return "top";
2803
+ }
2804
+ }
2805
+ function defaultAnchorsForGeometry(source, target, direction) {
2806
+ const dx = target.x + target.width / 2 - (source.x + source.width / 2);
2807
+ const dy = target.y + target.height / 2 - (source.y + source.height / 2);
2808
+ if (Math.abs(dy) > Math.abs(dx)) {
2809
+ return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
2810
+ }
2811
+ if (Math.abs(dx) > 0) {
2812
+ return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
2813
+ }
2814
+ return {
2815
+ sourceAnchor: defaultSourceAnchor(direction),
2816
+ targetAnchor: defaultTargetAnchor(direction)
2817
+ };
2818
+ }
2819
+ function defaultTargetAnchor(direction) {
2820
+ switch (direction) {
2821
+ case "LR":
2822
+ return "left";
2823
+ case "RL":
2824
+ return "right";
2825
+ case "TB":
2826
+ return "top";
2827
+ case "BT":
2828
+ return "bottom";
2829
+ }
2830
+ }
2831
+ function expandedObstacleCandidates(source, target, direction, obstacles) {
2832
+ if (obstacles.length === 0) {
2833
+ return [];
2834
+ }
2835
+ const margin = 16;
2836
+ const candidates = [];
2837
+ if (direction === "TB" || direction === "BT") {
2838
+ const lanes = sortedUniqueLanes(
2839
+ obstacles.flatMap((obstacle) => [
2840
+ obstacle.x - margin,
2841
+ obstacle.x + obstacle.width + margin
2842
+ ]),
2843
+ (source.x + target.x) / 2
2844
+ );
2845
+ for (const laneX of lanes) {
2846
+ candidates.push([
2847
+ source,
2848
+ { x: laneX, y: source.y },
2849
+ { x: laneX, y: target.y },
2850
+ target
2851
+ ]);
2852
+ }
2853
+ } else {
2854
+ const lanes = sortedUniqueLanes(
2855
+ obstacles.flatMap((obstacle) => [
2856
+ obstacle.y - margin,
2857
+ obstacle.y + obstacle.height + margin
2858
+ ]),
2859
+ (source.y + target.y) / 2
2860
+ );
2861
+ for (const laneY of lanes) {
2862
+ candidates.push([
2863
+ source,
2864
+ { x: source.x, y: laneY },
2865
+ { x: target.x, y: laneY },
2866
+ target
2867
+ ]);
2868
+ }
2869
+ }
2870
+ return candidates;
2871
+ }
2872
+ function sortedUniqueLanes(lanes, midpoint) {
2873
+ return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
2874
+ const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
2875
+ return distance === 0 ? left - right : distance;
2876
+ });
2877
+ }
2304
2878
  function routeIntersectsObstacles(points, obstacles) {
2305
2879
  for (let index = 0; index < points.length - 1; index += 1) {
2306
2880
  const a = points[index];
@@ -2379,12 +2953,17 @@ function solveDiagram(diagram, options = {}) {
2379
2953
  options,
2380
2954
  diagnostics
2381
2955
  );
2956
+ const coordinatedSwimlanes = coordinateSwimlanes(
2957
+ diagram.swimlanes ?? [],
2958
+ constrained.boxes
2959
+ );
2382
2960
  const groupBoxes = new Map(
2383
2961
  coordinatedGroups.map((group) => [group.id, group.box])
2384
2962
  );
2385
2963
  const coordinatedEdges = coordinateEdges(
2386
2964
  edges,
2387
2965
  nodeGeometryById,
2966
+ coordinatedNodes,
2388
2967
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
2389
2968
  diagram.direction,
2390
2969
  options,
@@ -2392,8 +2971,18 @@ function solveDiagram(diagram, options = {}) {
2392
2971
  );
2393
2972
  const allBoxes = [
2394
2973
  ...coordinatedNodes.map((node) => node.box),
2395
- ...groupBoxes.values()
2974
+ ...coordinatedNodes.flatMap(
2975
+ (node) => (node.ports ?? []).flatMap(
2976
+ (port) => port.label === void 0 ? [port.box] : [port.box, portLabelBox(port)]
2977
+ )
2978
+ ),
2979
+ ...groupBoxes.values(),
2980
+ ...coordinatedSwimlanes.flatMap(
2981
+ (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
2982
+ )
2396
2983
  ];
2984
+ const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
2985
+ const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
2397
2986
  return {
2398
2987
  id: diagram.id,
2399
2988
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -2401,8 +2990,10 @@ function solveDiagram(diagram, options = {}) {
2401
2990
  nodes: coordinatedNodes,
2402
2991
  edges: coordinatedEdges,
2403
2992
  groups: coordinatedGroups,
2993
+ ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
2404
2994
  diagnostics,
2405
- bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
2995
+ bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
2996
+ ...frame === void 0 ? {} : { frame },
2406
2997
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
2407
2998
  };
2408
2999
  }
@@ -2428,6 +3019,9 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
2428
3019
  coordinated.push({
2429
3020
  id: node.id,
2430
3021
  ...node.label === void 0 ? {} : { label: node.label },
3022
+ ...node.style === void 0 ? {} : { style: node.style },
3023
+ ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
3024
+ ...node.compartments === void 0 ? {} : { compartments: node.compartments },
2431
3025
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
2432
3026
  shape: node.shape,
2433
3027
  ...node.metadata === void 0 ? {} : { metadata: node.metadata },
@@ -2438,6 +3032,142 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
2438
3032
  }
2439
3033
  return coordinated;
2440
3034
  }
3035
+ function coordinatePorts(node, nodeBox, portShifting) {
3036
+ const portsBySide = /* @__PURE__ */ new Map();
3037
+ for (const port of node.ports ?? []) {
3038
+ const ports = portsBySide.get(port.side) ?? [];
3039
+ ports.push(port);
3040
+ portsBySide.set(port.side, ports);
3041
+ }
3042
+ const coordinated = [];
3043
+ for (const [side, ports] of portsBySide) {
3044
+ const sorted = [...ports ?? []].sort((a, b) => {
3045
+ const order = (a.order ?? 0) - (b.order ?? 0);
3046
+ return order === 0 ? a.id.localeCompare(b.id) : order;
3047
+ });
3048
+ for (let index = 0; index < sorted.length; index += 1) {
3049
+ const port = sorted[index];
3050
+ if (port === void 0) {
3051
+ continue;
3052
+ }
3053
+ const anchor = portAnchor(
3054
+ nodeBox,
3055
+ side,
3056
+ index,
3057
+ sorted.length,
3058
+ portShifting
3059
+ );
3060
+ const box = portBox(anchor);
3061
+ coordinated.push({ ...port, box, anchor });
3062
+ }
3063
+ }
3064
+ return coordinated.sort((a, b) => a.id.localeCompare(b.id));
3065
+ }
3066
+ function portAnchor(nodeBox, side, index, count, portShifting) {
3067
+ const shiftingEnabled = portShifting?.enabled ?? true;
3068
+ const spacing = portShifting?.spacing ?? 24;
3069
+ const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
3070
+ switch (side) {
3071
+ case "left":
3072
+ return {
3073
+ x: nodeBox.x,
3074
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
3075
+ };
3076
+ case "right":
3077
+ return {
3078
+ x: nodeBox.x + nodeBox.width,
3079
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
3080
+ };
3081
+ case "top":
3082
+ return {
3083
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
3084
+ y: nodeBox.y
3085
+ };
3086
+ case "bottom":
3087
+ return {
3088
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
3089
+ y: nodeBox.y + nodeBox.height
3090
+ };
3091
+ }
3092
+ }
3093
+ function portBox(anchor) {
3094
+ const size = 10;
3095
+ return {
3096
+ x: anchor.x - size / 2,
3097
+ y: anchor.y - size / 2,
3098
+ width: size,
3099
+ height: size
3100
+ };
3101
+ }
3102
+ function portLabelBox(port) {
3103
+ const textWidth = Math.max(0, (port.label?.text.length ?? 0) * 6);
3104
+ const height = 12;
3105
+ const gap = 8;
3106
+ const x = port.side === "left" ? port.anchor.x - gap - textWidth : port.anchor.x + gap;
3107
+ return {
3108
+ x,
3109
+ y: port.anchor.y - 8 - height,
3110
+ width: textWidth,
3111
+ height
3112
+ };
3113
+ }
3114
+ function coordinateSwimlanes(swimlanes, nodeBoxes) {
3115
+ const titleSize = 28;
3116
+ const padding = 16;
3117
+ return swimlanes.map((swimlane) => {
3118
+ const laneBoxes = swimlane.lanes.flatMap((lane) => {
3119
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3120
+ return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
3121
+ });
3122
+ const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
3123
+ const outer = expand(laneUnion, padding, titleSize);
3124
+ const laneCount = Math.max(1, swimlane.lanes.length);
3125
+ const lanes = swimlane.lanes.map((lane, index) => {
3126
+ const box = swimlane.orientation === "vertical" ? {
3127
+ x: outer.x + outer.width / laneCount * index,
3128
+ y: outer.y,
3129
+ width: outer.width / laneCount,
3130
+ height: outer.height
3131
+ } : {
3132
+ x: outer.x,
3133
+ y: outer.y + outer.height / laneCount * index,
3134
+ width: outer.width,
3135
+ height: outer.height / laneCount
3136
+ };
3137
+ return { ...lane, box };
3138
+ });
3139
+ return { ...swimlane, lanes, box: outer };
3140
+ });
3141
+ }
3142
+ function coordinateFrame(frame, contentBounds) {
3143
+ const padding = 32;
3144
+ const titleHeight = 28;
3145
+ const titleWidth = Math.max(180, frame.titleTab.length * 7);
3146
+ const box = {
3147
+ x: contentBounds.x - padding,
3148
+ y: contentBounds.y - padding - titleHeight,
3149
+ width: contentBounds.width + padding * 2,
3150
+ height: contentBounds.height + padding * 2 + titleHeight
3151
+ };
3152
+ return {
3153
+ ...frame,
3154
+ box,
3155
+ titleBox: {
3156
+ x: box.x,
3157
+ y: box.y,
3158
+ width: Math.min(titleWidth, box.width * 0.8),
3159
+ height: titleHeight
3160
+ }
3161
+ };
3162
+ }
3163
+ function expand(box, padding, titleSize) {
3164
+ return {
3165
+ x: box.x - padding,
3166
+ y: box.y - padding - titleSize,
3167
+ width: box.width + padding * 2,
3168
+ height: box.height + padding * 2 + titleSize
3169
+ };
3170
+ }
2441
3171
  function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2442
3172
  const coordinated = [];
2443
3173
  const groupBoxes = /* @__PURE__ */ new Map();
@@ -2486,8 +3216,11 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2486
3216
  }
2487
3217
  return coordinated;
2488
3218
  }
2489
- function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
3219
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
2490
3220
  const coordinated = [];
3221
+ const coordinatedNodeById = new Map(
3222
+ coordinatedNodes.map((node) => [node.id, node])
3223
+ );
2491
3224
  for (const edge of edges) {
2492
3225
  const source = nodes.get(edge.source.nodeId);
2493
3226
  const target = nodes.get(edge.target.nodeId);
@@ -2505,11 +3238,13 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
2505
3238
  });
2506
3239
  continue;
2507
3240
  }
3241
+ const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
3242
+ const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
2508
3243
  const route = routeEdge({
2509
3244
  kind: options.routeKind ?? "orthogonal",
2510
3245
  direction,
2511
- source,
2512
- target,
3246
+ source: portGeometry(source, sourcePort),
3247
+ target: portGeometry(target, targetPort),
2513
3248
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
2514
3249
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
2515
3250
  obstacles: obstacles.filter(
@@ -2529,6 +3264,21 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
2529
3264
  }
2530
3265
  return coordinated;
2531
3266
  }
3267
+ function portGeometry(nodeGeometry, port) {
3268
+ if (port === void 0) {
3269
+ return nodeGeometry;
3270
+ }
3271
+ return {
3272
+ ...nodeGeometry,
3273
+ box: port.box,
3274
+ center: port.anchor,
3275
+ anchors: nodeGeometry.anchors.map((anchor) => ({
3276
+ name: anchor.name,
3277
+ point: port.anchor
3278
+ })),
3279
+ obstacleBox: port.box
3280
+ };
3281
+ }
2532
3282
  function stableById(items) {
2533
3283
  return [...items].sort((a, b) => a.id.localeCompare(b.id));
2534
3284
  }
@@ -2589,7 +3339,8 @@ function renderDiagramDsl(source, options = {}) {
2589
3339
  return { diagnostics };
2590
3340
  }
2591
3341
  const solved = solveDiagram(normalized.diagram, {
2592
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
3342
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3343
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
2593
3344
  });
2594
3345
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
2595
3346
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -2628,6 +3379,22 @@ function renderDiagramDsl(source, options = {}) {
2628
3379
  function toSolveDiagnostic(diagnostic) {
2629
3380
  return { ...diagnostic, layer: "solve" };
2630
3381
  }
3382
+ function solvePortShiftingOption(value) {
3383
+ if (!isJsonObject(value)) {
3384
+ return {};
3385
+ }
3386
+ const portShifting = {};
3387
+ if (value.enabled === false) {
3388
+ portShifting.enabled = false;
3389
+ }
3390
+ if (typeof value.spacing === "number") {
3391
+ portShifting.spacing = value.spacing;
3392
+ }
3393
+ return { portShifting };
3394
+ }
3395
+ function isJsonObject(value) {
3396
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3397
+ }
2631
3398
  function toExportDiagnostic(diagnostic) {
2632
3399
  return { ...diagnostic, layer: "export" };
2633
3400
  }
@@ -2761,6 +3528,6 @@ function isPointLikeRecord(value) {
2761
3528
  return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
2762
3529
  }
2763
3530
 
2764
- export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute, solveDiagram, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
3531
+ export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createDefaultTextMeasurer, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute, solveDiagram, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
2765
3532
  //# sourceMappingURL=index.js.map
2766
3533
  //# sourceMappingURL=index.js.map