refactor: event instances

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-03 19:10:00 -04:00
parent acbe0ad5ca
commit 37fa7b911e
17 changed files with 498 additions and 479 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* 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.startMs < rangeEndExclusive.getTime()
&& instance.endMs > rangeStartMs;
});
// Sort: longer events first, then by start date
multiDayEvents.sort((a, b) => {
if (b.durationMs !== a.durationMs) return b.durationMs - a.durationMs;
return a.startMs - b.startMs;
});
// Lane assignment - track which days are occupied in each lane
const lanes: boolean[][] = [];
for (const instance of multiDayEvents) {
const eventStart = startOfLocalDay(new Date(instance.startMs));
const eventEnd = startOfLocalDay(new Date(instance.endMs - 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 };
}