fix: Physically mounted handles with proper standoffs and powder-coat material

- All 6 handle types now have cylindrical mount standoffs (pootjes)
  connecting grip to door face: r=6mm, length=40mm
- Z-positioning: grip sits at PROFILE_DEPTH/2 + MOUNT_LENGTH (60mm from
  center), no more floating handles inside the door
- Material: replaced chrome HandleMaterial (metalness=0.95) with
  PowderCoatMaterial matching door frame texture (roughness=0.7, metalness=0.6)
- UGreep fully redesigned: proper U-shape with 2 standoffs + vertical bar
- All handles cast shadows onto the door frame for depth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu
2026-02-14 01:19:54 +00:00
parent 87be70e78b
commit fbc9fefeea

View File

@@ -1,9 +1,25 @@
"use client"; "use client";
import { RoundedBox } from "@react-three/drei"; import { Suspense } from "react";
import { RoundedBox, useTexture } from "@react-three/drei";
import * as THREE from "three"; import * as THREE from "three";
interface HandleProps { // ============================================
// PHYSICAL CONSTANTS (mm converted to meters)
// ============================================
const PROFILE_DEPTH_M = 0.04; // 40mm profile depth
const DOOR_FACE_Z = PROFILE_DEPTH_M / 2; // 20mm - front face of door
const MOUNT_RADIUS = 0.006; // 6mm radius standoff cylinders
const MOUNT_LENGTH = 0.04; // 40mm standoff length
const MOUNT_CENTER_Z = DOOR_FACE_Z + MOUNT_LENGTH / 2; // Center of mount
const GRIP_CENTER_Z = DOOR_FACE_Z + MOUNT_LENGTH; // Front face of mount = grip center
const GRIP_RADIUS = 0.01; // 10mm radius for round grips
const GRIP_BAR_SIZE = 0.02; // 20mm for square grip cross-section
export interface HandleProps {
finish: string; finish: string;
doorWidth: number; doorWidth: number;
doorHeight: number; doorHeight: number;
@@ -11,66 +27,190 @@ interface HandleProps {
stileWidth: number; stileWidth: number;
} }
// Steel material for handles // ============================================
const HandleMaterial = ({ color }: { color: string }) => ( // MATERIALS
<meshStandardMaterial // ============================================
color={color}
roughness={0.3}
metalness={0.95}
envMapIntensity={1.5}
/>
);
/** /**
* Beugelgreep - Vertical bar handle with mounting blocks * Powder-coated steel material matching door frame finish.
* Classic industrial style, common on steel pivot doors * Loaded with texture for visual continuity with the frame.
*/ */
export function Beugelgreep({ finish, doorHeight, railDepth }: HandleProps) { function HandleMaterialTextured({ color, finish }: { color: string; finish: string }) {
const color = ({ try {
zwart: "#1a1a1a", const texturePath = {
brons: "#8B6F47", zwart: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg",
grijs: "#525252", brons: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-brons.jpg",
}[finish] || "#1a1a1a") as string; grijs: "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-antraciet.jpg",
}[finish] || "/textures/aluwdoors/aluwdoors-configurator-metaalkleur-zwart.jpg";
const handleLength = Math.min(doorHeight * 0.35, 0.8); // Max 80cm const texture = useTexture(texturePath);
const barDiameter = 0.025; // 25mm diameter bar texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const mountBlockSize: [number, number, number] = [0.04, 0.06, 0.03]; // Mount block dimensions texture.repeat.set(0.2, 1);
texture.colorSpace = THREE.SRGBColorSpace;
return (
<meshStandardMaterial
map={texture}
color={color}
roughness={0.7}
metalness={0.6}
envMapIntensity={1.5}
/>
);
} catch {
return <HandleMaterialFallback color={color} />;
}
}
function HandleMaterialFallback({ color }: { color: string }) {
return (
<meshStandardMaterial
color={color}
roughness={0.7}
metalness={0.6}
envMapIntensity={1.5}
/>
);
}
/** Wrap textured material in Suspense */
function PowderCoatMaterial({ color, finish }: { color: string; finish: string }) {
return (
<Suspense fallback={<HandleMaterialFallback color={color} />}>
<HandleMaterialTextured color={color} finish={finish} />
</Suspense>
);
}
function getColor(finish: string): string {
return { zwart: "#1a1a1a", brons: "#8B6F47", grijs: "#525252" }[finish] || "#1a1a1a";
}
// ============================================
// SHARED MOUNT COMPONENT
// ============================================
/**
* A single cylindrical standoff (pootje) connecting handle to door face.
* Rotated 90° on X to point outward from the door surface.
*/
function MountStandoff({
position,
color,
finish,
}: {
position: [number, number, number];
color: string;
finish: string;
}) {
return (
<mesh
position={position}
rotation={[Math.PI / 2, 0, 0]}
castShadow
>
<cylinderGeometry args={[MOUNT_RADIUS, MOUNT_RADIUS, MOUNT_LENGTH, 16]} />
<PowderCoatMaterial color={color} finish={finish} />
</mesh>
);
}
// ============================================
// HANDLE COMPONENTS
// ============================================
/**
* U-Greep: Proper U-shaped bar handle with two standoff mounts.
* The grip sits 40mm off the door face, connected by two cylindrical pootjes.
*/
export function UGreep({ finish, doorHeight }: HandleProps) {
const color = getColor(finish);
const gripLength = Math.min(doorHeight * 0.25, 0.6); // Max 60cm, proportional
const mountSpacing = gripLength - GRIP_BAR_SIZE; // Distance between mount centers
return ( return (
<group position={[0, 0, railDepth / 2 + 0.02]}> <group position={[0, 0, 0]}>
{/* Main vertical bar */} {/* Top mount standoff */}
<mesh castShadow> <MountStandoff
<cylinderGeometry args={[barDiameter / 2, barDiameter / 2, handleLength, 32]} /> position={[0, mountSpacing / 2, MOUNT_CENTER_Z]}
<HandleMaterial color={color} /> color={color}
</mesh> finish={finish}
/>
{/* Top mounting block */} {/* Bottom mount standoff */}
<MountStandoff
position={[0, -mountSpacing / 2, MOUNT_CENTER_Z]}
color={color}
finish={finish}
/>
{/* Vertical grip bar */}
<RoundedBox
args={[GRIP_BAR_SIZE, gripLength, GRIP_BAR_SIZE]}
radius={0.003}
smoothness={4}
position={[0, 0, GRIP_CENTER_Z]}
castShadow
receiveShadow
>
<PowderCoatMaterial color={color} finish={finish} />
</RoundedBox>
</group>
);
}
/**
* Beugelgreep: Vertical bar handle (round) with mounting blocks.
* Two rectangular mounting blocks press against the door face,
* with a round bar connecting them.
*/
export function Beugelgreep({ finish, doorHeight }: HandleProps) {
const color = getColor(finish);
const gripLength = Math.min(doorHeight * 0.35, 0.8); // Max 80cm
const barDiameter = 0.025; // 25mm
const mountBlockSize: [number, number, number] = [0.04, 0.05, MOUNT_LENGTH];
const mountSpacing = gripLength * 0.85;
return (
<group position={[0, 0, 0]}>
{/* Top mounting block (sits on door face, extends outward) */}
<RoundedBox <RoundedBox
args={mountBlockSize} args={mountBlockSize}
radius={0.003} radius={0.003}
position={[0, handleLength / 2 + mountBlockSize[1] / 2, -0.01]} smoothness={4}
position={[0, mountSpacing / 2, MOUNT_CENTER_Z]}
castShadow castShadow
receiveShadow
> >
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</RoundedBox> </RoundedBox>
{/* Bottom mounting block */} {/* Bottom mounting block */}
<RoundedBox <RoundedBox
args={mountBlockSize} args={mountBlockSize}
radius={0.003} radius={0.003}
position={[0, -handleLength / 2 - mountBlockSize[1] / 2, -0.01]} smoothness={4}
position={[0, -mountSpacing / 2, MOUNT_CENTER_Z]}
castShadow castShadow
receiveShadow
> >
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</RoundedBox> </RoundedBox>
{/* Mounting screws (detail) */} {/* Main vertical round bar */}
{[ <mesh position={[0, 0, GRIP_CENTER_Z]} castShadow>
[0, handleLength / 2 + mountBlockSize[1] / 2, 0.005], <cylinderGeometry args={[barDiameter / 2, barDiameter / 2, gripLength, 32]} />
[0, -handleLength / 2 - mountBlockSize[1] / 2, 0.005], <PowderCoatMaterial color={color} finish={finish} />
].map((pos, i) => ( </mesh>
<mesh key={i} position={pos as [number, number, number]} castShadow>
<cylinderGeometry args={[0.003, 0.003, 0.008, 16]} /> {/* Mounting screw details */}
{[mountSpacing / 2, -mountSpacing / 2].map((y, i) => (
<mesh
key={i}
position={[0, y, DOOR_FACE_Z + MOUNT_LENGTH + 0.002]}
castShadow
>
<cylinderGeometry args={[0.003, 0.003, 0.005, 12]} />
<meshStandardMaterial color="#2a2a2a" metalness={0.9} roughness={0.1} /> <meshStandardMaterial color="#2a2a2a" metalness={0.9} roughness={0.1} />
</mesh> </mesh>
))} ))}
@@ -79,32 +219,44 @@ export function Beugelgreep({ finish, doorHeight, railDepth }: HandleProps) {
} }
/** /**
* Hoekgreep - L-shaped corner handle * Hoekgreep: L-shaped corner handle with standoff mounts.
* Minimalist flush-mount design * Horizontal bar + vertical bar meeting at a rounded corner.
*/ */
export function Hoekgreep({ finish, doorWidth, railDepth, stileWidth }: HandleProps) { export function Hoekgreep({ finish, doorWidth, stileWidth }: HandleProps) {
const color = ({ const color = getColor(finish);
zwart: "#1a1a1a", const horizontalLength = 0.15;
brons: "#8B6F47", const verticalLength = 0.12;
grijs: "#525252", const barThickness = 0.02;
}[finish] || "#1a1a1a") as string; const barWidth = 0.03;
const horizontalLength = 0.15; // 15cm horizontal // Position near right stile
const verticalLength = 0.12; // 12cm vertical const xPos = doorWidth / 2 - stileWidth - 0.12;
const barThickness = 0.02; // 20mm thick
const barWidth = 0.03; // 30mm wide
return ( return (
<group position={[doorWidth / 2 - stileWidth - 0.12, 0, railDepth / 2 + 0.015]}> <group position={[xPos, 0, 0]}>
{/* Top mount standoff */}
<MountStandoff
position={[horizontalLength * 0.8, verticalLength / 2, MOUNT_CENTER_Z]}
color={color}
finish={finish}
/>
{/* Bottom mount standoff */}
<MountStandoff
position={[0, -verticalLength * 0.3, MOUNT_CENTER_Z]}
color={color}
finish={finish}
/>
{/* Horizontal bar */} {/* Horizontal bar */}
<RoundedBox <RoundedBox
args={[horizontalLength, barWidth, barThickness]} args={[horizontalLength, barWidth, barThickness]}
radius={0.003} radius={0.003}
smoothness={4} smoothness={4}
position={[horizontalLength / 2, verticalLength / 2, 0]} position={[horizontalLength / 2, verticalLength / 2, GRIP_CENTER_Z]}
castShadow castShadow
> >
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</RoundedBox> </RoundedBox>
{/* Vertical bar */} {/* Vertical bar */}
@@ -112,157 +264,173 @@ export function Hoekgreep({ finish, doorWidth, railDepth, stileWidth }: HandlePr
args={[barWidth, verticalLength, barThickness]} args={[barWidth, verticalLength, barThickness]}
radius={0.003} radius={0.003}
smoothness={4} smoothness={4}
position={[0, 0, 0]} position={[0, 0, GRIP_CENTER_Z]}
castShadow castShadow
> >
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</RoundedBox> </RoundedBox>
{/* Corner radius (decorative) */} {/* Corner radius */}
<mesh position={[0.015, verticalLength / 2 - 0.015, 0]} rotation={[0, 0, 0]} castShadow> <mesh
<torusGeometry args={[0.015, 0.01, 16, 32, Math.PI / 2]} /> position={[0.015, verticalLength / 2 - 0.015, GRIP_CENTER_Z]}
<HandleMaterial color={color} /> castShadow
>
<torusGeometry args={[0.015, barThickness / 2, 16, 32, Math.PI / 2]} />
<PowderCoatMaterial color={color} finish={finish} />
</mesh> </mesh>
</group> </group>
); );
} }
/** /**
* Maangreep - Crescent/moon shaped recessed handle * Maangreep: Crescent/moon shaped handle with standoff mounts.
* Elegant curved design for flush doors * Curved torus section mounted on two pootjes.
*/ */
export function Maangreep({ finish, doorWidth, railDepth, stileWidth }: HandleProps) { export function Maangreep({ finish, doorWidth, stileWidth }: HandleProps) {
const color = ({ const color = getColor(finish);
zwart: "#1a1a1a", const curveRadius = 0.08;
brons: "#8B6F47", const xPos = doorWidth / 2 - stileWidth - 0.12;
grijs: "#525252",
}[finish] || "#1a1a1a") as string;
const curveRadius = 0.08; // 8cm radius
const handleDepth = 0.025; // 25mm deep recess
return ( return (
<group position={[doorWidth / 2 - stileWidth - 0.12, 0, railDepth / 2]}> <group position={[xPos, 0, 0]}>
{/* Left mount standoff */}
<MountStandoff
position={[-curveRadius * 0.8, 0, MOUNT_CENTER_Z]}
color={color}
finish={finish}
/>
{/* Right mount standoff */}
<MountStandoff
position={[curveRadius * 0.8, 0, MOUNT_CENTER_Z]}
color={color}
finish={finish}
/>
{/* Main curved handle body */} {/* Main curved handle body */}
<mesh rotation={[Math.PI / 2, 0, 0]} castShadow> <mesh
rotation={[Math.PI / 2, 0, 0]}
position={[0, 0, GRIP_CENTER_Z]}
castShadow
>
<torusGeometry args={[curveRadius, 0.015, 16, 32, Math.PI]} /> <torusGeometry args={[curveRadius, 0.015, 16, 32, Math.PI]} />
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</mesh> </mesh>
{/* Left end cap */} {/* Left end cap */}
<mesh position={[-curveRadius, 0, 0]} castShadow> <mesh position={[-curveRadius, 0, GRIP_CENTER_Z]} castShadow>
<sphereGeometry args={[0.015, 32, 32]} /> <sphereGeometry args={[0.015, 32, 32]} />
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</mesh> </mesh>
{/* Right end cap */} {/* Right end cap */}
<mesh position={[curveRadius, 0, 0]} castShadow> <mesh position={[curveRadius, 0, GRIP_CENTER_Z]} castShadow>
<sphereGeometry args={[0.015, 32, 32]} /> <sphereGeometry args={[0.015, 32, 32]} />
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</mesh> </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> </group>
); );
} }
/** /**
* Ovaalgreep - Oval/elliptical pull handle * Ovaalgreep: Oval/elliptical pull handle with standoff mounts.
* Modern minimalist design * Extruded ellipse shape mounted on two pootjes.
*/ */
export function Ovaalgreep({ finish, doorWidth, railDepth, stileWidth }: HandleProps) { export function Ovaalgreep({ finish, doorWidth, stileWidth }: HandleProps) {
const color = ({ const color = getColor(finish);
zwart: "#1a1a1a", const xPos = doorWidth / 2 - stileWidth - 0.12;
brons: "#8B6F47",
grijs: "#525252",
}[finish] || "#1a1a1a") as string;
// Create oval shape using THREE.Shape
const shape = new THREE.Shape(); const shape = new THREE.Shape();
const rx = 0.06; // 6cm horizontal radius const rx = 0.06;
const ry = 0.03; // 3cm vertical radius const ry = 0.03;
// Draw ellipse
for (let i = 0; i <= 64; i++) { for (let i = 0; i <= 64; i++) {
const angle = (i / 64) * Math.PI * 2; const angle = (i / 64) * Math.PI * 2;
const x = Math.cos(angle) * rx; const x = Math.cos(angle) * rx;
const y = Math.sin(angle) * ry; const y = Math.sin(angle) * ry;
if (i === 0) { if (i === 0) shape.moveTo(x, y);
shape.moveTo(x, y); else shape.lineTo(x, y);
} else {
shape.lineTo(x, y);
}
} }
const extrudeSettings = {
depth: 0.02,
bevelEnabled: true,
bevelThickness: 0.003,
bevelSize: 0.003,
bevelSegments: 8,
};
return ( return (
<group position={[doorWidth / 2 - stileWidth - 0.12, 0, railDepth / 2 + 0.015]}> <group position={[xPos, 0, 0]}>
{/* Oval handle ring */} {/* Top mount standoff */}
<mesh castShadow rotation={[0, 0, 0]}> <MountStandoff
<extrudeGeometry args={[shape, extrudeSettings]} /> position={[0, ry * 0.6, MOUNT_CENTER_Z]}
<HandleMaterial color={color} /> color={color}
finish={finish}
/>
{/* Bottom mount standoff */}
<MountStandoff
position={[0, -ry * 0.6, MOUNT_CENTER_Z]}
color={color}
finish={finish}
/>
{/* Oval handle */}
<mesh position={[0, 0, GRIP_CENTER_Z - 0.01]} castShadow>
<extrudeGeometry
args={[shape, {
depth: 0.02,
bevelEnabled: true,
bevelThickness: 0.003,
bevelSize: 0.003,
bevelSegments: 8,
}]}
/>
<PowderCoatMaterial color={color} finish={finish} />
</mesh> </mesh>
</group> </group>
); );
} }
/** /**
* Klink - Traditional door handle with lever * Klink: Traditional lever handle with rosette, standoff-mounted.
* Classic hinged door handle * Rosette plate against door, lever extending horizontally.
*/ */
export function Klink({ finish, doorWidth, railDepth, stileWidth }: HandleProps) { export function Klink({ finish, doorWidth, stileWidth }: HandleProps) {
const color = ({ const color = getColor(finish);
zwart: "#1a1a1a", const leverLength = 0.12;
brons: "#8B6F47", const xPos = doorWidth / 2 - stileWidth - 0.1;
grijs: "#525252",
}[finish] || "#1a1a1a") as string;
const leverLength = 0.12; // 12cm lever
const leverThickness = 0.015; // 15mm thick
return ( return (
<group position={[doorWidth / 2 - stileWidth - 0.1, 0, railDepth / 2 + 0.01]}> <group position={[xPos, 0, 0]}>
{/* Mounting rosette (round plate) */} {/* Mounting rosette (flat against door face) */}
<mesh castShadow> <mesh position={[0, 0, DOOR_FACE_Z + 0.004]} castShadow>
<cylinderGeometry args={[0.03, 0.03, 0.008, 32]} /> <cylinderGeometry args={[0.03, 0.03, 0.008, 32]} />
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</mesh>
{/* Square spindle standoff (connects rosette to lever) */}
<mesh
position={[0, 0, MOUNT_CENTER_Z]}
rotation={[Math.PI / 2, 0, 0]}
castShadow
>
<cylinderGeometry args={[0.008, 0.008, MOUNT_LENGTH * 0.6, 8]} />
<PowderCoatMaterial color={color} finish={finish} />
</mesh> </mesh>
{/* Lever handle */} {/* Lever handle */}
<RoundedBox <RoundedBox
args={[leverLength, 0.02, leverThickness]} args={[leverLength, 0.02, 0.015]}
radius={0.005} radius={0.005}
smoothness={4} smoothness={4}
position={[leverLength / 2, 0, 0]} position={[leverLength / 2, 0, GRIP_CENTER_Z]}
rotation={[0, 0, -0.15]} rotation={[0, 0, -0.15]}
castShadow castShadow
> >
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</RoundedBox> </RoundedBox>
{/* Lever end (ergonomic grip) */} {/* Lever end grip */}
<mesh position={[leverLength, -0.015, 0]} castShadow> <mesh position={[leverLength, -0.015, GRIP_CENTER_Z]} castShadow>
<sphereGeometry args={[0.012, 32, 32]} /> <sphereGeometry args={[0.012, 32, 32]} />
<HandleMaterial color={color} /> <PowderCoatMaterial color={color} finish={finish} />
</mesh> </mesh>
{/* Lock cylinder (detail) */} {/* Lock cylinder below */}
<mesh position={[0, -0.045, 0]} castShadow> <mesh position={[0, -0.045, DOOR_FACE_Z + 0.005]} castShadow>
<cylinderGeometry args={[0.008, 0.008, 0.01, 16]} /> <cylinderGeometry args={[0.008, 0.008, 0.01, 16]} />
<meshStandardMaterial <meshStandardMaterial
color={finish === "brons" ? "#6B5434" : "#2a2a2a"} color={finish === "brons" ? "#6B5434" : "#2a2a2a"}
@@ -273,27 +441,3 @@ export function Klink({ finish, doorWidth, railDepth, stileWidth }: HandleProps)
</group> </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] || "#1a1a1a") as string;
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>
);
}