| 154 | } |
| 155 | |
| 156 | function autoLayout(spec) { |
| 157 | const style = spec.style || 'single'; |
| 158 | const dir = spec.direction || 'horizontal'; |
| 159 | const padding = 2; |
| 160 | const arrowLen = spec.arrowLength || 6; |
| 161 | |
| 162 | // Calculate box dimensions |
| 163 | const boxes = spec.boxes.map(b => { |
| 164 | const lines = b.label.split('\n'); |
| 165 | const maxLineLen = Math.max(...lines.map(l => l.length)); |
| 166 | const w = Math.max(maxLineLen + 4, 8); // min width 8 |
| 167 | const h = lines.length + 2; |
| 168 | return { ...b, w, h, lines }; |
| 169 | }); |
| 170 | |
| 171 | // Auto-position if no explicit coordinates |
| 172 | const needsLayout = boxes.some(b => b.x === undefined || b.y === undefined); |
| 173 | |
| 174 | // Check if any arrows have labels (need extra row for label above arrow) |
| 175 | const hasArrowLabels = spec.arrows && spec.arrows.some(a => a.label); |
| 176 | |
| 177 | if (needsLayout) { |
| 178 | if (dir === 'horizontal') { |
| 179 | let curX = 0; |
| 180 | const maxH = Math.max(...boxes.map(b => b.h)); |
| 181 | const yOffset = hasArrowLabels ? 1 : 0; // extra row for arrow labels |
| 182 | boxes.forEach(b => { |
| 183 | b.x = curX; |
| 184 | b.y = yOffset + Math.floor((maxH - b.h) / 2); // vertically center |
| 185 | curX += b.w + arrowLen; |
| 186 | }); |
| 187 | } else { |
| 188 | let curY = 0; |
| 189 | const maxW = Math.max(...boxes.map(b => b.w)); |
| 190 | boxes.forEach(b => { |
| 191 | b.x = Math.floor((maxW - b.w) / 2); // horizontally center |
| 192 | b.y = curY; |
| 193 | curY += b.h + 3; // gap for arrow + label |
| 194 | }); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // Calculate grid size |
| 199 | const totalW = Math.max(...boxes.map(b => b.x + b.w)) + padding; |
| 200 | const totalH = Math.max(...boxes.map(b => b.y + b.h)) + padding; |
| 201 | const grid = new Grid(totalW, totalH); |
| 202 | |
| 203 | // Index boxes by ID |
| 204 | const boxMap = new Map(); |
| 205 | boxes.forEach(b => { |
| 206 | const rect = grid.box(b.x, b.y, b.w, b.h, b.label, style); |
| 207 | boxMap.set(b.id, { ...rect, ...b }); |
| 208 | }); |
| 209 | |
| 210 | // Draw arrows |
| 211 | if (spec.arrows) { |
| 212 | spec.arrows.forEach(a => { |
| 213 | const from = boxMap.get(a.from); |