Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | 160x 160x 18x 142x 186x 186x 142x 142x 142x 18x 18x 18x 43x 43x 43x 43x 43x 18x 142x 142x 186x 186x 48x 48x 48x 48x 14x 14x 14x 14x 14x 186x 172x 142x 142x 142x 142x 172x 186x 172x 142x 96x | import type { PackedRow, LayoutConfig } from "./types";
/** Node size info for packing */
interface NodeSize {
id: number;
width: number;
height: number;
}
/**
* Pack nodes into rows using First-Fit Decreasing Height (FFDH) algorithm
*
* Strategy:
* 1. Sort nodes by height descending (tall nodes first)
* 2. For each node, try to fit into existing row
* 3. If no row fits, create new row
* 4. Rows are sorted by height for visual consistency
*
* @param nodeIds - Node IDs to pack
* @param sizes - Map of node ID to size
* @param config - Layout configuration
* @returns Array of packed rows
*/
export function packNodesIntoRows(
nodeIds: number[],
sizes: Map<number, { width: number; height: number }>,
config: LayoutConfig
): PackedRow[] {
Iif (nodeIds.length === 0) return [];
// maxColumns: 0=auto, 1=disabled (vertical stack), 2+=fixed columns
if (config.maxColumns === 1) {
// Vertical stacking - one node per row
return packVertically(nodeIds, sizes, config.verticalGap);
}
// Build sized node list
const nodeSizes: NodeSize[] = nodeIds.map((id) => {
const size = sizes.get(id);
return {
id,
width: size?.width ?? 200,
height: size?.height ?? 100,
};
});
// Sort by height descending (FFDH: tall nodes first)
nodeSizes.sort((a, b) => b.height - a.height);
Iif (config.maxColumns >= 2) {
// Fixed column count
return packFixedColumns(nodeSizes, config.maxColumns, config.verticalGap);
}
// Auto mode: use maxRowWidth
return packByWidth(nodeSizes, config.maxRowWidth, config.horizontalGap, config.verticalGap);
}
/**
* Pack nodes vertically (one per row) - legacy behavior
*/
function packVertically(
nodeIds: number[],
sizes: Map<number, { width: number; height: number }>,
verticalGap: number
): PackedRow[] {
const rows: PackedRow[] = [];
let yOffset = 0;
for (const id of nodeIds) {
const size = sizes.get(id);
const height = size?.height ?? 100;
const width = size?.width ?? 200;
rows.push({
nodes: [id],
height,
yOffset,
width,
});
yOffset += height + verticalGap;
}
return rows;
}
/**
* Pack nodes into fixed number of columns
* Distributes nodes round-robin by height to balance columns
*/
function packFixedColumns(
nodeSizes: NodeSize[],
columnCount: number,
verticalGap: number
): PackedRow[] {
// Group nodes by similar height into rows
const rows: PackedRow[] = [];
let currentRow: NodeSize[] = [];
for (const node of nodeSizes) {
currentRow.push(node);
if (currentRow.length >= columnCount) {
// Row is full
const rowHeight = Math.max(...currentRow.map((n) => n.height));
const rowWidth = currentRow.reduce((sum, n) => sum + n.width, 0);
rows.push({
nodes: currentRow.map((n) => n.id),
height: rowHeight,
yOffset: 0, // Set later
width: rowWidth,
});
currentRow = [];
}
}
// Handle remaining nodes in last partial row
if (currentRow.length > 0) {
const rowHeight = Math.max(...currentRow.map((n) => n.height));
const rowWidth = currentRow.reduce((sum, n) => sum + n.width, 0);
rows.push({
nodes: currentRow.map((n) => n.id),
height: rowHeight,
yOffset: 0,
width: rowWidth,
});
}
// Calculate y offsets
let yOffset = 0;
for (const row of rows) {
row.yOffset = yOffset;
yOffset += row.height + verticalGap;
}
return rows;
}
/**
* Pack nodes by maximum row width (auto column count)
* Uses FFDH: First-Fit Decreasing Height
*/
function packByWidth(
nodeSizes: NodeSize[],
maxRowWidth: number,
horizontalGap: number,
verticalGap: number
): PackedRow[] {
const rows: Array<{
nodes: NodeSize[];
height: number;
currentWidth: number;
}> = [];
for (const node of nodeSizes) {
// Try to fit into existing row
let placed = false;
for (const row of rows) {
const newWidth = row.currentWidth + horizontalGap + node.width;
// Check if node fits and heights are compatible
// Allow 50% height variance to keep rows visually balanced
const heightRatio = row.height > 0 ? node.height / row.height : 1;
const heightCompatible = heightRatio >= 0.5 && heightRatio <= 2.0;
if (newWidth <= maxRowWidth && heightCompatible) {
row.nodes.push(node);
row.currentWidth = newWidth;
row.height = Math.max(row.height, node.height);
placed = true;
break;
}
}
if (!placed) {
// Create new row
rows.push({
nodes: [node],
height: node.height,
currentWidth: node.width,
});
}
}
// Sort rows by their tallest node height (tallest first for visual hierarchy)
rows.sort((a, b) => b.height - a.height);
// Convert to PackedRow format with y offsets
const result: PackedRow[] = [];
let yOffset = 0;
for (const row of rows) {
result.push({
nodes: row.nodes.map((n) => n.id),
height: row.height,
yOffset,
width: row.currentWidth,
});
yOffset += row.height + verticalGap;
}
return result;
}
/**
* Calculate total height of packed rows
*/
export function getPackedHeight(rows: PackedRow[]): number {
if (rows.length === 0) return 0;
const lastRow = rows[rows.length - 1];
return lastRow.yOffset + lastRow.height;
}
/**
* Calculate max width across all packed rows
*/
export function getPackedWidth(rows: PackedRow[]): number {
return Math.max(0, ...rows.map((r) => r.width));
}
|