Includes room interior with floor, walls, glass you can see through, and all uncommitted production changes that were running live. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
995 lines
35 KiB
TypeScript
995 lines
35 KiB
TypeScript
"use client";
|
|
|
|
import { Canvas } from "@react-three/fiber";
|
|
import {
|
|
OrbitControls,
|
|
PerspectiveCamera,
|
|
Environment,
|
|
ContactShadows,
|
|
RoundedBox,
|
|
} from "@react-three/drei";
|
|
import { Door3DEnhanced } from "./door-3d-enhanced";
|
|
import { useConfiguratorStore } from "@/lib/store";
|
|
import {
|
|
STELRUIMTE,
|
|
WALL_THICKNESS,
|
|
mmToMeters,
|
|
} from "@/lib/door-models";
|
|
import * as THREE from "three";
|
|
|
|
// ============================================
|
|
// MATERIALS
|
|
// ============================================
|
|
|
|
const WallMaterial = () => (
|
|
<meshStandardMaterial color="#f5f2ed" roughness={0.95} metalness={0} />
|
|
);
|
|
|
|
const RevealMaterial = () => (
|
|
<meshStandardMaterial color="#e8e4dd" roughness={1.0} metalness={0} />
|
|
);
|
|
|
|
const FloorMaterial = () => (
|
|
<meshStandardMaterial color="#d4c4a8" roughness={0.7} metalness={0.02} />
|
|
);
|
|
|
|
const WoodMaterial = () => (
|
|
<meshStandardMaterial color="#7a6550" roughness={0.55} metalness={0.05} />
|
|
);
|
|
|
|
const DarkMetalMaterial = () => (
|
|
<meshStandardMaterial color="#1a1a1a" roughness={0.3} metalness={0.8} />
|
|
);
|
|
|
|
// ============================================
|
|
// WALL CONTAINER WITH PRECISE HOLE
|
|
// ============================================
|
|
|
|
function WallContainer({
|
|
holeWidth,
|
|
holeHeight,
|
|
wallThickness,
|
|
}: {
|
|
holeWidth: number;
|
|
holeHeight: number;
|
|
wallThickness: number;
|
|
}) {
|
|
const wallWidth = 4.0;
|
|
const wallHeight = 3.0;
|
|
const halfHoleW = holeWidth / 2;
|
|
const halfWallT = wallThickness / 2;
|
|
|
|
const leftSectionWidth = (wallWidth - holeWidth) / 2;
|
|
const leftSectionX = -(halfHoleW + leftSectionWidth / 2);
|
|
const rightSectionWidth = leftSectionWidth;
|
|
const rightSectionX = halfHoleW + rightSectionWidth / 2;
|
|
const topSectionHeight = wallHeight - holeHeight;
|
|
const topSectionY = holeHeight + topSectionHeight / 2;
|
|
const gapPerSide = mmToMeters(STELRUIMTE / 2);
|
|
|
|
return (
|
|
<group position={[0, 0, 0]}>
|
|
{/* Main wall sections */}
|
|
<mesh position={[leftSectionX, wallHeight / 2, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[leftSectionWidth, wallHeight, wallThickness]} />
|
|
<WallMaterial />
|
|
</mesh>
|
|
<mesh position={[rightSectionX, wallHeight / 2, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[rightSectionWidth, wallHeight, wallThickness]} />
|
|
<WallMaterial />
|
|
</mesh>
|
|
{topSectionHeight > 0.01 && (
|
|
<mesh position={[0, topSectionY, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[holeWidth, topSectionHeight, wallThickness]} />
|
|
<WallMaterial />
|
|
</mesh>
|
|
)}
|
|
|
|
{/* Reveal surfaces */}
|
|
<mesh position={[-halfHoleW + 0.001, holeHeight / 2, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[0.002, holeHeight, wallThickness]} />
|
|
<RevealMaterial />
|
|
</mesh>
|
|
<mesh position={[halfHoleW - 0.001, holeHeight / 2, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[0.002, holeHeight, wallThickness]} />
|
|
<RevealMaterial />
|
|
</mesh>
|
|
<mesh position={[0, holeHeight - 0.001, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[holeWidth, 0.002, wallThickness]} />
|
|
<RevealMaterial />
|
|
</mesh>
|
|
|
|
{/* Reveal depth surfaces */}
|
|
<mesh position={[-halfHoleW + gapPerSide / 2, holeHeight / 2, 0]} receiveShadow>
|
|
<boxGeometry args={[gapPerSide, holeHeight, wallThickness - 0.01]} />
|
|
<RevealMaterial />
|
|
</mesh>
|
|
<mesh position={[halfHoleW - gapPerSide / 2, holeHeight / 2, 0]} receiveShadow>
|
|
<boxGeometry args={[gapPerSide, holeHeight, wallThickness - 0.01]} />
|
|
<RevealMaterial />
|
|
</mesh>
|
|
<mesh position={[0, holeHeight - gapPerSide / 2, 0]} receiveShadow>
|
|
<boxGeometry args={[holeWidth - gapPerSide * 2, gapPerSide, wallThickness - 0.01]} />
|
|
<RevealMaterial />
|
|
</mesh>
|
|
|
|
{/* Baseboard - front side */}
|
|
<mesh position={[leftSectionX, 0.04, halfWallT + 0.005]} castShadow>
|
|
<boxGeometry args={[leftSectionWidth, 0.08, 0.012]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
<mesh position={[rightSectionX, 0.04, halfWallT + 0.005]} castShadow>
|
|
<boxGeometry args={[rightSectionWidth, 0.08, 0.012]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// ROOM STRUCTURE
|
|
// ============================================
|
|
|
|
function RoomShell() {
|
|
const wallHeight = 3.0;
|
|
const roomDepth = 3.5;
|
|
const roomWidth = 7.0; // Much wider than the 4m back wall
|
|
const sideWallThickness = 0.08;
|
|
const floorSize = 10;
|
|
|
|
return (
|
|
<group>
|
|
{/* Front floor (viewer side) */}
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, roomDepth / 2]} receiveShadow>
|
|
<planeGeometry args={[floorSize, roomDepth + 1]} />
|
|
<FloorMaterial />
|
|
</mesh>
|
|
|
|
{/* Back floor (behind wall) */}
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, -1.5]} receiveShadow>
|
|
<planeGeometry args={[floorSize, 3]} />
|
|
<FloorMaterial />
|
|
</mesh>
|
|
|
|
{/* Floor plank lines (viewer side) */}
|
|
{Array.from({ length: 20 }).map((_, i) => {
|
|
const x = -2.7 + i * 0.27;
|
|
return (
|
|
<mesh key={`plank-${i}`} rotation={[-Math.PI / 2, 0, 0]} position={[x, 0.001, roomDepth / 2]}>
|
|
<planeGeometry args={[0.003, roomDepth + 1]} />
|
|
<meshStandardMaterial color="#b0a08a" roughness={0.9} />
|
|
</mesh>
|
|
);
|
|
})}
|
|
|
|
{/* Left side wall */}
|
|
<mesh position={[-roomWidth / 2, wallHeight / 2, roomDepth / 2]} receiveShadow>
|
|
<boxGeometry args={[sideWallThickness, wallHeight, roomDepth]} />
|
|
<WallMaterial />
|
|
</mesh>
|
|
|
|
{/* Right side wall */}
|
|
<mesh position={[roomWidth / 2, wallHeight / 2, roomDepth / 2]} receiveShadow>
|
|
<boxGeometry args={[sideWallThickness, wallHeight, roomDepth]} />
|
|
<WallMaterial />
|
|
</mesh>
|
|
|
|
{/* Left side baseboard */}
|
|
<mesh position={[-roomWidth / 2 + sideWallThickness / 2 + 0.006, 0.04, roomDepth / 2]}>
|
|
<boxGeometry args={[0.012, 0.08, roomDepth]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
|
|
{/* Right side baseboard */}
|
|
<mesh position={[roomWidth / 2 - sideWallThickness / 2 - 0.006, 0.04, roomDepth / 2]}>
|
|
<boxGeometry args={[0.012, 0.08, roomDepth]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
|
|
{/* Ceiling (partial - hint near the wall) */}
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, wallHeight, 0.6]}>
|
|
<planeGeometry args={[roomWidth, 1.5]} />
|
|
<meshStandardMaterial color="#f8f6f2" roughness={1} />
|
|
</mesh>
|
|
|
|
{/* Back room - bedroom visible through glass door */}
|
|
<Bedroom />
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// BEDROOM (visible through the glass door)
|
|
// ============================================
|
|
|
|
function Bedroom() {
|
|
const wallWidth = 4.0;
|
|
const wallHeight = 3.0;
|
|
const roomDepth = 4.0;
|
|
const backZ = -roomDepth;
|
|
|
|
return (
|
|
<group>
|
|
{/* Back wall */}
|
|
<mesh position={[0, wallHeight / 2, backZ]} receiveShadow>
|
|
<planeGeometry args={[wallWidth, wallHeight]} />
|
|
<meshStandardMaterial color="#e8e2d8" roughness={0.95} />
|
|
</mesh>
|
|
|
|
{/* Left side wall (bedroom) */}
|
|
<mesh position={[-wallWidth / 2 + 0.04, wallHeight / 2, backZ / 2]} receiveShadow>
|
|
<boxGeometry args={[0.08, wallHeight, roomDepth]} />
|
|
<meshStandardMaterial color="#ece6dc" roughness={0.95} />
|
|
</mesh>
|
|
|
|
{/* Right side wall (bedroom) */}
|
|
<mesh position={[wallWidth / 2 - 0.04, wallHeight / 2, backZ / 2]} receiveShadow>
|
|
<boxGeometry args={[0.08, wallHeight, roomDepth]} />
|
|
<meshStandardMaterial color="#ece6dc" roughness={0.95} />
|
|
</mesh>
|
|
|
|
{/* Bedroom ceiling */}
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, wallHeight, backZ / 2]}>
|
|
<planeGeometry args={[wallWidth, roomDepth]} />
|
|
<meshStandardMaterial color="#f5f2ee" roughness={1} />
|
|
</mesh>
|
|
|
|
{/* Bedroom baseboard - back wall */}
|
|
<mesh position={[0, 0.04, backZ + 0.006]}>
|
|
<boxGeometry args={[wallWidth - 0.16, 0.08, 0.012]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
|
|
{/* Bedroom baseboard - left */}
|
|
<mesh position={[-wallWidth / 2 + 0.085, 0.04, backZ / 2]}>
|
|
<boxGeometry args={[0.012, 0.08, roomDepth - 0.1]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
|
|
{/* Bedroom baseboard - right */}
|
|
<mesh position={[wallWidth / 2 - 0.085, 0.04, backZ / 2]}>
|
|
<boxGeometry args={[0.012, 0.08, roomDepth - 0.1]} />
|
|
<meshStandardMaterial color="#d5d0c8" roughness={0.7} />
|
|
</mesh>
|
|
|
|
{/* Window on back wall (bright rectangle suggesting daylight) */}
|
|
<mesh position={[0, 1.6, backZ + 0.01]}>
|
|
<planeGeometry args={[1.2, 1.0]} />
|
|
<meshStandardMaterial color="#d4e6f1" roughness={0.1} emissive="#b8d4e8" emissiveIntensity={0.3} />
|
|
</mesh>
|
|
{/* Window frame */}
|
|
{[
|
|
{ pos: [0, 2.1, backZ + 0.015] as [number, number, number], size: [1.26, 0.03, 0.02] as [number, number, number] },
|
|
{ pos: [0, 1.1, backZ + 0.015] as [number, number, number], size: [1.26, 0.03, 0.02] as [number, number, number] },
|
|
{ pos: [-0.615, 1.6, backZ + 0.015] as [number, number, number], size: [0.03, 1.06, 0.02] as [number, number, number] },
|
|
{ pos: [0.615, 1.6, backZ + 0.015] as [number, number, number], size: [0.03, 1.06, 0.02] as [number, number, number] },
|
|
{ pos: [0, 1.6, backZ + 0.015] as [number, number, number], size: [0.02, 1.0, 0.02] as [number, number, number] },
|
|
{ pos: [0, 1.6, backZ + 0.015] as [number, number, number], size: [1.2, 0.02, 0.02] as [number, number, number] },
|
|
].map((f, i) => (
|
|
<mesh key={`wf-${i}`} position={f.pos}>
|
|
<boxGeometry args={f.size} />
|
|
<meshStandardMaterial color="#f5f2ed" roughness={0.6} />
|
|
</mesh>
|
|
))}
|
|
{/* Window light */}
|
|
<rectAreaLight
|
|
position={[0, 1.6, backZ + 0.05]}
|
|
width={1.2}
|
|
height={1.0}
|
|
intensity={2.5}
|
|
color="#e8f0ff"
|
|
/>
|
|
|
|
{/* === BED === */}
|
|
<group position={[0, 0, backZ + 1.2]}>
|
|
{/* Bed frame base */}
|
|
<mesh position={[0, 0.18, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[1.6, 0.12, 2.1]} />
|
|
<meshStandardMaterial color="#c4b496" roughness={0.6} metalness={0.02} />
|
|
</mesh>
|
|
|
|
{/* Mattress */}
|
|
<RoundedBox args={[1.5, 0.2, 2.0]} radius={0.03} smoothness={4} position={[0, 0.34, 0]} castShadow receiveShadow>
|
|
<meshStandardMaterial color="#f5f2ee" roughness={0.95} />
|
|
</RoundedBox>
|
|
|
|
{/* Duvet/blanket */}
|
|
<RoundedBox args={[1.46, 0.08, 1.5]} radius={0.02} smoothness={4} position={[0, 0.47, 0.2]} castShadow>
|
|
<meshStandardMaterial color="#d4cfc5" roughness={0.92} />
|
|
</RoundedBox>
|
|
|
|
{/* Duvet fold at top */}
|
|
<RoundedBox args={[1.46, 0.12, 0.25]} radius={0.04} smoothness={4} position={[0, 0.48, -0.55]} castShadow>
|
|
<meshStandardMaterial color="#ddd8ce" roughness={0.92} />
|
|
</RoundedBox>
|
|
|
|
{/* Pillows */}
|
|
<RoundedBox args={[0.55, 0.1, 0.35]} radius={0.04} smoothness={4} position={[-0.35, 0.48, -0.78]} castShadow>
|
|
<meshStandardMaterial color="#f0ece6" roughness={0.95} />
|
|
</RoundedBox>
|
|
<RoundedBox args={[0.55, 0.1, 0.35]} radius={0.04} smoothness={4} position={[0.35, 0.48, -0.78]} castShadow>
|
|
<meshStandardMaterial color="#f0ece6" roughness={0.95} />
|
|
</RoundedBox>
|
|
|
|
{/* Headboard */}
|
|
<RoundedBox args={[1.65, 0.9, 0.06]} radius={0.01} smoothness={4} position={[0, 0.7, -1.03]} castShadow>
|
|
<meshStandardMaterial color="#7a6a52" roughness={0.55} metalness={0.05} />
|
|
</RoundedBox>
|
|
|
|
{/* Bed legs (metal) */}
|
|
{[
|
|
[-0.72, 0, -0.97],
|
|
[0.72, 0, -0.97],
|
|
[-0.72, 0, 0.97],
|
|
[0.72, 0, 0.97],
|
|
].map(([x, _, z], i) => (
|
|
<mesh key={`bl-${i}`} position={[x, 0.06, z]} castShadow>
|
|
<cylinderGeometry args={[0.015, 0.015, 0.12, 8]} />
|
|
<meshStandardMaterial color="#1a1a1a" roughness={0.3} metalness={0.8} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
|
|
{/* === NIGHTSTAND LEFT === */}
|
|
<group position={[-1.1, 0, backZ + 0.6]}>
|
|
<RoundedBox args={[0.45, 0.38, 0.38]} radius={0.008} smoothness={4} position={[0, 0.28, 0]} castShadow receiveShadow>
|
|
<meshStandardMaterial color="#8a7a62" roughness={0.55} metalness={0.05} />
|
|
</RoundedBox>
|
|
{/* Legs */}
|
|
{[[-0.17, -0.14], [0.17, -0.14], [-0.17, 0.14], [0.17, 0.14]].map(([x, z], i) => (
|
|
<mesh key={`nsl-${i}`} position={[x, 0.045, z]} castShadow>
|
|
<cylinderGeometry args={[0.006, 0.008, 0.09, 6]} />
|
|
<meshStandardMaterial color="#1a1a1a" roughness={0.3} metalness={0.8} />
|
|
</mesh>
|
|
))}
|
|
{/* Lamp on nightstand */}
|
|
<mesh position={[0, 0.52, 0]} castShadow>
|
|
<cylinderGeometry args={[0.04, 0.06, 0.06, 16]} />
|
|
<meshStandardMaterial color="#c8bfb0" roughness={0.85} />
|
|
</mesh>
|
|
<mesh position={[0, 0.6, 0]} castShadow>
|
|
<cylinderGeometry args={[0.005, 0.005, 0.1, 6]} />
|
|
<meshStandardMaterial color="#b8a890" roughness={0.5} metalness={0.3} />
|
|
</mesh>
|
|
<mesh position={[0, 0.68, 0]} castShadow>
|
|
<cylinderGeometry args={[0.03, 0.08, 0.1, 16, 1, true]} />
|
|
<meshStandardMaterial color="#f5f0e8" roughness={0.9} side={THREE.DoubleSide} />
|
|
</mesh>
|
|
<pointLight position={[0, 0.65, 0]} intensity={0.15} color="#ffeedd" distance={2} decay={2} />
|
|
</group>
|
|
|
|
{/* === NIGHTSTAND RIGHT === */}
|
|
<group position={[1.1, 0, backZ + 0.6]}>
|
|
<RoundedBox args={[0.45, 0.38, 0.38]} radius={0.008} smoothness={4} position={[0, 0.28, 0]} castShadow receiveShadow>
|
|
<meshStandardMaterial color="#8a7a62" roughness={0.55} metalness={0.05} />
|
|
</RoundedBox>
|
|
{[[-0.17, -0.14], [0.17, -0.14], [-0.17, 0.14], [0.17, 0.14]].map(([x, z], i) => (
|
|
<mesh key={`nsr-${i}`} position={[x, 0.045, z]} castShadow>
|
|
<cylinderGeometry args={[0.006, 0.008, 0.09, 6]} />
|
|
<meshStandardMaterial color="#1a1a1a" roughness={0.3} metalness={0.8} />
|
|
</mesh>
|
|
))}
|
|
{/* Small plant on nightstand */}
|
|
<mesh position={[0, 0.52, 0]} castShadow>
|
|
<cylinderGeometry args={[0.035, 0.03, 0.06, 12]} />
|
|
<meshStandardMaterial color="#4a4a4a" roughness={0.88} />
|
|
</mesh>
|
|
<mesh position={[0, 0.58, 0]} castShadow>
|
|
<sphereGeometry args={[0.05, 10, 10]} />
|
|
<meshStandardMaterial color="#4a6a3a" roughness={0.88} />
|
|
</mesh>
|
|
<mesh position={[0.03, 0.62, 0.01]} castShadow>
|
|
<sphereGeometry args={[0.035, 8, 8]} />
|
|
<meshStandardMaterial color="#3a5a2c" roughness={0.88} />
|
|
</mesh>
|
|
</group>
|
|
|
|
{/* === BEDROOM RUG === */}
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.003, backZ + 2.0]} receiveShadow>
|
|
<planeGeometry args={[1.8, 1.2]} />
|
|
<meshStandardMaterial color="#b8a88a" roughness={1.0} />
|
|
</mesh>
|
|
|
|
{/* === CEILING LIGHT (bedroom) === */}
|
|
<mesh position={[0, 2.97, backZ / 2]}>
|
|
<cylinderGeometry args={[0.15, 0.15, 0.02, 24]} />
|
|
<meshStandardMaterial color="#f5f2ee" roughness={0.5} emissive="#fff5e0" emissiveIntensity={0.1} />
|
|
</mesh>
|
|
<pointLight position={[0, 2.9, backZ / 2]} intensity={0.5} color="#ffeedd" distance={5} decay={2} />
|
|
|
|
{/* === WALL ART (bedroom back wall) === */}
|
|
<group position={[-0.8, 1.8, backZ + 0.01]}>
|
|
<mesh>
|
|
<planeGeometry args={[0.35, 0.45]} />
|
|
<meshStandardMaterial color="#d8c8a8" roughness={0.9} />
|
|
</mesh>
|
|
{/* Simple abstract circle */}
|
|
<mesh position={[0, 0.02, 0.001]}>
|
|
<circleGeometry args={[0.1, 24]} />
|
|
<meshStandardMaterial color="#baa882" roughness={0.85} />
|
|
</mesh>
|
|
{/* Frame */}
|
|
{[
|
|
[0, 0.225, 0.35, 0.02],
|
|
[0, -0.225, 0.35, 0.02],
|
|
[-0.175, 0, 0.02, 0.45],
|
|
[0.175, 0, 0.02, 0.45],
|
|
].map(([x, y, w, h], i) => (
|
|
<mesh key={`art-${i}`} position={[x, y, 0.003]}>
|
|
<planeGeometry args={[w, h]} />
|
|
<meshStandardMaterial color="#2a2a2a" roughness={0.4} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
|
|
{/* Second art piece */}
|
|
<group position={[0.8, 1.75, backZ + 0.01]}>
|
|
<mesh>
|
|
<planeGeometry args={[0.3, 0.4]} />
|
|
<meshStandardMaterial color="#c8d4c4" roughness={0.9} />
|
|
</mesh>
|
|
{[
|
|
[0, 0.2, 0.3, 0.02],
|
|
[0, -0.2, 0.3, 0.02],
|
|
[-0.15, 0, 0.02, 0.4],
|
|
[0.15, 0, 0.02, 0.4],
|
|
].map(([x, y, w, h], i) => (
|
|
<mesh key={`art2-${i}`} position={[x, y, 0.003]}>
|
|
<planeGeometry args={[w, h]} />
|
|
<meshStandardMaterial color="#2a2a2a" roughness={0.4} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: SIDEBOARD
|
|
// ============================================
|
|
|
|
function Sideboard() {
|
|
const bodyW = 0.9;
|
|
const bodyH = 0.44;
|
|
const bodyD = 0.36;
|
|
const legH = 0.12;
|
|
const yBody = legH + bodyH / 2;
|
|
const wallFaceZ = 0.075; // Front face of main wall
|
|
|
|
return (
|
|
<group position={[-1.4, 0, wallFaceZ + bodyD / 2 + 0.02]}>
|
|
{/* Body */}
|
|
<RoundedBox args={[bodyW, bodyH, bodyD]} radius={0.008} smoothness={4} position={[0, yBody, 0]} castShadow receiveShadow>
|
|
<WoodMaterial />
|
|
</RoundedBox>
|
|
|
|
{/* Top surface (darker edge) */}
|
|
<mesh position={[0, yBody + bodyH / 2 + 0.005, 0]} castShadow>
|
|
<boxGeometry args={[bodyW + 0.01, 0.01, bodyD + 0.01]} />
|
|
<meshStandardMaterial color="#5c4a38" roughness={0.5} metalness={0.05} />
|
|
</mesh>
|
|
|
|
{/* Metal legs */}
|
|
{[
|
|
[-bodyW / 2 + 0.06, 0, -bodyD / 2 + 0.06],
|
|
[bodyW / 2 - 0.06, 0, -bodyD / 2 + 0.06],
|
|
[-bodyW / 2 + 0.06, 0, bodyD / 2 - 0.06],
|
|
[bodyW / 2 - 0.06, 0, bodyD / 2 - 0.06],
|
|
].map(([x, _, z], i) => (
|
|
<mesh key={i} position={[x, legH / 2, z]} castShadow>
|
|
<cylinderGeometry args={[0.008, 0.01, legH, 8]} />
|
|
<DarkMetalMaterial />
|
|
</mesh>
|
|
))}
|
|
|
|
{/* Drawer line (decorative) */}
|
|
<mesh position={[0, yBody, bodyD / 2 + 0.001]}>
|
|
<planeGeometry args={[bodyW - 0.04, 0.002]} />
|
|
<meshStandardMaterial color="#5c4a38" roughness={0.5} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: DECORATIVE OBJECTS
|
|
// ============================================
|
|
|
|
function VaseWithBranches() {
|
|
const wallFaceZ = 0.075;
|
|
|
|
return (
|
|
<group position={[-1.55, 0.59, wallFaceZ + 0.18 + 0.02]}>
|
|
{/* Vase body */}
|
|
<mesh castShadow>
|
|
<cylinderGeometry args={[0.04, 0.055, 0.16, 16]} />
|
|
<meshStandardMaterial color="#c8baa6" roughness={0.92} metalness={0} />
|
|
</mesh>
|
|
{/* Vase neck */}
|
|
<mesh position={[0, 0.1, 0]} castShadow>
|
|
<cylinderGeometry args={[0.022, 0.04, 0.04, 16]} />
|
|
<meshStandardMaterial color="#c8baa6" roughness={0.92} metalness={0} />
|
|
</mesh>
|
|
{/* Rim */}
|
|
<mesh position={[0, 0.12, 0]}>
|
|
<torusGeometry args={[0.022, 0.003, 8, 16]} />
|
|
<meshStandardMaterial color="#b8a890" roughness={0.85} />
|
|
</mesh>
|
|
|
|
{/* Dried branches */}
|
|
{[
|
|
{ rot: [0.2, 0, 0.1] as [number, number, number], h: 0.3 },
|
|
{ rot: [-0.15, 1.2, -0.1] as [number, number, number], h: 0.28 },
|
|
{ rot: [0.1, 2.5, 0.2] as [number, number, number], h: 0.25 },
|
|
].map((branch, i) => (
|
|
<mesh key={i} position={[0, 0.12, 0]} rotation={branch.rot} castShadow>
|
|
<cylinderGeometry args={[0.002, 0.003, branch.h, 4]} />
|
|
<meshStandardMaterial color="#8a7a60" roughness={0.95} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function BookStack() {
|
|
const wallFaceZ = 0.075;
|
|
|
|
return (
|
|
<group position={[-1.2, 0.59, wallFaceZ + 0.18 + 0.02]}>
|
|
{[
|
|
{ w: 0.15, h: 0.022, d: 0.1, color: "#2c3e50", y: 0 },
|
|
{ w: 0.14, h: 0.018, d: 0.1, color: "#7f8c8d", y: 0.02 },
|
|
{ w: 0.13, h: 0.016, d: 0.1, color: "#8e4a3b", y: 0.037 },
|
|
].map((book, i) => (
|
|
<mesh key={i} position={[0.01 * (i - 1), book.y + book.h / 2, 0]} castShadow>
|
|
<boxGeometry args={[book.w, book.h, book.d]} />
|
|
<meshStandardMaterial color={book.color} roughness={0.85} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: TALL PLANT
|
|
// ============================================
|
|
|
|
function TallPlant() {
|
|
const wallFaceZ = 0.075;
|
|
|
|
return (
|
|
<group position={[1.45, 0, wallFaceZ + 0.16]}>
|
|
{/* Pot */}
|
|
<mesh position={[0, 0.13, 0]} castShadow receiveShadow>
|
|
<cylinderGeometry args={[0.12, 0.09, 0.26, 16]} />
|
|
<meshStandardMaterial color="#3d3d3d" roughness={0.88} metalness={0.1} />
|
|
</mesh>
|
|
{/* Pot rim */}
|
|
<mesh position={[0, 0.265, 0]}>
|
|
<torusGeometry args={[0.12, 0.008, 8, 16]} />
|
|
<meshStandardMaterial color="#333333" roughness={0.8} metalness={0.15} />
|
|
</mesh>
|
|
{/* Soil */}
|
|
<mesh position={[0, 0.25, 0]}>
|
|
<cylinderGeometry args={[0.11, 0.11, 0.02, 16]} />
|
|
<meshStandardMaterial color="#4a3728" roughness={1} />
|
|
</mesh>
|
|
{/* Trunk */}
|
|
<mesh position={[0, 0.55, 0]} castShadow>
|
|
<cylinderGeometry args={[0.012, 0.018, 0.6, 8]} />
|
|
<meshStandardMaterial color="#6b5b3e" roughness={0.9} />
|
|
</mesh>
|
|
{/* Secondary trunk */}
|
|
<mesh position={[0.02, 0.48, 0.01]} rotation={[0, 0, 0.15]} castShadow>
|
|
<cylinderGeometry args={[0.006, 0.012, 0.35, 6]} />
|
|
<meshStandardMaterial color="#6b5b3e" roughness={0.9} />
|
|
</mesh>
|
|
|
|
{/* Foliage clusters */}
|
|
{[
|
|
[0, 0.92, 0, 0.14],
|
|
[0.08, 0.84, 0.04, 0.11],
|
|
[-0.07, 0.86, -0.03, 0.10],
|
|
[0.04, 0.78, -0.06, 0.09],
|
|
[-0.03, 0.97, 0.03, 0.10],
|
|
[0.1, 0.72, 0.02, 0.08],
|
|
[-0.05, 0.76, 0.05, 0.08],
|
|
].map(([x, y, z, r], i) => (
|
|
<mesh key={i} position={[x, y, z]} castShadow>
|
|
<sphereGeometry args={[r, 10, 10]} />
|
|
<meshStandardMaterial color={i % 2 === 0 ? "#3a5a2c" : "#4a6a38"} roughness={0.88} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: PENDANT LIGHT
|
|
// ============================================
|
|
|
|
function PendantLight() {
|
|
return (
|
|
<group position={[0.4, 0, 1.4]}>
|
|
{/* Cord */}
|
|
<mesh position={[0, 2.9, 0]}>
|
|
<cylinderGeometry args={[0.002, 0.002, 0.25, 6]} />
|
|
<meshStandardMaterial color="#1a1a1a" roughness={0.5} metalness={0.3} />
|
|
</mesh>
|
|
{/* Canopy (ceiling mount) */}
|
|
<mesh position={[0, 3.0, 0]}>
|
|
<cylinderGeometry args={[0.03, 0.03, 0.015, 16]} />
|
|
<DarkMetalMaterial />
|
|
</mesh>
|
|
{/* Shade - open bottom */}
|
|
<mesh position={[0, 2.72, 0]} castShadow>
|
|
<cylinderGeometry args={[0.04, 0.13, 0.18, 24, 1, true]} />
|
|
<meshStandardMaterial color="#2a2a2a" roughness={0.45} metalness={0.4} side={THREE.DoubleSide} />
|
|
</mesh>
|
|
{/* Shade rim */}
|
|
<mesh position={[0, 2.63, 0]}>
|
|
<torusGeometry args={[0.13, 0.004, 8, 24]} />
|
|
<meshStandardMaterial color="#222222" roughness={0.3} metalness={0.6} />
|
|
</mesh>
|
|
{/* Warm point light (simulates bulb glow) */}
|
|
<pointLight position={[0, 2.68, 0]} intensity={0.6} color="#ffe8cc" distance={4} decay={2} />
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: PICTURE FRAME
|
|
// ============================================
|
|
|
|
function PictureFrame() {
|
|
const frameW = 0.45;
|
|
const frameH = 0.55;
|
|
const border = 0.025;
|
|
const wallFaceZ = 0.076;
|
|
|
|
return (
|
|
<group position={[-1.4, 1.65, wallFaceZ]}>
|
|
{/* Canvas/art (abstract warm tones) */}
|
|
<mesh position={[0, 0, 0.001]}>
|
|
<planeGeometry args={[frameW - border * 2, frameH - border * 2]} />
|
|
<meshStandardMaterial color="#d4c5a0" roughness={0.95} />
|
|
</mesh>
|
|
{/* Abstract shape 1 */}
|
|
<mesh position={[-0.04, 0.03, 0.002]}>
|
|
<circleGeometry args={[0.06, 24]} />
|
|
<meshStandardMaterial color="#b8987a" roughness={0.9} />
|
|
</mesh>
|
|
{/* Abstract shape 2 */}
|
|
<mesh position={[0.06, -0.04, 0.002]}>
|
|
<circleGeometry args={[0.04, 24]} />
|
|
<meshStandardMaterial color="#a08060" roughness={0.9} />
|
|
</mesh>
|
|
|
|
{/* Frame border */}
|
|
{[
|
|
{ pos: [0, frameH / 2 - border / 2, 0.005] as [number, number, number], size: [frameW, border, 0.018] as [number, number, number] },
|
|
{ pos: [0, -frameH / 2 + border / 2, 0.005] as [number, number, number], size: [frameW, border, 0.018] as [number, number, number] },
|
|
{ pos: [-frameW / 2 + border / 2, 0, 0.005] as [number, number, number], size: [border, frameH, 0.018] as [number, number, number] },
|
|
{ pos: [frameW / 2 - border / 2, 0, 0.005] as [number, number, number], size: [border, frameH, 0.018] as [number, number, number] },
|
|
].map((side, i) => (
|
|
<mesh key={i} position={side.pos} castShadow>
|
|
<boxGeometry args={side.size} />
|
|
<meshStandardMaterial color="#1a1a1a" roughness={0.35} metalness={0.3} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: RUG
|
|
// ============================================
|
|
|
|
function Rug() {
|
|
return (
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.004, 1.3]} receiveShadow>
|
|
<planeGeometry args={[1.4, 0.9]} />
|
|
<meshStandardMaterial color="#c4b496" roughness={1.0} metalness={0} />
|
|
</mesh>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// FURNITURE: SMALL SIDE TABLE (right side)
|
|
// ============================================
|
|
|
|
function SideTable() {
|
|
const wallFaceZ = 0.075;
|
|
const tableR = 0.18;
|
|
const tableH = 0.5;
|
|
|
|
return (
|
|
<group position={[1.45, 0, wallFaceZ + tableR + 0.5]}>
|
|
{/* Tabletop */}
|
|
<mesh position={[0, tableH, 0]} castShadow receiveShadow>
|
|
<cylinderGeometry args={[tableR, tableR, 0.02, 24]} />
|
|
<meshStandardMaterial color="#6b5b45" roughness={0.5} metalness={0.05} />
|
|
</mesh>
|
|
{/* Single leg */}
|
|
<mesh position={[0, tableH / 2, 0]} castShadow>
|
|
<cylinderGeometry args={[0.015, 0.015, tableH, 8]} />
|
|
<DarkMetalMaterial />
|
|
</mesh>
|
|
{/* Base */}
|
|
<mesh position={[0, 0.01, 0]} castShadow>
|
|
<cylinderGeometry args={[0.12, 0.12, 0.02, 24]} />
|
|
<DarkMetalMaterial />
|
|
</mesh>
|
|
{/* Small decorative candle */}
|
|
<mesh position={[0.04, tableH + 0.05, -0.02]} castShadow>
|
|
<cylinderGeometry args={[0.02, 0.02, 0.08, 12]} />
|
|
<meshStandardMaterial color="#f0ece4" roughness={0.95} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// DOOR + WALL COMPOSITION
|
|
// ============================================
|
|
|
|
/**
|
|
* Side panel: a fixed glass panel with steel frame, rendered next to the door.
|
|
*/
|
|
function SidePanelMesh({
|
|
panelWidth,
|
|
panelHeight,
|
|
finish,
|
|
glassColor,
|
|
}: {
|
|
panelWidth: number; // meters
|
|
panelHeight: number; // meters
|
|
finish: import("@/lib/store").Finish;
|
|
glassColor: import("@/lib/store").GlassColor;
|
|
}) {
|
|
const profileW = mmToMeters(40);
|
|
const profileD = mmToMeters(40);
|
|
const glassThick = mmToMeters(7);
|
|
|
|
const FRAME_COLORS_LOCAL: Record<string, string> = {
|
|
zwart: "#1a1a1a", brons: "#8B6F47", grijs: "#525252",
|
|
goud: "#B8860B", beige: "#C8B88A", ral: "#4A6741",
|
|
};
|
|
const GLASS_COLORS_LOCAL: Record<string, { color: string; transmission: number; roughness: number }> = {
|
|
helder: { color: "#eff6ff", transmission: 0.98, roughness: 0.05 },
|
|
grijs: { color: "#3a3a3a", transmission: 0.85, roughness: 0.1 },
|
|
brons: { color: "#8B6F47", transmission: 0.85, roughness: 0.1 },
|
|
"mat-blank": { color: "#e8e8e8", transmission: 0.7, roughness: 0.3 },
|
|
"mat-brons": { color: "#A0845C", transmission: 0.6, roughness: 0.35 },
|
|
"mat-zwart": { color: "#1a1a1a", transmission: 0.5, roughness: 0.4 },
|
|
};
|
|
|
|
const frameColor = FRAME_COLORS_LOCAL[finish] || "#1a1a1a";
|
|
const glassProps = GLASS_COLORS_LOCAL[glassColor] || GLASS_COLORS_LOCAL.helder;
|
|
const innerW = panelWidth - profileW * 2;
|
|
const innerH = panelHeight - profileW * 2;
|
|
|
|
return (
|
|
<group position={[0, panelHeight / 2, 0]}>
|
|
{/* Left stile */}
|
|
<mesh position={[-panelWidth / 2 + profileW / 2, 0, 0]} castShadow>
|
|
<boxGeometry args={[profileW, panelHeight, profileD]} />
|
|
<meshStandardMaterial color={frameColor} roughness={0.7} metalness={0.6} />
|
|
</mesh>
|
|
{/* Right stile */}
|
|
<mesh position={[panelWidth / 2 - profileW / 2, 0, 0]} castShadow>
|
|
<boxGeometry args={[profileW, panelHeight, profileD]} />
|
|
<meshStandardMaterial color={frameColor} roughness={0.7} metalness={0.6} />
|
|
</mesh>
|
|
{/* Top rail */}
|
|
<mesh position={[0, panelHeight / 2 - profileW / 2, 0]} castShadow>
|
|
<boxGeometry args={[innerW, profileW, profileD]} />
|
|
<meshStandardMaterial color={frameColor} roughness={0.7} metalness={0.6} />
|
|
</mesh>
|
|
{/* Bottom rail */}
|
|
<mesh position={[0, -panelHeight / 2 + profileW / 2, 0]} castShadow>
|
|
<boxGeometry args={[innerW, profileW, profileD]} />
|
|
<meshStandardMaterial color={frameColor} roughness={0.7} metalness={0.6} />
|
|
</mesh>
|
|
{/* Glass */}
|
|
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[innerW - 0.01, innerH - 0.01, glassThick]} />
|
|
<meshPhysicalMaterial
|
|
transmission={glassProps.transmission}
|
|
roughness={glassProps.roughness}
|
|
thickness={0.007}
|
|
ior={1.5}
|
|
color={glassProps.color}
|
|
transparent
|
|
opacity={0.98}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function DoorInWall() {
|
|
const { doorType, doorConfig, sidePanel, doorLeafWidth, sidePanelWidth, height, finish, glassColor } =
|
|
useConfiguratorStore();
|
|
|
|
const doorWidthM = mmToMeters(doorLeafWidth);
|
|
const doorHeightM = mmToMeters(height);
|
|
const wallThicknessM = mmToMeters(WALL_THICKNESS);
|
|
const stelruimteM = mmToMeters(STELRUIMTE);
|
|
const sidePanelWidthM = mmToMeters(sidePanelWidth);
|
|
const isDouble = doorConfig === "dubbele";
|
|
|
|
// Total door section width (all leaves)
|
|
const doorSectionW = isDouble ? doorWidthM * 2 : doorWidthM;
|
|
|
|
// Total hole width including side panels
|
|
let holeWidthM = doorSectionW + stelruimteM;
|
|
if (sidePanel === "links" || sidePanel === "rechts") holeWidthM += sidePanelWidthM;
|
|
if (sidePanel === "beide") holeWidthM += sidePanelWidthM * 2;
|
|
|
|
const holeHeightM = doorHeightM + stelruimteM / 2;
|
|
const doorZOffset = doorType === "taats" ? 0 : wallThicknessM * 0.15;
|
|
|
|
// Calculate X offset for the door(s) when side panels shift the center
|
|
let doorCenterX = 0;
|
|
if (sidePanel === "links") doorCenterX = sidePanelWidthM / 2;
|
|
if (sidePanel === "rechts") doorCenterX = -sidePanelWidthM / 2;
|
|
|
|
return (
|
|
<>
|
|
<WallContainer
|
|
holeWidth={holeWidthM}
|
|
holeHeight={holeHeightM}
|
|
wallThickness={wallThicknessM}
|
|
/>
|
|
|
|
<group position={[0, 0, doorZOffset]}>
|
|
{/* Door leaf(s) */}
|
|
{isDouble ? (
|
|
<group position={[doorCenterX, 0, 0]}>
|
|
<group position={[doorWidthM / 2, 0, 0]}>
|
|
<Door3DEnhanced />
|
|
</group>
|
|
<group position={[-doorWidthM / 2, 0, 0]} scale={[-1, 1, 1]}>
|
|
<Door3DEnhanced />
|
|
</group>
|
|
</group>
|
|
) : (
|
|
<group position={[doorCenterX, 0, 0]}>
|
|
<Door3DEnhanced />
|
|
</group>
|
|
)}
|
|
|
|
{/* Side panels */}
|
|
{(sidePanel === "links" || sidePanel === "beide") && sidePanelWidthM > 0 && (
|
|
<group position={[doorCenterX - doorSectionW / 2 - sidePanelWidthM / 2, 0, 0]}>
|
|
<SidePanelMesh
|
|
panelWidth={sidePanelWidthM}
|
|
panelHeight={doorHeightM}
|
|
finish={finish}
|
|
glassColor={glassColor}
|
|
/>
|
|
</group>
|
|
)}
|
|
{(sidePanel === "rechts" || sidePanel === "beide") && sidePanelWidthM > 0 && (
|
|
<group position={[doorCenterX + doorSectionW / 2 + sidePanelWidthM / 2, 0, 0]}>
|
|
<SidePanelMesh
|
|
panelWidth={sidePanelWidthM}
|
|
panelHeight={doorHeightM}
|
|
finish={finish}
|
|
glassColor={glassColor}
|
|
/>
|
|
</group>
|
|
)}
|
|
</group>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// LIGHTING
|
|
// ============================================
|
|
|
|
function Lighting() {
|
|
return (
|
|
<>
|
|
<ambientLight intensity={0.4} />
|
|
|
|
{/* Main sunlight */}
|
|
<directionalLight
|
|
position={[3, 6, 8]}
|
|
intensity={1.2}
|
|
castShadow
|
|
shadow-mapSize-width={4096}
|
|
shadow-mapSize-height={4096}
|
|
shadow-camera-far={50}
|
|
shadow-camera-left={-10}
|
|
shadow-camera-right={10}
|
|
shadow-camera-top={10}
|
|
shadow-camera-bottom={-10}
|
|
shadow-bias={-0.0001}
|
|
/>
|
|
|
|
{/* Fill light from behind wall (simulates light from back room) */}
|
|
<directionalLight position={[-2, 4, -4]} intensity={0.25} />
|
|
|
|
{/* Viewer-side fill to show furniture */}
|
|
<directionalLight position={[2, 3, 6]} intensity={0.35} />
|
|
|
|
{/* Top-down for reveal and furniture shadows */}
|
|
<directionalLight
|
|
position={[0, 8, 1]}
|
|
intensity={0.2}
|
|
castShadow
|
|
shadow-mapSize-width={2048}
|
|
shadow-mapSize-height={2048}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// MAIN SCENE EXPORT
|
|
// ============================================
|
|
|
|
export function Scene3D() {
|
|
return (
|
|
<Canvas
|
|
shadows
|
|
gl={{
|
|
antialias: true,
|
|
toneMapping: THREE.ACESFilmicToneMapping,
|
|
toneMappingExposure: 1.15,
|
|
outputColorSpace: THREE.SRGBColorSpace,
|
|
preserveDrawingBuffer: true,
|
|
}}
|
|
style={{ background: "#f0ede8" }}
|
|
>
|
|
<PerspectiveCamera makeDefault position={[0, 1.4, 4.2]} fov={45} />
|
|
|
|
<OrbitControls
|
|
enablePan={false}
|
|
enableZoom={true}
|
|
minDistance={2.5}
|
|
maxDistance={6}
|
|
minPolarAngle={Math.PI / 3}
|
|
maxPolarAngle={Math.PI / 2.1}
|
|
maxAzimuthAngle={Math.PI / 3}
|
|
minAzimuthAngle={-Math.PI / 3}
|
|
target={[0, 1.1, 0]}
|
|
enableDamping
|
|
dampingFactor={0.05}
|
|
/>
|
|
|
|
<Lighting />
|
|
|
|
<Environment preset="apartment" blur={0.6} environmentIntensity={1.0} />
|
|
|
|
<ContactShadows
|
|
position={[0, 0.005, 0.8]}
|
|
opacity={0.45}
|
|
scale={20}
|
|
blur={2.5}
|
|
far={4}
|
|
resolution={2048}
|
|
/>
|
|
|
|
{/* Room structure */}
|
|
<RoomShell />
|
|
|
|
{/* Door in wall */}
|
|
<DoorInWall />
|
|
|
|
{/* Interior furniture */}
|
|
<Sideboard />
|
|
<VaseWithBranches />
|
|
<BookStack />
|
|
<TallPlant />
|
|
<PendantLight />
|
|
<PictureFrame />
|
|
<Rug />
|
|
<SideTable />
|
|
</Canvas>
|
|
);
|
|
}
|