@crazyhappyone/auto-graph 0.0.2 → 0.0.4

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
@@ -1200,6 +1200,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1200
1200
  const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
1201
1201
  const routeKind = dsl.routing?.kind ?? "orthogonal";
1202
1202
  const portShifting = normalizePortShifting(dsl.routing?.portShifting);
1203
+ const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
1203
1204
  const diagram = {
1204
1205
  id: options.id ?? dsl.id ?? "diagram",
1205
1206
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -1213,6 +1214,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
1213
1214
  ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
1214
1215
  metadata: {
1215
1216
  routeKind,
1217
+ ...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
1216
1218
  ...portShifting === void 0 ? {} : { portShifting }
1217
1219
  }
1218
1220
  };
@@ -1383,14 +1385,17 @@ function formatCompartmentEntry(value) {
1383
1385
  return `${entry[0]}: ${entry[1]}`;
1384
1386
  }
1385
1387
  function normalizeSwimlanes(dsl) {
1386
- return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
1388
+ return Object.keys(dsl.swimlanes ?? {}).map((id) => {
1387
1389
  const swimlane = dsl.swimlanes?.[id];
1388
1390
  const label = toLabel(swimlane?.label);
1389
1391
  return {
1390
1392
  id,
1391
1393
  ...label === void 0 ? {} : { label },
1392
1394
  orientation: swimlane?.orientation ?? "vertical",
1393
- lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
1395
+ layout: swimlane?.layout ?? "overlay",
1396
+ ...swimlane?.headerHeight === void 0 ? {} : { headerHeight: swimlane.headerHeight },
1397
+ ...swimlane?.padding === void 0 ? {} : { padding: swimlane.padding },
1398
+ lanes: Object.keys(swimlane?.lanes ?? {}).map((laneId) => {
1394
1399
  const lane = swimlane?.lanes[laneId];
1395
1400
  const laneLabel = toLabel(lane?.label);
1396
1401
  return {
@@ -1468,11 +1473,6 @@ function validateReferences(dsl) {
1468
1473
  const diagnostics = [];
1469
1474
  const nodeIds = new Set(Object.keys(dsl.nodes));
1470
1475
  const groupIds = new Set(Object.keys(dsl.groups ?? {}));
1471
- const swimlaneLaneIds = new Set(
1472
- Object.entries(dsl.swimlanes ?? {}).flatMap(
1473
- ([swimlaneId, swimlane]) => Object.keys(swimlane.lanes).map((laneId) => `${swimlaneId}.${laneId}`)
1474
- )
1475
- );
1476
1476
  (dsl.edges ?? []).forEach((edge, index) => {
1477
1477
  if (typeof edge === "string") {
1478
1478
  return;
@@ -1580,10 +1580,12 @@ function validateReferences(dsl) {
1580
1580
  break;
1581
1581
  case "containment": {
1582
1582
  const container = constraint.containerId ?? constraint.container;
1583
- if (container !== void 0 && !hasNodeOrGroup(container, nodeIds, groupIds, swimlaneLaneIds)) {
1584
- diagnostics.push(
1585
- referenceMissing(["constraints", index, "container"], container)
1586
- );
1583
+ if (container !== void 0) {
1584
+ if (!nodeIds.has(container)) {
1585
+ diagnostics.push(
1586
+ referenceMissing(["constraints", index, "container"], container)
1587
+ );
1588
+ }
1587
1589
  }
1588
1590
  (constraint.childIds ?? constraint.children ?? []).forEach(
1589
1591
  (child, childIndex) => {
@@ -1660,6 +1662,10 @@ var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
1660
1662
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
1661
1663
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
1662
1664
  var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
1665
+ var primaryReadingDirectionSchema = zod.z.enum([
1666
+ "top_to_bottom",
1667
+ "top-to-bottom"
1668
+ ]);
1663
1669
  var nodeShapeSchema = zod.z.enum([
1664
1670
  "rectangle",
1665
1671
  "rounded-rectangle",
@@ -1670,6 +1676,7 @@ var nodeShapeSchema = zod.z.enum([
1670
1676
  "cylinder"
1671
1677
  ]);
1672
1678
  var finiteNumberSchema = zod.z.number().finite();
1679
+ var nonNegativeNumberSchema = finiteNumberSchema.min(0);
1673
1680
  var pointSchema = zod.z.object({
1674
1681
  x: finiteNumberSchema,
1675
1682
  y: finiteNumberSchema
@@ -1756,6 +1763,9 @@ var groupSchema = zod.z.object({
1756
1763
  var swimlaneSchema = zod.z.object({
1757
1764
  label: labelSchema.optional(),
1758
1765
  orientation: zod.z.enum(["vertical", "horizontal"]).optional(),
1766
+ layout: zod.z.enum(["overlay", "contract"]).optional(),
1767
+ headerHeight: nonNegativeNumberSchema.optional(),
1768
+ padding: nonNegativeNumberSchema.optional(),
1759
1769
  lanes: zod.z.record(
1760
1770
  zod.z.string(),
1761
1771
  zod.z.object({
@@ -1821,7 +1831,8 @@ var diagramDslSchema = zod.z.object({
1821
1831
  title: zod.z.string().optional(),
1822
1832
  direction: directionSchema.optional(),
1823
1833
  layout: zod.z.object({
1824
- direction: directionSchema.optional()
1834
+ direction: directionSchema.optional(),
1835
+ primaryReadingDirection: primaryReadingDirectionSchema.optional()
1825
1836
  }).optional(),
1826
1837
  routing: zod.z.object({
1827
1838
  kind: routeKindSchema.optional(),
@@ -2351,15 +2362,30 @@ function renderSwimlane(swimlane) {
2351
2362
  lines.push(
2352
2363
  ` <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}"/>`
2353
2364
  );
2354
- if (lane.label?.text !== void 0) {
2365
+ if (lane.headerBox !== void 0) {
2366
+ lines.push(
2367
+ ` <rect class="swimlane-header" data-lane-header="${escapeAttribute(`${swimlane.id}.${lane.id}`)}" x="${formatNumber(lane.headerBox.x)}" y="${formatNumber(lane.headerBox.y)}" width="${formatNumber(lane.headerBox.width)}" height="${formatNumber(lane.headerBox.height)}" fill="#f3f4f6" stroke="${STROKE}"/>`
2368
+ );
2369
+ }
2370
+ if (lane.contentBox !== void 0) {
2355
2371
  lines.push(
2356
- ` <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>`
2372
+ ` <rect class="swimlane-content" data-lane-content="${escapeAttribute(`${swimlane.id}.${lane.id}`)}" x="${formatNumber(lane.contentBox.x)}" y="${formatNumber(lane.contentBox.y)}" width="${formatNumber(lane.contentBox.width)}" height="${formatNumber(lane.contentBox.height)}" fill="none" stroke="none"/>`
2357
2373
  );
2358
2374
  }
2375
+ if (lane.label?.text !== void 0) {
2376
+ const labelBox = lane.headerBox ?? lane.box;
2377
+ lines.push(renderSwimlaneLabel(swimlane, lane.label.text, labelBox));
2378
+ }
2359
2379
  }
2360
2380
  lines.push(" </g>");
2361
2381
  return lines;
2362
2382
  }
2383
+ function renderSwimlaneLabel(swimlane, text, labelBox) {
2384
+ const x = labelBox.x + labelBox.width / 2;
2385
+ const y = labelBox.y + labelBox.height / 2;
2386
+ const transform = swimlane.orientation === "horizontal" ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
2387
+ return ` <text class="swimlane-label" x="${formatNumber(x)}" y="${formatNumber(y)}" text-anchor="middle" dominant-baseline="middle"${transform} font-family="${FONT_FAMILY}" font-size="12" fill="#111827">${escapeXml(text)}</text>`;
2388
+ }
2363
2389
  function renderPorts(node) {
2364
2390
  return (node.ports ?? []).flatMap((port) => [
2365
2391
  ` <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)}"/>`,
@@ -2425,7 +2451,7 @@ function renderRect(box, attributes) {
2425
2451
  function renderLabel(label, box, item) {
2426
2452
  const labelLayout = item.labelLayout;
2427
2453
  if (labelLayout?.lines !== void 0 && labelLayout.lines.length > 0) {
2428
- const offset = isAbsoluteLabelLayout(labelLayout.box, box) ? { x: 0, y: 0 } : { x: box.x, y: box.y };
2454
+ const offset = { x: box.x, y: box.y };
2429
2455
  return [
2430
2456
  ` <text class="label" data-for="${escapeAttribute(item.id)}" font-family="${FONT_FAMILY}" font-size="${formatNumber(labelLayout.font.fontSize)}" fill="#111827">`,
2431
2457
  ...labelLayout.lines.map(
@@ -2524,9 +2550,6 @@ function pathPointsBeforeArrowhead(points) {
2524
2550
  };
2525
2551
  return [...points.slice(0, -1), base];
2526
2552
  }
2527
- function isAbsoluteLabelLayout(labelBox, itemBox) {
2528
- 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;
2529
- }
2530
2553
  function shapePoints(shape, box) {
2531
2554
  const left = box.x;
2532
2555
  const right = box.x + box.width;
@@ -2940,6 +2963,19 @@ function solveDiagram(diagram, options = {}) {
2940
2963
  constraints
2941
2964
  });
2942
2965
  diagnostics.push(...constrained.diagnostics);
2966
+ const swimlaneContracts = applySwimlaneLayoutContracts(
2967
+ diagram.swimlanes ?? [],
2968
+ constraints,
2969
+ edges,
2970
+ isTopToBottomReadingDirection(diagram.metadata?.primaryReadingDirection),
2971
+ constrained.boxes,
2972
+ constrained.locks,
2973
+ options?.overlapSpacing ?? 40
2974
+ );
2975
+ if (swimlaneContracts.layouts.size > 0) {
2976
+ removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
2977
+ }
2978
+ diagnostics.push(...swimlaneContracts.diagnostics);
2943
2979
  const coordinatedNodes = coordinateNodes(
2944
2980
  nodes,
2945
2981
  constrained.boxes,
@@ -2964,7 +3000,8 @@ function solveDiagram(diagram, options = {}) {
2964
3000
  );
2965
3001
  const coordinatedSwimlanes = coordinateSwimlanes(
2966
3002
  diagram.swimlanes ?? [],
2967
- constrained.boxes
3003
+ constrained.boxes,
3004
+ swimlaneContracts.layouts
2968
3005
  );
2969
3006
  const groupBoxes = new Map(
2970
3007
  coordinatedGroups.map((group) => [group.id, group.box])
@@ -3006,6 +3043,591 @@ function solveDiagram(diagram, options = {}) {
3006
3043
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
3007
3044
  };
3008
3045
  }
3046
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing) {
3047
+ const layouts = /* @__PURE__ */ new Map();
3048
+ const diagnostics = [];
3049
+ const movedChildIds = /* @__PURE__ */ new Set();
3050
+ for (const swimlane of swimlanes) {
3051
+ if ((swimlane.layout ?? "overlay") !== "contract") {
3052
+ continue;
3053
+ }
3054
+ if (swimlane.lanes.length === 0) {
3055
+ continue;
3056
+ }
3057
+ const layout2 = applySingleSwimlaneContract(
3058
+ swimlane,
3059
+ edges,
3060
+ topToBottomFlow,
3061
+ nodeBoxes,
3062
+ locks,
3063
+ diagnostics,
3064
+ movedChildIds
3065
+ );
3066
+ if (layout2 !== void 0) {
3067
+ layouts.set(swimlane.id, layout2);
3068
+ }
3069
+ }
3070
+ if (layouts.size > 0) {
3071
+ diagnostics.push(
3072
+ ...reportSwimlaneOverlaps(nodeBoxes, locks, overlapSpacing),
3073
+ ...reportSwimlaneConstraintInvalidations(
3074
+ constraints,
3075
+ nodeBoxes,
3076
+ movedChildIds
3077
+ )
3078
+ );
3079
+ }
3080
+ return { layouts, diagnostics, movedChildIds };
3081
+ }
3082
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds) {
3083
+ const headerHeight = swimlane.headerHeight ?? 28;
3084
+ const padding = swimlane.padding ?? 16;
3085
+ const laneBounds = swimlane.lanes.map((lane) => {
3086
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3087
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
3088
+ });
3089
+ const populatedBounds = laneBounds.filter(
3090
+ (box) => box !== void 0
3091
+ );
3092
+ if (populatedBounds.length === 0) {
3093
+ return void 0;
3094
+ }
3095
+ if (swimlane.orientation === "vertical") {
3096
+ return applyVerticalSwimlaneContract(
3097
+ swimlane,
3098
+ edges,
3099
+ topToBottomFlow,
3100
+ nodeBoxes,
3101
+ laneBounds,
3102
+ headerHeight,
3103
+ padding,
3104
+ locks,
3105
+ diagnostics,
3106
+ movedChildIds
3107
+ );
3108
+ }
3109
+ return applyHorizontalSwimlaneContract(
3110
+ swimlane,
3111
+ nodeBoxes,
3112
+ laneBounds,
3113
+ headerHeight,
3114
+ padding,
3115
+ locks,
3116
+ diagnostics,
3117
+ movedChildIds
3118
+ );
3119
+ }
3120
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
3121
+ const populatedBounds = laneBounds.filter(
3122
+ (box) => box !== void 0
3123
+ );
3124
+ const top = Math.min(...populatedBounds.map((box) => box.y));
3125
+ const left = Math.min(...populatedBounds.map((box) => box.x));
3126
+ const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
3127
+ const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
3128
+ const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
3129
+ const rankStackGap = Math.max(8, padding / 2);
3130
+ const maxRankStackHeight = maxVerticalRankStackHeight(
3131
+ swimlane,
3132
+ nodeBoxes,
3133
+ flowRanks,
3134
+ rankStackGap
3135
+ );
3136
+ const rankSpacing = Math.max(96, maxRankStackHeight + padding);
3137
+ const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
3138
+ const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
3139
+ const laneContentTop = top + headerHeight + padding;
3140
+ for (let index = 0; index < swimlane.lanes.length; index += 1) {
3141
+ const lane = swimlane.lanes[index];
3142
+ const bounds = laneBounds[index];
3143
+ if (lane === void 0 || bounds === void 0) {
3144
+ continue;
3145
+ }
3146
+ const target = {
3147
+ x: left + slotWidth * index + padding,
3148
+ y: laneContentTop
3149
+ };
3150
+ if (maxRank === 0) {
3151
+ moveLaneChildren(
3152
+ lane.children,
3153
+ nodeBoxes,
3154
+ locks,
3155
+ diagnostics,
3156
+ movedChildIds,
3157
+ {
3158
+ x: target.x - bounds.x,
3159
+ y: target.y - bounds.y
3160
+ }
3161
+ );
3162
+ continue;
3163
+ }
3164
+ moveRankedVerticalLaneChildren(
3165
+ lane.children,
3166
+ nodeBoxes,
3167
+ locks,
3168
+ diagnostics,
3169
+ movedChildIds,
3170
+ flowRanks,
3171
+ rankSpacing,
3172
+ rankStackGap,
3173
+ {
3174
+ x: target.x - bounds.x,
3175
+ y: laneContentTop
3176
+ }
3177
+ );
3178
+ }
3179
+ return {
3180
+ box: {
3181
+ x: left,
3182
+ y: top,
3183
+ width: slotWidth * swimlane.lanes.length,
3184
+ height: contentHeight + padding * 2 + headerHeight
3185
+ },
3186
+ slotWidth,
3187
+ slotHeight: contentHeight + padding * 2 + headerHeight
3188
+ };
3189
+ }
3190
+ function isTopToBottomReadingDirection(value) {
3191
+ return value === "top_to_bottom" || value === "top-to-bottom";
3192
+ }
3193
+ function rankVerticalSwimlaneChildren(swimlane, edges) {
3194
+ const childOrder = /* @__PURE__ */ new Map();
3195
+ for (const lane of swimlane.lanes) {
3196
+ for (const childId of lane.children) {
3197
+ if (!childOrder.has(childId)) {
3198
+ childOrder.set(childId, childOrder.size);
3199
+ }
3200
+ }
3201
+ }
3202
+ if (childOrder.size === 0) {
3203
+ return /* @__PURE__ */ new Map();
3204
+ }
3205
+ const childIds = new Set(childOrder.keys());
3206
+ const relevantEdges = edges.filter(
3207
+ (edge) => childIds.has(edge.source.nodeId) && childIds.has(edge.target.nodeId) && edge.source.nodeId !== edge.target.nodeId
3208
+ );
3209
+ if (relevantEdges.length === 0) {
3210
+ return /* @__PURE__ */ new Map();
3211
+ }
3212
+ const ranks = new Map([...childIds].map((id) => [id, 0]));
3213
+ const outgoing = /* @__PURE__ */ new Map();
3214
+ const inDegree = new Map([...childIds].map((id) => [id, 0]));
3215
+ for (const edge of relevantEdges) {
3216
+ const targets = outgoing.get(edge.source.nodeId) ?? [];
3217
+ targets.push(edge.target.nodeId);
3218
+ outgoing.set(edge.source.nodeId, targets);
3219
+ inDegree.set(
3220
+ edge.target.nodeId,
3221
+ (inDegree.get(edge.target.nodeId) ?? 0) + 1
3222
+ );
3223
+ }
3224
+ const queue = [...childIds].filter((id) => (inDegree.get(id) ?? 0) === 0).sort((a, b) => (childOrder.get(a) ?? 0) - (childOrder.get(b) ?? 0));
3225
+ let visited = 0;
3226
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
3227
+ const sourceId = queue[cursor];
3228
+ if (sourceId === void 0) {
3229
+ continue;
3230
+ }
3231
+ visited += 1;
3232
+ for (const targetId of outgoing.get(sourceId) ?? []) {
3233
+ ranks.set(
3234
+ targetId,
3235
+ Math.max(ranks.get(targetId) ?? 0, (ranks.get(sourceId) ?? 0) + 1)
3236
+ );
3237
+ const nextInDegree = (inDegree.get(targetId) ?? 0) - 1;
3238
+ inDegree.set(targetId, nextInDegree);
3239
+ if (nextInDegree === 0) {
3240
+ queue.push(targetId);
3241
+ }
3242
+ }
3243
+ }
3244
+ return visited === childIds.size ? ranks : rankCyclicSwimlaneChildren(childIds, relevantEdges);
3245
+ }
3246
+ function rankCyclicSwimlaneChildren(childIds, edges) {
3247
+ const maxRank = Math.max(0, childIds.size - 1);
3248
+ const ranks = new Map([...childIds].map((id) => [id, 0]));
3249
+ for (let iteration = 0; iteration < childIds.size; iteration += 1) {
3250
+ let changed = false;
3251
+ for (const edge of edges) {
3252
+ const nextRank = Math.min(
3253
+ maxRank,
3254
+ (ranks.get(edge.source.nodeId) ?? 0) + 1
3255
+ );
3256
+ if (nextRank > (ranks.get(edge.target.nodeId) ?? 0)) {
3257
+ ranks.set(edge.target.nodeId, nextRank);
3258
+ changed = true;
3259
+ }
3260
+ }
3261
+ if (!changed) {
3262
+ break;
3263
+ }
3264
+ }
3265
+ return ranks;
3266
+ }
3267
+ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
3268
+ let maxHeight = 0;
3269
+ for (const lane of swimlane.lanes) {
3270
+ for (const stack of rankStacks(
3271
+ lane.children,
3272
+ nodeBoxes,
3273
+ flowRanks
3274
+ ).values()) {
3275
+ const height = stack.reduce(
3276
+ (total, item, index) => total + item.box.height + (index === 0 ? 0 : gap),
3277
+ 0
3278
+ );
3279
+ maxHeight = Math.max(maxHeight, height);
3280
+ }
3281
+ }
3282
+ return maxHeight;
3283
+ }
3284
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
3285
+ for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
3286
+ let yOffset = 0;
3287
+ for (const item of stack) {
3288
+ const { childId, box } = item;
3289
+ if (locks.has(childId)) {
3290
+ diagnostics.push({
3291
+ severity: "warning",
3292
+ code: "constraints.locked-target-not-moved",
3293
+ message: `Locked child ${childId} was not moved into contract swimlane slot.`,
3294
+ path: ["swimlanes"],
3295
+ detail: { nodeId: childId }
3296
+ });
3297
+ continue;
3298
+ }
3299
+ const next = {
3300
+ ...box,
3301
+ x: box.x + target.x,
3302
+ y: target.y + rank * rankSpacing + yOffset
3303
+ };
3304
+ if (next.x !== box.x || next.y !== box.y) {
3305
+ movedChildIds.add(childId);
3306
+ }
3307
+ nodeBoxes.set(childId, next);
3308
+ yOffset += box.height + rankStackGap;
3309
+ }
3310
+ }
3311
+ }
3312
+ function rankStacks(childIds, nodeBoxes, flowRanks) {
3313
+ const stacks = /* @__PURE__ */ new Map();
3314
+ for (const childId of childIds) {
3315
+ const box = nodeBoxes.get(childId);
3316
+ if (box === void 0) {
3317
+ continue;
3318
+ }
3319
+ const rank = flowRanks.get(childId) ?? 0;
3320
+ const stack = stacks.get(rank) ?? [];
3321
+ stack.push({ childId, box });
3322
+ stacks.set(rank, stack);
3323
+ }
3324
+ for (const stack of stacks.values()) {
3325
+ stack.sort((a, b) => {
3326
+ const deltaY = a.box.y - b.box.y;
3327
+ return deltaY === 0 ? a.childId.localeCompare(b.childId) : deltaY;
3328
+ });
3329
+ }
3330
+ return stacks;
3331
+ }
3332
+ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
3333
+ const populatedBounds = laneBounds.filter(
3334
+ (box) => box !== void 0
3335
+ );
3336
+ const top = Math.min(...populatedBounds.map((box) => box.y));
3337
+ const left = Math.min(...populatedBounds.map((box) => box.x));
3338
+ const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + headerHeight + padding * 2;
3339
+ const slotHeight = Math.max(...populatedBounds.map((box) => box.height)) + padding * 2;
3340
+ for (let index = 0; index < swimlane.lanes.length; index += 1) {
3341
+ const lane = swimlane.lanes[index];
3342
+ const bounds = laneBounds[index];
3343
+ if (lane === void 0 || bounds === void 0) {
3344
+ continue;
3345
+ }
3346
+ const target = {
3347
+ x: left + headerHeight + padding,
3348
+ y: top + slotHeight * index + padding
3349
+ };
3350
+ moveLaneChildren(
3351
+ lane.children,
3352
+ nodeBoxes,
3353
+ locks,
3354
+ diagnostics,
3355
+ movedChildIds,
3356
+ {
3357
+ x: target.x - bounds.x,
3358
+ y: target.y - bounds.y
3359
+ }
3360
+ );
3361
+ }
3362
+ return {
3363
+ box: {
3364
+ x: left,
3365
+ y: top,
3366
+ width: slotWidth,
3367
+ height: slotHeight * swimlane.lanes.length
3368
+ },
3369
+ slotWidth,
3370
+ slotHeight
3371
+ };
3372
+ }
3373
+ function moveLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, offset) {
3374
+ for (const childId of childIds) {
3375
+ const box = nodeBoxes.get(childId);
3376
+ if (box === void 0) {
3377
+ continue;
3378
+ }
3379
+ if (locks.has(childId)) {
3380
+ diagnostics.push({
3381
+ severity: "warning",
3382
+ code: "constraints.locked-target-not-moved",
3383
+ message: `Locked child ${childId} was not moved into contract swimlane slot.`,
3384
+ path: ["swimlanes"],
3385
+ detail: { nodeId: childId }
3386
+ });
3387
+ continue;
3388
+ }
3389
+ if (offset.x !== 0 || offset.y !== 0) {
3390
+ movedChildIds.add(childId);
3391
+ }
3392
+ nodeBoxes.set(childId, {
3393
+ ...box,
3394
+ x: box.x + offset.x,
3395
+ y: box.y + offset.y
3396
+ });
3397
+ }
3398
+ }
3399
+ function removeResolvedOverlapDiagnostics(diagnostics, nodeBoxes) {
3400
+ for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
3401
+ const diagnostic = diagnostics[index];
3402
+ if (diagnostic?.code !== "constraints.overlap.unresolved") {
3403
+ continue;
3404
+ }
3405
+ const firstId = detailString(diagnostic, "firstId");
3406
+ const secondId = detailString(diagnostic, "secondId");
3407
+ const first = firstId === void 0 ? void 0 : nodeBoxes.get(firstId);
3408
+ const second = secondId === void 0 ? void 0 : nodeBoxes.get(secondId);
3409
+ if (first !== void 0 && second !== void 0 && !intersectsAabb(first, second)) {
3410
+ diagnostics.splice(index, 1);
3411
+ }
3412
+ }
3413
+ }
3414
+ function reportSwimlaneConstraintInvalidations(constraints, nodeBoxes, movedChildIds) {
3415
+ const diagnostics = [];
3416
+ for (const constraint of constraints) {
3417
+ const invalidatedNodeIds = movedConstraintNodeIds(
3418
+ constraint,
3419
+ nodeBoxes,
3420
+ movedChildIds
3421
+ );
3422
+ if (invalidatedNodeIds.length === 0) {
3423
+ continue;
3424
+ }
3425
+ diagnostics.push({
3426
+ severity: "warning",
3427
+ code: "constraints.swimlane-contract.invalidated",
3428
+ message: `Contract swimlane placement moved node(s) after ${constraint.kind} constraint solving; final geometry no longer satisfies that constraint.`,
3429
+ path: ["swimlanes"],
3430
+ detail: {
3431
+ constraintKind: constraint.kind,
3432
+ ...constraint.id === void 0 ? {} : { constraintId: constraint.id },
3433
+ nodeIds: invalidatedNodeIds
3434
+ }
3435
+ });
3436
+ }
3437
+ return diagnostics;
3438
+ }
3439
+ function movedConstraintNodeIds(constraint, nodeBoxes, movedChildIds) {
3440
+ switch (constraint.kind) {
3441
+ case "exact-position":
3442
+ return [];
3443
+ case "containment":
3444
+ return movedContainmentViolations(constraint, nodeBoxes, movedChildIds);
3445
+ case "relative-position":
3446
+ return movedRelativeViolations(constraint, nodeBoxes, movedChildIds);
3447
+ case "align":
3448
+ return movedAlignViolations(constraint, nodeBoxes, movedChildIds);
3449
+ case "distribute":
3450
+ return movedDistributeViolations(constraint, nodeBoxes, movedChildIds);
3451
+ }
3452
+ }
3453
+ function movedContainmentViolations(constraint, nodeBoxes, movedChildIds) {
3454
+ const container = nodeBoxes.get(constraint.containerId);
3455
+ if (container === void 0) {
3456
+ return [];
3457
+ }
3458
+ const content = paddedContentBox(container, constraint.padding);
3459
+ return constraint.childIds.filter((childId) => {
3460
+ if (!movedChildIds.has(childId)) {
3461
+ return false;
3462
+ }
3463
+ const child = nodeBoxes.get(childId);
3464
+ return child !== void 0 && !boxInside(child, content);
3465
+ });
3466
+ }
3467
+ function movedRelativeViolations(constraint, nodeBoxes, movedChildIds) {
3468
+ if (!movedChildIds.has(constraint.sourceId) && !movedChildIds.has(constraint.referenceId)) {
3469
+ return [];
3470
+ }
3471
+ const source = nodeBoxes.get(constraint.sourceId);
3472
+ const reference = nodeBoxes.get(constraint.referenceId);
3473
+ if (source === void 0 || reference === void 0) {
3474
+ return [];
3475
+ }
3476
+ return sameBoxPosition(
3477
+ source,
3478
+ expectedRelativeBox(source, reference, constraint)
3479
+ ) ? [] : [constraint.sourceId];
3480
+ }
3481
+ function movedAlignViolations(constraint, nodeBoxes, movedChildIds) {
3482
+ if (!constraint.targetIds.some((id) => movedChildIds.has(id))) {
3483
+ return [];
3484
+ }
3485
+ const targets = constraint.targetIds.map((id) => ({ id, box: nodeBoxes.get(id) })).filter(
3486
+ (target) => target.box !== void 0
3487
+ );
3488
+ const anchor = targets[0];
3489
+ if (anchor === void 0) {
3490
+ return [];
3491
+ }
3492
+ const expected = alignmentValue2(anchor.box, constraint.axis);
3493
+ return targets.filter(
3494
+ (target) => movedChildIds.has(target.id) && !sameNumber(alignmentValue2(target.box, constraint.axis), expected)
3495
+ ).map((target) => target.id);
3496
+ }
3497
+ function movedDistributeViolations(constraint, nodeBoxes, movedChildIds) {
3498
+ if (!constraint.targetIds.some((id) => movedChildIds.has(id))) {
3499
+ return [];
3500
+ }
3501
+ const targets = constraint.targetIds.map((id) => ({ id, box: nodeBoxes.get(id) })).filter(
3502
+ (target) => target.box !== void 0
3503
+ ).sort((a, b) => {
3504
+ const delta = constraint.axis === "horizontal" ? a.box.x - b.box.x : a.box.y - b.box.y;
3505
+ return delta === 0 ? a.id.localeCompare(b.id) : delta;
3506
+ });
3507
+ if (targets.length < 3) {
3508
+ return [];
3509
+ }
3510
+ const first = targets[0];
3511
+ const last = targets.at(-1);
3512
+ if (first === void 0 || last === void 0) {
3513
+ return [];
3514
+ }
3515
+ const expectedSpacing = constraint.spacing ?? (distributionStart2(last.box, constraint.axis) - distributionStart2(first.box, constraint.axis)) / (targets.length - 1);
3516
+ return targets.slice(1).filter((target, index) => {
3517
+ const previous = targets[index];
3518
+ if (previous === void 0 || !movedChildIds.has(target.id)) {
3519
+ return false;
3520
+ }
3521
+ return !sameNumber(
3522
+ distributionStart2(target.box, constraint.axis) - distributionStart2(previous.box, constraint.axis),
3523
+ expectedSpacing
3524
+ );
3525
+ }).map((target) => target.id);
3526
+ }
3527
+ function expectedRelativeBox(source, reference, constraint) {
3528
+ const offset = constraint.offset ?? { x: 0, y: 0 };
3529
+ switch (constraint.relation) {
3530
+ case "above":
3531
+ return {
3532
+ ...source,
3533
+ x: reference.x + offset.x,
3534
+ y: reference.y - source.height + offset.y
3535
+ };
3536
+ case "right-of":
3537
+ return {
3538
+ ...source,
3539
+ x: reference.x + reference.width + offset.x,
3540
+ y: reference.y + offset.y
3541
+ };
3542
+ case "below":
3543
+ return {
3544
+ ...source,
3545
+ x: reference.x + offset.x,
3546
+ y: reference.y + reference.height + offset.y
3547
+ };
3548
+ case "left-of":
3549
+ return {
3550
+ ...source,
3551
+ x: reference.x - source.width + offset.x,
3552
+ y: reference.y + offset.y
3553
+ };
3554
+ }
3555
+ }
3556
+ function paddedContentBox(container, padding) {
3557
+ const margin = padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
3558
+ return {
3559
+ x: container.x + margin.left,
3560
+ y: container.y + margin.top,
3561
+ width: container.width - margin.left - margin.right,
3562
+ height: container.height - margin.top - margin.bottom
3563
+ };
3564
+ }
3565
+ function boxInside(child, container) {
3566
+ return child.x >= container.x && child.y >= container.y && child.x + child.width <= container.x + container.width && child.y + child.height <= container.y + container.height;
3567
+ }
3568
+ function sameBoxPosition(first, second) {
3569
+ return sameNumber(first.x, second.x) && sameNumber(first.y, second.y);
3570
+ }
3571
+ function sameNumber(first, second) {
3572
+ return Math.abs(first - second) < 1e-3;
3573
+ }
3574
+ function alignmentValue2(box, axis) {
3575
+ switch (axis) {
3576
+ case "x":
3577
+ case "left":
3578
+ return box.x;
3579
+ case "y":
3580
+ case "top":
3581
+ return box.y;
3582
+ case "center-x":
3583
+ return box.x + box.width / 2;
3584
+ case "center-y":
3585
+ return box.y + box.height / 2;
3586
+ case "right":
3587
+ return box.x + box.width;
3588
+ case "bottom":
3589
+ return box.y + box.height;
3590
+ }
3591
+ }
3592
+ function distributionStart2(box, axis) {
3593
+ return axis === "horizontal" ? box.x : box.y;
3594
+ }
3595
+ function detailString(diagnostic, key) {
3596
+ const value = diagnostic.detail?.[key];
3597
+ return typeof value === "string" ? value : void 0;
3598
+ }
3599
+ function reportSwimlaneOverlaps(nodeBoxes, locks, overlapSpacing) {
3600
+ const diagnostics = [];
3601
+ const ids = [...nodeBoxes.keys()].sort();
3602
+ for (const firstId of ids) {
3603
+ for (const secondId of ids) {
3604
+ if (firstId >= secondId) {
3605
+ continue;
3606
+ }
3607
+ const first = nodeBoxes.get(firstId);
3608
+ const second = nodeBoxes.get(secondId);
3609
+ if (first === void 0 || second === void 0) {
3610
+ continue;
3611
+ }
3612
+ if (!intersectsAabb(first, second)) {
3613
+ continue;
3614
+ }
3615
+ diagnostics.push({
3616
+ severity: "warning",
3617
+ code: "constraints.overlap.unresolved",
3618
+ message: `Boxes ${firstId} and ${secondId} still overlap after contract swimlane placement with configured spacing ${overlapSpacing}.`,
3619
+ path: ["swimlanes"],
3620
+ detail: {
3621
+ firstId,
3622
+ secondId,
3623
+ firstLocked: locks.has(firstId),
3624
+ secondLocked: locks.has(secondId)
3625
+ }
3626
+ });
3627
+ }
3628
+ }
3629
+ return diagnostics;
3630
+ }
3009
3631
  function coordinateNodes(nodes, boxes, options, diagnostics) {
3010
3632
  const coordinated = [];
3011
3633
  for (const node of nodes) {
@@ -3120,16 +3742,70 @@ function portLabelBox(port) {
3120
3742
  height
3121
3743
  };
3122
3744
  }
3123
- function coordinateSwimlanes(swimlanes, nodeBoxes) {
3124
- const titleSize = 28;
3125
- const padding = 16;
3745
+ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
3126
3746
  return swimlanes.map((swimlane) => {
3127
- const laneBoxes = swimlane.lanes.flatMap((lane) => {
3747
+ const layout2 = swimlane.layout ?? "overlay";
3748
+ const headerHeight = swimlane.headerHeight ?? 28;
3749
+ const padding = swimlane.padding ?? 16;
3750
+ const contractLayout = layouts.get(swimlane.id);
3751
+ if (layout2 === "contract" && contractLayout !== void 0) {
3752
+ const lanes2 = swimlane.lanes.map((lane, index) => {
3753
+ const box = swimlane.orientation === "vertical" ? {
3754
+ x: contractLayout.box.x + contractLayout.slotWidth * index,
3755
+ y: contractLayout.box.y,
3756
+ width: contractLayout.slotWidth,
3757
+ height: contractLayout.box.height
3758
+ } : {
3759
+ x: contractLayout.box.x,
3760
+ y: contractLayout.box.y + contractLayout.slotHeight * index,
3761
+ width: contractLayout.box.width,
3762
+ height: contractLayout.slotHeight
3763
+ };
3764
+ const headerBox = swimlane.orientation === "vertical" ? {
3765
+ x: box.x,
3766
+ y: box.y,
3767
+ width: box.width,
3768
+ height: headerHeight
3769
+ } : {
3770
+ x: box.x,
3771
+ y: box.y,
3772
+ width: headerHeight,
3773
+ height: box.height
3774
+ };
3775
+ const contentBox2 = swimlane.orientation === "vertical" ? {
3776
+ x: box.x,
3777
+ y: box.y + headerHeight,
3778
+ width: box.width,
3779
+ height: Math.max(0, box.height - headerHeight)
3780
+ } : {
3781
+ x: box.x + headerHeight,
3782
+ y: box.y,
3783
+ width: Math.max(0, box.width - headerHeight),
3784
+ height: box.height
3785
+ };
3786
+ return {
3787
+ ...lane,
3788
+ box,
3789
+ headerBox,
3790
+ contentBox: contentBox2
3791
+ };
3792
+ });
3793
+ return {
3794
+ ...swimlane,
3795
+ lanes: lanes2,
3796
+ box: contractLayout.box,
3797
+ ...headerHeight === void 0 ? {} : { headerHeight },
3798
+ ...padding === void 0 ? {} : { padding }
3799
+ };
3800
+ }
3801
+ const laneContentBoxes = swimlane.lanes.map((lane) => {
3128
3802
  const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3129
- return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
3803
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
3130
3804
  });
3131
- const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
3132
- const outer = expand(laneUnion, padding, titleSize);
3805
+ const laneUnion = laneContentBoxes.filter((box) => box !== void 0).length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(
3806
+ laneContentBoxes.filter((box) => box !== void 0)
3807
+ );
3808
+ const outer = expand(laneUnion, padding, headerHeight);
3133
3809
  const laneCount = Math.max(1, swimlane.lanes.length);
3134
3810
  const lanes = swimlane.lanes.map((lane, index) => {
3135
3811
  const box = swimlane.orientation === "vertical" ? {
@@ -3143,9 +3819,42 @@ function coordinateSwimlanes(swimlanes, nodeBoxes) {
3143
3819
  width: outer.width,
3144
3820
  height: outer.height / laneCount
3145
3821
  };
3146
- return { ...lane, box };
3822
+ const headerBox = layout2 === "contract" ? swimlane.orientation === "vertical" ? {
3823
+ x: box.x,
3824
+ y: box.y,
3825
+ width: box.width,
3826
+ height: headerHeight
3827
+ } : {
3828
+ x: box.x,
3829
+ y: box.y,
3830
+ width: headerHeight,
3831
+ height: box.height
3832
+ } : void 0;
3833
+ const contentBox2 = layout2 === "contract" ? swimlane.orientation === "vertical" ? {
3834
+ x: box.x,
3835
+ y: box.y + headerHeight,
3836
+ width: box.width,
3837
+ height: Math.max(0, box.height - headerHeight)
3838
+ } : {
3839
+ x: box.x + headerHeight,
3840
+ y: box.y,
3841
+ width: Math.max(0, box.width - headerHeight),
3842
+ height: box.height
3843
+ } : void 0;
3844
+ return {
3845
+ ...lane,
3846
+ box,
3847
+ ...headerBox === void 0 ? {} : { headerBox },
3848
+ ...contentBox2 === void 0 ? {} : { contentBox: contentBox2 }
3849
+ };
3147
3850
  });
3148
- return { ...swimlane, lanes, box: outer };
3851
+ return {
3852
+ ...swimlane,
3853
+ lanes,
3854
+ box: outer,
3855
+ ...headerHeight === void 0 ? {} : { headerHeight },
3856
+ ...padding === void 0 ? {} : { padding }
3857
+ };
3149
3858
  });
3150
3859
  }
3151
3860
  function coordinateFrame(frame, contentBounds) {