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";
|
"use client";
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef, useMemo } from "react";
|
||||||
import { useConfiguratorStore } from "@/lib/store";
|
import { useConfiguratorStore } from "@/lib/store";
|
||||||
import { RoundedBox } from "@react-three/drei";
|
import { RoundedBox } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
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
|
// Steel material - fallback to solid color for now
|
||||||
const SteelMaterial = ({ color }: { color: string }) => (
|
const SteelMaterial = ({ color }: { color: string }) => (
|
||||||
@@ -43,7 +57,7 @@ function DimensionLabel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Door3DEnhanced() {
|
export function Door3DEnhanced() {
|
||||||
const { doorType, gridType, finish, handle, doorLeafWidth, height } =
|
const { doorType, gridType, finish, handle, glassPattern, doorLeafWidth, height } =
|
||||||
useConfiguratorStore();
|
useConfiguratorStore();
|
||||||
const doorRef = useRef<THREE.Group>(null);
|
const doorRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
@@ -151,47 +165,134 @@ export function Door3DEnhanced() {
|
|||||||
</RoundedBox>
|
</RoundedBox>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GLASS PANEL - Sits inside the frame */}
|
{/* GLASS PANELS - Pattern-based decorative glass */}
|
||||||
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
{glassPattern === "standard" && (
|
||||||
<boxGeometry
|
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
||||||
args={[
|
<boxGeometry
|
||||||
doorWidth - stileWidth * 2,
|
args={[
|
||||||
doorHeight - railHeight * 2,
|
doorWidth - stileWidth * 2,
|
||||||
glassThickness,
|
doorHeight - railHeight * 2,
|
||||||
]}
|
glassThickness,
|
||||||
/>
|
]}
|
||||||
<GlassMaterial />
|
/>
|
||||||
</mesh>
|
<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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* HANDLE - Klink for Scharnier */}
|
{glassPattern === "dt9-rounded" && (
|
||||||
{doorType === "scharnier" && handle === "klink" && (
|
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
||||||
<group position={[doorWidth / 2 - stileWidth - 0.1, 0, railDepth / 2 + 0.01]}>
|
<extrudeGeometry
|
||||||
<RoundedBox args={[0.08, 0.02, 0.02]} radius={0.003} smoothness={4} castShadow>
|
args={[
|
||||||
<SteelMaterial color={frameColor} />
|
createRoundedCornerGlass(
|
||||||
</RoundedBox>
|
doorWidth - stileWidth * 2,
|
||||||
<mesh position={[0.04, 0, 0]} castShadow>
|
doorHeight - railHeight * 2,
|
||||||
<sphereGeometry args={[0.015, 32, 32]} />
|
0.12
|
||||||
<meshStandardMaterial
|
),
|
||||||
color={finish === "brons" ? "#6B5434" : frameColor}
|
{ depth: glassThickness, bevelEnabled: false },
|
||||||
metalness={0.95}
|
]}
|
||||||
roughness={0.05}
|
/>
|
||||||
envMapIntensity={1.2}
|
<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>
|
</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 */}
|
{/* 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 { Canvas } from "@react-three/fiber";
|
||||||
import { OrbitControls, PerspectiveCamera, Environment, ContactShadows } from "@react-three/drei";
|
import { OrbitControls, PerspectiveCamera, Environment, ContactShadows } from "@react-three/drei";
|
||||||
import { Door3DEnhanced } from "./door-3d-enhanced";
|
import { Door3DEnhanced } from "./door-3d-enhanced";
|
||||||
|
import { useConfiguratorStore } from "@/lib/store";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
function Room() {
|
function LivingRoom({ doorWidth, doorHeight }: { doorWidth: number; doorHeight: number }) {
|
||||||
const wallThickness = 0.15;
|
const wallThickness = 0.15;
|
||||||
const doorWidth = 1.3;
|
const roomWidth = 8;
|
||||||
const doorHeight = 2.5;
|
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 (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Floor - Clean shadow catcher */}
|
{/* Floor - Modern light wood */}
|
||||||
<mesh
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
<planeGeometry args={[roomWidth * 2, roomDepth * 2]} />
|
||||||
position={[0, 0, 0]}
|
<meshStandardMaterial color="#e8dcc4" roughness={0.8} metalness={0} />
|
||||||
receiveShadow
|
|
||||||
>
|
|
||||||
<planeGeometry args={[15, 15]} />
|
|
||||||
<meshStandardMaterial color="#f5f5f5" roughness={0.9} metalness={0} />
|
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Proper Doorway with Reveal */}
|
{/* Back Wall with Dynamic Doorway */}
|
||||||
<group position={[0, 0, -wallThickness / 2]}>
|
<group position={[0, 0, -wallThickness / 2]}>
|
||||||
{/* Left Pillar */}
|
{/* Left Pillar - Dynamic height */}
|
||||||
<mesh position={[-(doorWidth / 2 + wallThickness / 2), doorHeight / 2, 0]} receiveShadow castShadow>
|
<mesh
|
||||||
<boxGeometry args={[wallThickness, doorHeight + wallThickness, wallThickness]} />
|
position={[-(doorwayWidth / 2 + wallThickness / 2), roomHeight / 2, 0]}
|
||||||
<meshStandardMaterial color="#fafafa" roughness={1} />
|
receiveShadow
|
||||||
|
castShadow
|
||||||
|
>
|
||||||
|
<boxGeometry
|
||||||
|
args={[wallThickness, roomHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#f5f5f5" roughness={1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Right Pillar */}
|
{/* Right Pillar - Dynamic height */}
|
||||||
<mesh position={[doorWidth / 2 + wallThickness / 2, doorHeight / 2, 0]} receiveShadow castShadow>
|
<mesh
|
||||||
<boxGeometry args={[wallThickness, doorHeight + wallThickness, wallThickness]} />
|
position={[doorwayWidth / 2 + wallThickness / 2, roomHeight / 2, 0]}
|
||||||
<meshStandardMaterial color="#fafafa" roughness={1} />
|
receiveShadow
|
||||||
|
castShadow
|
||||||
|
>
|
||||||
|
<boxGeometry
|
||||||
|
args={[wallThickness, roomHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#f5f5f5" roughness={1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Top Lintel */}
|
{/* Doorway Frame - Left */}
|
||||||
<mesh position={[0, doorHeight + wallThickness / 2, 0]} receiveShadow castShadow>
|
<mesh
|
||||||
<boxGeometry args={[doorWidth + wallThickness * 2, wallThickness, wallThickness]} />
|
position={[-(doorwayWidth / 2), doorHeight / 2, 0]}
|
||||||
<meshStandardMaterial color="#fafafa" roughness={1} />
|
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>
|
</mesh>
|
||||||
|
|
||||||
{/* Main Wall - Left Section */}
|
{/* Main Wall - Left Section */}
|
||||||
<mesh position={[-doorWidth - wallThickness * 2, 2.5, 0]} receiveShadow castShadow>
|
<mesh
|
||||||
<boxGeometry args={[6, 5, wallThickness]} />
|
position={[-(doorwayWidth / 2 + wallThickness + (roomWidth - doorwayWidth) / 4), roomHeight / 2, 0]}
|
||||||
<meshStandardMaterial color="#fafafa" roughness={1} />
|
receiveShadow
|
||||||
|
castShadow
|
||||||
|
>
|
||||||
|
<boxGeometry
|
||||||
|
args={[(roomWidth - doorwayWidth) / 2, roomHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#f5f5f5" roughness={1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Main Wall - Right Section */}
|
{/* Main Wall - Right Section */}
|
||||||
<mesh position={[doorWidth + wallThickness * 2, 2.5, 0]} receiveShadow castShadow>
|
<mesh
|
||||||
<boxGeometry args={[6, 5, wallThickness]} />
|
position={[doorwayWidth / 2 + wallThickness + (roomWidth - doorwayWidth) / 4, roomHeight / 2, 0]}
|
||||||
<meshStandardMaterial color="#fafafa" roughness={1} />
|
receiveShadow
|
||||||
|
castShadow
|
||||||
|
>
|
||||||
|
<boxGeometry
|
||||||
|
args={[(roomWidth - doorwayWidth) / 2, roomHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#f5f5f5" roughness={1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Main Wall - Top Section */}
|
{/* Main Wall - Top Section (above doorway) */}
|
||||||
<mesh position={[0, doorHeight + wallThickness + 1.25, 0]} receiveShadow castShadow>
|
<mesh
|
||||||
<boxGeometry args={[doorWidth + wallThickness * 2, 2.5, wallThickness]} />
|
position={[0, doorwayHeight + (roomHeight - doorwayHeight) / 2, 0]}
|
||||||
<meshStandardMaterial color="#fafafa" roughness={1} />
|
receiveShadow
|
||||||
|
castShadow
|
||||||
|
>
|
||||||
|
<boxGeometry
|
||||||
|
args={[doorwayWidth + wallThickness * 2, roomHeight - doorwayHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#f5f5f5" roughness={1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Side Walls for depth */}
|
{/* Left Wall */}
|
||||||
<mesh position={[-7, 2.5, 2]} receiveShadow castShadow>
|
<mesh position={[-roomWidth / 2, roomHeight / 2, roomDepth / 2]} receiveShadow castShadow>
|
||||||
<boxGeometry args={[0.15, 5, 10]} />
|
<boxGeometry args={[wallThickness, roomHeight, roomDepth]} />
|
||||||
<meshStandardMaterial color="#fcfcfc" roughness={1} />
|
<meshStandardMaterial color="#fafafa" roughness={1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
<mesh position={[7, 2.5, 2]} receiveShadow castShadow>
|
{/* Right Wall */}
|
||||||
<boxGeometry args={[0.15, 5, 10]} />
|
<mesh position={[roomWidth / 2, roomHeight / 2, roomDepth / 2]} receiveShadow castShadow>
|
||||||
<meshStandardMaterial color="#fcfcfc" roughness={1} />
|
<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>
|
</mesh>
|
||||||
</group>
|
</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() {
|
function Lighting() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Strong ambient for flat, technical drawing look */}
|
{/* Ambient for overall illumination */}
|
||||||
<ambientLight intensity={0.8} />
|
<ambientLight intensity={0.6} />
|
||||||
|
|
||||||
{/* Front key light - straight on */}
|
{/* Main directional light (sunlight from window) */}
|
||||||
<directionalLight
|
<directionalLight
|
||||||
position={[0, 5, 10]}
|
position={[5, 6, 8]}
|
||||||
intensity={2}
|
intensity={1.5}
|
||||||
castShadow
|
castShadow
|
||||||
shadow-mapSize-width={4096}
|
shadow-mapSize-width={4096}
|
||||||
shadow-mapSize-height={4096}
|
shadow-mapSize-height={4096}
|
||||||
@@ -96,9 +188,11 @@ function Lighting() {
|
|||||||
shadow-bias={-0.0001}
|
shadow-bias={-0.0001}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Subtle side light for depth */}
|
{/* Fill light from opposite side */}
|
||||||
<directionalLight position={[-2, 2, 3]} intensity={0.3} />
|
<directionalLight position={[-3, 3, 5]} intensity={0.4} />
|
||||||
<directionalLight position={[2, 2, 3]} intensity={0.3} />
|
|
||||||
|
{/* Subtle top light */}
|
||||||
|
<directionalLight position={[0, 8, 2]} intensity={0.3} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -110,24 +204,24 @@ export function Scene3D() {
|
|||||||
gl={{
|
gl={{
|
||||||
antialias: true,
|
antialias: true,
|
||||||
toneMapping: THREE.ACESFilmicToneMapping,
|
toneMapping: THREE.ACESFilmicToneMapping,
|
||||||
toneMappingExposure: 1.3,
|
toneMappingExposure: 1.2,
|
||||||
outputColorSpace: THREE.SRGBColorSpace,
|
outputColorSpace: THREE.SRGBColorSpace,
|
||||||
}}
|
}}
|
||||||
style={{ background: "#fafafa" }}
|
style={{ background: "#fafafa" }}
|
||||||
>
|
>
|
||||||
{/* Camera - More frontal view for technical drawing aesthetic */}
|
{/* Camera - Zoomed out for room context */}
|
||||||
<PerspectiveCamera makeDefault position={[0, 1.2, 3.5]} fov={35} />
|
<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
|
<OrbitControls
|
||||||
enablePan={false}
|
enablePan={false}
|
||||||
enableZoom={true}
|
enableZoom={true}
|
||||||
minDistance={3}
|
minDistance={4}
|
||||||
maxDistance={5}
|
maxDistance={8}
|
||||||
minPolarAngle={Math.PI / 2.5}
|
minPolarAngle={Math.PI / 3}
|
||||||
maxPolarAngle={Math.PI / 2.1}
|
maxPolarAngle={Math.PI / 2.1}
|
||||||
maxAzimuthAngle={Math.PI / 12}
|
maxAzimuthAngle={Math.PI / 4}
|
||||||
minAzimuthAngle={-Math.PI / 12}
|
minAzimuthAngle={-Math.PI / 4}
|
||||||
target={[0, 1.2, 0]}
|
target={[0, 1.2, 0]}
|
||||||
enableDamping
|
enableDamping
|
||||||
dampingFactor={0.05}
|
dampingFactor={0.05}
|
||||||
@@ -137,23 +231,20 @@ export function Scene3D() {
|
|||||||
<Lighting />
|
<Lighting />
|
||||||
|
|
||||||
{/* City/Apartment Environment for realistic steel reflections */}
|
{/* 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 */}
|
{/* High-Resolution Contact Shadows for grounding */}
|
||||||
<ContactShadows
|
<ContactShadows
|
||||||
position={[0, 0.01, 0]}
|
position={[0, 0.01, 0]}
|
||||||
opacity={0.5}
|
opacity={0.4}
|
||||||
scale={10}
|
scale={12}
|
||||||
blur={2}
|
blur={2.5}
|
||||||
far={1}
|
far={2}
|
||||||
resolution={1024}
|
resolution={1024}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* The Room */}
|
|
||||||
<Room />
|
|
||||||
|
|
||||||
{/* The Door - Enhanced with textures and dimensions */}
|
{/* The Door - Enhanced with textures and dimensions */}
|
||||||
<Door3DEnhanced />
|
<DoorWithRoom />
|
||||||
</Canvas>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { useConfiguratorStore, type Finish, type Handle } from "@/lib/store";
|
import { useConfiguratorStore, type Finish, type Handle } from "@/lib/store";
|
||||||
|
import { glassPatternOptions, type GlassPattern } from "@/lib/glass-patterns";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
const finishOptions: Array<{
|
const finishOptions: Array<{
|
||||||
@@ -22,17 +23,46 @@ const handleOptions: Array<{
|
|||||||
label: string;
|
label: string;
|
||||||
description: 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",
|
value: "u-greep",
|
||||||
label: "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() {
|
export function StepOptions() {
|
||||||
const { finish, handle, setFinish, setHandle } = useConfiguratorStore();
|
const { finish, handle, glassPattern, setFinish, setHandle, setGlassPattern } =
|
||||||
|
useConfiguratorStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@@ -95,6 +125,51 @@ export function StepOptions() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Handle Selection */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Greep</h2>
|
<h2 className="mb-2 text-lg font-bold text-[#1A2E2E]">Greep</h2>
|
||||||
|
|||||||
198
lib/glass-patterns.ts
Normal file
198
lib/glass-patterns.ts
Normal 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;
|
||||||
10
lib/store.ts
10
lib/store.ts
@@ -8,11 +8,12 @@ import {
|
|||||||
type DoorConfig,
|
type DoorConfig,
|
||||||
type SidePanel,
|
type SidePanel,
|
||||||
} from './calculations';
|
} from './calculations';
|
||||||
|
import type { GlassPattern } from './glass-patterns';
|
||||||
|
|
||||||
export type DoorType = 'taats' | 'scharnier' | 'paneel';
|
export type DoorType = 'taats' | 'scharnier' | 'paneel';
|
||||||
export type GridType = '3-vlak' | '4-vlak' | 'geen';
|
export type GridType = '3-vlak' | '4-vlak' | 'geen';
|
||||||
export type Finish = 'zwart' | 'brons' | 'grijs';
|
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 {
|
interface ConfiguratorState {
|
||||||
// Door configuration
|
// Door configuration
|
||||||
@@ -24,6 +25,7 @@ interface ConfiguratorState {
|
|||||||
gridType: GridType;
|
gridType: GridType;
|
||||||
finish: Finish;
|
finish: Finish;
|
||||||
handle: Handle;
|
handle: Handle;
|
||||||
|
glassPattern: GlassPattern;
|
||||||
|
|
||||||
// Dimensions (in mm)
|
// Dimensions (in mm)
|
||||||
width: number;
|
width: number;
|
||||||
@@ -43,6 +45,7 @@ interface ConfiguratorState {
|
|||||||
setGridType: (type: GridType) => void;
|
setGridType: (type: GridType) => void;
|
||||||
setFinish: (finish: Finish) => void;
|
setFinish: (finish: Finish) => void;
|
||||||
setHandle: (handle: Handle) => void;
|
setHandle: (handle: Handle) => void;
|
||||||
|
setGlassPattern: (pattern: GlassPattern) => void;
|
||||||
setWidth: (width: number) => void;
|
setWidth: (width: number) => void;
|
||||||
setHeight: (height: number) => void;
|
setHeight: (height: number) => void;
|
||||||
setDimensions: (width: number, height: number) => void;
|
setDimensions: (width: number, height: number) => void;
|
||||||
@@ -68,7 +71,8 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
sidePanel: 'geen',
|
sidePanel: 'geen',
|
||||||
gridType: '3-vlak',
|
gridType: '3-vlak',
|
||||||
finish: 'zwart',
|
finish: 'zwart',
|
||||||
handle: 'u-greep',
|
handle: 'beugelgreep',
|
||||||
|
glassPattern: 'standard',
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 2400,
|
height: 2400,
|
||||||
|
|
||||||
@@ -101,6 +105,8 @@ export const useConfiguratorStore = create<ConfiguratorState>((set, get) => ({
|
|||||||
|
|
||||||
setHandle: (handle) => set({ handle }),
|
setHandle: (handle) => set({ handle }),
|
||||||
|
|
||||||
|
setGlassPattern: (glassPattern) => set({ glassPattern }),
|
||||||
|
|
||||||
setWidth: (width) => {
|
setWidth: (width) => {
|
||||||
const { doorConfig, sidePanel } = get();
|
const { doorConfig, sidePanel } = get();
|
||||||
const minWidth = calculateHoleMinWidth(doorConfig, sidePanel);
|
const minWidth = calculateHoleMinWidth(doorConfig, sidePanel);
|
||||||
|
|||||||
Reference in New Issue
Block a user