/** * Multi-Day Lane Layout * Assigns multi-day/all-day instances to horizontal lanes so they render * as spanning bars across day columns without overlapping. */ import type { CalendarInstance } from '@/types/instance' import { startOfLocalDay } from './date' export interface MultiDaySegment { instance: CalendarInstance; startCol: number; span: number; lane: number; isStart: boolean; isEnd: boolean; } export interface LaneAssignment { segments: MultiDaySegment[]; laneCount: number; } export function layoutMultiDayLanes( instances: CalendarInstance[], rangeStart: Date, rangeEnd: Date, columnCount: number, getColumnIndex: (date: Date) => number ): LaneAssignment { const segments: MultiDaySegment[] = []; // Filter multi-day/all-day events that overlap this range const rangeStartMs = startOfLocalDay(rangeStart).getTime(); const rangeEndExclusive = startOfLocalDay(new Date(rangeEnd)); rangeEndExclusive.setDate(rangeEndExclusive.getDate() + 1); const multiDayEvents = instances.filter(instance => { return instance.timeless && instance.start < rangeEndExclusive.getTime() && instance.end > rangeStartMs; }); // Sort: longer events first, then by start date multiDayEvents.sort((a, b) => { if (b.duration !== a.duration) return b.duration - a.duration; return a.start - b.start; }); // Lane assignment - track which days are occupied in each lane const lanes: boolean[][] = []; for (const instance of multiDayEvents) { const eventStart = startOfLocalDay(new Date(instance.start)); const eventEnd = startOfLocalDay(new Date(instance.end - 1)); // Clamp to visible range const segStart = eventStart < rangeStart ? rangeStart : eventStart; const segEnd = eventEnd > rangeEnd ? rangeEnd : eventEnd; const startCol = getColumnIndex(segStart); const endCol = getColumnIndex(segEnd); // Skip if outside visible columns if (startCol < 0 || startCol >= columnCount) continue; const span = Math.min(endCol, columnCount - 1) - startCol + 1; // Find available lane let lane = 0; while (true) { if (!lanes[lane]) { lanes[lane] = new Array(columnCount).fill(false); } let isFree = true; for (let d = startCol; d < startCol + span; d++) { if (lanes[lane][d]) { isFree = false; break; } } if (isFree) { for (let d = startCol; d < startCol + span; d++) { lanes[lane][d] = true; } break; } lane++; } segments.push({ instance, startCol, span, lane, isStart: eventStart >= rangeStart, isEnd: eventEnd <= rangeEnd, }); } return { segments, laneCount: lanes.length }; }