@crazyhappyone/auto-graph 0.0.3 → 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 {
@@ -1657,6 +1662,10 @@ var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
1657
1662
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
1658
1663
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
1659
1664
  var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
1665
+ var primaryReadingDirectionSchema = zod.z.enum([
1666
+ "top_to_bottom",
1667
+ "top-to-bottom"
1668
+ ]);
1660
1669
  var nodeShapeSchema = zod.z.enum([
1661
1670
  "rectangle",
1662
1671
  "rounded-rectangle",
@@ -1667,6 +1676,7 @@ var nodeShapeSchema = zod.z.enum([
1667
1676
  "cylinder"
1668
1677
  ]);
1669
1678
  var finiteNumberSchema = zod.z.number().finite();
1679
+ var nonNegativeNumberSchema = finiteNumberSchema.min(0);
1670
1680
  var pointSchema = zod.z.object({
1671
1681
  x: finiteNumberSchema,
1672
1682
  y: finiteNumberSchema
@@ -1753,6 +1763,9 @@ var groupSchema = zod.z.object({
1753
1763
  var swimlaneSchema = zod.z.object({
1754
1764
  label: labelSchema.optional(),
1755
1765
  orientation: zod.z.enum(["vertical", "horizontal"]).optional(),
1766
+ layout: zod.z.enum(["overlay", "contract"]).optional(),
1767
+ headerHeight: nonNegativeNumberSchema.optional(),
1768
+ padding: nonNegativeNumberSchema.optional(),
1756
1769
  lanes: zod.z.record(
1757
1770
  zod.z.string(),
1758
1771
  zod.z.object({
@@ -1818,7 +1831,8 @@ var diagramDslSchema = zod.z.object({
1818
1831
  title: zod.z.string().optional(),
1819
1832
  direction: directionSchema.optional(),
1820
1833
  layout: zod.z.object({
1821
- direction: directionSchema.optional()
1834
+ direction: directionSchema.optional(),
1835
+ primaryReadingDirection: primaryReadingDirectionSchema.optional()
1822
1836
  }).optional(),
1823
1837
  routing: zod.z.object({
1824
1838
  kind: routeKindSchema.optional(),
@@ -2348,15 +2362,30 @@ function renderSwimlane(swimlane) {
2348
2362
  lines.push(
2349
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}"/>`
2350
2364
  );
2351
- 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) {
2352
2371
  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>`
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"/>`
2354
2373
  );
2355
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
+ }
2356
2379
  }
2357
2380
  lines.push(" </g>");
2358
2381
  return lines;
2359
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
+ }
2360
2389
  function renderPorts(node) {
2361
2390
  return (node.ports ?? []).flatMap((port) => [
2362
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)}"/>`,
@@ -2934,6 +2963,19 @@ function solveDiagram(diagram, options = {}) {
2934
2963
  constraints
2935
2964
  });
2936
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);
2937
2979
  const coordinatedNodes = coordinateNodes(
2938
2980
  nodes,
2939
2981
  constrained.boxes,
@@ -2958,7 +3000,8 @@ function solveDiagram(diagram, options = {}) {
2958
3000
  );
2959
3001
  const coordinatedSwimlanes = coordinateSwimlanes(
2960
3002
  diagram.swimlanes ?? [],
2961
- constrained.boxes
3003
+ constrained.boxes,
3004
+ swimlaneContracts.layouts
2962
3005
  );
2963
3006
  const groupBoxes = new Map(
2964
3007
  coordinatedGroups.map((group) => [group.id, group.box])
@@ -3000,6 +3043,591 @@ function solveDiagram(diagram, options = {}) {
3000
3043
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
3001
3044
  };
3002
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
+ }
3003
3631
  function coordinateNodes(nodes, boxes, options, diagnostics) {
3004
3632
  const coordinated = [];
3005
3633
  for (const node of nodes) {
@@ -3114,16 +3742,70 @@ function portLabelBox(port) {
3114
3742
  height
3115
3743
  };
3116
3744
  }
3117
- function coordinateSwimlanes(swimlanes, nodeBoxes) {
3118
- const titleSize = 28;
3119
- const padding = 16;
3745
+ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
3120
3746
  return swimlanes.map((swimlane) => {
3121
- 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) => {
3122
3802
  const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
3123
- return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
3803
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
3124
3804
  });
3125
- const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
3126
- 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);
3127
3809
  const laneCount = Math.max(1, swimlane.lanes.length);
3128
3810
  const lanes = swimlane.lanes.map((lane, index) => {
3129
3811
  const box = swimlane.orientation === "vertical" ? {
@@ -3137,9 +3819,42 @@ function coordinateSwimlanes(swimlanes, nodeBoxes) {
3137
3819
  width: outer.width,
3138
3820
  height: outer.height / laneCount
3139
3821
  };
3140
- 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
+ };
3141
3850
  });
3142
- 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
+ };
3143
3858
  });
3144
3859
  }
3145
3860
  function coordinateFrame(frame, contentBounds) {