Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Parametric Kitchen Cabinet Generator</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script> | |
<style> | |
.slider-container { | |
transition: all 0.3s ease; | |
} | |
.slider-container:hover { | |
transform: translateY(-2px); | |
} | |
.preview-container { | |
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); | |
transition: all 0.3s ease; | |
} | |
.cabinet-part { | |
display: none; | |
position: absolute; | |
background-color: rgba(255, 255, 255, 0.9); | |
padding: 15px; | |
border-radius: 8px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
z-index: 10; | |
} | |
.progress-ring { | |
stroke-dasharray: 314; /* 2 * π * r where r is 50 */ | |
stroke-dashoffset: 0; | |
transition: stroke-dashoffset 0.5s ease; | |
} | |
#modelViewer { | |
background-color: #f5f3f0; | |
border-radius: 8px; | |
overflow: hidden; | |
position: relative; | |
} | |
#priceSection { | |
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
} | |
.door-handle { | |
position: relative; | |
z-index: 5; | |
} | |
.shelf { | |
box-shadow: 0 -2px 5px rgba(0,0,0,0.1); | |
} | |
#loadingOverlay { | |
transition: opacity 0.3s ease; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-10 text-center"> | |
<h1 class="text-4xl font-bold text-gray-800 mb-2">Parametric Kitchen Cabinet Generator</h1> | |
<p class="text-gray-600 max-w-2xl mx-auto">Design your perfect kitchen cabinet with real-time 3D preview and instant pricing.</p> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
<!-- Controls Section --> | |
<div class="lg:col-span-1 bg-white rounded-xl shadow-lg p-6"> | |
<div class="mb-8"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-4">Cabinet Configuration</h2> | |
<!-- Cabinet Type --> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Cabinet Type</label> | |
<span id="cabinetTypeDisplay" class="text-gray-500">Wall</span> | |
</div> | |
<select id="cabinetType" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"> | |
<option value="wall">Wall Cabinet</option> | |
<option value="base">Base Cabinet</option> | |
<option value="tall">Tall Cabinet</option> | |
<option value="island">Island Cabinet</option> | |
</select> | |
</div> | |
<!-- Dimensions --> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Width (inches)</label> | |
<span id="widthValue" class="text-gray-500">24</span> | |
</div> | |
<input id="widthSlider" type="range" min="12" max="48" value="24" step="3" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Height (inches)</label> | |
<span id="heightValue" class="text-gray-500">36</span> | |
</div> | |
<input id="heightSlider" type="range" min="12" max="96" value="36" step="3" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Depth (inches)</label> | |
<span id="depthValue" class="text-gray-500">12</span> | |
</div> | |
<input id="depthSlider" type="range" min="12" max="24" value="12" step="3" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<!-- Doors --> | |
<div class="mb-6"> | |
<label class="block text-gray-700 font-medium mb-2">Number of Doors</label> | |
<div class="flex space-x-4"> | |
<button id="oneDoor" class="door-btn px-4 py-2 border rounded-lg flex-1 bg-indigo-100 text-indigo-700 border-indigo-300">1 Door</button> | |
<button id="twoDoors" class="door-btn px-4 py-2 border rounded-lg flex-1 hover:bg-gray-100">2 Doors</button> | |
</div> | |
</div> | |
<!-- Shelves --> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Number of Shelves</label> | |
<span id="shelvesValue" class="text-gray-500">1</span> | |
</div> | |
<input id="shelvesSlider" type="range" min="0" max="5" value="1" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<!-- Drawers (only for base cabinets) --> | |
<div class="mb-6 slider-container" id="drawersContainer" style="display: none;"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Number of Drawers</label> | |
<span id="drawersValue" class="text-gray-500">0</span> | |
</div> | |
<input id="drawersSlider" type="range" min="0" max="3" value="0" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
</div> | |
<!-- Material --> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Material</label> | |
<span id="materialDisplay" class="text-gray-500">Oak</span> | |
</div> | |
<select id="material" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"> | |
<option value="oak">Oak</option> | |
<option value="maple">Maple</option> | |
<option value="cherry">Cherry</option> | |
<option value="walnut">Walnut</option> | |
<option value="laminate">Laminate</option> | |
<option value="painted">Painted MDF</option> | |
</select> | |
</div> | |
<!-- Color --> | |
<div class="mb-6 slider-container"> | |
<div class="flex justify-between items-center mb-2"> | |
<label class="text-gray-700 font-medium">Color</label> | |
<span id="colorDisplay" class="text-gray-500">Natural</span> | |
</div> | |
<select id="color" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"> | |
<option value="natural">Natural</option> | |
<option value="white">White</option> | |
<option value="black">Black</option> | |
<option value="gray">Gray</option> | |
<option value="navy">Navy Blue</option> | |
<option value="green">Forest Green</option> | |
</select> | |
</div> | |
<!-- Features --> | |
<div class="mb-6"> | |
<label class="block text-gray-700 font-medium mb-2">Features</label> | |
<div class="space-y-2"> | |
<label class="flex items-center space-x-3"> | |
<input type="checkbox" id="softClose" class="form-checkbox h-5 w-5 text-indigo-600 rounded"> | |
<span class="text-gray-700">Soft-Close Hinges</span> | |
</label> | |
<label class="flex items-center space-x-3"> | |
<input type="checkbox" id="pullOutShelves" class="form-checkbox h-5 w-5 text-indigo-600 rounded"> | |
<span class="text-gray-700">Pull-Out Shelves</span> | |
</label> | |
<label class="flex items-center space-x-3"> | |
<input type="checkbox" id="lighting" class="form-checkbox h-5 w-5 text-indigo-600 rounded"> | |
<span class="text-gray-700">LED Lighting</span> | |
</label> | |
<label class="flex items-center space-x-3"> | |
<input type="checkbox" id="glassDoors" class="form-checkbox h-5 w-5 text-indigo-600 rounded"> | |
<span class="text-gray-700">Glass Doors</span> | |
</label> | |
</div> | |
</div> | |
</div> | |
<div id="priceSection" class="p-5 rounded-xl"> | |
<h3 class="text-lg font-semibold text-gray-800 mb-3">Estimated Price</h3> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="text-gray-600">Base Price</span> | |
<span id="basePrice" class="font-medium">$300.00</span> | |
</div> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="text-gray-600">Material Upgrade</span> | |
<span id="materialPrice" class="font-medium">$50.00</span> | |
</div> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="text-gray-600">Features</span> | |
<span id="featuresPrice" class="font-medium">$75.00</span> | |
</div> | |
<div class="flex justify-between items-center mb-4"> | |
<span class="text-gray-600">Options</span> | |
<span id="optionsPrice" class="font-medium">$25.00</span> | |
</div> | |
<div class="border-t border-gray-300 pt-4"> | |
<div class="flex justify-between items-center"> | |
<span class="text-xl font-bold text-gray-800">Total</span> | |
<span id="totalPrice" class="text-2xl font-bold text-indigo-600">$450.00</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Preview Section --> | |
<div class="lg:col-span-2 relative"> | |
<div class="bg-white rounded-xl shadow-lg overflow-hidden"> | |
<div class="p-4 border-b border-gray-200"> | |
<div class="flex justify-between items-center"> | |
<h2 class="text-xl font-semibold text-gray-800">3D Model Preview</h2> | |
<div class="flex space-x-2"> | |
<button id="downloadUSDZ" class="px-3 py-1 bg-indigo-100 text-indigo-700 rounded-lg text-sm flex items-center hover:bg-indigo-200"> | |
<i class="fas fa-download mr-2"></i> USDZ | |
</button> | |
<button id="downloadGLB" class="px-3 py-1 bg-indigo-600 text-white rounded-lg text-sm flex items-center hover:bg-indigo-700"> | |
<i class="fas fa-download mr-2"></i> GLB | |
</button> | |
</div> | |
</div> | |
</div> | |
<div id="modelViewer" class="w-full h-96 rounded-b-xl"> | |
<div id="loadingOverlay" class="absolute inset-0 bg-gray-100 bg-opacity-70 flex flex-col items-center justify-center opacity-0 pointer-events-none"> | |
<div class="w-16 h-16 relative mb-4"> | |
<svg class="w-full h-full" viewBox="0 0 100 100"> | |
<circle class="text-gray-400" cx="50" cy="50" r="40" stroke-width="8" stroke="currentColor" fill="none"></circle> | |
<circle class="progress-ring text-indigo-600" cx="50" cy="50" r="40" stroke-width="8" stroke="currentColor" fill="none" stroke-dashoffset="314"></circle> | |
</svg> | |
</div> | |
<p class="text-gray-600">Generating 3D model...</p> | |
</div> | |
</div> | |
</div> | |
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6"> | |
<div class="bg-white p-6 rounded-xl shadow-lg"> | |
<h3 class="text-lg font-semibold text-gray-800 mb-4">Cabinet Specifications</h3> | |
<div class="space-y-3"> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Exterior Dimensions:</span> | |
<span id="dimensionsDisplay" class="font-medium">24" W × 36" H × 12" D</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Interior Capacity:</span> | |
<span id="capacityDisplay" class="font-medium">6.0 cu.ft</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Material Thickness:</span> | |
<span class="font-medium">0.75"</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Shelves:</span> | |
<span id="shelvesDisplay" class="font-medium">1</span> | |
</div> | |
<div class="flex justify-between" id="drawersDisplayContainer" style="display: none;"> | |
<span class="text-gray-600">Drawers:</span> | |
<span id="drawersDisplay" class="font-medium">0</span> | |
</div> | |
</div> | |
</div> | |
<div class="bg-white p-6 rounded-xl shadow-lg"> | |
<h3 class="text-lg font-semibold text-gray-800 mb-4">Order Information</h3> | |
<div class="space-y-3"> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Production Time:</span> | |
<span class="font-medium">2-3 weeks</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Shipping:</span> | |
<span class="font-medium">Flat rate $49.00</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-600">Warranty:</span> | |
<span class="font-medium">10 years</span> | |
</div> | |
</div> | |
<button class="w-full mt-6 py-3 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors"> | |
Add to Cart | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// DOM Elements | |
const widthSlider = document.getElementById('widthSlider'); | |
const heightSlider = document.getElementById('heightSlider'); | |
const depthSlider = document.getElementById('depthSlider'); | |
const widthValue = document.getElementById('widthValue'); | |
const heightValue = document.getElementById('heightValue'); | |
const depthValue = document.getElementById('depthValue'); | |
const oneDoorBtn = document.getElementById('oneDoor'); | |
const twoDoorsBtn = document.getElementById('twoDoors'); | |
const cabinetType = document.getElementById('cabinetType'); | |
const cabinetTypeDisplay = document.getElementById('cabinetTypeDisplay'); | |
const material = document.getElementById('material'); | |
const materialDisplay = document.getElementById('materialDisplay'); | |
const color = document.getElementById('color'); | |
const colorDisplay = document.getElementById('colorDisplay'); | |
const softClose = document.getElementById('softClose'); | |
const pullOutShelves = document.getElementById('pullOutShelves'); | |
const lighting = document.getElementById('lighting'); | |
const glassDoors = document.getElementById('glassDoors'); | |
const basePrice = document.getElementById('basePrice'); | |
const materialPrice = document.getElementById('materialPrice'); | |
const featuresPrice = document.getElementById('featuresPrice'); | |
const optionsPrice = document.getElementById('optionsPrice'); | |
const totalPrice = document.getElementById('totalPrice'); | |
const dimensionsDisplay = document.getElementById('dimensionsDisplay'); | |
const capacityDisplay = document.getElementById('capacityDisplay'); | |
const modelViewer = document.getElementById('modelViewer'); | |
const loadingOverlay = document.getElementById('loadingOverlay'); | |
const downloadUSDZ = document.getElementById('downloadUSDZ'); | |
const downloadGLB = document.getElementById('downloadGLB'); | |
const progressRing = document.querySelector('.progress-ring'); | |
const shelvesSlider = document.getElementById('shelvesSlider'); | |
const shelvesValue = document.getElementById('shelvesValue'); | |
const shelvesDisplay = document.getElementById('shelvesDisplay'); | |
const drawersSlider = document.getElementById('drawersSlider'); | |
const drawersValue = document.getElementById('drawersValue'); | |
const drawersContainer = document.getElementById('drawersContainer'); | |
const drawersDisplay = document.getElementById('drawersDisplay'); | |
const drawersDisplayContainer = document.getElementById('drawersDisplayContainer'); | |
// Three.js variables | |
let scene, camera, renderer, controls, cabinetGroup; | |
// Initialize the 3D viewer | |
function initThreeJS() { | |
// Create scene | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0xf5f3f0); | |
// Create camera | |
camera = new THREE.PerspectiveCamera(60, modelViewer.clientWidth / modelViewer.clientHeight, 0.1, 1000); | |
camera.position.set(30, 30, 30); | |
// Create renderer | |
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
renderer.setSize(modelViewer.clientWidth, modelViewer.clientHeight); | |
renderer.shadowMap.enabled = true; | |
modelViewer.appendChild(renderer.domElement); | |
// Add lights | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(20, 30, 20); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
scene.add(directionalLight); | |
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4); | |
directionalLight2.position.set(-20, -30, -20); | |
scene.add(directionalLight2); | |
// Add controls | |
controls = new THREE.OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
// Create initial cabinet | |
cabinetGroup = new THREE.Group(); | |
scene.add(cabinetGroup); | |
updateCabinetModel(); | |
// Add a grid helper and axes helper for debugging (can be removed in production) | |
const gridHelper = new THREE.GridHelper(100, 100); | |
scene.add(gridHelper); | |
const axesHelper = new THREE.AxesHelper(5); | |
axesHelper.position.y = 0.1; // Slightly above ground | |
scene.add(axesHelper); | |
// Handle window resize | |
window.addEventListener('resize', onWindowResize); | |
// Start animation loop | |
animate(); | |
} | |
function onWindowResize() { | |
camera.aspect = modelViewer.clientWidth / modelViewer.clientHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(modelViewer.clientWidth, modelViewer.clientHeight); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
if (controls) controls.update(); | |
if (renderer) renderer.render(scene, camera); | |
} | |
// Create cabinet geometry with detailed components | |
function updateCabinetModel() { | |
showLoading(); | |
// Clear previous cabinet | |
while (cabinetGroup.children.length > 0) { | |
cabinetGroup.remove(cabinetGroup.children[0]); | |
} | |
// Get current values | |
const width = parseFloat(widthSlider.value); | |
const height = parseFloat(heightSlider.value); | |
const depth = parseFloat(depthSlider.value); | |
const type = cabinetType.value; | |
const isTwoDoors = twoDoorsBtn.classList.contains('bg-indigo-100'); | |
const colorValue = color.value; | |
const shelvesCount = parseInt(shelvesSlider.value); | |
const drawersCount = cabinetType.value === 'base' ? parseInt(drawersSlider.value) : 0; | |
const hasGlassDoors = glassDoors.checked; | |
const thickness = 0.75; // Material thickness in inches | |
// Set main material | |
let mainMaterial = createMaterial(colorValue, false); | |
let interiorMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xF5F5F5, | |
roughness: 0.7, | |
metalness: 0.0 | |
}); | |
let shelfMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xEAEAEA, | |
roughness: 0.6, | |
metalness: 0.0 | |
}); | |
// These factors help with converting inches to Three.js units | |
// Since Three.js uses arbitrary units, we'll treat 1 unit = 1 inch | |
const doorThickness = 0.75; | |
const handleSize = 2.0; // Door handle size | |
const shelfThickness = 0.5; | |
// Create the cabinet box structure first | |
let cabinetBox = new THREE.Group(); | |
cabinetGroup.add(cabinetBox); | |
// Bottom panel | |
createPanel(width, thickness, depth, | |
[0, thickness/2, 0], | |
mainMaterial, cabinetBox); | |
// Top panel | |
createPanel(width, thickness, depth, | |
[0, height - thickness/2, 0], | |
mainMaterial, cabinetBox); | |
// Left side | |
createPanel(thickness, height - 2*thickness, depth, | |
[-width/2 + thickness/2, height/2, 0], | |
mainMaterial, cabinetBox); | |
// Right side | |
createPanel(thickness, height - 2*thickness, depth, | |
[width/2 - thickness/2, height/2, 0], | |
mainMaterial, cabinetBox); | |
// Back panel | |
createPanel(width - 2*thickness, height - 2*thickness, thickness, | |
[0, height/2, -depth/2 + thickness/2], | |
mainMaterial, cabinetBox); | |
// Calculate interior height (accounting for drawers if they exist) | |
const interiorHeight = height - 2*thickness - (drawersCount > 0 ? (height * 0.33) : 0); | |
// Middle divider if two doors (only extends down to drawer section if drawers exist) | |
if (isTwoDoors) { | |
const dividerHeight = drawersCount > 0 ? height - 2*thickness - (height * 0.33) : height - 2*thickness; | |
createPanel(thickness, dividerHeight, depth, | |
[0, (drawersCount > 0 ? (height/2 - (height * 0.33)/2) : height/2), 0], | |
mainMaterial, cabinetBox); | |
} | |
// Create doors | |
const doorHeight = drawersCount > 0 ? height - thickness - (height * 0.33) : height - 2*thickness; | |
const doorYPosition = drawersCount > 0 ? height/2 - (height * 0.33)/2 : height/2; | |
if (isTwoDoors) { | |
const doorWidth = (width - thickness) / 2; | |
// Left door | |
createDoor(doorWidth, doorHeight, doorThickness, | |
[-(width - thickness)/2 + doorThickness/2, doorYPosition, depth/2 - doorThickness/2], | |
mainMaterial, hasGlassDoors, true, cabinetBox); | |
// Right door | |
createDoor(doorWidth, doorHeight, doorThickness, | |
[(width - thickness)/2 - doorThickness/2, doorYPosition, depth/2 - doorThickness/2], | |
mainMaterial, hasGlassDoors, false, cabinetBox); | |
} else { | |
// Single door | |
const doorWidth = width; | |
createDoor(doorWidth, doorHeight, doorThickness, | |
[0, doorYPosition, depth/2 - doorThickness/2], | |
mainMaterial, hasGlassDoors, false, cabinetBox); | |
} | |
// Add shelves only in the upper section (above any drawers) | |
const availableHeight = interiorHeight; | |
const shelfSpacing = availableHeight / (shelvesCount + 1); | |
for (let i = 0; i < shelvesCount; i++) { | |
const shelfY = thickness + (i + 1) * shelfSpacing; | |
if (drawersCount > 0) { | |
// Adjust shelf position to be above the drawer section | |
const drawerSectionHeight = height * 0.33; | |
shelfY = thickness + (i + 1) * shelfSpacing + (height - thickness - drawerSectionHeight - availableHeight); | |
} | |
createShelf(width - 2*thickness - 0.5, shelfThickness, depth - thickness - 0.5, | |
[0, shelfY, 0], | |
shelfMaterial, cabinetBox); | |
} | |
// Add drawers for base cabinets | |
if (type === 'base' && drawersCount > 0) { | |
const drawerHeight = (height * 0.33) / drawersCount; | |
const drawerFrontMaterial = createMaterial(colorValue, true); // Slightly different for drawer fronts | |
for (let i = 0; i < drawersCount; i++) { | |
const drawerY = thickness + i * drawerHeight + drawerHeight/2; | |
createDrawer(width, drawerHeight, depth * 0.8, | |
[0, drawerY, depth/2 - depth*0.1], | |
drawerFrontMaterial, mainMaterial, cabinetBox); | |
} | |
} | |
// Adjust camera target based on cabinet size | |
controls.target.set(0, height/2, 0); | |
controls.update(); | |
camera.position.set(width * 1.2, height * 1.2, depth * 1.2); | |
// Update capacity calculation | |
const interiorWidth = width - 2*thickness; | |
const interiorDepth = depth - thickness; | |
const capacity = (interiorWidth * interiorHeight * interiorDepth) / 1728; // convert to cubic feet | |
// Hide loading after a delay | |
setTimeout(hideLoading, 800); | |
// Update displays | |
capacityDisplay.textContent = `${capacity.toFixed(1)} cu.ft`; | |
dimensionsDisplay.textContent = `${width}" W × ${height}" H × ${depth}" D`; | |
shelvesDisplay.textContent = shelvesCount; | |
if (drawersCount > 0) { | |
drawersDisplay.textContent = drawersCount; | |
} | |
} | |
function createMaterial(colorValue, isDrawerFront = false) { | |
let materialOptions = {}; | |
if (colorValue === 'natural') { | |
materialOptions = { | |
color: 0xC19A6B, | |
roughness: isDrawerFront ? 0.3 : 0.4, | |
metalness: 0.1 | |
}; | |
} else if (colorValue === 'white') { | |
materialOptions = { | |
color: 0xF5F5F5, | |
roughness: 0.1, | |
metalness: 0.05 | |
}; | |
} else if (colorValue === 'black') { | |
materialOptions = { | |
color: 0x222222, | |
roughness: 0.3, | |
metalness: 0.2 | |
}; | |
} else if (colorValue === 'gray') { | |
materialOptions = { | |
color: 0x808080, | |
roughness: 0.3, | |
metalness: 0.15 | |
}; | |
} else if (colorValue === 'navy') { | |
materialOptions = { | |
color: 0x2C3E50, | |
roughness: 0.3, | |
metalness: 0.1 | |
}; | |
} else { // green | |
materialOptions = { | |
color: 0x3B5E2E, | |
roughness: 0.3, | |
metalness: 0.1 | |
}; | |
} | |
return new THREE.MeshStandardMaterial(materialOptions); | |
} | |
function createPanel(width, height, depth, position, material, parentGroup) { | |
const geometry = new THREE.BoxGeometry(width, height, depth); | |
const panel = new THREE.Mesh(geometry, material); | |
panel.castShadow = true; | |
panel.receiveShadow = true; | |
panel.position.set(position[0], position[1], position[2]); | |
parentGroup.add(panel); | |
return panel; | |
} | |
function createDoor(width, height, thickness, position, material, hasGlass, isLeftDoor, parentGroup) { | |
const doorGroup = new THREE.Group(); | |
doorGroup.position.set(position[0], position[1], position[2]); | |
// Door frame | |
const frameGeometry = new THREE.BoxGeometry(width, height, thickness); | |
const frame = new THREE.Mesh(frameGeometry, material); | |
frame.castShadow = true; | |
frame.receiveShadow = true; | |
doorGroup.add(frame); | |
// Glass panel if selected | |
if (hasGlass) { | |
const glassWidth = width * 0.9; | |
const glassHeight = height * 0.8; | |
const glassGeometry = new THREE.BoxGeometry(glassWidth, glassHeight, thickness * 0.05); | |
const glassMaterial = new THREE.MeshPhysicalMaterial({ | |
color: 0xE6F0FF, | |
transmission: 0.9, | |
roughness: 0.0, | |
metalness: 0.0, | |
transparent: true, | |
opacity: 0.8, | |
ior: 1.5, | |
thickness: 0.1 | |
}); | |
const glass = new THREE.Mesh(glassGeometry, glassMaterial); | |
glass.position.z = thickness * 0.3; | |
doorGroup.add(glass); | |
} | |
// Handle | |
const handleGeometry = new THREE.CylinderGeometry(0.5, 0.5, 3, 16); | |
const handleMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); | |
const handle = new THREE.Mesh(handleGeometry, handleMaterial); | |
handle.rotation.z = Math.PI/2; | |
handle.position.set(isLeftDoor ? -width/2 + 2 : width/2 - 2, 0, thickness); | |
doorGroup.add(handle); | |
// Hinges (simple representation) | |
const hingeGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.5, 16); | |
const hingeMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 }); | |
// Top hinge | |
const topHinge = new THREE.Mesh(hingeGeometry, hingeMaterial); | |
topHinge.rotation.x = Math.PI/2; | |
topHinge.position.set(isLeftDoor ? width/2 - 1 : -width/2 + 1, height/2 - 2, -thickness/2); | |
doorGroup.add(topHinge); | |
// Bottom hinge | |
const bottomHinge = new THREE.Mesh(hingeGeometry, hingeMaterial); | |
bottomHinge.rotation.x = Math.PI/2; | |
bottomHinge.position.set(isLeftDoor ? width/2 - 1 : -width/2 + 1, -height/2 + 2, -thickness/2); | |
doorGroup.add(bottomHinge); | |
parentGroup.add(doorGroup); | |
return doorGroup; | |
} | |
function createShelf(width, thickness, depth, position, material, parentGroup) { | |
const shelfGroup = new THREE.Group(); | |
shelfGroup.position.set(position[0], position[1], position[2]); | |
// Main shelf | |
const shelfGeometry = new THREE.BoxGeometry(width, thickness, depth); | |
const shelf = new THREE.Mesh(shelfGeometry, material); | |
shelf.castShadow = true; | |
shelf.receiveShadow = true; | |
shelfGroup.add(shelf); | |
// Shelf supports | |
const supportGeometry = new THREE.BoxGeometry(0.5, 1.5, 0.5); | |
const supportMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC }); | |
// Left support | |
const leftSupport = new THREE.Mesh(supportGeometry, supportMaterial); | |
leftSupport.position.set(-width/2 + 0.25, -thickness/2 - 0.75, 0); | |
shelfGroup.add(leftSupport); | |
// Right support | |
const rightSupport = new THREE.Mesh(supportGeometry, supportMaterial); | |
rightSupport.position.set(width/2 - 0.25, -thickness/2 - 0.75, 0); | |
shelfGroup.add(rightSupport); | |
if (width > 24) { | |
// Middle support for wider shelves | |
const midSupport = new THREE.Mesh(supportGeometry, supportMaterial); | |
midSupport.position.set(0, -thickness/2 - 0.75, 0); | |
shelfGroup.add(midSupport); | |
} | |
parentGroup.add(shelfGroup); | |
return shelfGroup; | |
} | |
function createDrawer(width, height, depth, position, frontMaterial, sideMaterial, parentGroup) { | |
const drawerGroup = new THREE.Group(); | |
drawerGroup.position.set(position[0], position[1], position[2]); | |
// Drawer body | |
const bodyWidth = width * 0.9; | |
const bodyHeight = height * 0.85; | |
const bodyDepth = depth; | |
// Front | |
const frontGeometry = new THREE.BoxGeometry(width, height, 0.5); | |
const front = new THREE.Mesh(frontGeometry, frontMaterial); | |
front.position.set(0, 0, depth/2 + 0.25); | |
drawerGroup.add(front); | |
// Bottom | |
const bottomGeometry = new THREE.BoxGeometry(width * 0.9, 0.2, depth); | |
const bottom = new THREE.Mesh(bottomGeometry, sideMaterial); | |
bottom.position.set(0, -height/2 + 0.1, 0); | |
drawerGroup.add(bottom); | |
// Sides | |
const sideWidth = width * 0.9; | |
const sideGeometry = new THREE.BoxGeometry(0.2, bodyHeight, depth); | |
// Left side | |
const leftSide = new THREE.Mesh(sideGeometry, sideMaterial); | |
leftSide.position.set(-sideWidth/2, 0, 0); | |
drawerGroup.add(leftSide); | |
// Right side | |
const rightSide = new THREE.Mesh(sideGeometry, sideMaterial); | |
rightSide.position.set(sideWidth/2, 0, 0); | |
drawerGroup.add(rightSide); | |
// Back | |
const backGeometry = new THREE.BoxGeometry(sideWidth, bodyHeight, 0.2); | |
const back = new THREE.Mesh(backGeometry, sideMaterial); | |
back.position.set(0, 0, -depth/2 + 0.1); | |
drawerGroup.add(back); | |
// Handle (as a bar) | |
const handleGeometry = new THREE.CylinderGeometry(0.3, 0.3, 3, 16); | |
const handleMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); | |
const handle = new THREE.Mesh(handleGeometry, handleMaterial); | |
handle.rotation.z = Math.PI/2; | |
handle.position.set(0, 0, depth/2 + 0.3); | |
drawerGroup.add(handle); | |
// Drawer slides | |
const slideGeometry = new THREE.BoxGeometry(0.1, 0.5, depth * 1.2); | |
const slideMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 }); | |
// Left slide | |
const leftSlide = new THREE.Mesh(slideGeometry, slideMaterial); | |
leftSlide.position.set(-width/2 + 0.25, -height/2 + 0.25, 0); | |
drawerGroup.add(leftSlide); | |
// Right slide | |
const rightSlide = new THREE.Mesh(slideGeometry, slideMaterial); | |
rightSlide.position.set(width/2 - 0.25, -height/2 + 0.25, 0); | |
drawerGroup.add(rightSlide); | |
parentGroup.add(drawerGroup); | |
return drawerGroup; | |
} | |
function showLoading() { | |
loadingOverlay.style.opacity = 1; | |
loadingOverlay.style.pointerEvents = 'auto'; | |
let progress = 0; | |
progressRing.style.strokeDashoffset = 314; | |
const interval = setInterval(() => { | |
progress += 10; | |
const offset = 314 * (1 - progress/100); | |
progressRing.style.strokeDashoffset = offset; | |
if (progress >= 100) { | |
clearInterval(interval); | |
} | |
}, 50); | |
} | |
function hideLoading() { | |
loadingOverlay.style.opacity = 0; | |
loadingOverlay.style.pointerEvents = 'none'; | |
} | |
// Update price calculation | |
function updatePrice() { | |
const width = parseFloat(widthSlider.value); | |
const height = parseFloat(heightSlider.value); | |
const depth = parseFloat(depthSlider.value); | |
const mat = material.value; | |
const isTwoDoors = twoDoorsBtn.classList.contains('bg-indigo-100'); | |
const hasSoftClose = softClose.checked; | |
const hasPullOutShelves = pullOutShelves.checked; | |
const hasLighting = lighting.checked; | |
const hasGlassDoors = glassDoors.checked; | |
const shelvesCount = parseInt(shelvesSlider.value); | |
const drawersCount = cabinetType.value === 'base' ? parseInt(drawersSlider.value) : 0; | |
// Calculate base price based on size and type | |
let base = 100; | |
const sizeFactor = (width * height * depth) / 10000; | |
base += sizeFactor * 100; | |
if (cabinetType.value === 'base') base *= 1.3; | |
else if (cabinetType.value === 'tall') base *= 1.4; | |
else if (cabinetType.value === 'island') base *= 1.6; | |
// Material premium | |
let matPremium = 0; | |
if (mat === 'cherry' || mat === 'walnut') matPremium = 120; | |
else if (mat === 'maple') matPremium = 80; | |
else if (mat === 'oak') matPremium = 40; | |
else if (mat === 'painted') matPremium = 60; | |
else if (mat === 'laminate') matPremium = 0; | |
// Features | |
let featuresPremium = 0; | |
if (hasSoftClose) featuresPremium += 40; | |
if (hasPullOutShelves) featuresPremium += 80; | |
if (hasLighting) featuresPremium += 60; | |
if (hasGlassDoors) featuresPremium += 90; | |
// Options (shelves, drawers) | |
let optionsPremium = shelvesCount * 20; | |
optionsPremium += drawersCount * 45; | |
if (isTwoDoors) optionsPremium += 35; | |
// Update displays | |
basePrice.textContent = `$${base.toFixed(2)}`; | |
materialPrice.textContent = `$${matPremium.toFixed(2)}`; | |
featuresPrice.textContent = `$${featuresPremium.toFixed(2)}`; | |
optionsPrice.textContent = `$${optionsPremium.toFixed(2)}`; | |
totalPrice.textContent = `$${(base + matPremium + featuresPremium + optionsPremium).toFixed(2)}`; | |
} | |
// Event listeners | |
widthSlider.addEventListener('input', () => { | |
widthValue.textContent = widthSlider.value; | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
heightSlider.addEventListener('input', () => { | |
heightValue.textContent = heightSlider.value; | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
depthSlider.addEventListener('input', () => { | |
depthValue.textContent = depthSlider.value; | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
oneDoorBtn.addEventListener('click', () => { | |
oneDoorBtn.classList.add('bg-indigo-100', 'text-indigo-700', 'border-indigo-300'); | |
twoDoorsBtn.classList.remove('bg-indigo-100', 'text-indigo-700', 'border-indigo-300'); | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
twoDoorsBtn.addEventListener('click', () => { | |
twoDoorsBtn.classList.add('bg-indigo-100', 'text-indigo-700', 'border-indigo-300'); | |
oneDoorBtn.classList.remove('bg-indigo-100', 'text-indigo-700', 'border-indigo-300'); | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
shelvesSlider.addEventListener('input', () => { | |
shelvesValue.textContent = shelvesSlider.value; | |
shelvesDisplay.textContent = shelvesSlider.value; | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
drawersSlider.addEventListener('input', () => { | |
drawersValue.textContent = drawersSlider.value; | |
drawersDisplay.textContent = drawersSlider.value; | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
cabinetType.addEventListener('change', () => { | |
cabinetTypeDisplay.textContent = cabinetType.options[cabinetType.selectedIndex].text; | |
// Show/hide drawers control based on cabinet type | |
if (cabinetType.value === 'base') { | |
drawersContainer.style.display = 'block'; | |
drawersDisplayContainer.style.display = 'flex'; | |
} else { | |
drawersContainer.style.display = 'none'; | |
drawersDisplayContainer.style.display = 'none'; | |
drawersSlider.value = 0; | |
drawersValue.textContent = '0'; | |
drawersDisplay.textContent = '0'; | |
} | |
// Adjust default dimensions based on cabinet type | |
if (cabinetType.value === 'wall') { | |
heightSlider.value = 36; | |
depthSlider.value = 12; | |
heightValue.textContent = '36'; | |
depthValue.textContent = '12'; | |
} else if (cabinetType.value === 'base') { | |
heightSlider.value = 34.5; | |
depthSlider.value = 24; | |
heightValue.textContent = '34.5'; | |
depthValue.textContent = '24'; | |
} else if (cabinetType.value === 'tall') { | |
heightSlider.value = 84; | |
depthSlider.value = 24; | |
heightValue.textContent = '84'; | |
depthValue.textContent = '24'; | |
} else if (cabinetType.value === 'island') { | |
heightSlider.value = 36; | |
depthSlider.value = 24; | |
heightValue.textContent = '36'; | |
depthValue.textContent = '24'; | |
} | |
updateCabinetModel(); | |
updatePrice(); | |
}); | |
material.addEventListener('change', () => { | |
materialDisplay.textContent = material.options[material.selectedIndex].text; | |
updatePrice(); | |
updateCabinetModel(); | |
}); | |
color.addEventListener('change', () => { | |
colorDisplay.textContent = color.options[color.selectedIndex].text; | |
updatePrice(); | |
updateCabinetModel(); | |
}); | |
softClose.addEventListener('change', updatePrice); | |
pullOutShelves.addEventListener('change', updatePrice); | |
lighting.addEventListener('change', updatePrice); | |
glassDoors.addEventListener('change', () => { | |
updatePrice(); | |
updateCabinetModel(); | |
}); | |
// Simulate download buttons (in a real app, these would generate actual files) | |
downloadUSDZ.addEventListener('click', () => { | |
alert('In a real implementation, this would generate and download a USDZ file of your cabinet design.'); | |
}); | |
downloadGLB.addEventListener('click', () => { | |
alert('In a real implementation, this would generate and download a GLB/GLTF file of your cabinet design.'); | |
}); | |
// Initialize | |
window.addEventListener('load', () => { | |
initThreeJS(); | |
updatePrice(); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=gladiopeace/cabinet" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |