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:
@@ -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 */}
|
||||
|
||||
305
components/configurator/handles-3d.tsx
Normal file
305
components/configurator/handles-3d.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
80
components/configurator/texture-loader.tsx
Normal file
80
components/configurator/texture-loader.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user