@crazyhappyone/auto-graph 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
  }
@@ -1288,18 +1465,37 @@ function validateReferences(dsl) {
1288
1465
  const diagnostics = [];
1289
1466
  const nodeIds = new Set(Object.keys(dsl.nodes));
1290
1467
  const groupIds = new Set(Object.keys(dsl.groups ?? {}));
1468
+ const swimlaneLaneIds = new Set(
1469
+ Object.entries(dsl.swimlanes ?? {}).flatMap(
1470
+ ([swimlaneId, swimlane]) => Object.keys(swimlane.lanes).map((laneId) => `${swimlaneId}.${laneId}`)
1471
+ )
1472
+ );
1291
1473
  (dsl.edges ?? []).forEach((edge, index) => {
1292
1474
  if (typeof edge === "string") {
1293
1475
  return;
1294
1476
  }
1295
- const sourceId = edge.sourceId ?? edge.source;
1296
- const targetId = edge.targetId ?? edge.target;
1477
+ const sourceId = edge.sourceId ?? endpointNodeId(edge.source);
1478
+ const targetId = edge.targetId ?? endpointNodeId(edge.target);
1479
+ const sourceEndpoint = endpoint(edge.source, edge.sourceId);
1480
+ const targetEndpoint = endpoint(edge.target, edge.targetId);
1297
1481
  if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
1298
1482
  diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
1299
1483
  }
1300
1484
  if (targetId !== void 0 && !nodeIds.has(targetId)) {
1301
1485
  diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
1302
1486
  }
1487
+ validateEndpointPort(
1488
+ dsl,
1489
+ sourceEndpoint,
1490
+ ["edges", index, "source"],
1491
+ diagnostics
1492
+ );
1493
+ validateEndpointPort(
1494
+ dsl,
1495
+ targetEndpoint,
1496
+ ["edges", index, "target"],
1497
+ diagnostics
1498
+ );
1303
1499
  });
1304
1500
  for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
1305
1501
  (group.nodes ?? []).forEach((nodeId, index) => {
@@ -1317,6 +1513,27 @@ function validateReferences(dsl) {
1317
1513
  }
1318
1514
  });
1319
1515
  }
1516
+ for (const [swimlaneId, swimlane] of Object.entries(dsl.swimlanes ?? {})) {
1517
+ for (const [laneId, lane] of Object.entries(swimlane.lanes)) {
1518
+ (lane.children ?? []).forEach((child, childIndex) => {
1519
+ if (!nodeIds.has(child)) {
1520
+ diagnostics.push(
1521
+ referenceMissing(
1522
+ [
1523
+ "swimlanes",
1524
+ swimlaneId,
1525
+ "lanes",
1526
+ laneId,
1527
+ "children",
1528
+ childIndex
1529
+ ],
1530
+ child
1531
+ )
1532
+ );
1533
+ }
1534
+ });
1535
+ }
1536
+ }
1320
1537
  (dsl.constraints ?? []).forEach((constraint, index) => {
1321
1538
  switch (constraint.kind) {
1322
1539
  case "exact-position": {
@@ -1360,7 +1577,7 @@ function validateReferences(dsl) {
1360
1577
  break;
1361
1578
  case "containment": {
1362
1579
  const container = constraint.containerId ?? constraint.container;
1363
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
1580
+ if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds, swimlaneLaneIds)) {
1364
1581
  diagnostics.push(
1365
1582
  referenceMissing(["constraints", index, "container"], container)
1366
1583
  );
@@ -1393,8 +1610,23 @@ function referenceMissing(path, id) {
1393
1610
  hint: "Define the referenced node or group id, or update this reference."
1394
1611
  };
1395
1612
  }
1396
- function hasNodeOrGroup(id, nodeIds, groupIds) {
1397
- return nodeIds.has(id) || groupIds.has(id);
1613
+ function hasNodeOrGroup(id, nodeIds, groupIds, swimlaneLaneIds = /* @__PURE__ */ new Set()) {
1614
+ return nodeIds.has(id) || groupIds.has(id) || swimlaneLaneIds.has(id);
1615
+ }
1616
+ function endpointNodeId(endpointValue) {
1617
+ if (typeof endpointValue === "string" || endpointValue === void 0) {
1618
+ return endpointValue;
1619
+ }
1620
+ return endpointValue.node;
1621
+ }
1622
+ function validateEndpointPort(dsl, endpointValue, path, diagnostics) {
1623
+ if (endpointValue.portId === void 0) {
1624
+ return;
1625
+ }
1626
+ const node = dsl.nodes[endpointValue.nodeId];
1627
+ if (node !== void 0 && node.ports?.[endpointValue.portId] === void 0) {
1628
+ diagnostics.push(referenceMissing([...path, "port"], endpointValue.portId));
1629
+ }
1398
1630
  }
1399
1631
  function toLabel(value) {
1400
1632
  if (value === void 0) {
@@ -1423,6 +1655,8 @@ function point(value) {
1423
1655
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
1424
1656
  var routeKindSchema = z.enum(["orthogonal", "straight"]);
1425
1657
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
1658
+ var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
1659
+ var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
1426
1660
  var nodeShapeSchema = z.enum([
1427
1661
  "rectangle",
1428
1662
  "rounded-rectangle",
@@ -1450,18 +1684,49 @@ var labelSchema = z.union([
1450
1684
  maxWidth: finiteNumberSchema.optional()
1451
1685
  })
1452
1686
  ]);
1687
+ var styleSchema = z.object({
1688
+ fill: z.string().optional(),
1689
+ stroke: z.string().optional()
1690
+ });
1691
+ var portSideSchema = z.enum(["top", "right", "bottom", "left"]);
1692
+ var portKindSchema = z.enum(["proxy", "flow"]);
1693
+ var portSchema = z.object({
1694
+ label: labelSchema.optional(),
1695
+ side: portSideSchema,
1696
+ kind: portKindSchema.optional(),
1697
+ order: finiteNumberSchema.optional(),
1698
+ style: styleSchema.optional()
1699
+ });
1700
+ var compartmentsSchema = z.object({
1701
+ stereotype: z.string().optional(),
1702
+ name: z.string().optional(),
1703
+ properties: z.array(z.record(z.string(), z.string()).or(z.string())).optional(),
1704
+ constraints: z.array(z.string()).optional()
1705
+ });
1453
1706
  var nodeSchema = z.object({
1454
1707
  label: labelSchema.optional(),
1455
1708
  shape: nodeShapeSchema.optional(),
1456
- position: pointSchema.optional()
1709
+ position: pointSchema.optional(),
1710
+ style: styleSchema.optional(),
1711
+ ports: z.record(z.string(), portSchema).optional(),
1712
+ compartments: compartmentsSchema.optional()
1457
1713
  });
1714
+ var endpointSchema = z.union([
1715
+ z.string(),
1716
+ z.object({
1717
+ node: z.string(),
1718
+ port: z.string().optional()
1719
+ })
1720
+ ]);
1458
1721
  var structuredEdgeSchema = z.object({
1459
1722
  id: z.string().optional(),
1460
- source: z.string().optional(),
1461
- target: z.string().optional(),
1723
+ source: endpointSchema.optional(),
1724
+ target: endpointSchema.optional(),
1462
1725
  sourceId: z.string().optional(),
1463
1726
  targetId: z.string().optional(),
1464
- label: labelSchema.optional()
1727
+ label: labelSchema.optional(),
1728
+ style: edgeStrokeStyleSchema.optional(),
1729
+ arrowhead: edgeArrowheadSchema.optional()
1465
1730
  }).superRefine((edge, context) => {
1466
1731
  if (edge.source === void 0 && edge.sourceId === void 0) {
1467
1732
  context.addIssue({
@@ -1485,6 +1750,17 @@ var groupSchema = z.object({
1485
1750
  groups: z.array(z.string()).optional(),
1486
1751
  padding: insetsSchema.optional()
1487
1752
  });
1753
+ var swimlaneSchema = z.object({
1754
+ label: labelSchema.optional(),
1755
+ orientation: z.enum(["vertical", "horizontal"]).optional(),
1756
+ lanes: z.record(
1757
+ z.string(),
1758
+ z.object({
1759
+ label: labelSchema.optional(),
1760
+ children: z.array(z.string()).optional()
1761
+ })
1762
+ )
1763
+ });
1488
1764
  var exactPositionConstraintSchema = z.object({
1489
1765
  kind: z.literal("exact-position"),
1490
1766
  target: z.string().optional(),
@@ -1545,12 +1821,24 @@ var diagramDslSchema = z.object({
1545
1821
  direction: directionSchema.optional()
1546
1822
  }).optional(),
1547
1823
  routing: z.object({
1548
- kind: routeKindSchema.optional()
1824
+ kind: routeKindSchema.optional(),
1825
+ portShifting: z.object({
1826
+ enabled: z.boolean().optional(),
1827
+ spacing: finiteNumberSchema.optional()
1828
+ }).optional()
1549
1829
  }).optional(),
1550
1830
  nodes: z.record(z.string(), nodeSchema),
1551
1831
  edges: z.array(edgeSchema).optional(),
1552
1832
  groups: z.record(z.string(), groupSchema).optional(),
1833
+ swimlanes: z.record(z.string(), swimlaneSchema).optional(),
1553
1834
  constraints: z.array(constraintSchema).optional(),
1835
+ frame: z.object({
1836
+ kind: z.string(),
1837
+ context: z.string().optional(),
1838
+ name: z.string().optional(),
1839
+ titleTab: z.string(),
1840
+ style: styleSchema.optional()
1841
+ }).optional(),
1554
1842
  output: z.object({
1555
1843
  format: outputFormatSchema.optional()
1556
1844
  }).optional()
@@ -1826,11 +2114,12 @@ function renderArrow(edge) {
1826
2114
  height: box.height
1827
2115
  }),
1828
2116
  backgroundColor: "transparent",
2117
+ strokeStyle: edge.style ?? "solid",
1829
2118
  points: relativePoints,
1830
2119
  startBinding: { elementId: `node:${edge.source.nodeId}`, focus: 0, gap: 0 },
1831
2120
  endBinding: { elementId: `node:${edge.target.nodeId}`, focus: 0, gap: 0 },
1832
2121
  startArrowhead: null,
1833
- endArrowhead: "arrow"
2122
+ endArrowhead: mapArrowhead(edge.arrowhead)
1834
2123
  };
1835
2124
  }
1836
2125
  function renderText(id, label, box, containerId, groupIds) {
@@ -1908,6 +2197,16 @@ function mapShape(shape) {
1908
2197
  return "cylinder";
1909
2198
  }
1910
2199
  }
2200
+ function mapArrowhead(arrowhead) {
2201
+ switch (arrowhead) {
2202
+ case void 0:
2203
+ return "arrow";
2204
+ case "triangle":
2205
+ return "triangle";
2206
+ case "hollowTriangle":
2207
+ return "triangle_outline";
2208
+ }
2209
+ }
1911
2210
  function createGroupMembership(groups) {
1912
2211
  const membership = /* @__PURE__ */ new Map();
1913
2212
  for (const group of groups) {
@@ -1974,19 +2273,28 @@ function exportSvg(diagram, options = {}) {
1974
2273
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
1975
2274
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
1976
2275
  ` <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"/>`,
2276
+ ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame))],
2277
+ ...(diagram.swimlanes ?? []).flatMap(
2278
+ (swimlane) => renderSwimlane(swimlane)
2279
+ ),
1977
2280
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
1978
2281
  ...diagram.edges.flatMap((edge) => {
1979
- const path = renderEdgePath(edge.points, edge.id);
2282
+ const path = renderEdgePath(edge);
1980
2283
  if (path === void 0) {
1981
2284
  return [];
1982
2285
  }
1983
- return [indent(path), indent(renderArrowhead(edge.points, edge.id))];
2286
+ return [indent(path), indent(renderArrowhead(edge))];
1984
2287
  }),
1985
2288
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
2289
+ ...diagram.nodes.flatMap((node) => renderCompartments(node)),
2290
+ ...diagram.nodes.flatMap((node) => renderPorts(node)),
1986
2291
  ...diagram.groups.flatMap(
1987
2292
  (group) => renderLabel(group.label, group.box, group)
1988
2293
  ),
1989
- ...diagram.nodes.flatMap((node) => renderLabel(node.label, node.box, node)),
2294
+ ...diagram.nodes.flatMap(
2295
+ (node) => node.compartments === void 0 ? renderLabel(node.label, node.box, node) : []
2296
+ ),
2297
+ ...diagram.edges.flatMap((edge) => renderEdgeLabel(edge)),
1990
2298
  "</svg>"
1991
2299
  ];
1992
2300
  return `${lines.join("\n")}
@@ -1996,7 +2304,9 @@ function renderGroup2(group) {
1996
2304
  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
2305
  }
1998
2306
  function renderNode2(node) {
1999
- const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${NODE_FILL}" stroke="${STROKE}"`;
2307
+ const fill = node.style?.fill ?? NODE_FILL;
2308
+ const stroke = node.style?.stroke ?? STROKE;
2309
+ const common = `class="node node-${node.shape}" data-id="${escapeAttribute(node.id)}" fill="${escapeAttribute(fill)}" stroke="${escapeAttribute(stroke)}"`;
2000
2310
  switch (node.shape) {
2001
2311
  case "rectangle":
2002
2312
  return renderRect(node.box, common);
@@ -2012,16 +2322,111 @@ function renderNode2(node) {
2012
2322
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
2013
2323
  }
2014
2324
  }
2325
+ function renderFrame(frame) {
2326
+ const stroke = frame.style?.stroke ?? "#6b7280";
2327
+ const fill = frame.style?.fill ?? "transparent";
2328
+ return [
2329
+ `<g class="sysml-frame" data-kind="${escapeAttribute(frame.kind)}">`,
2330
+ ` <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)}"/>`,
2331
+ ` <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)}"/>`,
2332
+ ` <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>`,
2333
+ "</g>"
2334
+ ].join("\n");
2335
+ }
2336
+ function renderSwimlane(swimlane) {
2337
+ if (swimlane.box === void 0) {
2338
+ return [];
2339
+ }
2340
+ const lines = [
2341
+ ` <g class="swimlane" data-id="${escapeAttribute(swimlane.id)}">`,
2342
+ ` <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}"/>`
2343
+ ];
2344
+ for (const lane of swimlane.lanes) {
2345
+ if (lane.box === void 0) {
2346
+ continue;
2347
+ }
2348
+ lines.push(
2349
+ ` <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}"/>`
2350
+ );
2351
+ if (lane.label?.text !== void 0) {
2352
+ lines.push(
2353
+ ` <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>`
2354
+ );
2355
+ }
2356
+ }
2357
+ lines.push(" </g>");
2358
+ return lines;
2359
+ }
2360
+ function renderPorts(node) {
2361
+ return (node.ports ?? []).flatMap((port) => [
2362
+ ` <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)}"/>`,
2363
+ ...port.label?.text === void 0 ? [] : [
2364
+ ` <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>`
2365
+ ]
2366
+ ]);
2367
+ }
2368
+ function renderCompartments(node) {
2369
+ const compartments2 = node.compartments;
2370
+ if (compartments2 === void 0) {
2371
+ return [];
2372
+ }
2373
+ const rows = [
2374
+ ...compartments2.stereotype === void 0 ? [] : [{ className: "stereotype", text: compartments2.stereotype }],
2375
+ {
2376
+ className: "name",
2377
+ text: compartments2.name ?? node.label?.text ?? node.id
2378
+ },
2379
+ ...(compartments2.properties ?? []).map((text) => ({
2380
+ className: "properties",
2381
+ text
2382
+ })),
2383
+ ...(compartments2.constraints ?? []).map((text) => ({
2384
+ className: "constraints",
2385
+ text
2386
+ }))
2387
+ ];
2388
+ const lineHeight = 16;
2389
+ const lines = [
2390
+ ` <g class="compartment" data-for="${escapeAttribute(node.id)}">`
2391
+ ];
2392
+ for (let index = 0; index < rows.length; index += 1) {
2393
+ const row = rows[index];
2394
+ if (row === void 0) {
2395
+ continue;
2396
+ }
2397
+ const y = node.box.y + 18 + index * lineHeight;
2398
+ if (index > 1) {
2399
+ lines.push(
2400
+ ` <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}"/>`
2401
+ );
2402
+ }
2403
+ lines.push(
2404
+ ` <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>`
2405
+ );
2406
+ }
2407
+ lines.push(" </g>");
2408
+ return lines;
2409
+ }
2410
+ function portLabelX(x, side) {
2411
+ if (side === "left") {
2412
+ return x - 8;
2413
+ }
2414
+ if (side === "right") {
2415
+ return x + 8;
2416
+ }
2417
+ return x + 8;
2418
+ }
2015
2419
  function renderRect(box, attributes) {
2016
2420
  return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
2017
2421
  }
2018
2422
  function renderLabel(label, box, item) {
2019
2423
  const labelLayout = item.labelLayout;
2020
2424
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
2425
+ const offset = isAbsoluteLabelLayout(labelLayout.box, box) ? { x: 0, y: 0 } : { x: box.x, y: box.y };
2021
2426
  return [
2022
2427
  ` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
2023
2428
  ...labelLayout.lines.map(
2024
- (line) => ` <tspan x="${formatNumber(line.box.x)}" y="${formatNumber(line.baselineY)}">${escapeXml(line.text)}</tspan>`
2429
+ (line) => ` <tspan x="${formatNumber(offset.x + line.box.x)}" y="${formatNumber(offset.y + line.baselineY)}">${escapeXml(line.text)}</tspan>`
2025
2430
  ),
2026
2431
  " </text>"
2027
2432
  ];
@@ -2033,15 +2438,91 @@ function renderLabel(label, box, item) {
2033
2438
  ` <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
2439
  ];
2035
2440
  }
2036
- function renderEdgePath(points, id) {
2037
- if (points.length < 2) {
2441
+ function renderEdgePath(edge) {
2442
+ if (edge.points.length < 2) {
2038
2443
  return void 0;
2039
2444
  }
2040
- return `<path class="edge" data-id="${escapeAttribute(id)}" d="${formatPath(points)}" fill="none" stroke="${EDGE_STROKE}" stroke-width="1.5"/>`;
2445
+ const dash = edge.style === "dashed" ? ' stroke-dasharray="6 4"' : "";
2446
+ 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
2447
  }
2042
- function renderArrowhead(points, id) {
2448
+ function renderEdgeLabel(edge) {
2449
+ if (edge.label?.text === void 0 || edge.points.length < 2) {
2450
+ return [];
2451
+ }
2452
+ const placement = labelPlacementOnPolyline(edge.points);
2453
+ if (placement === void 0) {
2454
+ return [];
2455
+ }
2456
+ return [
2457
+ ` <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>`
2458
+ ];
2459
+ }
2460
+ function renderArrowhead(edge) {
2461
+ const arrowhead = computeArrowhead(edge.points);
2462
+ const fill = edge.arrowhead === "hollowTriangle" ? "none" : EDGE_STROKE;
2463
+ return `<polygon class="edge-arrowhead" data-edge="${escapeAttribute(edge.id)}" points="${formatPoints([arrowhead.tip, arrowhead.left, arrowhead.right])}" fill="${fill}" stroke="${EDGE_STROKE}"/>`;
2464
+ }
2465
+ function labelPlacementOnPolyline(points) {
2466
+ const segments = nonZeroSegments(points);
2467
+ const totalLength = segments.reduce(
2468
+ (sum, segment) => sum + segment.length,
2469
+ 0
2470
+ );
2471
+ if (totalLength <= 0) {
2472
+ return void 0;
2473
+ }
2474
+ let remaining = totalLength / 2;
2475
+ for (const segment of segments) {
2476
+ if (remaining <= segment.length) {
2477
+ const ratio = remaining / segment.length;
2478
+ const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
2479
+ const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
2480
+ const offset2 = labelOffset(segment);
2481
+ return { x: x + offset2.x, y: y + offset2.y };
2482
+ }
2483
+ remaining -= segment.length;
2484
+ }
2485
+ const last = segments.at(-1);
2486
+ if (last === void 0) {
2487
+ return void 0;
2488
+ }
2489
+ const offset = labelOffset(last);
2490
+ return { x: last.end.x + offset.x, y: last.end.y + offset.y };
2491
+ }
2492
+ function nonZeroSegments(points) {
2493
+ const segments = [];
2494
+ for (let index = 0; index < points.length - 1; index += 1) {
2495
+ const start = points[index];
2496
+ const end = points[index + 1];
2497
+ if (start === void 0 || end === void 0) {
2498
+ continue;
2499
+ }
2500
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
2501
+ if (length > 0) {
2502
+ segments.push({ start, end, length });
2503
+ }
2504
+ }
2505
+ return segments;
2506
+ }
2507
+ function labelOffset(segment) {
2508
+ const offset = 10;
2509
+ const dx = segment.end.x - segment.start.x;
2510
+ const dy = segment.end.y - segment.start.y;
2511
+ return {
2512
+ x: -dy / segment.length * offset,
2513
+ y: dx / segment.length * offset
2514
+ };
2515
+ }
2516
+ function pathPointsBeforeArrowhead(points) {
2043
2517
  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}"/>`;
2518
+ const base = {
2519
+ x: (arrowhead.left.x + arrowhead.right.x) / 2,
2520
+ y: (arrowhead.left.y + arrowhead.right.y) / 2
2521
+ };
2522
+ return [...points.slice(0, -1), base];
2523
+ }
2524
+ function isAbsoluteLabelLayout(labelBox, itemBox) {
2525
+ 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;
2045
2526
  }
2046
2527
  function shapePoints(shape, box) {
2047
2528
  const left = box.x;
@@ -2228,20 +2709,33 @@ function isValidDimension(value) {
2228
2709
  // src/routing/routes.ts
2229
2710
  function routeEdge(input) {
2230
2711
  const diagnostics = [];
2712
+ const defaultAnchors = defaultAnchorsForGeometry(
2713
+ input.source.box,
2714
+ input.target.box,
2715
+ input.direction
2716
+ );
2231
2717
  const source = getEdgePort(
2232
2718
  input.source,
2233
2719
  input.target.center,
2234
- input.sourceAnchor
2720
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
2235
2721
  );
2236
2722
  const target = getEdgePort(
2237
2723
  input.target,
2238
2724
  input.source.center,
2239
- input.targetAnchor
2725
+ input.targetAnchor ?? defaultAnchors.targetAnchor
2240
2726
  );
2241
2727
  if ((input.kind ?? "orthogonal") === "straight") {
2242
2728
  return { points: simplifyRoute([source, target]), diagnostics };
2243
2729
  }
2244
2730
  const candidates = orthogonalCandidates(source, target, input.direction);
2731
+ candidates.push(
2732
+ ...expandedObstacleCandidates(
2733
+ source,
2734
+ target,
2735
+ input.direction,
2736
+ input.obstacles ?? []
2737
+ )
2738
+ );
2245
2739
  for (const candidate of candidates) {
2246
2740
  if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
2247
2741
  return { points: simplifyRoute(candidate), diagnostics };
@@ -2280,27 +2774,113 @@ function simplifyRoute(points) {
2280
2774
  function orthogonalCandidates(source, target, direction) {
2281
2775
  const midpointX = (source.x + target.x) / 2;
2282
2776
  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
- ];
2777
+ const candidates = [];
2287
2778
  if (direction === "TB" || direction === "BT") {
2288
2779
  candidates.push([
2289
2780
  source,
2290
- { x: midpointX, y: source.y },
2291
- { x: midpointX, y: target.y },
2781
+ { x: source.x, y: midpointY },
2782
+ { x: target.x, y: midpointY },
2292
2783
  target
2293
2784
  ]);
2294
2785
  } else {
2295
2786
  candidates.push([
2296
2787
  source,
2297
- { x: source.x, y: midpointY },
2298
- { x: target.x, y: midpointY },
2788
+ { x: midpointX, y: source.y },
2789
+ { x: midpointX, y: target.y },
2299
2790
  target
2300
2791
  ]);
2301
2792
  }
2793
+ candidates.push(
2794
+ [source, { x: target.x, y: source.y }, target],
2795
+ [source, { x: source.x, y: target.y }, target]
2796
+ );
2302
2797
  return candidates;
2303
2798
  }
2799
+ function defaultSourceAnchor(direction) {
2800
+ switch (direction) {
2801
+ case "LR":
2802
+ return "right";
2803
+ case "RL":
2804
+ return "left";
2805
+ case "TB":
2806
+ return "bottom";
2807
+ case "BT":
2808
+ return "top";
2809
+ }
2810
+ }
2811
+ function defaultAnchorsForGeometry(source, target, direction) {
2812
+ const dx = target.x + target.width / 2 - (source.x + source.width / 2);
2813
+ const dy = target.y + target.height / 2 - (source.y + source.height / 2);
2814
+ if (Math.abs(dy) > Math.abs(dx)) {
2815
+ return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
2816
+ }
2817
+ if (Math.abs(dx) > 0) {
2818
+ return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
2819
+ }
2820
+ return {
2821
+ sourceAnchor: defaultSourceAnchor(direction),
2822
+ targetAnchor: defaultTargetAnchor(direction)
2823
+ };
2824
+ }
2825
+ function defaultTargetAnchor(direction) {
2826
+ switch (direction) {
2827
+ case "LR":
2828
+ return "left";
2829
+ case "RL":
2830
+ return "right";
2831
+ case "TB":
2832
+ return "top";
2833
+ case "BT":
2834
+ return "bottom";
2835
+ }
2836
+ }
2837
+ function expandedObstacleCandidates(source, target, direction, obstacles) {
2838
+ if (obstacles.length === 0) {
2839
+ return [];
2840
+ }
2841
+ const margin = 16;
2842
+ const candidates = [];
2843
+ if (direction === "TB" || direction === "BT") {
2844
+ const lanes = sortedUniqueLanes(
2845
+ obstacles.flatMap((obstacle) => [
2846
+ obstacle.x - margin,
2847
+ obstacle.x + obstacle.width + margin
2848
+ ]),
2849
+ (source.x + target.x) / 2
2850
+ );
2851
+ for (const laneX of lanes) {
2852
+ candidates.push([
2853
+ source,
2854
+ { x: laneX, y: source.y },
2855
+ { x: laneX, y: target.y },
2856
+ target
2857
+ ]);
2858
+ }
2859
+ } else {
2860
+ const lanes = sortedUniqueLanes(
2861
+ obstacles.flatMap((obstacle) => [
2862
+ obstacle.y - margin,
2863
+ obstacle.y + obstacle.height + margin
2864
+ ]),
2865
+ (source.y + target.y) / 2
2866
+ );
2867
+ for (const laneY of lanes) {
2868
+ candidates.push([
2869
+ source,
2870
+ { x: source.x, y: laneY },
2871
+ { x: target.x, y: laneY },
2872
+ target
2873
+ ]);
2874
+ }
2875
+ }
2876
+ return candidates;
2877
+ }
2878
+ function sortedUniqueLanes(lanes, midpoint) {
2879
+ return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
2880
+ const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
2881
+ return distance === 0 ? left - right : distance;
2882
+ });
2883
+ }
2304
2884
  function routeIntersectsObstacles(points, obstacles) {
2305
2885
  for (let index = 0; index < points.length - 1; index += 1) {
2306
2886
  const a = points[index];
@@ -2379,12 +2959,17 @@ function solveDiagram(diagram, options = {}) {
2379
2959
  options,
2380
2960
  diagnostics
2381
2961
  );
2962
+ const coordinatedSwimlanes = coordinateSwimlanes(
2963
+ diagram.swimlanes ?? [],
2964
+ constrained.boxes
2965
+ );
2382
2966
  const groupBoxes = new Map(
2383
2967
  coordinatedGroups.map((group) => [group.id, group.box])
2384
2968
  );
2385
2969
  const coordinatedEdges = coordinateEdges(
2386
2970
  edges,
2387
2971
  nodeGeometryById,
2972
+ coordinatedNodes,
2388
2973
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
2389
2974
  diagram.direction,
2390
2975
  options,
@@ -2392,8 +2977,18 @@ function solveDiagram(diagram, options = {}) {
2392
2977
  );
2393
2978
  const allBoxes = [
2394
2979
  ...coordinatedNodes.map((node) => node.box),
2395
- ...groupBoxes.values()
2980
+ ...coordinatedNodes.flatMap(
2981
+ (node) => (node.ports ?? []).flatMap(
2982
+ (port) => port.label === void 0 ? [port.box] : [port.box, portLabelBox(port)]
2983
+ )
2984
+ ),
2985
+ ...groupBoxes.values(),
2986
+ ...coordinatedSwimlanes.flatMap(
2987
+ (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
2988
+ )
2396
2989
  ];
2990
+ const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
2991
+ const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
2397
2992
  return {
2398
2993
  id: diagram.id,
2399
2994
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -2401,8 +2996,10 @@ function solveDiagram(diagram, options = {}) {
2401
2996
  nodes: coordinatedNodes,
2402
2997
  edges: coordinatedEdges,
2403
2998
  groups: coordinatedGroups,
2999
+ ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
2404
3000
  diagnostics,
2405
- bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
3001
+ bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
3002
+ ...frame === void 0 ? {} : { frame },
2406
3003
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
2407
3004
  };
2408
3005
  }
@@ -2428,6 +3025,9 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
2428
3025
  coordinated.push({
2429
3026
  id: node.id,
2430
3027
  ...node.label === void 0 ? {} : { label: node.label },
3028
+ ...node.style === void 0 ? {} : { style: node.style },
3029
+ ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
3030
+ ...node.compartments === void 0 ? {} : { compartments: node.compartments },
2431
3031
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
2432
3032
  shape: node.shape,
2433
3033
  ...node.metadata === void 0 ? {} : { metadata: node.metadata },
@@ -2438,6 +3038,142 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
2438
3038
  }
2439
3039
  return coordinated;
2440
3040
  }
3041
+ function coordinatePorts(node, nodeBox, portShifting) {
3042
+ const portsBySide = /* @__PURE__ */ new Map();
3043
+ for (const port of node.ports ?? []) {
3044
+ const ports = portsBySide.get(port.side) ?? [];
3045
+ ports.push(port);
3046
+ portsBySide.set(port.side, ports);
3047
+ }
3048
+ const coordinated = [];
3049
+ for (const [side, ports] of portsBySide) {
3050
+ const sorted = [...ports ?? []].sort((a, b) => {
3051
+ const order = (a.order ?? 0) - (b.order ?? 0);
3052
+ return order === 0 ? a.id.localeCompare(b.id) : order;
3053
+ });
3054
+ for (let index = 0; index < sorted.length; index += 1) {
3055
+ const port = sorted[index];
3056
+ if (port === void 0) {
3057
+ continue;
3058
+ }
3059
+ const anchor = portAnchor(
3060
+ nodeBox,
3061
+ side,
3062
+ index,
3063
+ sorted.length,
3064
+ portShifting
3065
+ );
3066
+ const box = portBox(anchor);
3067
+ coordinated.push({ ...port, box, anchor });
3068
+ }
3069
+ }
3070
+ return coordinated.sort((a, b) => a.id.localeCompare(b.id));
3071
+ }
3072
+ function portAnchor(nodeBox, side, index, count, portShifting) {
3073
+ const shiftingEnabled = portShifting?.enabled ?? true;
3074
+ const spacing = portShifting?.spacing ?? 24;
3075
+ const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
3076
+ switch (side) {
3077
+ case "left":
3078
+ return {
3079
+ x: nodeBox.x,
3080
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
3081
+ };
3082
+ case "right":
3083
+ return {
3084
+ x: nodeBox.x + nodeBox.width,
3085
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
3086
+ };
3087
+ case "top":
3088
+ return {
3089
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
3090
+ y: nodeBox.y
3091
+ };
3092
+ case "bottom":
3093
+ return {
3094
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
3095
+ y: nodeBox.y + nodeBox.height
3096
+ };
3097
+ }
3098
+ }
3099
+ function portBox(anchor) {
3100
+ const size = 10;
3101
+ return {
3102
+ x: anchor.x - size / 2,
3103
+ y: anchor.y - size / 2,
3104
+ width: size,
3105
+ height: size
3106
+ };
3107
+ }
3108
+ function portLabelBox(port) {
3109
+ const textWidth = Math.max(0, (port.label?.text.length ?? 0) * 6);
3110
+ const height = 12;
3111
+ const gap = 8;
3112
+ const x = port.side === "left" ? port.anchor.x - gap - textWidth : port.anchor.x + gap;
3113
+ return {
3114
+ x,
3115
+ y: port.anchor.y - 8 - height,
3116
+ width: textWidth,
3117
+ height
3118
+ };
3119
+ }
3120
+ function coordinateSwimlanes(swimlanes, nodeBoxes) {
3121
+ const titleSize = 28;
3122
+ const padding = 16;
3123
+ return swimlanes.map((swimlane) => {
3124
+ const laneBoxes = swimlane.lanes.flatMap((lane) => {
3125
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3126
+ return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
3127
+ });
3128
+ const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
3129
+ const outer = expand(laneUnion, padding, titleSize);
3130
+ const laneCount = Math.max(1, swimlane.lanes.length);
3131
+ const lanes = swimlane.lanes.map((lane, index) => {
3132
+ const box = swimlane.orientation === "vertical" ? {
3133
+ x: outer.x + outer.width / laneCount * index,
3134
+ y: outer.y,
3135
+ width: outer.width / laneCount,
3136
+ height: outer.height
3137
+ } : {
3138
+ x: outer.x,
3139
+ y: outer.y + outer.height / laneCount * index,
3140
+ width: outer.width,
3141
+ height: outer.height / laneCount
3142
+ };
3143
+ return { ...lane, box };
3144
+ });
3145
+ return { ...swimlane, lanes, box: outer };
3146
+ });
3147
+ }
3148
+ function coordinateFrame(frame, contentBounds) {
3149
+ const padding = 32;
3150
+ const titleHeight = 28;
3151
+ const titleWidth = Math.max(180, frame.titleTab.length * 7);
3152
+ const box = {
3153
+ x: contentBounds.x - padding,
3154
+ y: contentBounds.y - padding - titleHeight,
3155
+ width: contentBounds.width + padding * 2,
3156
+ height: contentBounds.height + padding * 2 + titleHeight
3157
+ };
3158
+ return {
3159
+ ...frame,
3160
+ box,
3161
+ titleBox: {
3162
+ x: box.x,
3163
+ y: box.y,
3164
+ width: Math.min(titleWidth, box.width * 0.8),
3165
+ height: titleHeight
3166
+ }
3167
+ };
3168
+ }
3169
+ function expand(box, padding, titleSize) {
3170
+ return {
3171
+ x: box.x - padding,
3172
+ y: box.y - padding - titleSize,
3173
+ width: box.width + padding * 2,
3174
+ height: box.height + padding * 2 + titleSize
3175
+ };
3176
+ }
2441
3177
  function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2442
3178
  const coordinated = [];
2443
3179
  const groupBoxes = /* @__PURE__ */ new Map();
@@ -2486,8 +3222,11 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2486
3222
  }
2487
3223
  return coordinated;
2488
3224
  }
2489
- function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
3225
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
2490
3226
  const coordinated = [];
3227
+ const coordinatedNodeById = new Map(
3228
+ coordinatedNodes.map((node) => [node.id, node])
3229
+ );
2491
3230
  for (const edge of edges) {
2492
3231
  const source = nodes.get(edge.source.nodeId);
2493
3232
  const target = nodes.get(edge.target.nodeId);
@@ -2505,11 +3244,13 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
2505
3244
  });
2506
3245
  continue;
2507
3246
  }
3247
+ const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
3248
+ const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
2508
3249
  const route = routeEdge({
2509
3250
  kind: options.routeKind ?? "orthogonal",
2510
3251
  direction,
2511
- source,
2512
- target,
3252
+ source: portGeometry(source, sourcePort),
3253
+ target: portGeometry(target, targetPort),
2513
3254
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
2514
3255
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
2515
3256
  obstacles: obstacles.filter(
@@ -2529,6 +3270,21 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
2529
3270
  }
2530
3271
  return coordinated;
2531
3272
  }
3273
+ function portGeometry(nodeGeometry, port) {
3274
+ if (port === void 0) {
3275
+ return nodeGeometry;
3276
+ }
3277
+ return {
3278
+ ...nodeGeometry,
3279
+ box: port.box,
3280
+ center: port.anchor,
3281
+ anchors: nodeGeometry.anchors.map((anchor) => ({
3282
+ name: anchor.name,
3283
+ point: port.anchor
3284
+ })),
3285
+ obstacleBox: port.box
3286
+ };
3287
+ }
2532
3288
  function stableById(items) {
2533
3289
  return [...items].sort((a, b) => a.id.localeCompare(b.id));
2534
3290
  }
@@ -2589,7 +3345,8 @@ function renderDiagramDsl(source, options = {}) {
2589
3345
  return { diagnostics };
2590
3346
  }
2591
3347
  const solved = solveDiagram(normalized.diagram, {
2592
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
3348
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3349
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
2593
3350
  });
2594
3351
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
2595
3352
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -2628,6 +3385,22 @@ function renderDiagramDsl(source, options = {}) {
2628
3385
  function toSolveDiagnostic(diagnostic) {
2629
3386
  return { ...diagnostic, layer: "solve" };
2630
3387
  }
3388
+ function solvePortShiftingOption(value) {
3389
+ if (!isJsonObject(value)) {
3390
+ return {};
3391
+ }
3392
+ const portShifting = {};
3393
+ if (value.enabled === false) {
3394
+ portShifting.enabled = false;
3395
+ }
3396
+ if (typeof value.spacing === "number") {
3397
+ portShifting.spacing = value.spacing;
3398
+ }
3399
+ return { portShifting };
3400
+ }
3401
+ function isJsonObject(value) {
3402
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3403
+ }
2631
3404
  function toExportDiagnostic(diagnostic) {
2632
3405
  return { ...diagnostic, layer: "export" };
2633
3406
  }
@@ -2761,6 +3534,6 @@ function isPointLikeRecord(value) {
2761
3534
  return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
2762
3535
  }
2763
3536
 
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 };
3537
+ 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
3538
  //# sourceMappingURL=index.js.map
2766
3539
  //# sourceMappingURL=index.js.map