371 lines
12 KiB
React
371 lines
12 KiB
React
import { useMemo } from 'react';
|
|
import * as THREE from 'three';
|
|
|
|
/**
|
|
* Room3D component - Renders a 3D room based on AI analysis
|
|
* Used to display the wardrobe in context of the user's actual room
|
|
*/
|
|
|
|
// Floor material based on type
|
|
function FloorMaterial({ material, color }) {
|
|
const texture = useMemo(() => {
|
|
// In a real implementation, you'd load actual textures
|
|
// For now, we use colors with some roughness variation
|
|
return null;
|
|
}, [material]);
|
|
|
|
return (
|
|
<meshStandardMaterial
|
|
color={color || '#c4a574'}
|
|
roughness={material === 'wood' ? 0.7 : material === 'tile' ? 0.3 : 0.8}
|
|
metalness={0.05}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Get material properties from wall material type
|
|
function getWallMaterialProps(material, texture) {
|
|
const props = { roughness: 0.8, metalness: 0.02 };
|
|
switch (material) {
|
|
case 'paint':
|
|
props.roughness = texture === 'glossy' ? 0.3 : texture === 'smooth' ? 0.7 : 0.8;
|
|
break;
|
|
case 'wallpaper':
|
|
props.roughness = 0.85;
|
|
break;
|
|
case 'brick':
|
|
props.roughness = 0.95;
|
|
props.metalness = 0.0;
|
|
break;
|
|
case 'wood_panel':
|
|
props.roughness = 0.65;
|
|
props.metalness = 0.05;
|
|
break;
|
|
case 'concrete':
|
|
props.roughness = 0.9;
|
|
props.metalness = 0.01;
|
|
break;
|
|
case 'plaster':
|
|
props.roughness = 0.85;
|
|
break;
|
|
case 'stone':
|
|
props.roughness = 0.92;
|
|
props.metalness = 0.0;
|
|
break;
|
|
case 'tile':
|
|
props.roughness = 0.3;
|
|
props.metalness = 0.05;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return props;
|
|
}
|
|
|
|
// Wall with optional window/door cutouts
|
|
function Wall({ position, rotation, size, color, material, texture, windows = [], doors = [] }) {
|
|
const [width, height] = size;
|
|
const matProps = getWallMaterialProps(material, texture);
|
|
|
|
// Create wall geometry with cutouts (simplified - just overlays for now)
|
|
return (
|
|
<group position={position} rotation={rotation}>
|
|
{/* Main wall */}
|
|
<mesh receiveShadow>
|
|
<planeGeometry args={[width, height]} />
|
|
<meshStandardMaterial
|
|
color={color || '#f5f5f0'}
|
|
side={THREE.DoubleSide}
|
|
roughness={matProps.roughness}
|
|
metalness={matProps.metalness}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Windows */}
|
|
{windows.map((win, i) => {
|
|
const winWidth = width * win.width;
|
|
const winHeight = height * win.height;
|
|
const winX = (win.x - 0.5) * width;
|
|
const winY = (win.y - 0.5) * height;
|
|
|
|
return (
|
|
<group key={`window-${i}`} position={[winX, winY, 0.01]}>
|
|
{/* Window glass */}
|
|
<mesh>
|
|
<planeGeometry args={[winWidth, winHeight]} />
|
|
<meshStandardMaterial
|
|
color="#87CEEB"
|
|
transparent
|
|
opacity={0.3}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
{/* Window frame */}
|
|
<lineSegments>
|
|
<edgesGeometry args={[new THREE.PlaneGeometry(winWidth, winHeight)]} />
|
|
<lineBasicMaterial color="#555" linewidth={2} />
|
|
</lineSegments>
|
|
</group>
|
|
);
|
|
})}
|
|
|
|
{/* Doors */}
|
|
{doors.map((door, i) => {
|
|
const doorWidth = width * door.width;
|
|
const doorHeight = height * door.height;
|
|
const doorX = (door.x - 0.5) * width;
|
|
const doorY = -(height / 2) + (doorHeight / 2);
|
|
|
|
return (
|
|
<group key={`door-${i}`} position={[doorX, doorY, 0.01]}>
|
|
{/* Door opening (darker rectangle) */}
|
|
<mesh>
|
|
<planeGeometry args={[doorWidth, doorHeight]} />
|
|
<meshStandardMaterial color="#2a2a2a" side={THREE.DoubleSide} />
|
|
</mesh>
|
|
{/* Door frame */}
|
|
<lineSegments>
|
|
<edgesGeometry args={[new THREE.PlaneGeometry(doorWidth, doorHeight)]} />
|
|
<lineBasicMaterial color="#8b7355" linewidth={2} />
|
|
</lineSegments>
|
|
</group>
|
|
);
|
|
})}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default function Room3D({ roomConfig, wardrobeConfig }) {
|
|
if (!roomConfig) return null;
|
|
|
|
const { width, length, height, walls, windows, doors, floor, ceiling, wardrobePlacement } = roomConfig;
|
|
|
|
// Calculate wall positions
|
|
const halfWidth = width / 2;
|
|
const halfLength = length / 2;
|
|
const halfHeight = height / 2;
|
|
|
|
// Get windows/doors for each wall
|
|
const backWindows = windows.filter(w => w.wall === 'back').map(w => ({
|
|
x: w.x, y: w.y, width: w.width, height: w.height
|
|
}));
|
|
const leftWindows = windows.filter(w => w.wall === 'left').map(w => ({
|
|
x: w.x, y: w.y, width: w.width, height: w.height
|
|
}));
|
|
const rightWindows = windows.filter(w => w.wall === 'right').map(w => ({
|
|
x: w.x, y: w.y, width: w.width, height: w.height
|
|
}));
|
|
|
|
const backDoors = doors.filter(d => d.wall === 'back').map(d => ({
|
|
x: d.x, width: d.width, height: d.height
|
|
}));
|
|
const leftDoors = doors.filter(d => d.wall === 'left').map(d => ({
|
|
x: d.x, width: d.width, height: d.height
|
|
}));
|
|
const rightDoors = doors.filter(d => d.wall === 'right').map(d => ({
|
|
x: d.x, width: d.width, height: d.height
|
|
}));
|
|
|
|
// Calculate wardrobe position based on placement
|
|
const getWardrobePosition = () => {
|
|
if (!wardrobeConfig || !wardrobePlacement) return [0, 0, 0];
|
|
|
|
const wardrobeWidth = (wardrobeConfig.widthMm || 2400) / 1000;
|
|
const wardrobeDepth = (wardrobeConfig.depthMm || 600) / 1000;
|
|
|
|
switch (wardrobePlacement.wall) {
|
|
case 'back':
|
|
return [
|
|
((wardrobePlacement.xPercent / 100) - 0.5) * width + wardrobeWidth / 2,
|
|
0,
|
|
-halfLength + wardrobeDepth / 2 + 0.05
|
|
];
|
|
case 'left':
|
|
return [
|
|
-halfWidth + wardrobeDepth / 2 + 0.05,
|
|
0,
|
|
((wardrobePlacement.xPercent / 100) - 0.5) * length + wardrobeWidth / 2
|
|
];
|
|
case 'right':
|
|
return [
|
|
halfWidth - wardrobeDepth / 2 - 0.05,
|
|
0,
|
|
((wardrobePlacement.xPercent / 100) - 0.5) * length + wardrobeWidth / 2
|
|
];
|
|
default:
|
|
return [0, 0, -halfLength + wardrobeDepth / 2 + 0.05];
|
|
}
|
|
};
|
|
|
|
const wardrobePosition = getWardrobePosition();
|
|
const wardrobeRotation = wardrobePlacement?.wall === 'left'
|
|
? [0, Math.PI / 2, 0]
|
|
: wardrobePlacement?.wall === 'right'
|
|
? [0, -Math.PI / 2, 0]
|
|
: [0, 0, 0];
|
|
|
|
return (
|
|
<group>
|
|
{/* Floor */}
|
|
<mesh
|
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
position={[0, 0, 0]}
|
|
receiveShadow
|
|
>
|
|
<planeGeometry args={[width, length]} />
|
|
<FloorMaterial material={floor?.material} color={floor?.color} />
|
|
</mesh>
|
|
|
|
{/* Back wall */}
|
|
{walls?.back?.visible !== false && (
|
|
<Wall
|
|
position={[0, halfHeight, -halfLength]}
|
|
rotation={[0, 0, 0]}
|
|
size={[width, height]}
|
|
color={walls?.back?.color}
|
|
material={walls?.back?.material}
|
|
texture={walls?.back?.texture}
|
|
windows={backWindows}
|
|
doors={backDoors}
|
|
/>
|
|
)}
|
|
|
|
{/* Left wall */}
|
|
{walls?.left?.visible !== false && (
|
|
<Wall
|
|
position={[-halfWidth, halfHeight, 0]}
|
|
rotation={[0, Math.PI / 2, 0]}
|
|
size={[length, height]}
|
|
color={walls?.left?.color}
|
|
material={walls?.left?.material}
|
|
texture={walls?.left?.texture}
|
|
windows={leftWindows}
|
|
doors={leftDoors}
|
|
/>
|
|
)}
|
|
|
|
{/* Right wall */}
|
|
{walls?.right?.visible !== false && (
|
|
<Wall
|
|
position={[halfWidth, halfHeight, 0]}
|
|
rotation={[0, -Math.PI / 2, 0]}
|
|
size={[length, height]}
|
|
color={walls?.right?.color}
|
|
material={walls?.right?.material}
|
|
texture={walls?.right?.texture}
|
|
windows={rightWindows}
|
|
doors={rightDoors}
|
|
/>
|
|
)}
|
|
|
|
{/* Ceiling (plafond) */}
|
|
{ceiling?.visible !== false && (() => {
|
|
const ceilingColor = ceiling?.color || '#ffffff';
|
|
const ceilingType = ceiling?.type || 'flat';
|
|
const slopeAngle = ceiling?.slopeAngleDeg || 0;
|
|
const ceilingMatProps = getWallMaterialProps(ceiling?.material || 'paint', 'smooth');
|
|
|
|
if (ceilingType === 'sloped' && slopeAngle > 0) {
|
|
// Sloped ceiling - render as a tilted plane
|
|
const slopeRad = (slopeAngle * Math.PI) / 180;
|
|
const slopeDir = ceiling?.slopeDirection || 'front_to_back';
|
|
const slopeRotX = slopeDir === 'front_to_back' ? Math.PI / 2 + slopeRad * 0.5 : Math.PI / 2;
|
|
const slopeRotZ = slopeDir === 'left_to_right' ? slopeRad * 0.5 : 0;
|
|
const slopeW = slopeDir === 'left_to_right' ? width / Math.cos(slopeRad * 0.5) : width;
|
|
const slopeL = slopeDir === 'front_to_back' ? length / Math.cos(slopeRad * 0.5) : length;
|
|
return (
|
|
<mesh
|
|
rotation={[slopeRotX, 0, slopeRotZ]}
|
|
position={[0, height, 0]}
|
|
receiveShadow
|
|
>
|
|
<planeGeometry args={[slopeW, slopeL]} />
|
|
<meshStandardMaterial
|
|
color={ceilingColor}
|
|
side={THREE.DoubleSide}
|
|
roughness={ceilingMatProps.roughness}
|
|
metalness={ceilingMatProps.metalness}
|
|
/>
|
|
</mesh>
|
|
);
|
|
}
|
|
|
|
// Flat ceiling
|
|
return (
|
|
<mesh
|
|
rotation={[Math.PI / 2, 0, 0]}
|
|
position={[0, height, 0]}
|
|
receiveShadow
|
|
>
|
|
<planeGeometry args={[width, length]} />
|
|
<meshStandardMaterial
|
|
color={ceilingColor}
|
|
side={THREE.DoubleSide}
|
|
roughness={ceilingMatProps.roughness}
|
|
metalness={ceilingMatProps.metalness}
|
|
/>
|
|
</mesh>
|
|
);
|
|
})()}
|
|
|
|
{/* Wardrobe position indicator (for debugging/visualization) */}
|
|
{wardrobePlacement && (
|
|
<mesh position={[wardrobePosition[0], 0.01, wardrobePosition[2]]}>
|
|
<planeGeometry args={[
|
|
(wardrobeConfig?.widthMm || 2400) / 1000,
|
|
(wardrobeConfig?.depthMm || 600) / 1000
|
|
]} />
|
|
<meshBasicMaterial color="#0066cc" transparent opacity={0.3} />
|
|
</mesh>
|
|
)}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// Export position calculator for use by parent
|
|
export function calculateWardrobeTransform(roomConfig, wardrobeConfig) {
|
|
if (!roomConfig || !roomConfig.wardrobePlacement) {
|
|
return { position: [0, 0, 0], rotation: [0, 0, 0] };
|
|
}
|
|
|
|
const { width, length, wardrobePlacement } = roomConfig;
|
|
const halfWidth = width / 2;
|
|
const halfLength = length / 2;
|
|
const wardrobeWidth = (wardrobeConfig?.widthMm || 2400) / 1000;
|
|
const wardrobeDepth = (wardrobeConfig?.depthMm || 600) / 1000;
|
|
|
|
let position, rotation;
|
|
|
|
switch (wardrobePlacement.wall) {
|
|
case 'back':
|
|
position = [
|
|
((wardrobePlacement.xPercent / 100) - 0.5) * width + wardrobeWidth / 4,
|
|
0,
|
|
-halfLength + wardrobeDepth / 2 + 0.05
|
|
];
|
|
rotation = [0, 0, 0];
|
|
break;
|
|
case 'left':
|
|
position = [
|
|
-halfWidth + wardrobeDepth / 2 + 0.05,
|
|
0,
|
|
((wardrobePlacement.xPercent / 100) - 0.5) * length
|
|
];
|
|
rotation = [0, Math.PI / 2, 0];
|
|
break;
|
|
case 'right':
|
|
position = [
|
|
halfWidth - wardrobeDepth / 2 - 0.05,
|
|
0,
|
|
((wardrobePlacement.xPercent / 100) - 0.5) * length
|
|
];
|
|
rotation = [0, -Math.PI / 2, 0];
|
|
break;
|
|
default:
|
|
position = [0, 0, -halfLength + wardrobeDepth / 2 + 0.05];
|
|
rotation = [0, 0, 0];
|
|
}
|
|
|
|
return { position, rotation };
|
|
}
|