Files
house-plan-maker/apps/client/src/components/editor/three/WallMesh.tsx
T
alexei.dolgolyov af8b9fe00f 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
2026-04-05 22:34:03 +03:00

106 lines
2.7 KiB
TypeScript

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>
);
}