@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.cjs CHANGED
@@ -1,11 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ var module$1 = require('module');
3
4
  var pretext = require('@chenglou/pretext');
4
5
  var buffer = require('buffer');
5
6
  var yaml = require('yaml');
6
7
  var zod = require('zod');
7
8
  var dagre = require('@dagrejs/dagre');
8
9
 
10
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
9
11
  // src/geometry/boxes.ts
10
12
  function normalizeInsets(input = 0) {
11
13
  if (typeof input === "number") {
@@ -822,34 +824,34 @@ function assertFiniteNonNegative(value, label) {
822
824
  throw new TypeError(`${label} must be a finite non-negative width`);
823
825
  }
824
826
  }
825
- function validateTextStyle(style) {
826
- assertFinitePositive(style.fontSize, "fontSize");
827
- if (style.lineHeight !== void 0) {
828
- assertFinitePositive(style.lineHeight, "lineHeight");
827
+ function validateTextStyle(style2) {
828
+ assertFinitePositive(style2.fontSize, "fontSize");
829
+ if (style2.lineHeight !== void 0) {
830
+ assertFinitePositive(style2.lineHeight, "lineHeight");
829
831
  }
830
- if (style.letterSpacing !== void 0 && !Number.isFinite(style.letterSpacing)) {
832
+ if (style2.letterSpacing !== void 0 && !Number.isFinite(style2.letterSpacing)) {
831
833
  throw new TypeError("letterSpacing must be finite");
832
834
  }
833
835
  }
834
- function resolveLineHeight(style) {
835
- validateTextStyle(style);
836
- return style.lineHeight ?? style.fontSize * 1.2;
836
+ function resolveLineHeight(style2) {
837
+ validateTextStyle(style2);
838
+ return style2.lineHeight ?? style2.fontSize * 1.2;
837
839
  }
838
- function toCanvasFont(style) {
839
- validateTextStyle(style);
840
- const fontStyle = style.fontStyle === "italic" ? "italic " : "";
841
- const fontWeight = style.fontWeight ?? 400;
842
- return `${fontStyle}${fontWeight} ${style.fontSize}px ${style.fontFamily}`;
840
+ function toCanvasFont(style2) {
841
+ validateTextStyle(style2);
842
+ const fontStyle = style2.fontStyle === "italic" ? "italic " : "";
843
+ const fontWeight = style2.fontWeight ?? 400;
844
+ return `${fontStyle}${fontWeight} ${style2.fontSize}px ${style2.fontFamily}`;
843
845
  }
844
846
 
845
847
  // src/text/fallback.ts
846
848
  var DeterministicTextMeasurer = class {
847
- prepare(text, style) {
848
- validateTextStyle(style);
849
+ prepare(text, style2) {
850
+ validateTextStyle(style2);
849
851
  return {
850
852
  text,
851
- font: toCanvasFont(style),
852
- style: { ...style },
853
+ font: toCanvasFont(style2),
854
+ style: { ...style2 },
853
855
  backend: "deterministic"
854
856
  };
855
857
  }
@@ -908,9 +910,9 @@ var DeterministicTextMeasurer = class {
908
910
  return output;
909
911
  }
910
912
  };
911
- function getCharacterWidth(style) {
912
- const letterSpacing = style.letterSpacing ?? 0;
913
- return Math.max(0, style.fontSize * 0.6 + letterSpacing);
913
+ function getCharacterWidth(style2) {
914
+ const letterSpacing = style2.letterSpacing ?? 0;
915
+ return Math.max(0, style2.fontSize * 0.6 + letterSpacing);
914
916
  }
915
917
  function createLine(text, width, segmentIndex, start, end) {
916
918
  return {
@@ -931,27 +933,53 @@ function assertFinitePositiveLineHeight(lineHeight) {
931
933
  throw new TypeError("lineHeight must be finite and positive");
932
934
  }
933
935
  }
936
+ var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
937
+ function installNodeCanvasRuntime(loadNodeCanvasModule = loadDefaultNodeCanvasModule) {
938
+ if (typeof globalThis.OffscreenCanvas === "function") {
939
+ return true;
940
+ }
941
+ try {
942
+ const canvasModule = loadNodeCanvasModule();
943
+ const { createCanvas } = canvasModule;
944
+ const NodeOffscreenCanvas = class {
945
+ canvas;
946
+ constructor(width, height) {
947
+ this.canvas = createCanvas(width, height);
948
+ }
949
+ getContext(contextId) {
950
+ return contextId === "2d" ? this.canvas.getContext("2d") : null;
951
+ }
952
+ };
953
+ globalThis.OffscreenCanvas = NodeOffscreenCanvas;
954
+ return true;
955
+ } catch {
956
+ return false;
957
+ }
958
+ }
959
+ function loadDefaultNodeCanvasModule() {
960
+ return require2("@napi-rs/canvas");
961
+ }
934
962
  var RUNTIME_UNAVAILABLE = "text.pretext.runtime-unavailable";
935
963
  function isPretextRuntimeAvailable() {
936
964
  return typeof Intl.Segmenter === "function" && typeof globalThis.OffscreenCanvas === "function";
937
965
  }
938
966
  var PretextTextMeasurer = class {
939
- prepare(text, style) {
967
+ prepare(text, style2) {
940
968
  if (!isPretextRuntimeAvailable()) {
941
969
  throw new TypeError(RUNTIME_UNAVAILABLE);
942
970
  }
943
- validateTextStyle(style);
944
- const font = toCanvasFont(style);
971
+ validateTextStyle(style2);
972
+ const font = toCanvasFont(style2);
945
973
  const options = {
946
- ...style.whiteSpace === void 0 ? {} : { whiteSpace: style.whiteSpace },
947
- ...style.wordBreak === void 0 ? {} : { wordBreak: style.wordBreak },
948
- ...style.letterSpacing === void 0 ? {} : { letterSpacing: style.letterSpacing }
974
+ ...style2.whiteSpace === void 0 ? {} : { whiteSpace: style2.whiteSpace },
975
+ ...style2.wordBreak === void 0 ? {} : { wordBreak: style2.wordBreak },
976
+ ...style2.letterSpacing === void 0 ? {} : { letterSpacing: style2.letterSpacing }
949
977
  };
950
978
  const prepared = pretext.prepareWithSegments(text, font, options);
951
979
  return {
952
980
  text,
953
981
  font,
954
- style: { ...style },
982
+ style: { ...style2 },
955
983
  backend: "pretext",
956
984
  pretextPrepared: prepared
957
985
  };
@@ -1001,6 +1029,13 @@ function toInternalPrepared(prepared) {
1001
1029
  return prepared.pretextPrepared;
1002
1030
  }
1003
1031
 
1032
+ // src/text/default.ts
1033
+ function createDefaultTextMeasurer(options = {}) {
1034
+ const installRuntime = options.installNodeCanvasRuntime ?? installNodeCanvasRuntime;
1035
+ installRuntime();
1036
+ return isPretextRuntimeAvailable() ? new PretextTextMeasurer() : new DeterministicTextMeasurer();
1037
+ }
1038
+
1004
1039
  // src/labels/fit.ts
1005
1040
  function fitLabel(text, options, measurer) {
1006
1041
  return computeLabelLayout(text, options, measurer);
@@ -1067,6 +1102,7 @@ function computeLabelLayout(text, options, measurer) {
1067
1102
  fittedSize,
1068
1103
  padding,
1069
1104
  font: { ...options.font },
1105
+ textBackend: prepared.backend,
1070
1106
  lineHeight,
1071
1107
  lines: buildLines(textLayout, contentBox2, lineHeight),
1072
1108
  overflow,
@@ -1161,8 +1197,9 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1161
1197
  ...outputResult(dsl)
1162
1198
  };
1163
1199
  }
1164
- const measurer = options.textMeasurer ?? new DeterministicTextMeasurer();
1200
+ const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1165
1201
  const routeKind = dsl.routing?.kind ?? "orthogonal";
1202
+ const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1166
1203
  const diagram = {
1167
1204
  id: options.id ?? dsl.id ?? "diagram",
1168
1205
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1170,9 +1207,14 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1170
1207
  nodes: normalizeNodes(dsl, measurer),
1171
1208
  edges: normalizeEdges(dsl),
1172
1209
  groups: normalizeGroups(dsl, measurer),
1210
+ swimlanes: normalizeSwimlanes(dsl),
1173
1211
  constraints: normalizeConstraints(dsl),
1174
1212
  diagnostics: [],
1175
- metadata: { routeKind }
1213
+ ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
1214
+ metadata: {
1215
+ routeKind,
1216
+ ...portShifting === void 0 ? {} : { portShifting }
1217
+ }
1176
1218
  };
1177
1219
  return {
1178
1220
  diagram,
@@ -1180,6 +1222,15 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1180
1222
  ...outputResult(dsl)
1181
1223
  };
1182
1224
  }
1225
+ function normalizePortShifting(portShifting) {
1226
+ if (portShifting === void 0) {
1227
+ return void 0;
1228
+ }
1229
+ return {
1230
+ ...portShifting.enabled === void 0 ? {} : { enabled: portShifting.enabled },
1231
+ ...portShifting.spacing === void 0 ? {} : { spacing: portShifting.spacing }
1232
+ };
1233
+ }
1183
1234
  function outputResult(dsl) {
1184
1235
  return dsl.output?.format === void 0 ? {} : { output: { format: dsl.output.format } };
1185
1236
  }
@@ -1189,15 +1240,24 @@ function normalizeNodes(dsl, measurer) {
1189
1240
  const label = toLabel(node?.label);
1190
1241
  const labelLayout = label === void 0 ? void 0 : fitDslLabel(label, measurer);
1191
1242
  const fittedSize = labelLayout?.fittedSize;
1243
+ const nodeCompartments = node?.compartments === void 0 ? void 0 : compartments(node.compartments);
1244
+ const compartmentWidth = nodeCompartments === void 0 ? 0 : compartmentNaturalWidth(id, label, nodeCompartments, measurer);
1192
1245
  return {
1193
1246
  id,
1194
1247
  ...label === void 0 ? {} : { label },
1195
1248
  shape: node?.shape ?? "rectangle",
1196
1249
  ...node?.position === void 0 ? {} : { position: point(node.position) },
1250
+ ...node?.style === void 0 ? {} : { style: style(node.style) },
1251
+ ...node?.ports === void 0 ? {} : { ports: normalizePorts(node.ports) },
1252
+ ...nodeCompartments === void 0 ? {} : { compartments: nodeCompartments },
1197
1253
  size: {
1198
- width: Math.max(DEFAULT_NODE_MIN_SIZE.width, fittedSize?.width ?? 0),
1254
+ width: Math.max(
1255
+ DEFAULT_NODE_MIN_SIZE.width,
1256
+ fittedSize?.width ?? 0,
1257
+ compartmentWidth
1258
+ ),
1199
1259
  height: Math.max(
1200
- DEFAULT_NODE_MIN_SIZE.height,
1260
+ nodeCompartments === void 0 ? DEFAULT_NODE_MIN_SIZE.height : compartmentHeight(nodeCompartments),
1201
1261
  fittedSize?.height ?? 0
1202
1262
  )
1203
1263
  },
@@ -1206,11 +1266,42 @@ function normalizeNodes(dsl, measurer) {
1206
1266
  };
1207
1267
  });
1208
1268
  }
1269
+ function compartmentHeight(value) {
1270
+ const rowCount = (value.stereotype === void 0 ? 0 : 1) + 1 + (value.properties?.length ?? 0) + (value.constraints?.length ?? 0);
1271
+ const rowHeight = 16;
1272
+ const verticalPadding = 20;
1273
+ return Math.max(
1274
+ DEFAULT_NODE_MIN_SIZE.height,
1275
+ rowCount * rowHeight + verticalPadding
1276
+ );
1277
+ }
1278
+ function compartmentNaturalWidth(id, label, value, measurer) {
1279
+ const rows = compartmentRows(id, label, value);
1280
+ const maxRowWidth = rows.reduce((width, row) => {
1281
+ const prepared = measurer.prepare(row, DEFAULT_FONT);
1282
+ return Math.max(width, measurer.naturalWidth(prepared));
1283
+ }, 0);
1284
+ return Math.ceil(
1285
+ maxRowWidth + DEFAULT_NODE_PADDING.left + DEFAULT_NODE_PADDING.right
1286
+ );
1287
+ }
1288
+ function compartmentRows(id, label, value) {
1289
+ return [
1290
+ ...value.stereotype === void 0 ? [] : [value.stereotype],
1291
+ value.name ?? label?.text ?? id,
1292
+ ...value.properties ?? [],
1293
+ ...value.constraints ?? []
1294
+ ];
1295
+ }
1209
1296
  function normalizeEdges(dsl) {
1210
1297
  const counts = /* @__PURE__ */ new Map();
1211
1298
  return (dsl.edges ?? []).map((edge) => {
1212
- const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? edge.source ?? "";
1213
- const targetId = typeof edge === "string" ? "" : edge.targetId ?? edge.target ?? "";
1299
+ const source = typeof edge === "string" ? void 0 : edge.source;
1300
+ const target = typeof edge === "string" ? void 0 : edge.target;
1301
+ const sourceId = typeof edge === "string" ? "" : edge.sourceId ?? endpointNodeId(source) ?? "";
1302
+ const targetId = typeof edge === "string" ? "" : edge.targetId ?? endpointNodeId(target) ?? "";
1303
+ const sourceEndpoint = typeof edge === "string" ? { nodeId: sourceId } : endpoint(source, edge.sourceId);
1304
+ const targetEndpoint = typeof edge === "string" ? { nodeId: targetId } : endpoint(target, edge.targetId);
1214
1305
  const baseId = `${sourceId}-${targetId}`;
1215
1306
  const count = counts.get(baseId) ?? 0;
1216
1307
  counts.set(baseId, count + 1);
@@ -1218,9 +1309,96 @@ function normalizeEdges(dsl) {
1218
1309
  const label = typeof edge === "string" ? void 0 : toLabel(edge.label);
1219
1310
  return {
1220
1311
  id,
1221
- source: { nodeId: sourceId },
1222
- target: { nodeId: targetId },
1223
- ...label === void 0 ? {} : { label }
1312
+ source: sourceEndpoint,
1313
+ target: targetEndpoint,
1314
+ ...label === void 0 ? {} : { label },
1315
+ ...typeof edge === "string" || edge.style === void 0 ? {} : { style: edge.style },
1316
+ ...typeof edge === "string" || edge.arrowhead === void 0 ? {} : { arrowhead: edge.arrowhead }
1317
+ };
1318
+ });
1319
+ }
1320
+ function normalizePorts(ports) {
1321
+ return Object.keys(ports ?? {}).sort().map((id) => {
1322
+ const port = ports?.[id];
1323
+ const label = toLabel(port?.label);
1324
+ return {
1325
+ id,
1326
+ ...label === void 0 ? {} : { label },
1327
+ side: port?.side ?? "right",
1328
+ kind: port?.kind ?? "proxy",
1329
+ ...port?.order === void 0 ? {} : { order: port.order },
1330
+ ...port?.style === void 0 ? {} : { style: style(port.style) }
1331
+ };
1332
+ });
1333
+ }
1334
+ function endpoint(value, nodeIdOverride) {
1335
+ if (nodeIdOverride !== void 0) {
1336
+ return {
1337
+ nodeId: nodeIdOverride,
1338
+ ...typeof value === "object" && value.node === nodeIdOverride && value.port !== void 0 ? { portId: value.port } : {}
1339
+ };
1340
+ }
1341
+ if (value === void 0) {
1342
+ return { nodeId: "" };
1343
+ }
1344
+ if (typeof value === "string") {
1345
+ return { nodeId: value };
1346
+ }
1347
+ return {
1348
+ nodeId: value.node,
1349
+ ...value.port === void 0 ? {} : { portId: value.port }
1350
+ };
1351
+ }
1352
+ function style(value) {
1353
+ return {
1354
+ ...value.fill === void 0 ? {} : { fill: value.fill },
1355
+ ...value.stroke === void 0 ? {} : { stroke: value.stroke }
1356
+ };
1357
+ }
1358
+ function compartments(value) {
1359
+ return {
1360
+ ...value.stereotype === void 0 ? {} : { stereotype: value.stereotype },
1361
+ ...value.name === void 0 ? {} : { name: value.name },
1362
+ ...value.properties === void 0 ? {} : { properties: value.properties.map(formatCompartmentEntry) },
1363
+ ...value.constraints === void 0 ? {} : { constraints: [...value.constraints] }
1364
+ };
1365
+ }
1366
+ function normalizeFrame(frame) {
1367
+ return {
1368
+ kind: frame.kind,
1369
+ ...frame.context === void 0 ? {} : { context: frame.context },
1370
+ ...frame.name === void 0 ? {} : { name: frame.name },
1371
+ titleTab: frame.titleTab,
1372
+ ...frame.style === void 0 ? {} : { style: style(frame.style) }
1373
+ };
1374
+ }
1375
+ function formatCompartmentEntry(value) {
1376
+ if (typeof value === "string") {
1377
+ return value;
1378
+ }
1379
+ const [entry] = Object.entries(value);
1380
+ if (entry === void 0) {
1381
+ return "";
1382
+ }
1383
+ return `${entry[0]}: ${entry[1]}`;
1384
+ }
1385
+ function normalizeSwimlanes(dsl) {
1386
+ return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
1387
+ const swimlane = dsl.swimlanes?.[id];
1388
+ const label = toLabel(swimlane?.label);
1389
+ return {
1390
+ id,
1391
+ ...label === void 0 ? {} : { label },
1392
+ orientation: swimlane?.orientation ?? "vertical",
1393
+ lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
1394
+ const lane = swimlane?.lanes[laneId];
1395
+ const laneLabel = toLabel(lane?.label);
1396
+ return {
1397
+ id: laneId,
1398
+ ...laneLabel === void 0 ? {} : { label: laneLabel },
1399
+ children: [...lane?.children ?? []]
1400
+ };
1401
+ })
1224
1402
  };
1225
1403
  });
1226
1404
  }
@@ -1294,14 +1472,28 @@ function validateReferences(dsl) {
1294
1472
  if (typeof edge === "string") {
1295
1473
  return;
1296
1474
  }
1297
- const sourceId = edge.sourceId ?? edge.source;
1298
- const targetId = edge.targetId ?? edge.target;
1475
+ const sourceId = edge.sourceId ?? endpointNodeId(edge.source);
1476
+ const targetId = edge.targetId ?? endpointNodeId(edge.target);
1477
+ const sourceEndpoint = endpoint(edge.source, edge.sourceId);
1478
+ const targetEndpoint = endpoint(edge.target, edge.targetId);
1299
1479
  if (sourceId !== void 0 && !nodeIds.has(sourceId)) {
1300
1480
  diagnostics.push(referenceMissing(["edges", index, "source"], sourceId));
1301
1481
  }
1302
1482
  if (targetId !== void 0 && !nodeIds.has(targetId)) {
1303
1483
  diagnostics.push(referenceMissing(["edges", index, "target"], targetId));
1304
1484
  }
1485
+ validateEndpointPort(
1486
+ dsl,
1487
+ sourceEndpoint,
1488
+ ["edges", index, "source"],
1489
+ diagnostics
1490
+ );
1491
+ validateEndpointPort(
1492
+ dsl,
1493
+ targetEndpoint,
1494
+ ["edges", index, "target"],
1495
+ diagnostics
1496
+ );
1305
1497
  });
1306
1498
  for (const [groupId, group] of Object.entries(dsl.groups ?? {})) {
1307
1499
  (group.nodes ?? []).forEach((nodeId, index) => {
@@ -1319,6 +1511,27 @@ function validateReferences(dsl) {
1319
1511
  }
1320
1512
  });
1321
1513
  }
1514
+ for (const [swimlaneId, swimlane] of Object.entries(dsl.swimlanes ?? {})) {
1515
+ for (const [laneId, lane] of Object.entries(swimlane.lanes)) {
1516
+ (lane.children ?? []).forEach((child, childIndex) => {
1517
+ if (!nodeIds.has(child)) {
1518
+ diagnostics.push(
1519
+ referenceMissing(
1520
+ [
1521
+ "swimlanes",
1522
+ swimlaneId,
1523
+ "lanes",
1524
+ laneId,
1525
+ "children",
1526
+ childIndex
1527
+ ],
1528
+ child
1529
+ )
1530
+ );
1531
+ }
1532
+ });
1533
+ }
1534
+ }
1322
1535
  (dsl.constraints ?? []).forEach((constraint, index) => {
1323
1536
  switch (constraint.kind) {
1324
1537
  case "exact-position": {
@@ -1362,10 +1575,12 @@ function validateReferences(dsl) {
1362
1575
  break;
1363
1576
  case "containment": {
1364
1577
  const container = constraint.containerId ?? constraint.container;
1365
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds)) {
1366
- diagnostics.push(
1367
- referenceMissing(["constraints", index, "container"], container)
1368
- );
1578
+ if (container !== void 0) {
1579
+ if (!nodeIds.has(container)) {
1580
+ diagnostics.push(
1581
+ referenceMissing(["constraints", index, "container"], container)
1582
+ );
1583
+ }
1369
1584
  }
1370
1585
  (constraint.childIds ?? constraint.children ?? []).forEach(
1371
1586
  (child, childIndex) => {
@@ -1395,8 +1610,23 @@ function referenceMissing(path, id) {
1395
1610
  hint: "Define the referenced node or group id, or update this reference."
1396
1611
  };
1397
1612
  }
1398
- function hasNodeOrGroup(id, nodeIds, groupIds) {
1399
- 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
+ }
1400
1630
  }
1401
1631
  function toLabel(value) {
1402
1632
  if (value === void 0) {
@@ -1425,6 +1655,8 @@ function point(value) {
1425
1655
  var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
1426
1656
  var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
1427
1657
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
1658
+ var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
1659
+ var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
1428
1660
  var nodeShapeSchema = zod.z.enum([
1429
1661
  "rectangle",
1430
1662
  "rounded-rectangle",
@@ -1452,18 +1684,49 @@ var labelSchema = zod.z.union([
1452
1684
  maxWidth: finiteNumberSchema.optional()
1453
1685
  })
1454
1686
  ]);
1687
+ var styleSchema = zod.z.object({
1688
+ fill: zod.z.string().optional(),
1689
+ stroke: zod.z.string().optional()
1690
+ });
1691
+ var portSideSchema = zod.z.enum(["top", "right", "bottom", "left"]);
1692
+ var portKindSchema = zod.z.enum(["proxy", "flow"]);
1693
+ var portSchema = zod.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 = zod.z.object({
1701
+ stereotype: zod.z.string().optional(),
1702
+ name: zod.z.string().optional(),
1703
+ properties: zod.z.array(zod.z.record(zod.z.string(), zod.z.string()).or(zod.z.string())).optional(),
1704
+ constraints: zod.z.array(zod.z.string()).optional()
1705
+ });
1455
1706
  var nodeSchema = zod.z.object({
1456
1707
  label: labelSchema.optional(),
1457
1708
  shape: nodeShapeSchema.optional(),
1458
- position: pointSchema.optional()
1709
+ position: pointSchema.optional(),
1710
+ style: styleSchema.optional(),
1711
+ ports: zod.z.record(zod.z.string(), portSchema).optional(),
1712
+ compartments: compartmentsSchema.optional()
1459
1713
  });
1714
+ var endpointSchema = zod.z.union([
1715
+ zod.z.string(),
1716
+ zod.z.object({
1717
+ node: zod.z.string(),
1718
+ port: zod.z.string().optional()
1719
+ })
1720
+ ]);
1460
1721
  var structuredEdgeSchema = zod.z.object({
1461
1722
  id: zod.z.string().optional(),
1462
- source: zod.z.string().optional(),
1463
- target: zod.z.string().optional(),
1723
+ source: endpointSchema.optional(),
1724
+ target: endpointSchema.optional(),
1464
1725
  sourceId: zod.z.string().optional(),
1465
1726
  targetId: zod.z.string().optional(),
1466
- label: labelSchema.optional()
1727
+ label: labelSchema.optional(),
1728
+ style: edgeStrokeStyleSchema.optional(),
1729
+ arrowhead: edgeArrowheadSchema.optional()
1467
1730
  }).superRefine((edge, context) => {
1468
1731
  if (edge.source === void 0 && edge.sourceId === void 0) {
1469
1732
  context.addIssue({
@@ -1487,6 +1750,17 @@ var groupSchema = zod.z.object({
1487
1750
  groups: zod.z.array(zod.z.string()).optional(),
1488
1751
  padding: insetsSchema.optional()
1489
1752
  });
1753
+ var swimlaneSchema = zod.z.object({
1754
+ label: labelSchema.optional(),
1755
+ orientation: zod.z.enum(["vertical", "horizontal"]).optional(),
1756
+ lanes: zod.z.record(
1757
+ zod.z.string(),
1758
+ zod.z.object({
1759
+ label: labelSchema.optional(),
1760
+ children: zod.z.array(zod.z.string()).optional()
1761
+ })
1762
+ )
1763
+ });
1490
1764
  var exactPositionConstraintSchema = zod.z.object({
1491
1765
  kind: zod.z.literal("exact-position"),
1492
1766
  target: zod.z.string().optional(),
@@ -1547,12 +1821,24 @@ var diagramDslSchema = zod.z.object({
1547
1821
  direction: directionSchema.optional()
1548
1822
  }).optional(),
1549
1823
  routing: zod.z.object({
1550
- kind: routeKindSchema.optional()
1824
+ kind: routeKindSchema.optional(),
1825
+ portShifting: zod.z.object({
1826
+ enabled: zod.z.boolean().optional(),
1827
+ spacing: finiteNumberSchema.optional()
1828
+ }).optional()
1551
1829
  }).optional(),
1552
1830
  nodes: zod.z.record(zod.z.string(), nodeSchema),
1553
1831
  edges: zod.z.array(edgeSchema).optional(),
1554
1832
  groups: zod.z.record(zod.z.string(), groupSchema).optional(),
1833
+ swimlanes: zod.z.record(zod.z.string(), swimlaneSchema).optional(),
1555
1834
  constraints: zod.z.array(constraintSchema).optional(),
1835
+ frame: zod.z.object({
1836
+ kind: zod.z.string(),
1837
+ context: zod.z.string().optional(),
1838
+ name: zod.z.string().optional(),
1839
+ titleTab: zod.z.string(),
1840
+ style: styleSchema.optional()
1841
+ }).optional(),
1556
1842
  output: zod.z.object({
1557
1843
  format: outputFormatSchema.optional()
1558
1844
  }).optional()
@@ -1828,11 +2114,12 @@ function renderArrow(edge) {
1828
2114
  height: box.height
1829
2115
  }),
1830
2116
  backgroundColor: "transparent",
2117
+ strokeStyle: edge.style ?? "solid",
1831
2118
  points: relativePoints,
1832
2119
  startBinding: { elementId: `node:${edge.source.nodeId}`, focus: 0, gap: 0 },
1833
2120
  endBinding: { elementId: `node:${edge.target.nodeId}`, focus: 0, gap: 0 },
1834
2121
  startArrowhead: null,
1835
- endArrowhead: "arrow"
2122
+ endArrowhead: mapArrowhead(edge.arrowhead)
1836
2123
  };
1837
2124
  }
1838
2125
  function renderText(id, label, box, containerId, groupIds) {
@@ -1910,6 +2197,16 @@ function mapShape(shape) {
1910
2197
  return "cylinder";
1911
2198
  }
1912
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
+ }
1913
2210
  function createGroupMembership(groups) {
1914
2211
  const membership = /* @__PURE__ */ new Map();
1915
2212
  for (const group of groups) {
@@ -1976,19 +2273,28 @@ function exportSvg(diagram, options = {}) {
1976
2273
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
1977
2274
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
1978
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
+ ),
1979
2280
  ...diagram.groups.map((group) => indent(renderGroup2(group))),
1980
2281
  ...diagram.edges.flatMap((edge) => {
1981
- const path = renderEdgePath(edge.points, edge.id);
2282
+ const path = renderEdgePath(edge);
1982
2283
  if (path === void 0) {
1983
2284
  return [];
1984
2285
  }
1985
- return [indent(path), indent(renderArrowhead(edge.points, edge.id))];
2286
+ return [indent(path), indent(renderArrowhead(edge))];
1986
2287
  }),
1987
2288
  ...diagram.nodes.map((node) => indent(renderNode2(node))),
2289
+ ...diagram.nodes.flatMap((node) => renderCompartments(node)),
2290
+ ...diagram.nodes.flatMap((node) => renderPorts(node)),
1988
2291
  ...diagram.groups.flatMap(
1989
2292
  (group) => renderLabel(group.label, group.box, group)
1990
2293
  ),
1991
- ...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)),
1992
2298
  "</svg>"
1993
2299
  ];
1994
2300
  return `${lines.join("\n")}
@@ -1998,7 +2304,9 @@ function renderGroup2(group) {
1998
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"/>`;
1999
2305
  }
2000
2306
  function renderNode2(node) {
2001
- 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)}"`;
2002
2310
  switch (node.shape) {
2003
2311
  case "rectangle":
2004
2312
  return renderRect(node.box, common);
@@ -2014,16 +2322,111 @@ function renderNode2(node) {
2014
2322
  return `<path ${common} d="${formatCylinderPath(node.box)}"/>`;
2015
2323
  }
2016
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
+ }
2017
2419
  function renderRect(box, attributes) {
2018
2420
  return `<rect ${attributes} x="${formatNumber(box.x)}" y="${formatNumber(box.y)}" width="${formatNumber(box.width)}" height="${formatNumber(box.height)}"/>`;
2019
2421
  }
2020
2422
  function renderLabel(label, box, item) {
2021
2423
  const labelLayout = item.labelLayout;
2022
2424
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
2425
+ const offset = { x: box.x, y: box.y };
2023
2426
  return [
2024
2427
  ` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
2025
2428
  ...labelLayout.lines.map(
2026
- (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>`
2027
2430
  ),
2028
2431
  " </text>"
2029
2432
  ];
@@ -2035,15 +2438,88 @@ function renderLabel(label, box, item) {
2035
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>`
2036
2439
  ];
2037
2440
  }
2038
- function renderEdgePath(points, id) {
2039
- if (points.length < 2) {
2441
+ function renderEdgePath(edge) {
2442
+ if (edge.points.length < 2) {
2040
2443
  return void 0;
2041
2444
  }
2042
- 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}/>`;
2043
2447
  }
2044
- 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) {
2045
2517
  const arrowhead = computeArrowhead(points);
2046
- 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];
2047
2523
  }
2048
2524
  function shapePoints(shape, box) {
2049
2525
  const left = box.x;
@@ -2230,20 +2706,33 @@ function isValidDimension(value) {
2230
2706
  // src/routing/routes.ts
2231
2707
  function routeEdge(input) {
2232
2708
  const diagnostics = [];
2709
+ const defaultAnchors = defaultAnchorsForGeometry(
2710
+ input.source.box,
2711
+ input.target.box,
2712
+ input.direction
2713
+ );
2233
2714
  const source = getEdgePort(
2234
2715
  input.source,
2235
2716
  input.target.center,
2236
- input.sourceAnchor
2717
+ input.sourceAnchor ?? defaultAnchors.sourceAnchor
2237
2718
  );
2238
2719
  const target = getEdgePort(
2239
2720
  input.target,
2240
2721
  input.source.center,
2241
- input.targetAnchor
2722
+ input.targetAnchor ?? defaultAnchors.targetAnchor
2242
2723
  );
2243
2724
  if ((input.kind ?? "orthogonal") === "straight") {
2244
2725
  return { points: simplifyRoute([source, target]), diagnostics };
2245
2726
  }
2246
2727
  const candidates = orthogonalCandidates(source, target, input.direction);
2728
+ candidates.push(
2729
+ ...expandedObstacleCandidates(
2730
+ source,
2731
+ target,
2732
+ input.direction,
2733
+ input.obstacles ?? []
2734
+ )
2735
+ );
2247
2736
  for (const candidate of candidates) {
2248
2737
  if (!routeIntersectsObstacles(candidate, input.obstacles ?? [])) {
2249
2738
  return { points: simplifyRoute(candidate), diagnostics };
@@ -2282,27 +2771,113 @@ function simplifyRoute(points) {
2282
2771
  function orthogonalCandidates(source, target, direction) {
2283
2772
  const midpointX = (source.x + target.x) / 2;
2284
2773
  const midpointY = (source.y + target.y) / 2;
2285
- const candidates = [
2286
- [source, { x: target.x, y: source.y }, target],
2287
- [source, { x: source.x, y: target.y }, target]
2288
- ];
2774
+ const candidates = [];
2289
2775
  if (direction === "TB" || direction === "BT") {
2290
2776
  candidates.push([
2291
2777
  source,
2292
- { x: midpointX, y: source.y },
2293
- { x: midpointX, y: target.y },
2778
+ { x: source.x, y: midpointY },
2779
+ { x: target.x, y: midpointY },
2294
2780
  target
2295
2781
  ]);
2296
2782
  } else {
2297
2783
  candidates.push([
2298
2784
  source,
2299
- { x: source.x, y: midpointY },
2300
- { x: target.x, y: midpointY },
2785
+ { x: midpointX, y: source.y },
2786
+ { x: midpointX, y: target.y },
2301
2787
  target
2302
2788
  ]);
2303
2789
  }
2790
+ candidates.push(
2791
+ [source, { x: target.x, y: source.y }, target],
2792
+ [source, { x: source.x, y: target.y }, target]
2793
+ );
2304
2794
  return candidates;
2305
2795
  }
2796
+ function defaultSourceAnchor(direction) {
2797
+ switch (direction) {
2798
+ case "LR":
2799
+ return "right";
2800
+ case "RL":
2801
+ return "left";
2802
+ case "TB":
2803
+ return "bottom";
2804
+ case "BT":
2805
+ return "top";
2806
+ }
2807
+ }
2808
+ function defaultAnchorsForGeometry(source, target, direction) {
2809
+ const dx = target.x + target.width / 2 - (source.x + source.width / 2);
2810
+ const dy = target.y + target.height / 2 - (source.y + source.height / 2);
2811
+ if (Math.abs(dy) > Math.abs(dx)) {
2812
+ return dy >= 0 ? { sourceAnchor: "bottom", targetAnchor: "top" } : { sourceAnchor: "top", targetAnchor: "bottom" };
2813
+ }
2814
+ if (Math.abs(dx) > 0) {
2815
+ return dx >= 0 ? { sourceAnchor: "right", targetAnchor: "left" } : { sourceAnchor: "left", targetAnchor: "right" };
2816
+ }
2817
+ return {
2818
+ sourceAnchor: defaultSourceAnchor(direction),
2819
+ targetAnchor: defaultTargetAnchor(direction)
2820
+ };
2821
+ }
2822
+ function defaultTargetAnchor(direction) {
2823
+ switch (direction) {
2824
+ case "LR":
2825
+ return "left";
2826
+ case "RL":
2827
+ return "right";
2828
+ case "TB":
2829
+ return "top";
2830
+ case "BT":
2831
+ return "bottom";
2832
+ }
2833
+ }
2834
+ function expandedObstacleCandidates(source, target, direction, obstacles) {
2835
+ if (obstacles.length === 0) {
2836
+ return [];
2837
+ }
2838
+ const margin = 16;
2839
+ const candidates = [];
2840
+ if (direction === "TB" || direction === "BT") {
2841
+ const lanes = sortedUniqueLanes(
2842
+ obstacles.flatMap((obstacle) => [
2843
+ obstacle.x - margin,
2844
+ obstacle.x + obstacle.width + margin
2845
+ ]),
2846
+ (source.x + target.x) / 2
2847
+ );
2848
+ for (const laneX of lanes) {
2849
+ candidates.push([
2850
+ source,
2851
+ { x: laneX, y: source.y },
2852
+ { x: laneX, y: target.y },
2853
+ target
2854
+ ]);
2855
+ }
2856
+ } else {
2857
+ const lanes = sortedUniqueLanes(
2858
+ obstacles.flatMap((obstacle) => [
2859
+ obstacle.y - margin,
2860
+ obstacle.y + obstacle.height + margin
2861
+ ]),
2862
+ (source.y + target.y) / 2
2863
+ );
2864
+ for (const laneY of lanes) {
2865
+ candidates.push([
2866
+ source,
2867
+ { x: source.x, y: laneY },
2868
+ { x: target.x, y: laneY },
2869
+ target
2870
+ ]);
2871
+ }
2872
+ }
2873
+ return candidates;
2874
+ }
2875
+ function sortedUniqueLanes(lanes, midpoint) {
2876
+ return [...new Set(lanes)].filter((lane) => Number.isFinite(lane)).sort((left, right) => {
2877
+ const distance = Math.abs(left - midpoint) - Math.abs(right - midpoint);
2878
+ return distance === 0 ? left - right : distance;
2879
+ });
2880
+ }
2306
2881
  function routeIntersectsObstacles(points, obstacles) {
2307
2882
  for (let index = 0; index < points.length - 1; index += 1) {
2308
2883
  const a = points[index];
@@ -2381,12 +2956,17 @@ function solveDiagram(diagram, options = {}) {
2381
2956
  options,
2382
2957
  diagnostics
2383
2958
  );
2959
+ const coordinatedSwimlanes = coordinateSwimlanes(
2960
+ diagram.swimlanes ?? [],
2961
+ constrained.boxes
2962
+ );
2384
2963
  const groupBoxes = new Map(
2385
2964
  coordinatedGroups.map((group) => [group.id, group.box])
2386
2965
  );
2387
2966
  const coordinatedEdges = coordinateEdges(
2388
2967
  edges,
2389
2968
  nodeGeometryById,
2969
+ coordinatedNodes,
2390
2970
  [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
2391
2971
  diagram.direction,
2392
2972
  options,
@@ -2394,8 +2974,18 @@ function solveDiagram(diagram, options = {}) {
2394
2974
  );
2395
2975
  const allBoxes = [
2396
2976
  ...coordinatedNodes.map((node) => node.box),
2397
- ...groupBoxes.values()
2977
+ ...coordinatedNodes.flatMap(
2978
+ (node) => (node.ports ?? []).flatMap(
2979
+ (port) => port.label === void 0 ? [port.box] : [port.box, portLabelBox(port)]
2980
+ )
2981
+ ),
2982
+ ...groupBoxes.values(),
2983
+ ...coordinatedSwimlanes.flatMap(
2984
+ (swimlane) => swimlane.box === void 0 ? [] : [swimlane.box]
2985
+ )
2398
2986
  ];
2987
+ const contentBounds = allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes);
2988
+ const frame = diagram.frame === void 0 ? void 0 : coordinateFrame(diagram.frame, contentBounds);
2399
2989
  return {
2400
2990
  id: diagram.id,
2401
2991
  ...diagram.title === void 0 ? {} : { title: diagram.title },
@@ -2403,8 +2993,10 @@ function solveDiagram(diagram, options = {}) {
2403
2993
  nodes: coordinatedNodes,
2404
2994
  edges: coordinatedEdges,
2405
2995
  groups: coordinatedGroups,
2996
+ ...coordinatedSwimlanes.length === 0 ? {} : { swimlanes: coordinatedSwimlanes },
2406
2997
  diagnostics,
2407
- bounds: allBoxes.length === 0 ? { x: 0, y: 0, width: 0, height: 0 } : unionBoxes(allBoxes),
2998
+ bounds: frame === void 0 ? contentBounds : unionBoxes([contentBounds, frame.box, frame.titleBox]),
2999
+ ...frame === void 0 ? {} : { frame },
2408
3000
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
2409
3001
  };
2410
3002
  }
@@ -2430,6 +3022,9 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
2430
3022
  coordinated.push({
2431
3023
  id: node.id,
2432
3024
  ...node.label === void 0 ? {} : { label: node.label },
3025
+ ...node.style === void 0 ? {} : { style: node.style },
3026
+ ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
3027
+ ...node.compartments === void 0 ? {} : { compartments: node.compartments },
2433
3028
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
2434
3029
  shape: node.shape,
2435
3030
  ...node.metadata === void 0 ? {} : { metadata: node.metadata },
@@ -2440,6 +3035,142 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
2440
3035
  }
2441
3036
  return coordinated;
2442
3037
  }
3038
+ function coordinatePorts(node, nodeBox, portShifting) {
3039
+ const portsBySide = /* @__PURE__ */ new Map();
3040
+ for (const port of node.ports ?? []) {
3041
+ const ports = portsBySide.get(port.side) ?? [];
3042
+ ports.push(port);
3043
+ portsBySide.set(port.side, ports);
3044
+ }
3045
+ const coordinated = [];
3046
+ for (const [side, ports] of portsBySide) {
3047
+ const sorted = [...ports ?? []].sort((a, b) => {
3048
+ const order = (a.order ?? 0) - (b.order ?? 0);
3049
+ return order === 0 ? a.id.localeCompare(b.id) : order;
3050
+ });
3051
+ for (let index = 0; index < sorted.length; index += 1) {
3052
+ const port = sorted[index];
3053
+ if (port === void 0) {
3054
+ continue;
3055
+ }
3056
+ const anchor = portAnchor(
3057
+ nodeBox,
3058
+ side,
3059
+ index,
3060
+ sorted.length,
3061
+ portShifting
3062
+ );
3063
+ const box = portBox(anchor);
3064
+ coordinated.push({ ...port, box, anchor });
3065
+ }
3066
+ }
3067
+ return coordinated.sort((a, b) => a.id.localeCompare(b.id));
3068
+ }
3069
+ function portAnchor(nodeBox, side, index, count, portShifting) {
3070
+ const shiftingEnabled = portShifting?.enabled ?? true;
3071
+ const spacing = portShifting?.spacing ?? 24;
3072
+ const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
3073
+ switch (side) {
3074
+ case "left":
3075
+ return {
3076
+ x: nodeBox.x,
3077
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
3078
+ };
3079
+ case "right":
3080
+ return {
3081
+ x: nodeBox.x + nodeBox.width,
3082
+ y: nodeBox.y + nodeBox.height / 2 + centeredOffset
3083
+ };
3084
+ case "top":
3085
+ return {
3086
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
3087
+ y: nodeBox.y
3088
+ };
3089
+ case "bottom":
3090
+ return {
3091
+ x: nodeBox.x + nodeBox.width / 2 + centeredOffset,
3092
+ y: nodeBox.y + nodeBox.height
3093
+ };
3094
+ }
3095
+ }
3096
+ function portBox(anchor) {
3097
+ const size = 10;
3098
+ return {
3099
+ x: anchor.x - size / 2,
3100
+ y: anchor.y - size / 2,
3101
+ width: size,
3102
+ height: size
3103
+ };
3104
+ }
3105
+ function portLabelBox(port) {
3106
+ const textWidth = Math.max(0, (port.label?.text.length ?? 0) * 6);
3107
+ const height = 12;
3108
+ const gap = 8;
3109
+ const x = port.side === "left" ? port.anchor.x - gap - textWidth : port.anchor.x + gap;
3110
+ return {
3111
+ x,
3112
+ y: port.anchor.y - 8 - height,
3113
+ width: textWidth,
3114
+ height
3115
+ };
3116
+ }
3117
+ function coordinateSwimlanes(swimlanes, nodeBoxes) {
3118
+ const titleSize = 28;
3119
+ const padding = 16;
3120
+ return swimlanes.map((swimlane) => {
3121
+ const laneBoxes = swimlane.lanes.flatMap((lane) => {
3122
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3123
+ return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
3124
+ });
3125
+ const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
3126
+ const outer = expand(laneUnion, padding, titleSize);
3127
+ const laneCount = Math.max(1, swimlane.lanes.length);
3128
+ const lanes = swimlane.lanes.map((lane, index) => {
3129
+ const box = swimlane.orientation === "vertical" ? {
3130
+ x: outer.x + outer.width / laneCount * index,
3131
+ y: outer.y,
3132
+ width: outer.width / laneCount,
3133
+ height: outer.height
3134
+ } : {
3135
+ x: outer.x,
3136
+ y: outer.y + outer.height / laneCount * index,
3137
+ width: outer.width,
3138
+ height: outer.height / laneCount
3139
+ };
3140
+ return { ...lane, box };
3141
+ });
3142
+ return { ...swimlane, lanes, box: outer };
3143
+ });
3144
+ }
3145
+ function coordinateFrame(frame, contentBounds) {
3146
+ const padding = 32;
3147
+ const titleHeight = 28;
3148
+ const titleWidth = Math.max(180, frame.titleTab.length * 7);
3149
+ const box = {
3150
+ x: contentBounds.x - padding,
3151
+ y: contentBounds.y - padding - titleHeight,
3152
+ width: contentBounds.width + padding * 2,
3153
+ height: contentBounds.height + padding * 2 + titleHeight
3154
+ };
3155
+ return {
3156
+ ...frame,
3157
+ box,
3158
+ titleBox: {
3159
+ x: box.x,
3160
+ y: box.y,
3161
+ width: Math.min(titleWidth, box.width * 0.8),
3162
+ height: titleHeight
3163
+ }
3164
+ };
3165
+ }
3166
+ function expand(box, padding, titleSize) {
3167
+ return {
3168
+ x: box.x - padding,
3169
+ y: box.y - padding - titleSize,
3170
+ width: box.width + padding * 2,
3171
+ height: box.height + padding * 2 + titleSize
3172
+ };
3173
+ }
2443
3174
  function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2444
3175
  const coordinated = [];
2445
3176
  const groupBoxes = /* @__PURE__ */ new Map();
@@ -2488,8 +3219,11 @@ function coordinateGroups(groups, nodeBoxes, options, diagnostics) {
2488
3219
  }
2489
3220
  return coordinated;
2490
3221
  }
2491
- function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostics) {
3222
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, direction, options, diagnostics) {
2492
3223
  const coordinated = [];
3224
+ const coordinatedNodeById = new Map(
3225
+ coordinatedNodes.map((node) => [node.id, node])
3226
+ );
2493
3227
  for (const edge of edges) {
2494
3228
  const source = nodes.get(edge.source.nodeId);
2495
3229
  const target = nodes.get(edge.target.nodeId);
@@ -2507,11 +3241,13 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
2507
3241
  });
2508
3242
  continue;
2509
3243
  }
3244
+ const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
3245
+ const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
2510
3246
  const route = routeEdge({
2511
3247
  kind: options.routeKind ?? "orthogonal",
2512
3248
  direction,
2513
- source,
2514
- target,
3249
+ source: portGeometry(source, sourcePort),
3250
+ target: portGeometry(target, targetPort),
2515
3251
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
2516
3252
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
2517
3253
  obstacles: obstacles.filter(
@@ -2531,6 +3267,21 @@ function coordinateEdges(edges, nodes, obstacles, direction, options, diagnostic
2531
3267
  }
2532
3268
  return coordinated;
2533
3269
  }
3270
+ function portGeometry(nodeGeometry, port) {
3271
+ if (port === void 0) {
3272
+ return nodeGeometry;
3273
+ }
3274
+ return {
3275
+ ...nodeGeometry,
3276
+ box: port.box,
3277
+ center: port.anchor,
3278
+ anchors: nodeGeometry.anchors.map((anchor) => ({
3279
+ name: anchor.name,
3280
+ point: port.anchor
3281
+ })),
3282
+ obstacleBox: port.box
3283
+ };
3284
+ }
2534
3285
  function stableById(items) {
2535
3286
  return [...items].sort((a, b) => a.id.localeCompare(b.id));
2536
3287
  }
@@ -2591,7 +3342,8 @@ function renderDiagramDsl(source, options = {}) {
2591
3342
  return { diagnostics };
2592
3343
  }
2593
3344
  const solved = solveDiagram(normalized.diagram, {
2594
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal"
3345
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
3346
+ ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting)
2595
3347
  });
2596
3348
  const solveDiagnostics = solved.diagnostics.map(toSolveDiagnostic);
2597
3349
  if (hasErrorDiagnostics2(solveDiagnostics)) {
@@ -2630,6 +3382,22 @@ function renderDiagramDsl(source, options = {}) {
2630
3382
  function toSolveDiagnostic(diagnostic) {
2631
3383
  return { ...diagnostic, layer: "solve" };
2632
3384
  }
3385
+ function solvePortShiftingOption(value) {
3386
+ if (!isJsonObject(value)) {
3387
+ return {};
3388
+ }
3389
+ const portShifting = {};
3390
+ if (value.enabled === false) {
3391
+ portShifting.enabled = false;
3392
+ }
3393
+ if (typeof value.spacing === "number") {
3394
+ portShifting.spacing = value.spacing;
3395
+ }
3396
+ return { portShifting };
3397
+ }
3398
+ function isJsonObject(value) {
3399
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3400
+ }
2633
3401
  function toExportDiagnostic(diagnostic) {
2634
3402
  return { ...diagnostic, layer: "export" };
2635
3403
  }
@@ -2776,11 +3544,13 @@ exports.canonicalize = canonicalize;
2776
3544
  exports.computeArrowhead = computeArrowhead;
2777
3545
  exports.computeContainerGeometry = computeContainerGeometry;
2778
3546
  exports.computeShapeGeometry = computeShapeGeometry;
3547
+ exports.createDefaultTextMeasurer = createDefaultTextMeasurer;
2779
3548
  exports.expandBox = expandBox;
2780
3549
  exports.exportExcalidraw = exportExcalidraw;
2781
3550
  exports.exportSvg = exportSvg;
2782
3551
  exports.fitLabel = fitLabel;
2783
3552
  exports.getEdgePort = getEdgePort;
3553
+ exports.installNodeCanvasRuntime = installNodeCanvasRuntime;
2784
3554
  exports.intersectsAabb = intersectsAabb;
2785
3555
  exports.isPretextRuntimeAvailable = isPretextRuntimeAvailable;
2786
3556
  exports.normalizeDiagramDsl = normalizeDiagramDsl;