v1.0
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user