@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/cli/index.js CHANGED
@@ -430,15 +430,30 @@ function renderSwimlane(swimlane) {
430
430
  lines.push(
431
431
  ` <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}"/>`
432
432
  );
433
- if (lane.label?.text !== void 0) {
433
+ if (lane.headerBox !== void 0) {
434
434
  lines.push(
435
- ` <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>`
435
+ ` <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}"/>`
436
436
  );
437
437
  }
438
+ if (lane.contentBox !== void 0) {
439
+ lines.push(
440
+ ` <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"/>`
441
+ );
442
+ }
443
+ if (lane.label?.text !== void 0) {
444
+ const labelBox = lane.headerBox ?? lane.box;
445
+ lines.push(renderSwimlaneLabel(swimlane, lane.label.text, labelBox));
446
+ }
438
447
  }
439
448
  lines.push(" </g>");
440
449
  return lines;
441
450
  }
451
+ function renderSwimlaneLabel(swimlane, text, labelBox) {
452
+ const x = labelBox.x + labelBox.width / 2;
453
+ const y = labelBox.y + labelBox.height / 2;
454
+ const transform = swimlane.orientation === "horizontal" ? ` transform="rotate(-90 ${formatNumber(x)} ${formatNumber(y)})"` : "";
455
+ 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>`;
456
+ }
442
457
  function renderPorts(node) {
443
458
  return (node.ports ?? []).flatMap((port) => [
444
459
  ` <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)}"/>`,
@@ -1714,6 +1729,19 @@ function solveDiagram(diagram, options = {}) {
1714
1729
  constraints
1715
1730
  });
1716
1731
  diagnostics.push(...constrained.diagnostics);
1732
+ const swimlaneContracts = applySwimlaneLayoutContracts(
1733
+ diagram.swimlanes ?? [],
1734
+ constraints,
1735
+ edges,
1736
+ isTopToBottomReadingDirection(diagram.metadata?.primaryReadingDirection),
1737
+ constrained.boxes,
1738
+ constrained.locks,
1739
+ options?.overlapSpacing ?? 40
1740
+ );
1741
+ if (swimlaneContracts.layouts.size > 0) {
1742
+ removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
1743
+ }
1744
+ diagnostics.push(...swimlaneContracts.diagnostics);
1717
1745
  const coordinatedNodes = coordinateNodes(
1718
1746
  nodes,
1719
1747
  constrained.boxes,
@@ -1738,7 +1766,8 @@ function solveDiagram(diagram, options = {}) {
1738
1766
  );
1739
1767
  const coordinatedSwimlanes = coordinateSwimlanes(
1740
1768
  diagram.swimlanes ?? [],
1741
- constrained.boxes
1769
+ constrained.boxes,
1770
+ swimlaneContracts.layouts
1742
1771
  );
1743
1772
  const groupBoxes = new Map(
1744
1773
  coordinatedGroups.map((group) => [group.id, group.box])
@@ -1780,6 +1809,591 @@ function solveDiagram(diagram, options = {}) {
1780
1809
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
1781
1810
  };
1782
1811
  }
1812
+ function applySwimlaneLayoutContracts(swimlanes, constraints, edges, topToBottomFlow, nodeBoxes, locks, overlapSpacing) {
1813
+ const layouts = /* @__PURE__ */ new Map();
1814
+ const diagnostics = [];
1815
+ const movedChildIds = /* @__PURE__ */ new Set();
1816
+ for (const swimlane of swimlanes) {
1817
+ if ((swimlane.layout ?? "overlay") !== "contract") {
1818
+ continue;
1819
+ }
1820
+ if (swimlane.lanes.length === 0) {
1821
+ continue;
1822
+ }
1823
+ const layout2 = applySingleSwimlaneContract(
1824
+ swimlane,
1825
+ edges,
1826
+ topToBottomFlow,
1827
+ nodeBoxes,
1828
+ locks,
1829
+ diagnostics,
1830
+ movedChildIds
1831
+ );
1832
+ if (layout2 !== void 0) {
1833
+ layouts.set(swimlane.id, layout2);
1834
+ }
1835
+ }
1836
+ if (layouts.size > 0) {
1837
+ diagnostics.push(
1838
+ ...reportSwimlaneOverlaps(nodeBoxes, locks, overlapSpacing),
1839
+ ...reportSwimlaneConstraintInvalidations(
1840
+ constraints,
1841
+ nodeBoxes,
1842
+ movedChildIds
1843
+ )
1844
+ );
1845
+ }
1846
+ return { layouts, diagnostics, movedChildIds };
1847
+ }
1848
+ function applySingleSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, locks, diagnostics, movedChildIds) {
1849
+ const headerHeight = swimlane.headerHeight ?? 28;
1850
+ const padding = swimlane.padding ?? 16;
1851
+ const laneBounds = swimlane.lanes.map((lane) => {
1852
+ const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
1853
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
1854
+ });
1855
+ const populatedBounds = laneBounds.filter(
1856
+ (box) => box !== void 0
1857
+ );
1858
+ if (populatedBounds.length === 0) {
1859
+ return void 0;
1860
+ }
1861
+ if (swimlane.orientation === "vertical") {
1862
+ return applyVerticalSwimlaneContract(
1863
+ swimlane,
1864
+ edges,
1865
+ topToBottomFlow,
1866
+ nodeBoxes,
1867
+ laneBounds,
1868
+ headerHeight,
1869
+ padding,
1870
+ locks,
1871
+ diagnostics,
1872
+ movedChildIds
1873
+ );
1874
+ }
1875
+ return applyHorizontalSwimlaneContract(
1876
+ swimlane,
1877
+ nodeBoxes,
1878
+ laneBounds,
1879
+ headerHeight,
1880
+ padding,
1881
+ locks,
1882
+ diagnostics,
1883
+ movedChildIds
1884
+ );
1885
+ }
1886
+ function applyVerticalSwimlaneContract(swimlane, edges, topToBottomFlow, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
1887
+ const populatedBounds = laneBounds.filter(
1888
+ (box) => box !== void 0
1889
+ );
1890
+ const top = Math.min(...populatedBounds.map((box) => box.y));
1891
+ const left = Math.min(...populatedBounds.map((box) => box.x));
1892
+ const maxChildHeight = Math.max(...populatedBounds.map((box) => box.height));
1893
+ const flowRanks = topToBottomFlow ? rankVerticalSwimlaneChildren(swimlane, edges) : /* @__PURE__ */ new Map();
1894
+ const maxRank = flowRanks.size === 0 ? 0 : Math.max(...Array.from(flowRanks.values()));
1895
+ const rankStackGap = Math.max(8, padding / 2);
1896
+ const maxRankStackHeight = maxVerticalRankStackHeight(
1897
+ swimlane,
1898
+ nodeBoxes,
1899
+ flowRanks,
1900
+ rankStackGap
1901
+ );
1902
+ const rankSpacing = Math.max(96, maxRankStackHeight + padding);
1903
+ const contentHeight = maxRank === 0 ? maxChildHeight : maxRankStackHeight + maxRank * rankSpacing;
1904
+ const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + padding * 2;
1905
+ const laneContentTop = top + headerHeight + padding;
1906
+ for (let index = 0; index < swimlane.lanes.length; index += 1) {
1907
+ const lane = swimlane.lanes[index];
1908
+ const bounds = laneBounds[index];
1909
+ if (lane === void 0 || bounds === void 0) {
1910
+ continue;
1911
+ }
1912
+ const target = {
1913
+ x: left + slotWidth * index + padding,
1914
+ y: laneContentTop
1915
+ };
1916
+ if (maxRank === 0) {
1917
+ moveLaneChildren(
1918
+ lane.children,
1919
+ nodeBoxes,
1920
+ locks,
1921
+ diagnostics,
1922
+ movedChildIds,
1923
+ {
1924
+ x: target.x - bounds.x,
1925
+ y: target.y - bounds.y
1926
+ }
1927
+ );
1928
+ continue;
1929
+ }
1930
+ moveRankedVerticalLaneChildren(
1931
+ lane.children,
1932
+ nodeBoxes,
1933
+ locks,
1934
+ diagnostics,
1935
+ movedChildIds,
1936
+ flowRanks,
1937
+ rankSpacing,
1938
+ rankStackGap,
1939
+ {
1940
+ x: target.x - bounds.x,
1941
+ y: laneContentTop
1942
+ }
1943
+ );
1944
+ }
1945
+ return {
1946
+ box: {
1947
+ x: left,
1948
+ y: top,
1949
+ width: slotWidth * swimlane.lanes.length,
1950
+ height: contentHeight + padding * 2 + headerHeight
1951
+ },
1952
+ slotWidth,
1953
+ slotHeight: contentHeight + padding * 2 + headerHeight
1954
+ };
1955
+ }
1956
+ function isTopToBottomReadingDirection(value) {
1957
+ return value === "top_to_bottom" || value === "top-to-bottom";
1958
+ }
1959
+ function rankVerticalSwimlaneChildren(swimlane, edges) {
1960
+ const childOrder = /* @__PURE__ */ new Map();
1961
+ for (const lane of swimlane.lanes) {
1962
+ for (const childId of lane.children) {
1963
+ if (!childOrder.has(childId)) {
1964
+ childOrder.set(childId, childOrder.size);
1965
+ }
1966
+ }
1967
+ }
1968
+ if (childOrder.size === 0) {
1969
+ return /* @__PURE__ */ new Map();
1970
+ }
1971
+ const childIds = new Set(childOrder.keys());
1972
+ const relevantEdges = edges.filter(
1973
+ (edge) => childIds.has(edge.source.nodeId) && childIds.has(edge.target.nodeId) && edge.source.nodeId !== edge.target.nodeId
1974
+ );
1975
+ if (relevantEdges.length === 0) {
1976
+ return /* @__PURE__ */ new Map();
1977
+ }
1978
+ const ranks = new Map([...childIds].map((id) => [id, 0]));
1979
+ const outgoing = /* @__PURE__ */ new Map();
1980
+ const inDegree = new Map([...childIds].map((id) => [id, 0]));
1981
+ for (const edge of relevantEdges) {
1982
+ const targets = outgoing.get(edge.source.nodeId) ?? [];
1983
+ targets.push(edge.target.nodeId);
1984
+ outgoing.set(edge.source.nodeId, targets);
1985
+ inDegree.set(
1986
+ edge.target.nodeId,
1987
+ (inDegree.get(edge.target.nodeId) ?? 0) + 1
1988
+ );
1989
+ }
1990
+ const queue = [...childIds].filter((id) => (inDegree.get(id) ?? 0) === 0).sort((a, b) => (childOrder.get(a) ?? 0) - (childOrder.get(b) ?? 0));
1991
+ let visited = 0;
1992
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
1993
+ const sourceId = queue[cursor];
1994
+ if (sourceId === void 0) {
1995
+ continue;
1996
+ }
1997
+ visited += 1;
1998
+ for (const targetId of outgoing.get(sourceId) ?? []) {
1999
+ ranks.set(
2000
+ targetId,
2001
+ Math.max(ranks.get(targetId) ?? 0, (ranks.get(sourceId) ?? 0) + 1)
2002
+ );
2003
+ const nextInDegree = (inDegree.get(targetId) ?? 0) - 1;
2004
+ inDegree.set(targetId, nextInDegree);
2005
+ if (nextInDegree === 0) {
2006
+ queue.push(targetId);
2007
+ }
2008
+ }
2009
+ }
2010
+ return visited === childIds.size ? ranks : rankCyclicSwimlaneChildren(childIds, relevantEdges);
2011
+ }
2012
+ function rankCyclicSwimlaneChildren(childIds, edges) {
2013
+ const maxRank = Math.max(0, childIds.size - 1);
2014
+ const ranks = new Map([...childIds].map((id) => [id, 0]));
2015
+ for (let iteration = 0; iteration < childIds.size; iteration += 1) {
2016
+ let changed = false;
2017
+ for (const edge of edges) {
2018
+ const nextRank = Math.min(
2019
+ maxRank,
2020
+ (ranks.get(edge.source.nodeId) ?? 0) + 1
2021
+ );
2022
+ if (nextRank > (ranks.get(edge.target.nodeId) ?? 0)) {
2023
+ ranks.set(edge.target.nodeId, nextRank);
2024
+ changed = true;
2025
+ }
2026
+ }
2027
+ if (!changed) {
2028
+ break;
2029
+ }
2030
+ }
2031
+ return ranks;
2032
+ }
2033
+ function maxVerticalRankStackHeight(swimlane, nodeBoxes, flowRanks, gap) {
2034
+ let maxHeight = 0;
2035
+ for (const lane of swimlane.lanes) {
2036
+ for (const stack of rankStacks(
2037
+ lane.children,
2038
+ nodeBoxes,
2039
+ flowRanks
2040
+ ).values()) {
2041
+ const height = stack.reduce(
2042
+ (total, item, index) => total + item.box.height + (index === 0 ? 0 : gap),
2043
+ 0
2044
+ );
2045
+ maxHeight = Math.max(maxHeight, height);
2046
+ }
2047
+ }
2048
+ return maxHeight;
2049
+ }
2050
+ function moveRankedVerticalLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, flowRanks, rankSpacing, rankStackGap, target) {
2051
+ for (const [rank, stack] of rankStacks(childIds, nodeBoxes, flowRanks)) {
2052
+ let yOffset = 0;
2053
+ for (const item of stack) {
2054
+ const { childId, box } = item;
2055
+ if (locks.has(childId)) {
2056
+ diagnostics.push({
2057
+ severity: "warning",
2058
+ code: "constraints.locked-target-not-moved",
2059
+ message: `Locked child ${childId} was not moved into contract swimlane slot.`,
2060
+ path: ["swimlanes"],
2061
+ detail: { nodeId: childId }
2062
+ });
2063
+ continue;
2064
+ }
2065
+ const next = {
2066
+ ...box,
2067
+ x: box.x + target.x,
2068
+ y: target.y + rank * rankSpacing + yOffset
2069
+ };
2070
+ if (next.x !== box.x || next.y !== box.y) {
2071
+ movedChildIds.add(childId);
2072
+ }
2073
+ nodeBoxes.set(childId, next);
2074
+ yOffset += box.height + rankStackGap;
2075
+ }
2076
+ }
2077
+ }
2078
+ function rankStacks(childIds, nodeBoxes, flowRanks) {
2079
+ const stacks = /* @__PURE__ */ new Map();
2080
+ for (const childId of childIds) {
2081
+ const box = nodeBoxes.get(childId);
2082
+ if (box === void 0) {
2083
+ continue;
2084
+ }
2085
+ const rank = flowRanks.get(childId) ?? 0;
2086
+ const stack = stacks.get(rank) ?? [];
2087
+ stack.push({ childId, box });
2088
+ stacks.set(rank, stack);
2089
+ }
2090
+ for (const stack of stacks.values()) {
2091
+ stack.sort((a, b) => {
2092
+ const deltaY = a.box.y - b.box.y;
2093
+ return deltaY === 0 ? a.childId.localeCompare(b.childId) : deltaY;
2094
+ });
2095
+ }
2096
+ return stacks;
2097
+ }
2098
+ function applyHorizontalSwimlaneContract(swimlane, nodeBoxes, laneBounds, headerHeight, padding, locks, diagnostics, movedChildIds) {
2099
+ const populatedBounds = laneBounds.filter(
2100
+ (box) => box !== void 0
2101
+ );
2102
+ const top = Math.min(...populatedBounds.map((box) => box.y));
2103
+ const left = Math.min(...populatedBounds.map((box) => box.x));
2104
+ const slotWidth = Math.max(...populatedBounds.map((box) => box.width)) + headerHeight + padding * 2;
2105
+ const slotHeight = Math.max(...populatedBounds.map((box) => box.height)) + padding * 2;
2106
+ for (let index = 0; index < swimlane.lanes.length; index += 1) {
2107
+ const lane = swimlane.lanes[index];
2108
+ const bounds = laneBounds[index];
2109
+ if (lane === void 0 || bounds === void 0) {
2110
+ continue;
2111
+ }
2112
+ const target = {
2113
+ x: left + headerHeight + padding,
2114
+ y: top + slotHeight * index + padding
2115
+ };
2116
+ moveLaneChildren(
2117
+ lane.children,
2118
+ nodeBoxes,
2119
+ locks,
2120
+ diagnostics,
2121
+ movedChildIds,
2122
+ {
2123
+ x: target.x - bounds.x,
2124
+ y: target.y - bounds.y
2125
+ }
2126
+ );
2127
+ }
2128
+ return {
2129
+ box: {
2130
+ x: left,
2131
+ y: top,
2132
+ width: slotWidth,
2133
+ height: slotHeight * swimlane.lanes.length
2134
+ },
2135
+ slotWidth,
2136
+ slotHeight
2137
+ };
2138
+ }
2139
+ function moveLaneChildren(childIds, nodeBoxes, locks, diagnostics, movedChildIds, offset) {
2140
+ for (const childId of childIds) {
2141
+ const box = nodeBoxes.get(childId);
2142
+ if (box === void 0) {
2143
+ continue;
2144
+ }
2145
+ if (locks.has(childId)) {
2146
+ diagnostics.push({
2147
+ severity: "warning",
2148
+ code: "constraints.locked-target-not-moved",
2149
+ message: `Locked child ${childId} was not moved into contract swimlane slot.`,
2150
+ path: ["swimlanes"],
2151
+ detail: { nodeId: childId }
2152
+ });
2153
+ continue;
2154
+ }
2155
+ if (offset.x !== 0 || offset.y !== 0) {
2156
+ movedChildIds.add(childId);
2157
+ }
2158
+ nodeBoxes.set(childId, {
2159
+ ...box,
2160
+ x: box.x + offset.x,
2161
+ y: box.y + offset.y
2162
+ });
2163
+ }
2164
+ }
2165
+ function removeResolvedOverlapDiagnostics(diagnostics, nodeBoxes) {
2166
+ for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
2167
+ const diagnostic = diagnostics[index];
2168
+ if (diagnostic?.code !== "constraints.overlap.unresolved") {
2169
+ continue;
2170
+ }
2171
+ const firstId = detailString(diagnostic, "firstId");
2172
+ const secondId = detailString(diagnostic, "secondId");
2173
+ const first = firstId === void 0 ? void 0 : nodeBoxes.get(firstId);
2174
+ const second = secondId === void 0 ? void 0 : nodeBoxes.get(secondId);
2175
+ if (first !== void 0 && second !== void 0 && !intersectsAabb(first, second)) {
2176
+ diagnostics.splice(index, 1);
2177
+ }
2178
+ }
2179
+ }
2180
+ function reportSwimlaneConstraintInvalidations(constraints, nodeBoxes, movedChildIds) {
2181
+ const diagnostics = [];
2182
+ for (const constraint of constraints) {
2183
+ const invalidatedNodeIds = movedConstraintNodeIds(
2184
+ constraint,
2185
+ nodeBoxes,
2186
+ movedChildIds
2187
+ );
2188
+ if (invalidatedNodeIds.length === 0) {
2189
+ continue;
2190
+ }
2191
+ diagnostics.push({
2192
+ severity: "warning",
2193
+ code: "constraints.swimlane-contract.invalidated",
2194
+ message: `Contract swimlane placement moved node(s) after ${constraint.kind} constraint solving; final geometry no longer satisfies that constraint.`,
2195
+ path: ["swimlanes"],
2196
+ detail: {
2197
+ constraintKind: constraint.kind,
2198
+ ...constraint.id === void 0 ? {} : { constraintId: constraint.id },
2199
+ nodeIds: invalidatedNodeIds
2200
+ }
2201
+ });
2202
+ }
2203
+ return diagnostics;
2204
+ }
2205
+ function movedConstraintNodeIds(constraint, nodeBoxes, movedChildIds) {
2206
+ switch (constraint.kind) {
2207
+ case "exact-position":
2208
+ return [];
2209
+ case "containment":
2210
+ return movedContainmentViolations(constraint, nodeBoxes, movedChildIds);
2211
+ case "relative-position":
2212
+ return movedRelativeViolations(constraint, nodeBoxes, movedChildIds);
2213
+ case "align":
2214
+ return movedAlignViolations(constraint, nodeBoxes, movedChildIds);
2215
+ case "distribute":
2216
+ return movedDistributeViolations(constraint, nodeBoxes, movedChildIds);
2217
+ }
2218
+ }
2219
+ function movedContainmentViolations(constraint, nodeBoxes, movedChildIds) {
2220
+ const container = nodeBoxes.get(constraint.containerId);
2221
+ if (container === void 0) {
2222
+ return [];
2223
+ }
2224
+ const content = paddedContentBox(container, constraint.padding);
2225
+ return constraint.childIds.filter((childId) => {
2226
+ if (!movedChildIds.has(childId)) {
2227
+ return false;
2228
+ }
2229
+ const child = nodeBoxes.get(childId);
2230
+ return child !== void 0 && !boxInside(child, content);
2231
+ });
2232
+ }
2233
+ function movedRelativeViolations(constraint, nodeBoxes, movedChildIds) {
2234
+ if (!movedChildIds.has(constraint.sourceId) && !movedChildIds.has(constraint.referenceId)) {
2235
+ return [];
2236
+ }
2237
+ const source = nodeBoxes.get(constraint.sourceId);
2238
+ const reference = nodeBoxes.get(constraint.referenceId);
2239
+ if (source === void 0 || reference === void 0) {
2240
+ return [];
2241
+ }
2242
+ return sameBoxPosition(
2243
+ source,
2244
+ expectedRelativeBox(source, reference, constraint)
2245
+ ) ? [] : [constraint.sourceId];
2246
+ }
2247
+ function movedAlignViolations(constraint, nodeBoxes, movedChildIds) {
2248
+ if (!constraint.targetIds.some((id) => movedChildIds.has(id))) {
2249
+ return [];
2250
+ }
2251
+ const targets = constraint.targetIds.map((id) => ({ id, box: nodeBoxes.get(id) })).filter(
2252
+ (target) => target.box !== void 0
2253
+ );
2254
+ const anchor = targets[0];
2255
+ if (anchor === void 0) {
2256
+ return [];
2257
+ }
2258
+ const expected = alignmentValue2(anchor.box, constraint.axis);
2259
+ return targets.filter(
2260
+ (target) => movedChildIds.has(target.id) && !sameNumber(alignmentValue2(target.box, constraint.axis), expected)
2261
+ ).map((target) => target.id);
2262
+ }
2263
+ function movedDistributeViolations(constraint, nodeBoxes, movedChildIds) {
2264
+ if (!constraint.targetIds.some((id) => movedChildIds.has(id))) {
2265
+ return [];
2266
+ }
2267
+ const targets = constraint.targetIds.map((id) => ({ id, box: nodeBoxes.get(id) })).filter(
2268
+ (target) => target.box !== void 0
2269
+ ).sort((a, b) => {
2270
+ const delta = constraint.axis === "horizontal" ? a.box.x - b.box.x : a.box.y - b.box.y;
2271
+ return delta === 0 ? a.id.localeCompare(b.id) : delta;
2272
+ });
2273
+ if (targets.length < 3) {
2274
+ return [];
2275
+ }
2276
+ const first = targets[0];
2277
+ const last = targets.at(-1);
2278
+ if (first === void 0 || last === void 0) {
2279
+ return [];
2280
+ }
2281
+ const expectedSpacing = constraint.spacing ?? (distributionStart2(last.box, constraint.axis) - distributionStart2(first.box, constraint.axis)) / (targets.length - 1);
2282
+ return targets.slice(1).filter((target, index) => {
2283
+ const previous = targets[index];
2284
+ if (previous === void 0 || !movedChildIds.has(target.id)) {
2285
+ return false;
2286
+ }
2287
+ return !sameNumber(
2288
+ distributionStart2(target.box, constraint.axis) - distributionStart2(previous.box, constraint.axis),
2289
+ expectedSpacing
2290
+ );
2291
+ }).map((target) => target.id);
2292
+ }
2293
+ function expectedRelativeBox(source, reference, constraint) {
2294
+ const offset = constraint.offset ?? { x: 0, y: 0 };
2295
+ switch (constraint.relation) {
2296
+ case "above":
2297
+ return {
2298
+ ...source,
2299
+ x: reference.x + offset.x,
2300
+ y: reference.y - source.height + offset.y
2301
+ };
2302
+ case "right-of":
2303
+ return {
2304
+ ...source,
2305
+ x: reference.x + reference.width + offset.x,
2306
+ y: reference.y + offset.y
2307
+ };
2308
+ case "below":
2309
+ return {
2310
+ ...source,
2311
+ x: reference.x + offset.x,
2312
+ y: reference.y + reference.height + offset.y
2313
+ };
2314
+ case "left-of":
2315
+ return {
2316
+ ...source,
2317
+ x: reference.x - source.width + offset.x,
2318
+ y: reference.y + offset.y
2319
+ };
2320
+ }
2321
+ }
2322
+ function paddedContentBox(container, padding) {
2323
+ const margin = padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
2324
+ return {
2325
+ x: container.x + margin.left,
2326
+ y: container.y + margin.top,
2327
+ width: container.width - margin.left - margin.right,
2328
+ height: container.height - margin.top - margin.bottom
2329
+ };
2330
+ }
2331
+ function boxInside(child, container) {
2332
+ 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;
2333
+ }
2334
+ function sameBoxPosition(first, second) {
2335
+ return sameNumber(first.x, second.x) && sameNumber(first.y, second.y);
2336
+ }
2337
+ function sameNumber(first, second) {
2338
+ return Math.abs(first - second) < 1e-3;
2339
+ }
2340
+ function alignmentValue2(box, axis) {
2341
+ switch (axis) {
2342
+ case "x":
2343
+ case "left":
2344
+ return box.x;
2345
+ case "y":
2346
+ case "top":
2347
+ return box.y;
2348
+ case "center-x":
2349
+ return box.x + box.width / 2;
2350
+ case "center-y":
2351
+ return box.y + box.height / 2;
2352
+ case "right":
2353
+ return box.x + box.width;
2354
+ case "bottom":
2355
+ return box.y + box.height;
2356
+ }
2357
+ }
2358
+ function distributionStart2(box, axis) {
2359
+ return axis === "horizontal" ? box.x : box.y;
2360
+ }
2361
+ function detailString(diagnostic, key) {
2362
+ const value = diagnostic.detail?.[key];
2363
+ return typeof value === "string" ? value : void 0;
2364
+ }
2365
+ function reportSwimlaneOverlaps(nodeBoxes, locks, overlapSpacing) {
2366
+ const diagnostics = [];
2367
+ const ids = [...nodeBoxes.keys()].sort();
2368
+ for (const firstId of ids) {
2369
+ for (const secondId of ids) {
2370
+ if (firstId >= secondId) {
2371
+ continue;
2372
+ }
2373
+ const first = nodeBoxes.get(firstId);
2374
+ const second = nodeBoxes.get(secondId);
2375
+ if (first === void 0 || second === void 0) {
2376
+ continue;
2377
+ }
2378
+ if (!intersectsAabb(first, second)) {
2379
+ continue;
2380
+ }
2381
+ diagnostics.push({
2382
+ severity: "warning",
2383
+ code: "constraints.overlap.unresolved",
2384
+ message: `Boxes ${firstId} and ${secondId} still overlap after contract swimlane placement with configured spacing ${overlapSpacing}.`,
2385
+ path: ["swimlanes"],
2386
+ detail: {
2387
+ firstId,
2388
+ secondId,
2389
+ firstLocked: locks.has(firstId),
2390
+ secondLocked: locks.has(secondId)
2391
+ }
2392
+ });
2393
+ }
2394
+ }
2395
+ return diagnostics;
2396
+ }
1783
2397
  function coordinateNodes(nodes, boxes, options, diagnostics) {
1784
2398
  const coordinated = [];
1785
2399
  for (const node of nodes) {
@@ -1894,16 +2508,70 @@ function portLabelBox(port) {
1894
2508
  height
1895
2509
  };
1896
2510
  }
1897
- function coordinateSwimlanes(swimlanes, nodeBoxes) {
1898
- const titleSize = 28;
1899
- const padding = 16;
2511
+ function coordinateSwimlanes(swimlanes, nodeBoxes, layouts) {
1900
2512
  return swimlanes.map((swimlane) => {
1901
- const laneBoxes = swimlane.lanes.flatMap((lane) => {
2513
+ const layout2 = swimlane.layout ?? "overlay";
2514
+ const headerHeight = swimlane.headerHeight ?? 28;
2515
+ const padding = swimlane.padding ?? 16;
2516
+ const contractLayout = layouts.get(swimlane.id);
2517
+ if (layout2 === "contract" && contractLayout !== void 0) {
2518
+ const lanes2 = swimlane.lanes.map((lane, index) => {
2519
+ const box = swimlane.orientation === "vertical" ? {
2520
+ x: contractLayout.box.x + contractLayout.slotWidth * index,
2521
+ y: contractLayout.box.y,
2522
+ width: contractLayout.slotWidth,
2523
+ height: contractLayout.box.height
2524
+ } : {
2525
+ x: contractLayout.box.x,
2526
+ y: contractLayout.box.y + contractLayout.slotHeight * index,
2527
+ width: contractLayout.box.width,
2528
+ height: contractLayout.slotHeight
2529
+ };
2530
+ const headerBox = swimlane.orientation === "vertical" ? {
2531
+ x: box.x,
2532
+ y: box.y,
2533
+ width: box.width,
2534
+ height: headerHeight
2535
+ } : {
2536
+ x: box.x,
2537
+ y: box.y,
2538
+ width: headerHeight,
2539
+ height: box.height
2540
+ };
2541
+ const contentBox2 = swimlane.orientation === "vertical" ? {
2542
+ x: box.x,
2543
+ y: box.y + headerHeight,
2544
+ width: box.width,
2545
+ height: Math.max(0, box.height - headerHeight)
2546
+ } : {
2547
+ x: box.x + headerHeight,
2548
+ y: box.y,
2549
+ width: Math.max(0, box.width - headerHeight),
2550
+ height: box.height
2551
+ };
2552
+ return {
2553
+ ...lane,
2554
+ box,
2555
+ headerBox,
2556
+ contentBox: contentBox2
2557
+ };
2558
+ });
2559
+ return {
2560
+ ...swimlane,
2561
+ lanes: lanes2,
2562
+ box: contractLayout.box,
2563
+ ...headerHeight === void 0 ? {} : { headerHeight },
2564
+ ...padding === void 0 ? {} : { padding }
2565
+ };
2566
+ }
2567
+ const laneContentBoxes = swimlane.lanes.map((lane) => {
1902
2568
  const childBoxes = lane.children.map((child) => nodeBoxes.get(child)).filter((box) => box !== void 0);
1903
- return childBoxes.length === 0 ? [] : [unionBoxes(childBoxes)];
2569
+ return childBoxes.length === 0 ? void 0 : unionBoxes(childBoxes);
1904
2570
  });
1905
- const laneUnion = laneBoxes.length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(laneBoxes);
1906
- const outer = expand(laneUnion, padding, titleSize);
2571
+ const laneUnion = laneContentBoxes.filter((box) => box !== void 0).length === 0 ? { x: 0, y: 0, width: 120, height: 80 } : unionBoxes(
2572
+ laneContentBoxes.filter((box) => box !== void 0)
2573
+ );
2574
+ const outer = expand(laneUnion, padding, headerHeight);
1907
2575
  const laneCount = Math.max(1, swimlane.lanes.length);
1908
2576
  const lanes = swimlane.lanes.map((lane, index) => {
1909
2577
  const box = swimlane.orientation === "vertical" ? {
@@ -1917,9 +2585,42 @@ function coordinateSwimlanes(swimlanes, nodeBoxes) {
1917
2585
  width: outer.width,
1918
2586
  height: outer.height / laneCount
1919
2587
  };
1920
- return { ...lane, box };
2588
+ const headerBox = layout2 === "contract" ? swimlane.orientation === "vertical" ? {
2589
+ x: box.x,
2590
+ y: box.y,
2591
+ width: box.width,
2592
+ height: headerHeight
2593
+ } : {
2594
+ x: box.x,
2595
+ y: box.y,
2596
+ width: headerHeight,
2597
+ height: box.height
2598
+ } : void 0;
2599
+ const contentBox2 = layout2 === "contract" ? swimlane.orientation === "vertical" ? {
2600
+ x: box.x,
2601
+ y: box.y + headerHeight,
2602
+ width: box.width,
2603
+ height: Math.max(0, box.height - headerHeight)
2604
+ } : {
2605
+ x: box.x + headerHeight,
2606
+ y: box.y,
2607
+ width: Math.max(0, box.width - headerHeight),
2608
+ height: box.height
2609
+ } : void 0;
2610
+ return {
2611
+ ...lane,
2612
+ box,
2613
+ ...headerBox === void 0 ? {} : { headerBox },
2614
+ ...contentBox2 === void 0 ? {} : { contentBox: contentBox2 }
2615
+ };
1921
2616
  });
1922
- return { ...swimlane, lanes, box: outer };
2617
+ return {
2618
+ ...swimlane,
2619
+ lanes,
2620
+ box: outer,
2621
+ ...headerHeight === void 0 ? {} : { headerHeight },
2622
+ ...padding === void 0 ? {} : { padding }
2623
+ };
1923
2624
  });
1924
2625
  }
1925
2626
  function coordinateFrame(frame, contentBounds) {
@@ -2458,6 +3159,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
2458
3159
  const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
2459
3160
  const routeKind = dsl.routing?.kind ?? "orthogonal";
2460
3161
  const portShifting = normalizePortShifting(dsl.routing?.portShifting);
3162
+ const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
2461
3163
  const diagram = {
2462
3164
  id: options.id ?? dsl.id ?? "diagram",
2463
3165
  ...dsl.title === void 0 ? {} : { title: dsl.title },
@@ -2471,6 +3173,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
2471
3173
  ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
2472
3174
  metadata: {
2473
3175
  routeKind,
3176
+ ...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
2474
3177
  ...portShifting === void 0 ? {} : { portShifting }
2475
3178
  }
2476
3179
  };
@@ -2641,14 +3344,17 @@ function formatCompartmentEntry(value) {
2641
3344
  return `${entry[0]}: ${entry[1]}`;
2642
3345
  }
2643
3346
  function normalizeSwimlanes(dsl) {
2644
- return Object.keys(dsl.swimlanes ?? {}).sort().map((id) => {
3347
+ return Object.keys(dsl.swimlanes ?? {}).map((id) => {
2645
3348
  const swimlane = dsl.swimlanes?.[id];
2646
3349
  const label = toLabel(swimlane?.label);
2647
3350
  return {
2648
3351
  id,
2649
3352
  ...label === void 0 ? {} : { label },
2650
3353
  orientation: swimlane?.orientation ?? "vertical",
2651
- lanes: Object.keys(swimlane?.lanes ?? {}).sort().map((laneId) => {
3354
+ layout: swimlane?.layout ?? "overlay",
3355
+ ...swimlane?.headerHeight === void 0 ? {} : { headerHeight: swimlane.headerHeight },
3356
+ ...swimlane?.padding === void 0 ? {} : { padding: swimlane.padding },
3357
+ lanes: Object.keys(swimlane?.lanes ?? {}).map((laneId) => {
2652
3358
  const lane = swimlane?.lanes[laneId];
2653
3359
  const laneLabel = toLabel(lane?.label);
2654
3360
  return {
@@ -2956,6 +3662,10 @@ var routeKindSchema = z.enum(["orthogonal", "straight"]);
2956
3662
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
2957
3663
  var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
2958
3664
  var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
3665
+ var primaryReadingDirectionSchema = z.enum([
3666
+ "top_to_bottom",
3667
+ "top-to-bottom"
3668
+ ]);
2959
3669
  var nodeShapeSchema = z.enum([
2960
3670
  "rectangle",
2961
3671
  "rounded-rectangle",
@@ -2966,6 +3676,7 @@ var nodeShapeSchema = z.enum([
2966
3676
  "cylinder"
2967
3677
  ]);
2968
3678
  var finiteNumberSchema = z.number().finite();
3679
+ var nonNegativeNumberSchema = finiteNumberSchema.min(0);
2969
3680
  var pointSchema = z.object({
2970
3681
  x: finiteNumberSchema,
2971
3682
  y: finiteNumberSchema
@@ -3052,6 +3763,9 @@ var groupSchema = z.object({
3052
3763
  var swimlaneSchema = z.object({
3053
3764
  label: labelSchema.optional(),
3054
3765
  orientation: z.enum(["vertical", "horizontal"]).optional(),
3766
+ layout: z.enum(["overlay", "contract"]).optional(),
3767
+ headerHeight: nonNegativeNumberSchema.optional(),
3768
+ padding: nonNegativeNumberSchema.optional(),
3055
3769
  lanes: z.record(
3056
3770
  z.string(),
3057
3771
  z.object({
@@ -3117,7 +3831,8 @@ var diagramDslSchema = z.object({
3117
3831
  title: z.string().optional(),
3118
3832
  direction: directionSchema.optional(),
3119
3833
  layout: z.object({
3120
- direction: directionSchema.optional()
3834
+ direction: directionSchema.optional(),
3835
+ primaryReadingDirection: primaryReadingDirectionSchema.optional()
3121
3836
  }).optional(),
3122
3837
  routing: z.object({
3123
3838
  kind: routeKindSchema.optional(),