@crazyhappyone/auto-graph 0.0.3 → 0.0.5

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