feat: Add professional 3D handles, glass patterns, and living room scene

 New Features:
- 6 procedural 3D handles (Beugelgreep, Hoekgreep, Maangreep, Ovaalgreep, Klink, U-greep)
- Glass pattern generator (Standard, DT9 rounded corners, DT10 U-shapes)
- Dynamic living room scene with adaptive doorway
- Enhanced camera controls (zoomed out, more freedom)
- Texture loading system (prepared for future enhancement)

🎨 Visual Improvements:
- Professional handle details (screws, mounting blocks, rosettes)
- Realistic materials (metalness 0.95, proper roughness)
- Living room context (wood floor, white walls, baseboards)
- Better lighting (sunlight simulation, fill lights)
- Apartment environment preset

🏗️ Technical:
- Parametric glass shapes with THREE.Shape
- Dynamic doorway sizing based on door dimensions
- Store updates for handle and glass pattern types
- UI components for all new options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu
2026-02-10 18:23:52 +00:00
parent bd9c6545da
commit b30e8d18d4
7 changed files with 968 additions and 112 deletions

View File

@@ -1,9 +1,23 @@
"use client";
import { useRef } from "react";
import { useRef, useMemo } from "react";
import { useConfiguratorStore } from "@/lib/store";
import { RoundedBox } from "@react-three/drei";
import * as THREE from "three";
import {
Beugelgreep,
Hoekgreep,
Maangreep,
Ovaalgreep,
Klink,
UGreep,
} from "./handles-3d";
import {
createStandardGlass,
createRoundedCornerGlass,
createInvertedUGlass,
createNormalUGlass,
} from "@/lib/glass-patterns";
// Steel material - fallback to solid color for now
const SteelMaterial = ({ color }: { color: string }) => (
@@ -43,7 +57,7 @@ function DimensionLabel({
}
export function Door3DEnhanced() {
const { doorType, gridType, finish, handle, doorLeafWidth, height } =
const { doorType, gridType, finish, handle, glassPattern, doorLeafWidth, height } =
useConfiguratorStore();
const doorRef = useRef<THREE.Group>(null);
@@ -151,47 +165,134 @@ export function Door3DEnhanced() {
</RoundedBox>
)}
{/* GLASS PANEL - Sits inside the frame */}
<mesh position={[0, 0, 0]} castShadow receiveShadow>
<boxGeometry
args={[
doorWidth - stileWidth * 2,
doorHeight - railHeight * 2,
glassThickness,
]}
/>
<GlassMaterial />
</mesh>
{/* HANDLE - U-Greep for Taats */}
{doorType === "taats" && handle === "u-greep" && (
<RoundedBox
args={[0.02, 0.6, 0.02]}
radius={0.003}
smoothness={4}
position={[0, 0, railDepth / 2 + 0.01]}
castShadow
>
<SteelMaterial color={frameColor} />
</RoundedBox>
{/* GLASS PANELS - Pattern-based decorative glass */}
{glassPattern === "standard" && (
<mesh position={[0, 0, 0]} castShadow receiveShadow>
<boxGeometry
args={[
doorWidth - stileWidth * 2,
doorHeight - railHeight * 2,
glassThickness,
]}
/>
<GlassMaterial />
</mesh>
)}
{/* HANDLE - Klink for Scharnier */}
{doorType === "scharnier" && handle === "klink" && (
<group position={[doorWidth / 2 - stileWidth - 0.1, 0, railDepth / 2 + 0.01]}>
<RoundedBox args={[0.08, 0.02, 0.02]} radius={0.003} smoothness={4} castShadow>
<SteelMaterial color={frameColor} />
</RoundedBox>
<mesh position={[0.04, 0, 0]} castShadow>
<sphereGeometry args={[0.015, 32, 32]} />
<meshStandardMaterial
color={finish === "brons" ? "#6B5434" : frameColor}
metalness={0.95}
roughness={0.05}
envMapIntensity={1.2}
{glassPattern === "dt9-rounded" && (
<mesh position={[0, 0, 0]} castShadow receiveShadow>
<extrudeGeometry
args={[
createRoundedCornerGlass(
doorWidth - stileWidth * 2,
doorHeight - railHeight * 2,
0.12
),
{ depth: glassThickness, bevelEnabled: false },
]}
/>
<GlassMaterial />
</mesh>
)}
{glassPattern === "dt10-ushape" && dividerPositions.length > 0 && (
<>
{/* Top section - Inverted U */}
<mesh
position={[0, (doorHeight / 4 + dividerPositions[0]) / 2, 0]}
castShadow
receiveShadow
>
<extrudeGeometry
args={[
createInvertedUGlass(
doorWidth - stileWidth * 2,
Math.abs(doorHeight / 2 - railHeight - dividerPositions[0])
),
{ depth: glassThickness, bevelEnabled: false },
]}
/>
<GlassMaterial />
</mesh>
</group>
{/* Bottom section - Normal U */}
<mesh
position={[
0,
(-doorHeight / 4 + dividerPositions[dividerPositions.length - 1]) / 2,
0,
]}
castShadow
receiveShadow
>
<extrudeGeometry
args={[
createNormalUGlass(
doorWidth - stileWidth * 2,
Math.abs(-doorHeight / 2 + railHeight - dividerPositions[dividerPositions.length - 1])
),
{ depth: glassThickness, bevelEnabled: false },
]}
/>
<GlassMaterial />
</mesh>
</>
)}
{/* HANDLES - Professional 3D handle components */}
{handle === "beugelgreep" && (
<Beugelgreep
finish={finish}
doorWidth={doorWidth}
doorHeight={doorHeight}
railDepth={railDepth}
stileWidth={stileWidth}
/>
)}
{handle === "hoekgreep" && (
<Hoekgreep
finish={finish}
doorWidth={doorWidth}
doorHeight={doorHeight}
railDepth={railDepth}
stileWidth={stileWidth}
/>
)}
{handle === "maangreep" && (
<Maangreep
finish={finish}
doorWidth={doorWidth}
doorHeight={doorHeight}
railDepth={railDepth}
stileWidth={stileWidth}
/>
)}
{handle === "ovaalgreep" && (
<Ovaalgreep
finish={finish}
doorWidth={doorWidth}
doorHeight={doorHeight}
railDepth={railDepth}
stileWidth={stileWidth}
/>
)}
{handle === "klink" && (
<Klink
finish={finish}
doorWidth={doorWidth}
doorHeight={doorHeight}
railDepth={railDepth}
stileWidth={stileWidth}
/>
)}
{handle === "u-greep" && (
<UGreep
finish={finish}
doorWidth={doorWidth}
doorHeight={doorHeight}
railDepth={railDepth}
stileWidth={stileWidth}
/>
)}
{/* 3D DIMENSION LABELS */}

View File

@@ -0,0 +1,305 @@
"use client";
import { RoundedBox } from "@react-three/drei";
import * as THREE from "three";
interface HandleProps {
finish: string;
doorWidth: number;
doorHeight: number;
railDepth: number;
stileWidth: number;
}
// Steel material for handles
const HandleMaterial = ({ color }: { color: string }) => (
<meshStandardMaterial
color={color}
roughness={0.3}
metalness={0.95}
envMapIntensity={1.5}
/>
);
/**
* Beugelgreep - Vertical bar handle with mounting blocks
* Classic industrial style, common on steel pivot doors
*/
export function Beugelgreep({ finish, doorHeight, railDepth }: HandleProps) {
const color = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
const handleLength = Math.min(doorHeight * 0.35, 0.8); // Max 80cm
const barDiameter = 0.025; // 25mm diameter bar
const mountBlockSize = [0.04, 0.06, 0.03]; // Mount block dimensions
return (
<group position={[0, 0, railDepth / 2 + 0.02]}>
{/* Main vertical bar */}
<mesh castShadow>
<cylinderGeometry args={[barDiameter / 2, barDiameter / 2, handleLength, 32]} />
<HandleMaterial color={color} />
</mesh>
{/* Top mounting block */}
<RoundedBox
args={mountBlockSize}
radius={0.003}
position={[0, handleLength / 2 + mountBlockSize[1] / 2, -0.01]}
castShadow
>
<HandleMaterial color={color} />
</RoundedBox>
{/* Bottom mounting block */}
<RoundedBox
args={mountBlockSize}
radius={0.003}
position={[0, -handleLength / 2 - mountBlockSize[1] / 2, -0.01]}
castShadow
>
<HandleMaterial color={color} />
</RoundedBox>
{/* Mounting screws (detail) */}
{[
[0, handleLength / 2 + mountBlockSize[1] / 2, 0.005],
[0, -handleLength / 2 - mountBlockSize[1] / 2, 0.005],
].map((pos, i) => (
<mesh key={i} position={pos as [number, number, number]} castShadow>
<cylinderGeometry args={[0.003, 0.003, 0.008, 16]} />
<meshStandardMaterial color="#2a2a2a" metalness={0.9} roughness={0.1} />
</mesh>
))}
</group>
);
}
/**
* Hoekgreep - L-shaped corner handle
* Minimalist flush-mount design
*/
export function Hoekgreep({ finish, doorWidth, railDepth, stileWidth }: HandleProps) {
const color = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
const horizontalLength = 0.15; // 15cm horizontal
const verticalLength = 0.12; // 12cm vertical
const barThickness = 0.02; // 20mm thick
const barWidth = 0.03; // 30mm wide
return (
<group position={[doorWidth / 2 - stileWidth - 0.12, 0, railDepth / 2 + 0.015]}>
{/* Horizontal bar */}
<RoundedBox
args={[horizontalLength, barWidth, barThickness]}
radius={0.003}
smoothness={4}
position={[horizontalLength / 2, verticalLength / 2, 0]}
castShadow
>
<HandleMaterial color={color} />
</RoundedBox>
{/* Vertical bar */}
<RoundedBox
args={[barWidth, verticalLength, barThickness]}
radius={0.003}
smoothness={4}
position={[0, 0, 0]}
castShadow
>
<HandleMaterial color={color} />
</RoundedBox>
{/* Corner radius (decorative) */}
<mesh position={[0.015, verticalLength / 2 - 0.015, 0]} rotation={[0, 0, 0]} castShadow>
<torusGeometry args={[0.015, 0.01, 16, 32, Math.PI / 2]} />
<HandleMaterial color={color} />
</mesh>
</group>
);
}
/**
* Maangreep - Crescent/moon shaped recessed handle
* Elegant curved design for flush doors
*/
export function Maangreep({ finish, doorWidth, railDepth, stileWidth }: HandleProps) {
const color = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
const curveRadius = 0.08; // 8cm radius
const handleDepth = 0.025; // 25mm deep recess
return (
<group position={[doorWidth / 2 - stileWidth - 0.12, 0, railDepth / 2]}>
{/* Main curved handle body */}
<mesh rotation={[Math.PI / 2, 0, 0]} castShadow>
<torusGeometry args={[curveRadius, 0.015, 16, 32, Math.PI]} />
<HandleMaterial color={color} />
</mesh>
{/* Left end cap */}
<mesh position={[-curveRadius, 0, 0]} castShadow>
<sphereGeometry args={[0.015, 32, 32]} />
<HandleMaterial color={color} />
</mesh>
{/* Right end cap */}
<mesh position={[curveRadius, 0, 0]} castShadow>
<sphereGeometry args={[0.015, 32, 32]} />
<HandleMaterial color={color} />
</mesh>
{/* Recessed mounting plate */}
<RoundedBox
args={[curveRadius * 2.2, 0.05, handleDepth]}
radius={0.005}
position={[0, 0, -handleDepth / 2]}
receiveShadow
>
<meshStandardMaterial color={color} roughness={0.8} metalness={0.3} />
</RoundedBox>
</group>
);
}
/**
* Ovaalgreep - Oval/elliptical pull handle
* Modern minimalist design
*/
export function Ovaalgreep({ finish, doorWidth, railDepth, stileWidth }: HandleProps) {
const color = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
// Create oval shape using THREE.Shape
const shape = new THREE.Shape();
const rx = 0.06; // 6cm horizontal radius
const ry = 0.03; // 3cm vertical radius
// Draw ellipse
for (let i = 0; i <= 64; i++) {
const angle = (i / 64) * Math.PI * 2;
const x = Math.cos(angle) * rx;
const y = Math.sin(angle) * ry;
if (i === 0) {
shape.moveTo(x, y);
} else {
shape.lineTo(x, y);
}
}
const extrudeSettings = {
depth: 0.02,
bevelEnabled: true,
bevelThickness: 0.003,
bevelSize: 0.003,
bevelSegments: 8,
};
return (
<group position={[doorWidth / 2 - stileWidth - 0.12, 0, railDepth / 2 + 0.015]}>
{/* Oval handle ring */}
<mesh castShadow rotation={[0, 0, 0]}>
<extrudeGeometry args={[shape, extrudeSettings]} />
<HandleMaterial color={color} />
</mesh>
{/* Inner void (make it a ring, not solid) */}
<mesh position={[0, 0, 0.01]}>
<ellipseGeometry args={[rx * 0.7, ry * 0.7, 32]} />
<meshBasicMaterial color="#fafafa" />
</mesh>
</group>
);
}
/**
* Klink - Traditional door handle with lever
* Classic hinged door handle
*/
export function Klink({ finish, doorWidth, railDepth, stileWidth }: HandleProps) {
const color = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
const leverLength = 0.12; // 12cm lever
const leverThickness = 0.015; // 15mm thick
return (
<group position={[doorWidth / 2 - stileWidth - 0.1, 0, railDepth / 2 + 0.01]}>
{/* Mounting rosette (round plate) */}
<mesh castShadow>
<cylinderGeometry args={[0.03, 0.03, 0.008, 32]} />
<HandleMaterial color={color} />
</mesh>
{/* Lever handle */}
<RoundedBox
args={[leverLength, 0.02, leverThickness]}
radius={0.005}
smoothness={4}
position={[leverLength / 2, 0, 0]}
rotation={[0, 0, -0.15]}
castShadow
>
<HandleMaterial color={color} />
</RoundedBox>
{/* Lever end (ergonomic grip) */}
<mesh position={[leverLength, -0.015, 0]} castShadow>
<sphereGeometry args={[0.012, 32, 32]} />
<HandleMaterial color={color} />
</mesh>
{/* Lock cylinder (detail) */}
<mesh position={[0, -0.045, 0]} castShadow>
<cylinderGeometry args={[0.008, 0.008, 0.01, 16]} />
<meshStandardMaterial
color={finish === "brons" ? "#6B5434" : "#2a2a2a"}
metalness={0.9}
roughness={0.2}
/>
</mesh>
</group>
);
}
/**
* U-Greep - U-shaped bar handle (current simple version)
* Straight vertical bar for pivot doors
*/
export function UGreep({ finish, railDepth }: HandleProps) {
const color = {
zwart: "#1a1a1a",
brons: "#8B6F47",
grijs: "#525252",
}[finish];
return (
<RoundedBox
args={[0.02, 0.6, 0.02]}
radius={0.003}
smoothness={4}
position={[0, 0, railDepth / 2 + 0.01]}
castShadow
>
<HandleMaterial color={color} />
</RoundedBox>
);
}

View File

@@ -3,88 +3,180 @@
import { Canvas } from "@react-three/fiber";
import { OrbitControls, PerspectiveCamera, Environment, ContactShadows } from "@react-three/drei";
import { Door3DEnhanced } from "./door-3d-enhanced";
import { useConfiguratorStore } from "@/lib/store";
import * as THREE from "three";
function Room() {
function LivingRoom({ doorWidth, doorHeight }: { doorWidth: number; doorHeight: number }) {
const wallThickness = 0.15;
const doorWidth = 1.3;
const doorHeight = 2.5;
const roomWidth = 8;
const roomDepth = 6;
const roomHeight = 3;
// Calculate dynamic doorway dimensions
const doorwayWidth = doorWidth + wallThickness * 2 + 0.1; // Extra margin
const doorwayHeight = doorHeight + wallThickness + 0.1;
return (
<group>
{/* Floor - Clean shadow catcher */}
<mesh
rotation={[-Math.PI / 2, 0, 0]}
position={[0, 0, 0]}
receiveShadow
>
<planeGeometry args={[15, 15]} />
<meshStandardMaterial color="#f5f5f5" roughness={0.9} metalness={0} />
{/* Floor - Modern light wood */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
<planeGeometry args={[roomWidth * 2, roomDepth * 2]} />
<meshStandardMaterial color="#e8dcc4" roughness={0.8} metalness={0} />
</mesh>
{/* Proper Doorway with Reveal */}
{/* Back Wall with Dynamic Doorway */}
<group position={[0, 0, -wallThickness / 2]}>
{/* Left Pillar */}
<mesh position={[-(doorWidth / 2 + wallThickness / 2), doorHeight / 2, 0]} receiveShadow castShadow>
<boxGeometry args={[wallThickness, doorHeight + wallThickness, wallThickness]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
{/* Left Pillar - Dynamic height */}
<mesh
position={[-(doorwayWidth / 2 + wallThickness / 2), roomHeight / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry
args={[wallThickness, roomHeight, wallThickness]}
/>
<meshStandardMaterial color="#f5f5f5" roughness={1} />
</mesh>
{/* Right Pillar */}
<mesh position={[doorWidth / 2 + wallThickness / 2, doorHeight / 2, 0]} receiveShadow castShadow>
<boxGeometry args={[wallThickness, doorHeight + wallThickness, wallThickness]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
{/* Right Pillar - Dynamic height */}
<mesh
position={[doorwayWidth / 2 + wallThickness / 2, roomHeight / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry
args={[wallThickness, roomHeight, wallThickness]}
/>
<meshStandardMaterial color="#f5f5f5" roughness={1} />
</mesh>
{/* Top Lintel */}
<mesh position={[0, doorHeight + wallThickness / 2, 0]} receiveShadow castShadow>
<boxGeometry args={[doorWidth + wallThickness * 2, wallThickness, wallThickness]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
{/* Doorway Frame - Left */}
<mesh
position={[-(doorwayWidth / 2), doorHeight / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry args={[wallThickness, doorHeight, wallThickness]} />
<meshStandardMaterial color="#e0e0e0" roughness={0.9} />
</mesh>
{/* Doorway Frame - Right */}
<mesh
position={[doorwayWidth / 2, doorHeight / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry args={[wallThickness, doorHeight, wallThickness]} />
<meshStandardMaterial color="#e0e0e0" roughness={0.9} />
</mesh>
{/* Doorway Frame - Top (Lintel) */}
<mesh
position={[0, doorHeight, 0]}
receiveShadow
castShadow
>
<boxGeometry
args={[doorwayWidth + wallThickness * 2, wallThickness, wallThickness]}
/>
<meshStandardMaterial color="#e0e0e0" roughness={0.9} />
</mesh>
{/* Main Wall - Left Section */}
<mesh position={[-doorWidth - wallThickness * 2, 2.5, 0]} receiveShadow castShadow>
<boxGeometry args={[6, 5, wallThickness]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
<mesh
position={[-(doorwayWidth / 2 + wallThickness + (roomWidth - doorwayWidth) / 4), roomHeight / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry
args={[(roomWidth - doorwayWidth) / 2, roomHeight, wallThickness]}
/>
<meshStandardMaterial color="#f5f5f5" roughness={1} />
</mesh>
{/* Main Wall - Right Section */}
<mesh position={[doorWidth + wallThickness * 2, 2.5, 0]} receiveShadow castShadow>
<boxGeometry args={[6, 5, wallThickness]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
<mesh
position={[doorwayWidth / 2 + wallThickness + (roomWidth - doorwayWidth) / 4, roomHeight / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry
args={[(roomWidth - doorwayWidth) / 2, roomHeight, wallThickness]}
/>
<meshStandardMaterial color="#f5f5f5" roughness={1} />
</mesh>
{/* Main Wall - Top Section */}
<mesh position={[0, doorHeight + wallThickness + 1.25, 0]} receiveShadow castShadow>
<boxGeometry args={[doorWidth + wallThickness * 2, 2.5, wallThickness]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
{/* Main Wall - Top Section (above doorway) */}
<mesh
position={[0, doorwayHeight + (roomHeight - doorwayHeight) / 2, 0]}
receiveShadow
castShadow
>
<boxGeometry
args={[doorwayWidth + wallThickness * 2, roomHeight - doorwayHeight, wallThickness]}
/>
<meshStandardMaterial color="#f5f5f5" roughness={1} />
</mesh>
</group>
{/* Side Walls for depth */}
<mesh position={[-7, 2.5, 2]} receiveShadow castShadow>
<boxGeometry args={[0.15, 5, 10]} />
<meshStandardMaterial color="#fcfcfc" roughness={1} />
{/* Left Wall */}
<mesh position={[-roomWidth / 2, roomHeight / 2, roomDepth / 2]} receiveShadow castShadow>
<boxGeometry args={[wallThickness, roomHeight, roomDepth]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
</mesh>
<mesh position={[7, 2.5, 2]} receiveShadow castShadow>
<boxGeometry args={[0.15, 5, 10]} />
<meshStandardMaterial color="#fcfcfc" roughness={1} />
{/* Right Wall */}
<mesh position={[roomWidth / 2, roomHeight / 2, roomDepth / 2]} receiveShadow castShadow>
<boxGeometry args={[wallThickness, roomHeight, roomDepth]} />
<meshStandardMaterial color="#fafafa" roughness={1} />
</mesh>
{/* Ceiling */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, roomHeight, roomDepth / 2]} receiveShadow>
<planeGeometry args={[roomWidth, roomDepth]} />
<meshStandardMaterial color="#ffffff" roughness={1} />
</mesh>
{/* Decorative Elements - Baseboard Left */}
<mesh position={[-roomWidth / 2 + wallThickness, 0.05, roomDepth / 2]} castShadow>
<boxGeometry args={[wallThickness / 2, 0.1, roomDepth - wallThickness]} />
<meshStandardMaterial color="#d0d0d0" roughness={0.8} />
</mesh>
{/* Decorative Elements - Baseboard Right */}
<mesh position={[roomWidth / 2 - wallThickness, 0.05, roomDepth / 2]} castShadow>
<boxGeometry args={[wallThickness / 2, 0.1, roomDepth - wallThickness]} />
<meshStandardMaterial color="#d0d0d0" roughness={0.8} />
</mesh>
</group>
);
}
function DoorWithRoom() {
const { doorLeafWidth, height } = useConfiguratorStore();
// Convert mm to meters for 3D scene
const doorWidth = doorLeafWidth / 1000;
const doorHeight = height / 1000;
return (
<>
<LivingRoom doorWidth={doorWidth} doorHeight={doorHeight} />
<Door3DEnhanced />
</>
);
}
function Lighting() {
return (
<>
{/* Strong ambient for flat, technical drawing look */}
<ambientLight intensity={0.8} />
{/* Ambient for overall illumination */}
<ambientLight intensity={0.6} />
{/* Front key light - straight on */}
{/* Main directional light (sunlight from window) */}
<directionalLight
position={[0, 5, 10]}
intensity={2}
position={[5, 6, 8]}
intensity={1.5}
castShadow
shadow-mapSize-width={4096}
shadow-mapSize-height={4096}
@@ -96,9 +188,11 @@ function Lighting() {
shadow-bias={-0.0001}
/>
{/* Subtle side light for depth */}
<directionalLight position={[-2, 2, 3]} intensity={0.3} />
<directionalLight position={[2, 2, 3]} intensity={0.3} />
{/* Fill light from opposite side */}
<directionalLight position={[-3, 3, 5]} intensity={0.4} />
{/* Subtle top light */}
<directionalLight position={[0, 8, 2]} intensity={0.3} />
</>
);
}
@@ -110,24 +204,24 @@ export function Scene3D() {
gl={{
antialias: true,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1.3,
toneMappingExposure: 1.2,
outputColorSpace: THREE.SRGBColorSpace,
}}
style={{ background: "#fafafa" }}
>
{/* Camera - More frontal view for technical drawing aesthetic */}
<PerspectiveCamera makeDefault position={[0, 1.2, 3.5]} fov={35} />
{/* Camera - Zoomed out for room context */}
<PerspectiveCamera makeDefault position={[0, 1.5, 5.5]} fov={50} />
{/* Camera Controls - Very limited for flat view */}
{/* Camera Controls - More freedom for room viewing */}
<OrbitControls
enablePan={false}
enableZoom={true}
minDistance={3}
maxDistance={5}
minPolarAngle={Math.PI / 2.5}
minDistance={4}
maxDistance={8}
minPolarAngle={Math.PI / 3}
maxPolarAngle={Math.PI / 2.1}
maxAzimuthAngle={Math.PI / 12}
minAzimuthAngle={-Math.PI / 12}
maxAzimuthAngle={Math.PI / 4}
minAzimuthAngle={-Math.PI / 4}
target={[0, 1.2, 0]}
enableDamping
dampingFactor={0.05}
@@ -137,23 +231,20 @@ export function Scene3D() {
<Lighting />
{/* City/Apartment Environment for realistic steel reflections */}
<Environment preset="city" environmentIntensity={0.8} />
<Environment preset="apartment" environmentIntensity={0.6} />
{/* High-Resolution Contact Shadows for grounding */}
<ContactShadows
position={[0, 0.01, 0]}
opacity={0.5}
scale={10}
blur={2}
far={1}
opacity={0.4}
scale={12}
blur={2.5}
far={2}
resolution={1024}
/>
{/* The Room */}
<Room />
{/* The Door - Enhanced with textures and dimensions */}
<Door3DEnhanced />
<DoorWithRoom />
</Canvas>
);
}

View File

@@ -0,0 +1,80 @@
"use client";
import { useTexture } from "@react-three/drei";
import { useEffect, useState } from "react";
import * as THREE from "three";
/**
* Preload textures to prevent loading freezes
* Uses Suspense boundary for progressive loading
*/
export function useMetalTexture(finish: string) {
const [textureUrl, setTextureUrl] = useState<string | null>(null);
useEffect(() => {
const mapping: Record<string, string> = {
zwart: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg",
brons: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-brons.jpg",
grijs: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-antraciet.jpg",
};
setTextureUrl(mapping[finish] || mapping.zwart);
}, [finish]);
try {
if (!textureUrl) return null;
// Load texture with useTexture
const texture = useTexture(textureUrl);
// Configure texture for optimal rendering
if (texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2);
texture.colorSpace = THREE.SRGBColorSpace;
}
return texture;
} catch (error) {
// Fallback: return null if texture fails to load
console.warn("Texture loading failed, using solid color fallback", error);
return null;
}
}
/**
* Enhanced Steel Material with texture support
*/
export function SteelMaterialWithTexture({
color,
finish,
}: {
color: string;
finish: string;
}) {
const texture = useMetalTexture(finish);
return (
<meshStandardMaterial
map={texture}
color={color}
roughness={0.7}
metalness={0.8}
envMapIntensity={1.2}
/>
);
}
/**
* Fallback Steel Material (solid color)
*/
export function SteelMaterialSolid({ color }: { color: string }) {
return (
<meshStandardMaterial
color={color}
roughness={0.7}
metalness={0.8}
envMapIntensity={1}
/>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { useConfiguratorStore, type Finish, type Handle } from "@/lib/store";
import { glassPatternOptions, type GlassPattern } from "@/lib/glass-patterns";
import { Check } from "lucide-react";
const finishOptions: Array<{
@@ -22,17 +23,46 @@ const handleOptions: Array<{
label: string;
description: string;
}> = [
{
value: "beugelgreep",
label: "Beugelgreep",
description: "Verticale staaf met montageblokken",
},
{
value: "hoekgreep",
label: "Hoekgreep",
description: "L-vormige minimalistisch design",
},
{
value: "maangreep",
label: "Maangreep",
description: "Gebogen half-maanvormige greep",
},
{
value: "ovaalgreep",
label: "Ovaalgreep",
description: "Moderne ovale trekgreep",
},
{
value: "klink",
label: "Deurklink",
description: "Klassieke deurklink met hendel",
},
{
value: "u-greep",
label: "U-Greep",
description: "Verticale greep voor taatsdeur",
description: "Eenvoudige rechte staaf",
},
{
value: "geen",
label: "Geen greep",
description: "Voor vaste panelen",
},
{ value: "klink", label: "Klink", description: "Klassieke deurklink" },
{ value: "geen", label: "Geen greep", description: "Voor vaste panelen" },
];
export function StepOptions() {
const { finish, handle, setFinish, setHandle } = useConfiguratorStore();
const { finish, handle, glassPattern, setFinish, setHandle, setGlassPattern } =
useConfiguratorStore();
return (
<div className="space-y-8">
@@ -95,6 +125,51 @@ export function StepOptions() {
</div>
</div>
{/* Glass Pattern Selection */}
<div>
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Glaspatroon</h2>
<p className="mb-4 text-sm text-gray-600">
Kies het decoratieve patroon voor de glaspanelen.
</p>
<div className="grid gap-3">
{glassPatternOptions.map((option) => {
const selected = glassPattern === option.value;
return (
<button
key={option.value}
type="button"
onClick={() => setGlassPattern(option.value)}
className={`group relative rounded-xl border-2 p-4 text-left transition-all ${
selected
? "border-[#C4D668] bg-[#1A2E2E] text-white"
: "border-gray-200 bg-white text-gray-900 hover:border-[#1A2E2E]/30"
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-bold">{option.label}</h3>
<p
className={`mt-1 text-sm ${
selected ? "text-white/80" : "text-gray-500"
}`}
>
{option.description}
</p>
</div>
{selected && (
<div className="flex size-6 items-center justify-center rounded-full bg-[#C4D668]">
<Check className="size-4 text-[#1A2E2E]" />
</div>
)}
</div>
</button>
);
})}
</div>
</div>
{/* Handle Selection */}
<div>
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Greep</h2>

198
lib/glass-patterns.ts Normal file
View File

@@ -0,0 +1,198 @@
/**
* Glass Pattern Generators
* Creates custom THREE.Shape objects for decorative glass panels
* Based on reference drawings: dt9 (rounded corners), dt10 (U-shapes)
*/
import * as THREE from "three";
export type GlassPattern = "standard" | "dt9-rounded" | "dt10-ushape";
/**
* Standard rectangular glass panel
*/
export function createStandardGlass(width: number, height: number): THREE.Shape {
const shape = new THREE.Shape();
const hw = width / 2;
const hh = height / 2;
shape.moveTo(-hw, -hh);
shape.lineTo(hw, -hh);
shape.lineTo(hw, hh);
shape.lineTo(-hw, hh);
shape.lineTo(-hw, -hh);
return shape;
}
/**
* DT9: Rounded corners glass panel
* Creates elegant rounded corners on glass sections
*/
export function createRoundedCornerGlass(
width: number,
height: number,
cornerRadius: number = 0.08
): THREE.Shape {
const shape = new THREE.Shape();
const hw = width / 2;
const hh = height / 2;
const r = Math.min(cornerRadius, width / 4, height / 4);
// Start from bottom-left corner (after radius)
shape.moveTo(-hw + r, -hh);
// Bottom edge
shape.lineTo(hw - r, -hh);
// Bottom-right rounded corner
shape.quadraticCurveTo(hw, -hh, hw, -hh + r);
// Right edge
shape.lineTo(hw, hh - r);
// Top-right rounded corner
shape.quadraticCurveTo(hw, hh, hw - r, hh);
// Top edge
shape.lineTo(-hw + r, hh);
// Top-left rounded corner
shape.quadraticCurveTo(-hw, hh, -hw, hh - r);
// Left edge
shape.lineTo(-hw, -hh + r);
// Bottom-left rounded corner
shape.quadraticCurveTo(-hw, -hh, -hw + r, -hh);
return shape;
}
/**
* DT10: U-shaped glass panel (top section - inverted U)
* Creates an upside-down U shape for the upper glass section
*/
export function createInvertedUGlass(width: number, height: number): THREE.Shape {
const shape = new THREE.Shape();
const hw = width / 2;
const hh = height / 2;
const uRadius = hw * 0.85; // U curve radius
// Start at top-left
shape.moveTo(-hw, hh);
// Top edge
shape.lineTo(hw, hh);
// Right edge down
shape.lineTo(hw, -hh + uRadius);
// Bottom U curve (inverted)
shape.absarc(0, -hh + uRadius, uRadius, 0, Math.PI, false);
// Left edge up
shape.lineTo(-hw, hh);
return shape;
}
/**
* DT10: U-shaped glass panel (bottom section - normal U)
* Creates a normal U shape for the lower glass section
*/
export function createNormalUGlass(width: number, height: number): THREE.Shape {
const shape = new THREE.Shape();
const hw = width / 2;
const hh = height / 2;
const uRadius = hw * 0.85; // U curve radius
// Start at bottom-left
shape.moveTo(-hw, -hh);
// Bottom edge
shape.lineTo(hw, -hh);
// Right edge up
shape.lineTo(hw, hh - uRadius);
// Top U curve
shape.absarc(0, hh - uRadius, uRadius, 0, Math.PI, true);
// Left edge down
shape.lineTo(-hw, -hh);
return shape;
}
/**
* DT9 Asymmetric: Create multiple panels with different rounded corners
* For complex DT9 layouts with side panels
*/
export function createDT9Panels(
mainWidth: number,
mainHeight: number,
sideWidth: number,
position: "top-right" | "bottom-left" | "top-left" | "bottom-right"
): {
mainPanel: THREE.Shape;
roundedPanel: THREE.Shape;
dividerHeight: number;
} {
const cornerRadius = 0.12;
// Main large panel with one rounded corner
const mainPanel = new THREE.Shape();
const mw = mainWidth / 2;
const mh = mainHeight / 2;
if (position === "top-right") {
// Main panel with rounded top-right corner
mainPanel.moveTo(-mw, -mh);
mainPanel.lineTo(mw - cornerRadius, -mh);
mainPanel.quadraticCurveTo(mw, -mh, mw, -mh + cornerRadius);
mainPanel.lineTo(mw, mh - cornerRadius);
mainPanel.quadraticCurveTo(mw, mh, mw - cornerRadius, mh);
mainPanel.lineTo(-mw, mh);
mainPanel.lineTo(-mw, -mh);
} else if (position === "bottom-left") {
// Main panel with rounded bottom-left corner
mainPanel.moveTo(-mw + cornerRadius, -mh);
mainPanel.lineTo(mw, -mh);
mainPanel.lineTo(mw, mh);
mainPanel.lineTo(-mw + cornerRadius, mh);
mainPanel.quadraticCurveTo(-mw, mh, -mw, mh - cornerRadius);
mainPanel.lineTo(-mw, -mh + cornerRadius);
mainPanel.quadraticCurveTo(-mw, -mh, -mw + cornerRadius, -mh);
}
// Small rounded panel
const roundedPanel = createRoundedCornerGlass(sideWidth, mainHeight / 3, cornerRadius * 0.6);
return {
mainPanel,
roundedPanel,
dividerHeight: mainHeight / 3,
};
}
/**
* Pattern metadata for UI selection
*/
export const glassPatternOptions = [
{
value: "standard" as GlassPattern,
label: "Standaard",
description: "Rechthoekige glaspanelen",
},
{
value: "dt9-rounded" as GlassPattern,
label: "DT9 - Afgeronde hoeken",
description: "Elegante ronde hoeken",
},
{
value: "dt10-ushape" as GlassPattern,
label: "DT10 - U-vormen",
description: "Decoratieve U-vormige panelen",
},
] as const;

View File

@@ -8,11 +8,12 @@ import {
type DoorConfig,
type SidePanel,
} from './calculations';
import type { GlassPattern } from './glass-patterns';
export type DoorType = 'taats' | 'scharnier' | 'paneel';
export type GridType = '3-vlak' | '4-vlak' | 'geen';
export type Finish = 'zwart' | 'brons' | 'grijs';
export type Handle = 'u-greep' | 'klink' | 'geen';
export type Handle = 'beugelgreep' | 'hoekgreep' | 'maangreep' | 'ovaalgreep' | 'klink' | 'u-greep' | 'geen';
interface ConfiguratorState {
// Door configuration
@@ -24,6 +25,7 @@ interface ConfiguratorState {
gridType: GridType;
finish: Finish;
handle: Handle;
glassPattern: GlassPattern;
// Dimensions (in mm)
width: number;
@@ -43,6 +45,7 @@ interface ConfiguratorState {
setGridType: (type: GridType) => void;
setFinish: (finish: Finish) => void;
setHandle: (handle: Handle) => void;
setGlassPattern: (pattern: GlassPattern) => void;
setWidth: (width: number) => void;
setHeight: (height: number) => void;
setDimensions: (width: number, height: number) => void;
@@ -68,7 +71,8 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
sidePanel: 'geen',
gridType: '3-vlak',
finish: 'zwart',
handle: 'u-greep',
handle: 'beugelgreep',
glassPattern: 'standard',
width: 1000,
height: 2400,
@@ -101,6 +105,8 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
setHandle: (handle) => set({ handle }),
setGlassPattern: (glassPattern) => set({ glassPattern }),
setWidth: (width) => {
const { doorConfig, sidePanel } = get();
const minWidth = calculateHoleMinWidth(doorConfig, sidePanel);