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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user