/** * HeaderDrawerLayoutEngine - Calculates row placement for header items * * Prevents visual overlap by assigning items to different rows when * they occupy the same columns. Uses a track-based algorithm similar * to V1's AllDayLayoutEngine. * * Each row can hold multiple items as long as they don't overlap in columns. * When an item spans columns that are already occupied, it's placed in the * next available row. */ export interface IHeaderItemLayout { itemId: string; gridArea: string; // "row / col-start / row+1 / col-end" startColumn: number; endColumn: number; row: number; } export interface IHeaderItemInput { id: string; columnStart: number; // 0-based column index columnEnd: number; // 0-based end column (inclusive) } export class HeaderDrawerLayoutEngine { private tracks: boolean[][] = []; private columnCount: number; constructor(columnCount: number) { this.columnCount = columnCount; this.reset(); } /** * Reset tracks for new layout calculation */ reset(): void { this.tracks = [new Array(this.columnCount).fill(false)]; } /** * Calculate layout for all items * Items should be sorted by start column for optimal packing */ calculateLayout(items: IHeaderItemInput[]): IHeaderItemLayout[] { this.reset(); const layouts: IHeaderItemLayout[] = []; for (const item of items) { const row = this.findAvailableRow(item.columnStart, item.columnEnd); // Mark columns as occupied in this row for (let col = item.columnStart; col <= item.columnEnd; col++) { this.tracks[row][col] = true; } // gridArea format: "row / col-start / row+1 / col-end" // CSS grid uses 1-based indices layouts.push({ itemId: item.id, gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`, startColumn: item.columnStart, endColumn: item.columnEnd, row: row + 1 // 1-based for CSS }); } return layouts; } /** * Calculate layout for a single new item * Useful for real-time drag operations */ calculateSingleLayout(item: IHeaderItemInput): IHeaderItemLayout { const row = this.findAvailableRow(item.columnStart, item.columnEnd); // Mark columns as occupied for (let col = item.columnStart; col <= item.columnEnd; col++) { this.tracks[row][col] = true; } return { itemId: item.id, gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`, startColumn: item.columnStart, endColumn: item.columnEnd, row: row + 1 }; } /** * Find the first row where all columns in range are available */ private findAvailableRow(startCol: number, endCol: number): number { for (let row = 0; row < this.tracks.length; row++) { if (this.isRowAvailable(row, startCol, endCol)) { return row; } } // Add new row if all existing rows are occupied this.tracks.push(new Array(this.columnCount).fill(false)); return this.tracks.length - 1; } /** * Check if columns in range are all available in given row */ private isRowAvailable(row: number, startCol: number, endCol: number): boolean { for (let col = startCol; col <= endCol; col++) { if (this.tracks[row][col]) { return false; } } return true; } /** * Get the number of rows currently in use */ getRowCount(): number { return this.tracks.length; } /** * Update column count (e.g., when view changes) */ setColumnCount(count: number): void { this.columnCount = count; this.reset(); } }