feat: complete house plan maker application

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
This commit is contained in:
2026-04-05 22:34:03 +03:00
parent b84807bbdb
commit af8b9fe00f
188 changed files with 35795 additions and 0 deletions
@@ -0,0 +1,130 @@
import { Group, Rect, Line } from 'react-konva';
import type { ProjectedOpening } from '../utils/projectionMapping';
import { projectionToPixel } from '../utils/projectionMapping';
interface ProjectionWindowProps {
readonly projected: ProjectedOpening;
readonly wallHeight: number;
readonly scale: number;
readonly padding: number;
readonly isSelected: boolean;
readonly isDragging?: boolean;
readonly dragAlongWall?: number;
readonly onClick: () => void;
readonly onDragStart?: (openingId: string, evt: MouseEvent) => void;
}
/** Render a window in wall elevation view. */
export function ProjectionWindow({
projected,
wallHeight,
scale,
padding,
isSelected,
isDragging = false,
dragAlongWall,
onClick,
onDragStart,
}: ProjectionWindowProps) {
const { rect, opening } = projected;
const displayX = isDragging && dragAlongWall != null
? dragAlongWall - opening.width / 2
: rect.x;
const topLeft = projectionToPixel(displayX, rect.y + rect.height, wallHeight, scale, padding);
const pxWidth = rect.width * scale;
const pxHeight = rect.height * scale;
const frameInset = 3;
return (
<Group
onClick={onClick}
onMouseDown={(e) => {
if (onDragStart && e.evt.button === 0) {
onDragStart(opening.id, e.evt);
}
}}
>
{/* Drag ghost outline */}
{isDragging && (
<Rect
x={topLeft.x - 2}
y={topLeft.y - 2}
width={pxWidth + 4}
height={pxHeight + 4}
stroke="#2563eb"
strokeWidth={1}
dash={[3, 3]}
fill="transparent"
/>
)}
{/* Window frame (outer) */}
<Rect
x={topLeft.x}
y={topLeft.y}
width={pxWidth}
height={pxHeight}
fill="#dbeafe"
stroke={isSelected ? '#2563eb' : '#3b82f6'}
strokeWidth={isSelected ? 2.5 : 1.5}
/>
{/* Glass pane (inner rectangle) */}
<Rect
x={topLeft.x + frameInset}
y={topLeft.y + frameInset}
width={pxWidth - frameInset * 2}
height={pxHeight - frameInset * 2}
fill="#bfdbfe"
stroke="#93c5fd"
strokeWidth={0.5}
/>
{/* Horizontal mullion (center divider) */}
<Line
points={[
topLeft.x + frameInset,
topLeft.y + pxHeight / 2,
topLeft.x + pxWidth - frameInset,
topLeft.y + pxHeight / 2,
]}
stroke="#3b82f6"
strokeWidth={1}
/>
{/* Vertical mullion (center divider) */}
<Line
points={[
topLeft.x + pxWidth / 2,
topLeft.y + frameInset,
topLeft.x + pxWidth / 2,
topLeft.y + pxHeight - frameInset,
]}
stroke="#3b82f6"
strokeWidth={1}
/>
{/* Glass cross lines for indication */}
<Line
points={[
topLeft.x + frameInset,
topLeft.y + frameInset,
topLeft.x + pxWidth / 2,
topLeft.y + pxHeight / 2,
]}
stroke="#93c5fd"
strokeWidth={0.5}
opacity={0.6}
/>
<Line
points={[
topLeft.x + pxWidth - frameInset,
topLeft.y + frameInset,
topLeft.x + pxWidth / 2,
topLeft.y + pxHeight / 2,
]}
stroke="#93c5fd"
strokeWidth={0.5}
opacity={0.6}
/>
</Group>
);
}