From df8a45a2b2ffe98454e0254df17f71d2da8ca2d1 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 10 Feb 2026 16:43:16 +0000 Subject: [PATCH] Integrate comprehensive dimension calculation logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created complete business logic system for door dimensions: **New: lib/calculations.ts** - calculateHoleWidth: Total wall opening needed - calculateHoleMinWidth/MaxWidth: Dynamic limits based on config - calculateSidePanelWidth: Side panel sizing logic - calculateDoorLeafWidth: Individual door panel width - validateDimensions: Real-time validation - Constants: Frame profiles (80mm), min/max widths **Enhanced: lib/store.ts** - Added doorConfig: 'enkele' | 'dubbele' (single/double door) - Added sidePanel: 'geen' | 'links' | 'rechts' | 'beide' - Calculated fields: holeWidth, doorLeafWidth, sidePanelWidth, min/maxWidth - Auto-recalculation on config changes - setWidth/setHeight with automatic clamping to valid ranges - Internal recalculate() method updates all derived values **Redesigned: step-dimensions.tsx** - Door configuration selector (enkele/dubbele) - Side panel selector (geen/links/rechts/beide) - Width slider with dynamic min/max based on configuration - Height slider (1800-3000mm) - Real-time input fields synced with sliders - Summary panel showing: - Door leaf width - Total wall opening (hole width × height) - Side panel width (when applicable) - Premium tile-based UI (Dark Green/Pistachio theme) **Added: components/ui/slider.tsx** - Shadcn Slider component for smooth value selection **Connected: door-3d.tsx** - Door now uses doorLeafWidth from store (in mm, converted to meters) - Door height from store (in mm, converted to meters) - 3D door resizes in real-time as user drags sliders - Maintains realistic proportions **User Flow:** 1. User selects enkele/dubbele configuration 2. User selects side panels (if any) 3. Store calculates valid min/max widths 4. User drags width slider (clamped to valid range) 5. Store calculates door leaf width and side panel widths 6. 3D door updates instantly to show new dimensions 7. Summary shows all calculated dimensions **Result:** Professional dimension configurator with real business logic, automatic validation, and real-time 3D preview Co-Authored-By: Claude Sonnet 4.5 --- components/configurator/door-3d.tsx | 9 +- components/offerte/step-dimensions.tsx | 228 ++++++++++++++++++++----- components/ui/slider.tsx | 63 +++++++ lib/calculations.ts | 183 ++++++++++++++++++++ lib/store.ts | 97 ++++++++++- 5 files changed, 529 insertions(+), 51 deletions(-) create mode 100644 components/ui/slider.tsx create mode 100644 lib/calculations.ts 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));