import type { FurnitureItem } from '@house-plan-maker/shared'; interface OBB { readonly id: string; readonly cx: number; readonly cy: number; readonly halfW: number; readonly halfD: number; readonly cos: number; readonly sin: number; } function computeOBB(item: FurnitureItem): OBB { const rad = (item.rotation * Math.PI) / 180; return { id: item.id, cx: item.x + item.width / 2, cy: item.y + item.depth / 2, halfW: item.width / 2, halfD: item.depth / 2, cos: Math.cos(rad), sin: Math.sin(rad), }; } /** Get the 4 corners of an OBB. */ function getCorners(obb: OBB): [number, number][] { const { cx, cy, halfW, halfD, cos, sin } = obb; // Local corners at (±halfW, ±halfD), rotated and translated return [ [cx + halfW * cos - halfD * sin, cy + halfW * sin + halfD * cos], [cx - halfW * cos - halfD * sin, cy - halfW * sin + halfD * cos], [cx - halfW * cos + halfD * sin, cy - halfW * sin - halfD * cos], [cx + halfW * cos + halfD * sin, cy + halfW * sin - halfD * cos], ]; } /** Project corners onto an axis and return [min, max]. */ function projectOntoAxis(corners: [number, number][], ax: number, ay: number): [number, number] { let min = Infinity; let max = -Infinity; for (const [x, y] of corners) { const p = x * ax + y * ay; if (p < min) min = p; if (p > max) max = p; } return [min, max]; } /** SAT overlap test for two OBBs. */ function obbOverlap(a: OBB, b: OBB): boolean { const cornersA = getCorners(a); const cornersB = getCorners(b); // 4 potential separating axes: 2 from each OBB's edges const axes: [number, number][] = [ [a.cos, a.sin], [-a.sin, a.cos], [b.cos, b.sin], [-b.sin, b.cos], ]; for (const [ax, ay] of axes) { const [minA, maxA] = projectOntoAxis(cornersA, ax, ay); const [minB, maxB] = projectOntoAxis(cornersB, ax, ay); if (maxA <= minB || maxB <= minA) { return false; // Separating axis found — no overlap } } return true; // No separating axis — overlapping } /** * Find all furniture IDs that collide using proper OBB (rotation-aware) overlap. */ export function findCollidingFurniture( items: readonly FurnitureItem[], ): ReadonlySet { if (items.length < 2) return new Set(); const obbs = items.map(computeOBB); const colliding = new Set(); for (let i = 0; i < obbs.length; i++) { for (let j = i + 1; j < obbs.length; j++) { // Check vertical overlap first (elevation + height) const a = items[i]; const b = items[j]; const aBottom = a.elevationFromFloor; const aTop = a.elevationFromFloor + a.height; const bBottom = b.elevationFromFloor; const bTop = b.elevationFromFloor + b.height; if (aTop <= bBottom || bTop <= aBottom) continue; // no vertical overlap // Then check 2D footprint overlap if (obbOverlap(obbs[i], obbs[j])) { colliding.add(obbs[i].id); colliding.add(obbs[j].id); } } } return colliding; }