af8b9fe00f
Full-featured house/apartment floor plan editor with: - Turborepo monorepo (React/Vite client, Fastify/Prisma server, shared Zod schemas) - 2D room editor with walls, doors, windows, furniture, electrical elements - 3D room preview with Three.js (auto-hide nearest walls, bird's eye default) - Wall projection views with interactive drag (elevation, position) - Apartment floor plan view with room positioning - Copy/paste, alignment tools, measurement tool, annotations - Item-attached annotations with leader lines (visible on projections) - Door open direction (LEFT/RIGHT/INWARD/OUTWARD) with swing arc - Floor type textures (wood, tile, concrete, laminate, herringbone) - Wall color picker for 3D view - Furniture: bed, desk, wardrobe, sofa, table, chair, shelf, nightstand, dresser, bookcase, TV (with stand toggle), AC unit - Furniture elevation support (wall-mounted items) - Auto-save with dirty state tracking, batch save API - Rotation-aware collision detection (SAT/OBB) with 3D elevation check - Rotation-aware hit testing - i18n (English/Russian) with locale-aware number formatting - Dark mode with system preference detection - Undo/redo, keyboard shortcuts, scale bar - PDF/PNG/JSON export and JSON import - Focus trap modal, toast notifications, tooltips - Responsive layout with overlay palettes
106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
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<string> {
|
|
if (items.length < 2) return new Set();
|
|
|
|
const obbs = items.map(computeOBB);
|
|
const colliding = new Set<string>();
|
|
|
|
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;
|
|
}
|