Files
Ubuntu 3d788740cb feat: Latest production version with interior scene and glass
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>
2026-03-01 14:50:31 +00:00

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