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,105 @@
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import type { Wall, WallOpening } from '@house-plan-maker/shared';
|
||||
import {
|
||||
splitWallAroundOpenings,
|
||||
wallRotationY,
|
||||
wallSegmentCenter3D,
|
||||
type WallSegment,
|
||||
} from './utils/wallGeometry';
|
||||
|
||||
interface WallMeshProps {
|
||||
readonly wall: Wall;
|
||||
readonly openings: readonly WallOpening[];
|
||||
readonly wallHeight: number;
|
||||
readonly wallColor?: string;
|
||||
readonly selectedIds: ReadonlySet<string>;
|
||||
readonly onSelect?: (id: string) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_WALL_COLOR = '#f0ebe3';
|
||||
const WALL_SELECTED_COLOR = '#b8d4e3';
|
||||
|
||||
// ── Wall material cache ──
|
||||
const wallMaterialCache = new Map<string, THREE.MeshStandardMaterial>();
|
||||
|
||||
function getWallMaterial(color: string): THREE.MeshStandardMaterial {
|
||||
let mat = wallMaterialCache.get(color);
|
||||
if (!mat) {
|
||||
mat = new THREE.MeshStandardMaterial({ color, roughness: 0.7, side: THREE.DoubleSide });
|
||||
wallMaterialCache.set(color, mat);
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
const wallSelectedMaterial = new THREE.MeshStandardMaterial({
|
||||
color: WALL_SELECTED_COLOR,
|
||||
roughness: 0.7,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
|
||||
function WallSegmentMesh({
|
||||
wall,
|
||||
segment,
|
||||
thickness,
|
||||
wallColor,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: {
|
||||
readonly wall: Wall;
|
||||
readonly segment: WallSegment;
|
||||
readonly thickness: number;
|
||||
readonly wallColor: string;
|
||||
readonly isSelected: boolean;
|
||||
readonly onSelect?: (id: string) => void;
|
||||
}) {
|
||||
const segmentWidth = segment.endAlongWall - segment.startAlongWall;
|
||||
const segmentHeight = segment.topY - segment.bottomY;
|
||||
|
||||
const center = useMemo(
|
||||
() => wallSegmentCenter3D(wall, segment),
|
||||
[wall, segment],
|
||||
);
|
||||
|
||||
const rotY = useMemo(() => wallRotationY(wall), [wall]);
|
||||
|
||||
if (segmentWidth <= 0 || segmentHeight <= 0) return null;
|
||||
|
||||
return (
|
||||
<mesh
|
||||
position={center}
|
||||
rotation={[0, rotY, 0]}
|
||||
castShadow
|
||||
receiveShadow
|
||||
material={isSelected ? wallSelectedMaterial : getWallMaterial(wallColor)}
|
||||
onClick={onSelect ? (e) => { e.stopPropagation(); onSelect(wall.id); } : undefined}
|
||||
>
|
||||
<boxGeometry args={[segmentWidth, segmentHeight, thickness]} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export function WallMesh({ wall, openings, wallHeight, wallColor = DEFAULT_WALL_COLOR, selectedIds, onSelect }: WallMeshProps) {
|
||||
const segments = useMemo(
|
||||
() => splitWallAroundOpenings(wall, openings, wallHeight),
|
||||
[wall, openings, wallHeight],
|
||||
);
|
||||
|
||||
const isSelected = selectedIds.has(wall.id);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{segments.map((segment, i) => (
|
||||
<WallSegmentMesh
|
||||
key={`${wall.id}-seg-${i}`}
|
||||
wall={wall}
|
||||
segment={segment}
|
||||
thickness={wall.thickness}
|
||||
wallColor={wallColor}
|
||||
isSelected={isSelected}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user