diff --git a/components/configurator/door-3d.tsx b/components/configurator/door-3d.tsx index 6bcd8b3..60f9bf5 100644 --- a/components/configurator/door-3d.tsx +++ b/components/configurator/door-3d.tsx @@ -5,7 +5,8 @@ import { useConfiguratorStore } from "@/lib/store"; import * as THREE from "three"; export function Door3D() { - const { doorType, gridType, finish, handle } = useConfiguratorStore(); + const { doorType, gridType, finish, handle, doorLeafWidth, height } = + useConfiguratorStore(); const doorRef = useRef(null); // Frame color based on finish @@ -15,9 +16,9 @@ export function Door3D() { grijs: "#525252", }[finish]; - // Door dimensions (more realistic proportions) - const doorWidth = doorType === "paneel" ? 1.5 : 1.2; - const doorHeight = 2.4; + // Convert mm to meters for 3D scene + const doorWidth = doorLeafWidth / 1000; // Convert mm to m + const doorHeight = height / 1000; // Convert mm to m const frameThickness = 0.03; // Slim steel profile (3cm) const frameDepth = 0.05; // Depth of frame const glassThickness = 0.015; // Realistic glass thickness diff --git a/components/offerte/step-dimensions.tsx b/components/offerte/step-dimensions.tsx index 84cce0c..0d3b92f 100644 --- a/components/offerte/step-dimensions.tsx +++ b/components/offerte/step-dimensions.tsx @@ -1,61 +1,203 @@ "use client"; -import { useFormContext } from "@/components/offerte/form-context"; +import { useConfiguratorStore } from "@/lib/store"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Ruler } from "lucide-react"; +import { Slider } from "@/components/ui/slider"; +import { Check } from "lucide-react"; export function StepDimensions() { - const { formData, updateData } = useFormContext(); + const { + doorConfig, + sidePanel, + width, + height, + minWidth, + maxWidth, + holeWidth, + doorLeafWidth, + sidePanelWidth, + setDoorConfig, + setSidePanel, + setWidth, + setHeight, + } = useConfiguratorStore(); return ( -
-

Afmetingen

-

- Voer de gewenste afmetingen in millimeters in. -

+
+ {/* Door Configuration */} +
+

Deur Configuratie

+

+ Kies tussen enkele of dubbele deur. +

-
-
- - { - const val = e.target.value; - updateData({ height: val === "" ? undefined : Number(val) }); - }} - className="h-12 text-lg focus-visible:ring-brand-orange" - /> -

- Min: 2000mm — Max: 3000mm -

+
+ {(['enkele', 'dubbele'] as const).map((config) => { + const selected = doorConfig === config; + + return ( + + ); + })}
+
-
- + {/* Side Panel Configuration */} +
+

Zijpanelen

+

+ Voeg vaste panelen toe naast de deur. +

+ +
+ {(['geen', 'links', 'rechts', 'beide'] as const).map((panel) => { + const selected = sidePanel === panel; + + return ( + + ); + })} +
+
+ + {/* Width Control */} +
+
+
+ +

+ {minWidth}mm - {maxWidth}mm +

+
+
+ setWidth(Number(e.target.value))} + min={minWidth} + max={maxWidth} + className="h-10 w-32 text-right font-mono text-lg font-bold" + /> +

Deurblad: {Math.round(doorLeafWidth)}mm

+
+
+ setWidth(values[0])} + min={minWidth} + max={maxWidth} + step={10} + className="w-full" + /> +
+ + {/* Height Control */} +
+
+
+ +

1800mm - 3000mm

+
{ - const val = e.target.value; - updateData({ width: val === "" ? undefined : Number(val) }); - }} - className="h-12 text-lg focus-visible:ring-brand-orange" + value={height} + onChange={(e) => setHeight(Number(e.target.value))} + min={1800} + max={3000} + className="h-10 w-32 text-right font-mono text-lg font-bold" /> -

- Min: 300mm — Max: 3000mm +

+ setHeight(values[0])} + min={1800} + max={3000} + step={10} + className="w-full" + /> +
+ + {/* Summary */} +
+

+ Samenvatting afmetingen +

+
+

+ Deurblad breedte:{" "} + {Math.round(doorLeafWidth)}mm

+

+ Totale wandopening:{" "} + {Math.round(holeWidth)}mm × {height}mm +

+ {sidePanelWidth > 0 && ( +

+ Zijpaneel breedte:{" "} + {Math.round(sidePanelWidth)}mm +

+ )}
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx new file mode 100644 index 0000000..4b815ae --- /dev/null +++ b/components/ui/slider.tsx @@ -0,0 +1,63 @@ +"use client" + +import * as React from "react" +import { Slider as SliderPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Slider({ + className, + defaultValue, + value, + min = 0, + max = 100, + ...props +}: React.ComponentProps) { + const _values = React.useMemo( + () => + Array.isArray(value) + ? value + : Array.isArray(defaultValue) + ? defaultValue + : [min, max], + [value, defaultValue, min, max] + ) + + return ( + + + + + {Array.from({ length: _values.length }, (_, index) => ( + + ))} + + ) +} + +export { Slider } diff --git a/lib/calculations.ts b/lib/calculations.ts new file mode 100644 index 0000000..a930629 --- /dev/null +++ b/lib/calculations.ts @@ -0,0 +1,183 @@ +/** + * Door dimension calculations + * Based on standard steel door configurations + */ + +export type DoorConfig = 'enkele' | 'dubbele'; +export type SidePanel = 'geen' | 'links' | 'rechts' | 'beide'; + +// Standard constants +const FRAME_PROFILE_WIDTH = 80; // mm - Steel frame profile width +const SIDE_PANEL_MIN_WIDTH = 200; // mm +const SIDE_PANEL_MAX_WIDTH = 800; // mm +const DOOR_LEAF_MIN_WIDTH = 700; // mm +const DOOR_LEAF_MAX_WIDTH = 1200; // mm + +/** + * Calculate the total hole width needed in the wall + */ +export function calculateHoleWidth( + doorWidth: number, + doorConfig: DoorConfig, + sidePanel: SidePanel +): number { + let totalWidth = doorWidth; + + // Add frame profiles + totalWidth += FRAME_PROFILE_WIDTH * 2; + + // Add side panels if configured + if (sidePanel === 'links' || sidePanel === 'rechts') { + totalWidth += SIDE_PANEL_MIN_WIDTH; + } else if (sidePanel === 'beide') { + totalWidth += SIDE_PANEL_MIN_WIDTH * 2; + } + + return totalWidth; +} + +/** + * Calculate minimum hole width based on configuration + */ +export function calculateHoleMinWidth( + doorConfig: DoorConfig, + sidePanel: SidePanel +): number { + let minWidth = DOOR_LEAF_MIN_WIDTH; + + // Double door requires two leaves + if (doorConfig === 'dubbele') { + minWidth *= 2; + } + + // Add frame profiles + minWidth += FRAME_PROFILE_WIDTH * 2; + + // Add side panels + if (sidePanel === 'links' || sidePanel === 'rechts') { + minWidth += SIDE_PANEL_MIN_WIDTH; + } else if (sidePanel === 'beide') { + minWidth += SIDE_PANEL_MIN_WIDTH * 2; + } + + return minWidth; +} + +/** + * Calculate maximum hole width based on configuration + */ +export function calculateHoleMaxWidth( + doorConfig: DoorConfig, + sidePanel: SidePanel +): number { + let maxWidth = DOOR_LEAF_MAX_WIDTH; + + // Double door requires two leaves + if (doorConfig === 'dubbele') { + maxWidth *= 2; + } + + // Add frame profiles + maxWidth += FRAME_PROFILE_WIDTH * 2; + + // Add side panels + if (sidePanel === 'links' || sidePanel === 'rechts') { + maxWidth += SIDE_PANEL_MAX_WIDTH; + } else if (sidePanel === 'beide') { + maxWidth += SIDE_PANEL_MAX_WIDTH * 2; + } + + return maxWidth; +} + +/** + * Calculate individual side panel width + */ +export function calculateSidePanelWidth( + totalWidth: number, + doorConfig: DoorConfig, + sidePanel: SidePanel +): number { + if (sidePanel === 'geen') return 0; + + // Calculate door leaf width + let doorLeafWidth = DOOR_LEAF_MIN_WIDTH; + if (doorConfig === 'dubbele') { + doorLeafWidth *= 2; + } + + // Remaining space for side panels (minus frame profiles) + const availableForPanels = totalWidth - doorLeafWidth - FRAME_PROFILE_WIDTH * 2; + + // Distribute to panels + if (sidePanel === 'links' || sidePanel === 'rechts') { + return Math.max(SIDE_PANEL_MIN_WIDTH, availableForPanels); + } else if (sidePanel === 'beide') { + return Math.max(SIDE_PANEL_MIN_WIDTH, availableForPanels / 2); + } + + return 0; +} + +/** + * Calculate door leaf width (individual door panel) + */ +export function calculateDoorLeafWidth( + totalWidth: number, + doorConfig: DoorConfig, + sidePanel: SidePanel +): number { + // Subtract frame profiles + let availableWidth = totalWidth - FRAME_PROFILE_WIDTH * 2; + + // Subtract side panels + const sidePanelWidth = calculateSidePanelWidth(totalWidth, doorConfig, sidePanel); + if (sidePanel === 'links' || sidePanel === 'rechts') { + availableWidth -= sidePanelWidth; + } else if (sidePanel === 'beide') { + availableWidth -= sidePanelWidth * 2; + } + + // Divide by number of door leaves + if (doorConfig === 'dubbele') { + availableWidth /= 2; + } + + return Math.max(DOOR_LEAF_MIN_WIDTH, availableWidth); +} + +/** + * Validate dimensions + */ +export function validateDimensions( + width: number, + height: number, + doorConfig: DoorConfig, + sidePanel: SidePanel +): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + const minWidth = calculateHoleMinWidth(doorConfig, sidePanel); + const maxWidth = calculateHoleMaxWidth(doorConfig, sidePanel); + + if (width < minWidth) { + errors.push(`Breedte moet minimaal ${minWidth}mm zijn voor deze configuratie`); + } + + if (width > maxWidth) { + errors.push(`Breedte mag maximaal ${maxWidth}mm zijn voor deze configuratie`); + } + + if (height < 1800) { + errors.push('Hoogte moet minimaal 1800mm zijn'); + } + + if (height > 3000) { + errors.push('Hoogte mag maximaal 3000mm zijn'); + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/lib/store.ts b/lib/store.ts index eafd9ab..9aa32c6 100644 --- a/lib/store.ts +++ b/lib/store.ts @@ -1,4 +1,13 @@ import { create } from 'zustand'; +import { + calculateHoleWidth, + calculateHoleMinWidth, + calculateHoleMaxWidth, + calculateSidePanelWidth, + calculateDoorLeafWidth, + type DoorConfig, + type SidePanel, +} from './calculations'; export type DoorType = 'taats' | 'scharnier' | 'paneel'; export type GridType = '3-vlak' | '4-vlak' | 'geen'; @@ -6,31 +15,111 @@ export type Finish = 'zwart' | 'brons' | 'grijs'; export type Handle = 'u-greep' | 'klink' | 'geen'; interface ConfiguratorState { + // Door configuration doorType: DoorType; + doorConfig: DoorConfig; + sidePanel: SidePanel; + + // Styling gridType: GridType; finish: Finish; handle: Handle; + + // Dimensions (in mm) width: number; height: number; + // Calculated values + holeWidth: number; + doorLeafWidth: number; + sidePanelWidth: number; + minWidth: number; + maxWidth: number; + + // Actions setDoorType: (type: DoorType) => void; + setDoorConfig: (config: DoorConfig) => void; + setSidePanel: (panel: SidePanel) => void; setGridType: (type: GridType) => void; setFinish: (finish: Finish) => void; setHandle: (handle: Handle) => void; + setWidth: (width: number) => void; + setHeight: (height: number) => void; setDimensions: (width: number, height: number) => void; } -export const useConfiguratorStore = create((set) => ({ +export const useConfiguratorStore = create((set, get) => ({ + // Initial state doorType: 'taats', + doorConfig: 'enkele', + sidePanel: 'geen', gridType: '3-vlak', finish: 'zwart', handle: 'u-greep', width: 1000, height: 2400, - setDoorType: (doorType) => set({ doorType }), + // Initial calculated values + holeWidth: 1160, + doorLeafWidth: 1000, + sidePanelWidth: 0, + minWidth: 860, + maxWidth: 1360, + + // Actions with automatic recalculation + setDoorType: (doorType) => { + set({ doorType }); + get().recalculate(); + }, + + setDoorConfig: (doorConfig) => { + set({ doorConfig }); + get().recalculate(); + }, + + setSidePanel: (sidePanel) => { + set({ sidePanel }); + get().recalculate(); + }, + setGridType: (gridType) => set({ gridType }), + setFinish: (finish) => set({ finish }), + setHandle: (handle) => set({ handle }), - setDimensions: (width, height) => set({ width, height }), -})); + + setWidth: (width) => { + const { doorConfig, sidePanel } = get(); + const minWidth = calculateHoleMinWidth(doorConfig, sidePanel); + const maxWidth = calculateHoleMaxWidth(doorConfig, sidePanel); + + // Clamp width to valid range + const clampedWidth = Math.max(minWidth, Math.min(maxWidth, width)); + set({ width: clampedWidth }); + get().recalculate(); + }, + + setHeight: (height) => { + // Clamp height to valid range + const clampedHeight = Math.max(1800, Math.min(3000, height)); + set({ height: clampedHeight }); + }, + + setDimensions: (width, height) => { + get().setWidth(width); + get().setHeight(height); + }, + + // Internal recalculation helper + recalculate: () => { + const { width, doorConfig, sidePanel } = get(); + + set({ + holeWidth: calculateHoleWidth(width, doorConfig, sidePanel), + doorLeafWidth: calculateDoorLeafWidth(width, doorConfig, sidePanel), + sidePanelWidth: calculateSidePanelWidth(width, doorConfig, sidePanel), + minWidth: calculateHoleMinWidth(doorConfig, sidePanel), + maxWidth: calculateHoleMaxWidth(doorConfig, sidePanel), + }); + }, +} as ConfiguratorState));