
Door Configuration Tool
Professional Commercial Refrigeration Solutions

Door Configuration Tool
Saved Configurations
Configuration Input
Additional Options
Your Information
Attachments
Door Layout Preview
Click to expand view
Door Swing Configuration - Top View
L = Left Hinge, R = Right Hinge | Dashed lines indicate swing direction
Click to expand view
Configuration Summary
Frame Series:
Number of Doors:
Door Dimensions:
Net Opening:
Sections:
No Center Mullion:
Selected Options
Application Type:
Door Swing:
Swing Pattern:
Glass Package:
Lighting:
Shelving:
Frame Finish:
Handle Type:
Locks:
* Please send request and a member of the sales team will reach out within 24 hours with pricing.
Frame Configuration
Controls:
• Left click + drag: Rotate view
• Scroll: Zoom in/out
• Right click + drag: Pan
• Click door: Open/close
Door Swing Configuration - Expanded View
L = Left Hinge, R = Right Hinge | Click on any door to open/close individually
Door Layout - Expanded View
Front view of door configuration
Technical Reference
Your configuration request has been successfully sent to our sales team.
A dedicated sales representative will contact you within 24 business hours to discuss your requirements and provide a detailed quote.
We were unable to process your request at this time.
Please check your internet connection and try again. If the problem persists, you may contact our sales team directly at hsoto@cdsdoors.net or call (800) 478-3790.
Configuration ${index + 1}: ${config.series} - ${config.numDoors} Doors
`; configText += `Net Opening: ${Utils.floatToFraction16(config.netOpeningWidth)}" × ${Utils.floatToFraction16(config.netOpeningHeight)}" | `; configText += `${config.handleType} Handle | ${config.doorSwing} Swing`; if (config.noCenterMullion) { configText += ` | No Center Mullion`; } configText += `
`; configInfo.innerHTML = configText; const configActions = document.createElement('div'); configActions.className = 'config-actions'; configActions.innerHTML = ` `; configItem.appendChild(configInfo); configItem.appendChild(configActions); configList.appendChild(configItem); }); } addConfiguration() { if (!this.currentConfig) { this.showWarningMessage('Please calculate a configuration first'); return; } const configToSave = JSON.parse(JSON.stringify(this.currentConfig)); configToSave.timestamp = new Date().toISOString(); configToSave.notes = document.getElementById('notes').value; this.savedConfigurations.push(configToSave); this.updateConfigurationList(); this.showSuccessMessage('Configuration added to list!'); this.clearResults(); const notesElement = document.getElementById('notes'); if (notesElement) { notesElement.value = ''; } const exportAllBtn = document.getElementById('exportAllBtn'); if (exportAllBtn) { exportAllBtn.disabled = false; } } removeConfiguration(index) { this.savedConfigurations.splice(index, 1); this.updateConfigurationList(); } // ============================================== // DRAWING FUNCTIONS // ============================================== drawDoorLayout() { if (!this.currentConfig) return; const canvas = document.getElementById('doorCanvas'); this.drawDoorLayoutOnCanvas(canvas, false); } drawDoorLayoutOnCanvas(canvas, isExpanded = false, isPDFExport = false) { if (!this.currentConfig || !canvas) return false; try { const ctx = canvas.getContext('2d'); canvas.width = isExpanded ? 1400 : canvas.offsetWidth; canvas.height = isExpanded ? 700 : canvas.offsetHeight; if (isPDFExport) { ctx.scale(1, 1); } const { numDoors, sections, doorSwing, swingPattern, noCenterMullion } = this.currentConfig; let sectionCounts = []; if (sections === "One section") { sectionCounts = [numDoors]; } else { const parts = sections.split(", "); sectionCounts = parts.map(part => parseInt(part.split(" ")[0])); } const baseDoorWidth = isExpanded ? 80 : 40; const baseDoorHeight = isExpanded ? 160 : 80; let minimalGap = isExpanded ? 1 : 0.5; let centerPullGap = isExpanded ? 8 : 4; let sectionGap = isExpanded ? 10 : 5; const margin = isExpanded ? 60 : 30; let totalWidth = 0; let doorIndex = 0; sectionCounts.forEach((count, i) => { totalWidth += count * baseDoorWidth; for (let j = 0; j < count - 1; j++) { let currentHinge = 'L'; let nextHinge = 'L'; if (doorSwing === 'Center Pull') { currentHinge = (doorIndex + 1) % 2 === 1 ? 'L' : 'R'; nextHinge = (doorIndex + 2) % 2 === 1 ? 'L' : 'R'; } if (doorSwing === 'Center Pull' && noCenterMullion && currentHinge === 'R' && nextHinge === 'L') { totalWidth += centerPullGap; } else { totalWidth += minimalGap; } doorIndex++; } doorIndex++; if (i { let sectionWidth = count * scaledDoorWidth; for (let i = 0; i < count - 1; i++) { let currentHinge = 'L'; let nextHinge = 'L'; if (doorSwing === 'Center Pull') { currentHinge = ((doorIndex + i) + 1) % 2 === 1 ? 'L' : 'R'; nextHinge = ((doorIndex + i + 1) + 1) % 2 === 1 ? 'L' : 'R'; } if (doorSwing === 'Center Pull' && noCenterMullion && currentHinge === 'R' && nextHinge === 'L') { sectionWidth += scaledCenterPullGap; } else { sectionWidth += scaledMinimalGap; } } ctx.fillStyle = '#2B5397'; const sectionFontSize = isPDFExport ? 24 : (isExpanded ? 16 : 12) * scale; ctx.font = `${sectionFontSize}px Arial`; ctx.textAlign = 'center'; let sectionLabel = sections === "One section" ? "ONE SECTION" : sections.split(", ")[sectionIndex]; ctx.fillText(sectionLabel, currentX + sectionWidth / 2, startY - 20); for (let i = 0; i < count; i++) { const doorX = currentX; const currentDoorIndex = doorIndex + i; doorPositions.push({ x: doorX, y: startY, width: scaledDoorWidth, height: scaledDoorHeight, index: currentDoorIndex, number: doorNumber }); ctx.fillStyle = '#E6F0FF'; ctx.fillRect(doorX, startY, scaledDoorWidth, scaledDoorHeight); ctx.strokeStyle = '#808080'; ctx.lineWidth = isExpanded ? 3 : 2; ctx.strokeRect(doorX, startY, scaledDoorWidth, scaledDoorHeight); let hingeType = 'L'; if (doorSwing === 'Center Pull') { hingeType = doorNumber % 2 === 1 ? 'L' : 'R'; } else if (doorSwing === 'All Left') { hingeType = 'L'; } else if (doorSwing === 'All Right') { hingeType = 'R'; } else if (doorSwing === 'Custom' && swingPattern) { hingeType = swingPattern[currentDoorIndex] || 'L'; } const handleWidth = isExpanded ? 6 : 4; const handleHeight = scaledDoorHeight * 0.2; const handleY = startY + scaledDoorHeight * 0.4; const handleOffset = scaledDoorWidth * 0.1; const handleColor = this.currentConfig.handleType === 'standard_chrome' ? '#C0C0C0' : '#666'; ctx.fillStyle = handleColor; if (hingeType === 'L') { ctx.fillRect( doorX + scaledDoorWidth - handleOffset - handleWidth, handleY, handleWidth, handleHeight ); } else { ctx.fillRect( doorX + handleOffset, handleY, handleWidth, handleHeight ); } ctx.fillStyle = '#2B5397'; const doorNumFontSize = isPDFExport ? 20 : (isExpanded ? 14 : 10) * scale; ctx.font = `${doorNumFontSize}px Arial`; ctx.textAlign = 'center'; ctx.fillText(doorNumber, doorX + scaledDoorWidth / 2, startY + scaledDoorHeight + 30); if (doorSwing !== 'None') { const hingeFontSize = isPDFExport ? 16 : (isExpanded ? 12 : 8) * scale; ctx.font = `${hingeFontSize}px Arial`; ctx.fillStyle = '#666'; ctx.fillText(`(${hingeType})`, doorX + scaledDoorWidth / 2, startY + scaledDoorHeight + 45); } currentX += scaledDoorWidth; if (i < count - 1) { let currentHinge = hingeType; let nextHinge = 'L'; if (doorSwing === 'Center Pull') { nextHinge = (doorNumber + 1) % 2 === 1 ? 'L' : 'R'; } else if (doorSwing === 'All Left') { nextHinge = 'L'; } else if (doorSwing === 'All Right') { nextHinge = 'R'; } else if (doorSwing === 'Custom' && swingPattern) { nextHinge = swingPattern[currentDoorIndex + 1] || 'L'; } if (doorSwing === 'Center Pull' && noCenterMullion && currentHinge === 'R' && nextHinge === 'L') { currentX += scaledCenterPullGap; } else { currentX += scaledMinimalGap; } } doorNumber++; } doorIndex += count; if (sectionIndex 1) { ctx.strokeStyle = '#D91E18'; ctx.lineWidth = isExpanded ? 3 : 2; ctx.setLineDash([8, 4]); let sectionX = startX; doorIndex = 0; for (let i = 0; i < sectionCounts.length - 1; i++) { let sectionWidth = sectionCounts[i] * scaledDoorWidth; for (let j = 0; j 1) { labelY += 20; } ctx.fillText('NO CENTER MULLION', canvas.width / 2, labelY); } // Add dimensions if (true) { ctx.strokeStyle = '#333'; ctx.lineWidth = isPDFExport ? 3 : 1; const dimensionFontSize = isPDFExport ? 22 : (isExpanded ? 14 : 12); ctx.font = `bold ${dimensionFontSize}px Arial`; ctx.fillStyle = '#333'; let dimY = startY + scaledDoorHeight + 80; if (noCenterMullion) { dimY += 25; if (doorSwing === 'Center Pull' && sectionCounts.length > 1) { dimY += 20; } } ctx.beginPath(); ctx.moveTo(startX, dimY); ctx.lineTo(startX + totalWidth * scale, dimY); ctx.stroke(); const capSize = isPDFExport ? 10 : 5; ctx.beginPath(); ctx.moveTo(startX, dimY - capSize); ctx.lineTo(startX, dimY + capSize); ctx.moveTo(startX + totalWidth * scale, dimY - capSize); ctx.lineTo(startX + totalWidth * scale, dimY + capSize); ctx.stroke(); ctx.textAlign = 'center'; ctx.fillStyle = '#000'; ctx.fillText(`Net Opening: ${Utils.floatToFraction16(this.currentConfig.netOpeningWidth)}"`, canvas.width / 2, dimY + 25); ctx.fillText(`(${Utils.inchesToFeetAndInches(this.currentConfig.netOpeningWidth)})`, canvas.width / 2, dimY + 48); if (noCenterMullion) { const doorPairs = Math.floor(this.currentConfig.numDoors / 2); const mullionDeduct = doorPairs * 0.625; ctx.font = `${dimensionFontSize - 2}px Arial`; ctx.fillStyle = '#D91E18'; ctx.fillText(`includes ${Utils.floatToFraction16(mullionDeduct)}" mullion deduct`, canvas.width / 2, dimY + 70); } if (isExpanded && !isPDFExport) { const dimX = startX - 40; ctx.strokeStyle = '#333'; ctx.beginPath(); ctx.moveTo(dimX, startY); ctx.lineTo(dimX, startY + scaledDoorHeight); ctx.stroke(); ctx.beginPath(); ctx.moveTo(dimX - 5, startY); ctx.lineTo(dimX + 5, startY); ctx.moveTo(dimX - 5, startY + scaledDoorHeight); ctx.lineTo(dimX + 5, startY + scaledDoorHeight); ctx.stroke(); ctx.save(); ctx.translate(dimX - 20, startY + scaledDoorHeight / 2); ctx.rotate(-Math.PI / 2); ctx.fillText(`Height: ${Utils.floatToFraction16(this.currentConfig.netOpeningHeight)}"`, 0, 0); ctx.restore(); } } if (isExpanded && !isPDFExport) { canvas.doorPositions = doorPositions; const bottomTextY = canvas.height - 10; ctx.fillStyle = '#7f8c8d'; ctx.font = '12px Arial'; ctx.textAlign = 'center'; ctx.fillText('Click outside or press ESC to close', canvas.width / 2, bottomTextY); } else if (!isPDFExport) { ctx.fillStyle = '#2B5397'; ctx.font = '20px Arial'; ctx.textAlign = 'right'; ctx.fillText('⤢', canvas.width - 5, 25); } return true; } catch (error) { DebugUtils.error('Error drawing door layout', error); return false; } } drawDoorSwingView() { const swingType = document.getElementById('doorSwing').value; const swingSection = document.getElementById('swingViewSection'); if (!this.currentConfig || !swingSection) { if (swingSection) swingSection.style.display = 'none'; return; } swingSection.style.display = 'block'; const canvas = document.getElementById('swingCanvas'); this.drawSwingDiagram(canvas, false); } drawSwingDiagram(canvas, isExpanded = false, needsScaling = false) { if (!this.currentConfig || !canvas) return false; try { const ctx = canvas.getContext('2d'); const numDoors = this.currentConfig.numDoors; const swingType = document.getElementById('doorSwing').value; const noCenterMullion = this.currentConfig.noCenterMullion; if (isExpanded && !needsScaling) { canvas.width = Math.max(1200, numDoors * 100); canvas.height = 500; } else if (needsScaling) { canvas.width = 2000; canvas.height = 300; } else { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; } let doorWidth, wallHeight, swingRadius, fontSize, smallFontSize; let doorGap = 2; let sectionGap = 10; let gapLR, gapRL, gapCenterPull; if (isExpanded && !needsScaling) { doorWidth = Math.min(60, (canvas.width - 400) / (numDoors * 2)); wallHeight = 40; swingRadius = Math.min(120, doorWidth * 2); if (swingType === 'Center Pull') { gapLR = swingRadius; } else { gapLR = Math.max(doorGap, (swingRadius * 2) - doorWidth); } gapRL = Math.max(doorGap, swingRadius - doorWidth); gapCenterPull = noCenterMullion ? doorWidth * 0.15 : doorWidth * 0.3; fontSize = Math.max(16, Math.min(24, doorWidth * 0.4)); smallFontSize = Math.max(14, Math.min(20, doorWidth * 0.35)); } else if (needsScaling) { const scaleFactor = 0.7; doorWidth = 60 * scaleFactor; swingRadius = 90 * scaleFactor; if (swingType === 'Center Pull') { gapLR = swingRadius * scaleFactor; } else { gapLR = Math.max(doorGap, (swingRadius * 2) - doorWidth) * scaleFactor; } gapRL = Math.max(doorGap, swingRadius - doorWidth) * scaleFactor; gapCenterPull = noCenterMullion ? doorWidth * 0.15 : doorWidth * 0.3 * scaleFactor; wallHeight = 30 * scaleFactor; fontSize = Math.floor(20 * 0.8); smallFontSize = Math.floor(16 * 0.8); } else { doorWidth = 40; swingRadius = 50; if (swingType === 'Center Pull') { gapLR = (swingRadius * 2) - (doorWidth * 2); } else { gapLR = Math.max(doorGap, (swingRadius * 2) - doorWidth); } gapRL = Math.max(doorGap, swingRadius - doorWidth); gapCenterPull = noCenterMullion ? doorWidth * 0.15 : doorWidth * 0.3; wallHeight = 20; fontSize = 16; smallFontSize = 14; } let sectionCounts = []; if (this.currentConfig.sections === "One section") { sectionCounts = [numDoors]; } else { const parts = this.currentConfig.sections.split(", "); sectionCounts = parts.map(part => parseInt(part.split(" ")[0])); } let totalWidth = 0; let doorIndex = 0; sectionCounts.forEach((sectionCount, sectionIdx) => { for (let i = 0; i < sectionCount; i++) { totalWidth += doorWidth; if (i < sectionCount - 1) { let currentHinge = 'L'; let nextHinge = 'L'; if (swingType === 'Center Pull') { currentHinge = (doorIndex + 1) % 2 === 1 ? 'L' : 'R'; nextHinge = (doorIndex + 2) % 2 === 1 ? 'L' : 'R'; } else if (swingType === 'All Left') { currentHinge = 'L'; nextHinge = 'L'; } else if (swingType === 'All Right') { currentHinge = 'R'; nextHinge = 'R'; } else if (swingType === 'Custom' && this.customSwingPattern) { currentHinge = this.customSwingPattern[doorIndex] || 'L'; nextHinge = this.customSwingPattern[doorIndex + 1] || 'L'; } if (currentHinge === 'L' && nextHinge === 'R') { totalWidth += gapLR; } else if ((currentHinge === 'L' && nextHinge === 'L') || (currentHinge === 'R' && nextHinge === 'R')) { totalWidth += gapRL; } else { if (swingType === 'Center Pull' && noCenterMullion) { totalWidth += gapCenterPull; } else { totalWidth += doorGap; } } } doorIndex++; } if (sectionIdx availableWidth) { scale = availableWidth / totalWidth; doorWidth *= scale; gapLR *= scale; gapRL *= scale; gapCenterPull *= scale; swingRadius *= scale; doorGap *= scale; sectionGap *= scale; } const startX = (canvas.width - totalWidth * scale) / 2; const wallY = isExpanded ? 100 : (canvas.height - wallHeight - swingRadius) / 2; ctx.clearRect(0, 0, canvas.width, canvas.height); const doorPositions = []; ctx.fillStyle = '#7f8c8d'; ctx.fillRect(0, wallY, canvas.width, wallHeight); let currentX = startX; doorIndex = 0; sectionCounts.forEach((sectionCount, sectionIdx) => { for (let i = 0; i < sectionCount; i++) { const doorX = currentX; const doorNum = doorIndex + 1; let hingeType = 'L'; if (swingType === 'Center Pull') { hingeType = doorNum % 2 === 1 ? 'L' : 'R'; } else if (swingType === 'All Left') { hingeType = 'L'; } else if (swingType === 'All Right') { hingeType = 'R'; } else if (swingType === 'Custom' && this.customSwingPattern) { hingeType = this.customSwingPattern[doorIndex] || 'L'; } if (isExpanded) { doorPositions.push({ x: doorX, y: wallY, width: doorWidth, height: wallHeight + swingRadius, index: doorIndex, number: doorNum, hingeType: hingeType, isOpen: this.swingDoorStates[doorIndex] ? this.swingDoorStates[doorIndex].isOpen : false }); } const isOpen = this.swingDoorStates[doorIndex] ? this.swingDoorStates[doorIndex].isOpen : false; const currentAngle = this.swingDoorStates[doorIndex] ? this.swingDoorStates[doorIndex].currentAngle : 0; ctx.strokeStyle = '#2B5397'; ctx.lineWidth = Math.max(2, doorWidth * 0.05); ctx.beginPath(); if (hingeType === 'L') { const angleRad = (currentAngle - 90) * Math.PI / 180; const endX = doorX + Math.cos(angleRad) * swingRadius; const endY = wallY + wallHeight - Math.sin(angleRad) * swingRadius; ctx.moveTo(doorX, wallY + wallHeight); ctx.lineTo(endX, endY); } else { const angleRad = (currentAngle - 90) * Math.PI / 180; const endX = doorX + doorWidth + Math.cos(angleRad) * swingRadius; const endY = wallY + wallHeight - Math.sin(angleRad) * swingRadius; ctx.moveTo(doorX + doorWidth, wallY + wallHeight); ctx.lineTo(endX, endY); } ctx.stroke(); ctx.strokeStyle = '#D91E18'; ctx.lineWidth = Math.max(2, doorWidth * 0.04); ctx.setLineDash([5, 5]); ctx.beginPath(); if (hingeType === 'L') { ctx.arc(doorX, wallY + wallHeight, swingRadius, 0, Math.PI/2); } else { ctx.arc(doorX + doorWidth, wallY + wallHeight, swingRadius, Math.PI/2, Math.PI); } ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = '#2B5397'; ctx.font = `bold ${fontSize}px Arial`; ctx.textAlign = 'center'; ctx.fillText(doorNum, doorX + doorWidth/2, wallY + wallHeight + swingRadius + 30); ctx.font = `${smallFontSize}px Arial`; ctx.fillText(hingeType, doorX + doorWidth/2, wallY - 10); currentX += doorWidth; if (i < sectionCount - 1) { let currentHinge = hingeType; let nextHinge = 'L'; if (swingType === 'Center Pull') { nextHinge = (doorNum + 1) % 2 === 1 ? 'L' : 'R'; } else if (swingType === 'All Left') { nextHinge = 'L'; } else if (swingType === 'All Right') { nextHinge = 'R'; } else if (swingType === 'Custom' && this.customSwingPattern) { nextHinge = this.customSwingPattern[doorIndex + 1] || 'L'; } if (currentHinge === 'L' && nextHinge === 'R') { currentX += gapLR; } else if ((currentHinge === 'L' && nextHinge === 'L') || (currentHinge === 'R' && nextHinge === 'R')) { currentX += gapRL; } else { if (swingType === 'Center Pull' && noCenterMullion) { currentX += gapCenterPull; } else { currentX += doorGap + gapCenterPull; } } } doorIndex++; } if (sectionIdx 1) { labelY += 20; } ctx.fillText('NO CENTER MULLION', canvas.width / 2, labelY); } if (isExpanded) { canvas.doorPositions = doorPositions; } if (isExpanded && !needsScaling) { ctx.fillStyle = '#7f8c8d'; ctx.font = '14px Arial'; ctx.textAlign = 'center'; ctx.fillText('Click on any door to open/close | Click outside or press ESC to close', canvas.width / 2, canvas.height - 20); } if (!isExpanded) { ctx.fillStyle = '#2B5397'; ctx.font = '18px Arial'; ctx.textAlign = 'right'; ctx.fillText('⤢', canvas.width - 5, 20); } return true; } catch (error) { DebugUtils.error('Error drawing swing diagram', error); return false; } } // ============================================== // MODAL HANDLERS // ============================================== expandDoorView() { if (!this.currentConfig) return; const modal = document.getElementById('doorModal'); if (modal) { modal.style.display = 'block'; const expandedCanvas = document.getElementById('expandedDoorCanvas'); setTimeout(() => { this.drawDoorLayoutOnCanvas(expandedCanvas, true); }, 100); } } closeDoorModal() { const modal = document.getElementById('doorModal'); if (modal) { modal.style.display = 'none'; } } expandSwingView() { if (!this.currentConfig || document.getElementById('doorSwing').value === 'None') return; const modal = document.getElementById('swingModal'); if (modal) { modal.style.display = 'block'; this.initializeSwingDoorStates(); const expandedCanvas = document.getElementById('expandedSwingCanvas'); setTimeout(() => { this.drawSwingDiagram(expandedCanvas, true, false); }, 100); } } closeSwingModal() { this.resetSwingAnimation(); const modal = document.getElementById('swingModal'); if (modal) { modal.style.display = 'none'; } } openPDFModal(pdfPath, title) { const modal = document.getElementById('pdfModal'); const iframe = document.getElementById('pdfFrame'); const modalTitle = document.getElementById('modalTitle'); if (modal && iframe && modalTitle) { modalTitle.textContent = title; iframe.src = pdfPath; modal.style.display = 'block'; } } closePDFModal() { const modal = document.getElementById('pdfModal'); const iframe = document.getElementById('pdfFrame'); if (modal && iframe) { modal.style.display = 'none'; iframe.src = ''; } } showSalesModal(type) { if (type === 'success') { const modal = document.getElementById('salesSuccessModal'); if (modal) modal.style.display = 'block'; } else { const modal = document.getElementById('salesErrorModal'); if (modal) modal.style.display = 'block'; } } closeSalesModal(type) { if (type === 'success') { const modal = document.getElementById('salesSuccessModal'); if (modal) modal.style.display = 'none'; } else { const modal = document.getElementById('salesErrorModal'); if (modal) modal.style.display = 'none'; if (type === 'error') { this.sendToSales(); } } } // ============================================== // SWING ANIMATION FUNCTIONS // ============================================== initializeSwingDoorStates() { this.swingDoorStates = []; for (let i = 0; i { if (x >= door.x && x = door.y && y { const diff = state.targetAngle - state.currentAngle; if (Math.abs(diff) > 0.5) { state.currentAngle += diff * 0.15; const canvas = document.getElementById('expandedSwingCanvas'); this.drawSwingDiagram(canvas, true, false); requestAnimationFrame(animate); } else { state.currentAngle = state.targetAngle; const canvas = document.getElementById('expandedSwingCanvas'); this.drawSwingDiagram(canvas, true, false); } }; animate(); } animateSwingDoors() { if (this.swingDoorsAnimating || !this.currentConfig) return; this.swingDoorsAnimating = true; this.swingAnimationFrame = 0; const btn = document.getElementById('animateSwingBtn'); if (btn) { btn.disabled = true; } this.animateSwingFrame(); } animateSwingFrame() { const canvas = document.getElementById('expandedSwingCanvas'); const maxFrames = 120; // 2 seconds at 60fps if (this.swingAnimationFrame < maxFrames) { // Calculate animation progress (0 to 1 and back to 0) let progress; if (this.swingAnimationFrame { const hingeType = this.getHingeType(index); door.targetAngle = hingeType === 'L' ? 90 * progress : -90 * progress; door.currentAngle = door.targetAngle; }); // Redraw this.drawSwingDiagram(canvas, true, false); this.swingAnimationFrame++; this.swingAnimationId = requestAnimationFrame(() => this.animateSwingFrame()); } else { // Animation complete this.swingDoorsAnimating = false; this.swingAnimationFrame = 0; const btn = document.getElementById('animateSwingBtn'); if (btn) { btn.disabled = false; } // Reset to closed position this.swingDoorStates.forEach(door => { door.currentAngle = 0; door.targetAngle = 0; door.isOpen = false; }); // Draw final state this.drawSwingDiagram(canvas, true, false); } } resetSwingAnimation() { if (this.swingAnimationId) { cancelAnimationFrame(this.swingAnimationId); this.swingAnimationId = null; } this.swingDoorsAnimating = false; this.swingDoorStates.forEach(state => { state.isOpen = false; state.currentAngle = 0; state.targetAngle = 0; }); const canvas = document.getElementById('expandedSwingCanvas'); if (canvas) { this.drawSwingDiagram(canvas, true, false); } const animateBtn = document.getElementById('animateSwingBtn'); if (animateBtn) { animateBtn.disabled = false; } } // ============================================== // PDF GENERATION - UPDATED METHODS // ============================================== exportCurrentConfiguration() { if (!this.currentConfig) { this.showWarningMessage('Please calculate a configuration first'); return; } const formData = this.getValidatedFormData(); if (!formData) return; try { this.generatePDF(); this.showSuccessMessage('PDF exported successfully!'); } catch (error) { DebugUtils.error('Error generating PDF', error); this.showWarningMessage('Error generating PDF. Please try again.'); } } exportAllConfigurations() { if (this.savedConfigurations.length === 0) { this.showWarningMessage('No configurations to export'); return; } const formData = this.getValidatedFormData(); if (!formData) return; try { this.generateCombinedPDF(); this.showSuccessMessage('PDF exported successfully!'); } catch (error) { DebugUtils.error('Error generating combined PDF', error); this.showWarningMessage('Error generating PDF. Please try again.'); } } generatePDF(returnBlob = false) { const { jsPDF } = window.jspdf; const doc = new jsPDF(); const formData = this.getValidatedFormData(); if (!formData) return null; const quoteNumber = 'CDS-' + new Date().getTime().toString().slice(-8); const currentDate = new Date().toLocaleDateString(); const primaryBlue = [43, 83, 151]; const accentRed = [217, 30, 24]; const darkGray = [51, 51, 51]; const lightGray = [248, 248, 248]; // Header with blue background doc.setFillColor(...primaryBlue); doc.rect(0, 0, 210, 25, 'F'); // CDS Logo placeholder doc.setFillColor(255, 255, 255); doc.roundedRect(10, 7, 30, 11, 2, 2, 'F'); doc.setFontSize(12); doc.setTextColor(...primaryBlue); doc.setFont(undefined, 'bold'); doc.text('CDS', 25, 13.5, { align: 'center' }); doc.setFontSize(6); doc.setFont(undefined, 'normal'); doc.text('Commercial Display Systems', 25, 16, { align: 'center' }); // Title doc.setFontSize(14); doc.setFont(undefined, 'bold'); doc.setTextColor(255, 255, 255); doc.text('DOOR CONFIGURATION', 105, 15, { align: 'center' }); // Quote number and date in header doc.setFontSize(9); doc.setFont(undefined, 'normal'); doc.text(`#${quoteNumber} | ${currentDate}`, 200, 15, { align: 'right' }); // Company info bar doc.setFillColor(245, 245, 245); doc.rect(0, 25, 210, 8, 'F'); doc.setFontSize(8); doc.setTextColor(...darkGray); doc.text('17341 Sierra Highway, Canyon Country, CA 91351 | www.cdsdoors.net | (800) 478-3790', 105, 29, { align: 'center' }); // Customer Info Section this.addCustomerInfoPDF(doc, 35, formData); // Configuration title const titleY = 65; const configTitle = `${this.currentConfig.numDoors}-Door ${this.currentConfig.series} Series Configuration`; doc.setFontSize(16); doc.setFont(undefined, 'bold'); doc.setTextColor(...primaryBlue); doc.text(configTitle, 105, titleY, { align: 'center' }); // Door Layout Section const layoutY = 75; this.drawDoorLayoutPDF(doc, layoutY); // Door Swing Section const swingY = 155; if (this.currentConfig.doorSwing && this.currentConfig.doorSwing !== 'None') { this.drawDoorSwingDiagramPDF(doc, swingY); } // Configuration Details - now in 2 columns const detailsY = this.currentConfig.doorSwing !== 'None' ? 210 : 160; this.addConfigurationDetailsPDF(doc, detailsY); // Footer doc.setFillColor(...primaryBlue); doc.rect(0, 280, 210, 17, 'F'); doc.setTextColor(255, 255, 255); doc.setFontSize(8); doc.setFont(undefined, 'normal'); doc.text('Commercial Display Systems • Professional Refrigeration Solutions', 105, 290, { align: 'center' }); if (returnBlob) { return doc.output('blob'); } else { const timestamp = new Date().getTime(); const filename = `CDS_Configuration_${formData.customerCompany ? formData.customerCompany.replace(/\s+/g, '_') : formData.customerName.replace(/\s+/g, '_')}_${timestamp}.pdf`; doc.save(filename); } } drawDoorLayoutPDF(doc, startY) { const primaryBlue = [43, 83, 151]; const accentRed = [217, 30, 24]; const darkGray = [51, 51, 51]; // Section header doc.setFillColor(...accentRed); doc.rect(10, startY, 190, 6, 'F'); doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.setTextColor(255, 255, 255); doc.text('DOOR LAYOUT PREVIEW', 105, startY + 4, { align: 'center' }); // Calculate layout parameters const { numDoors, sections, doorSwing, noCenterMullion } = this.currentConfig; let sectionCounts = []; if (sections === "One section") { sectionCounts = [numDoors]; } else { const parts = sections.split(", "); sectionCounts = parts.map(part => parseInt(part.split(" ")[0])); } // Base dimensions (works well for 10 doors or fewer) const baseDoorWidth = 15; const baseDoorHeight = 30; const baseDoorGap = 0.5; const baseSectionGap = 3; const availableWidth = 185; // Usable PDF width // Calculate base total width without scaling let baseTotalWidth = numDoors * baseDoorWidth + (numDoors - 1) * baseDoorGap; sectionCounts.forEach((count, i) => { if (i availableWidth) { scaleFactor = availableWidth / baseTotalWidth; // Ensure minimum scale for readability scaleFactor = Math.max(scaleFactor, 0.1); } // Apply scaling to all dimensions const doorWidth = baseDoorWidth * scaleFactor; const doorHeight = baseDoorHeight * scaleFactor; const doorGap = baseDoorGap * scaleFactor; const sectionGap = baseSectionGap * scaleFactor; // Calculate actual total width with scaling let totalWidth = numDoors * doorWidth + (numDoors - 1) * doorGap; sectionCounts.forEach((count, i) => { if (i { // Section label doc.setFontSize(sectionLabelFontSize); doc.setFont(undefined, 'bold'); doc.setTextColor(...primaryBlue); let sectionWidth = count * doorWidth + (count - 1) * doorGap; let sectionLabel = sections === "One section" ? "ONE SECTION" : sections.split(", ")[sectionIndex]; // Add "END" to section labels when they contain "LEFT" or "RIGHT" if (sectionLabel.includes("DR LEFT") && !sectionLabel.includes("END")) { sectionLabel = sectionLabel.replace("DR LEFT", "DR LEFT END"); } else if (sectionLabel.includes("DR RIGHT") && !sectionLabel.includes("END")) { sectionLabel = sectionLabel.replace("DR RIGHT", "DR RIGHT END"); } // Adjust label position based on scale const labelOffset = Math.max(4, 6 * scaleFactor); doc.text(sectionLabel, currentX + sectionWidth / 2, doorY - labelOffset, { align: 'center' }); // Draw doors in section for (let i = 0; i < count; i++) { // Store last door position if (doorNum === numDoors) { lastDoorX = currentX; lastDoorNum = doorNum; } // Door glass doc.setFillColor(230, 240, 255); doc.rect(currentX, doorY, doorWidth, doorHeight, 'F'); // Door frame doc.setDrawColor(128, 128, 128); doc.setLineWidth(0.5 * scaleFactor); doc.rect(currentX, doorY, doorWidth, doorHeight, 'D'); // Handle (scaled) let hingeType = 'L'; if (doorSwing === 'Center Pull') { hingeType = doorNum % 2 === 1 ? 'L' : 'R'; } else if (doorSwing === 'All Left') { hingeType = 'L'; } else if (doorSwing === 'All Right') { hingeType = 'R'; } const handleWidth = Math.max(0.5, 1 * scaleFactor); const handleHeight = Math.max(2, 6 * scaleFactor); const handleY = doorY + doorHeight * 0.4; const handleOffset = doorWidth * 0.15; doc.setFillColor(102, 102, 102); if (hingeType === 'L') { doc.rect(currentX + doorWidth - handleOffset - handleWidth, handleY, handleWidth, handleHeight, 'F'); } else { doc.rect(currentX + handleOffset, handleY, handleWidth, handleHeight, 'F'); } // Door number doc.setFontSize(doorNumberFontSize); doc.setFont(undefined, 'normal'); doc.setTextColor(...darkGray); const doorNumberOffset = Math.max(3, 5 * scaleFactor); doc.text(doorNum.toString(), currentX + doorWidth / 2, doorY + doorHeight + doorNumberOffset, { align: 'center' }); // Hinge type if (doorSwing !== 'None') { doc.setFontSize(hingeTypeFontSize); const hingeOffset = Math.max(5, 8 * scaleFactor); doc.text(`(${hingeType})`, currentX + doorWidth / 2, doorY + doorHeight + hingeOffset, { align: 'center' }); } currentX += doorWidth + doorGap; doorNum++; } // Add section gap if (sectionIndex 0) { const dimensionLineWidth = 0.3 * scaleFactor; const arrowSize = Math.max(0.5, 1 * scaleFactor); // Door width dimension ABOVE last door const widthDimOffset = Math.max(5, 7 * scaleFactor); const widthDimY = doorY - widthDimOffset; doc.setDrawColor(0, 0, 0); doc.setLineWidth(dimensionLineWidth); // Extension lines const extensionOffset = Math.max(1, 2 * scaleFactor); doc.line(lastDoorX, doorY - 1, lastDoorX, widthDimY - extensionOffset); doc.line(lastDoorX + doorWidth, doorY - 1, lastDoorX + doorWidth, widthDimY - extensionOffset); // Dimension line doc.line(lastDoorX, widthDimY, lastDoorX + doorWidth, widthDimY); // Arrows doc.line(lastDoorX, widthDimY, lastDoorX + arrowSize, widthDimY - arrowSize/2); doc.line(lastDoorX, widthDimY, lastDoorX + arrowSize, widthDimY + arrowSize/2); doc.line(lastDoorX + doorWidth, widthDimY, lastDoorX + doorWidth - arrowSize, widthDimY - arrowSize/2); doc.line(lastDoorX + doorWidth, widthDimY, lastDoorX + doorWidth - arrowSize, widthDimY + arrowSize/2); // Width text doc.setFontSize(dimensionFontSize); doc.setFont(undefined, 'bold'); const widthCenterX = lastDoorX + (doorWidth / 2); const widthTextOffset = Math.max(1.5, 2 * scaleFactor); doc.text(`${Utils.floatToFraction16(this.currentConfig.doorWidth)}"`, widthCenterX, widthDimY - widthTextOffset, { align: 'center' }); // Door height dimension to RIGHT of last door const heightDimOffset = Math.max(6, 8 * scaleFactor); const heightDimX = lastDoorX + doorWidth + heightDimOffset; // Extension lines doc.line(lastDoorX + doorWidth + 1, doorY, heightDimX + extensionOffset, doorY); doc.line(lastDoorX + doorWidth + 1, doorY + doorHeight, heightDimX + extensionOffset, doorY + doorHeight); // Draw dimension line in two parts with gap for text const textGapSize = Math.max(6, 8 * scaleFactor); const doorTextGapStart = doorY + doorHeight/2 - textGapSize; const doorTextGapEnd = doorY + doorHeight/2 + textGapSize; // Top part of dimension line doc.line(heightDimX, doorY, heightDimX, doorTextGapStart); // Bottom part of dimension line doc.line(heightDimX, doorTextGapEnd, heightDimX, doorY + doorHeight); // Arrows doc.line(heightDimX, doorY, heightDimX - arrowSize/2, doorY + arrowSize); doc.line(heightDimX, doorY, heightDimX + arrowSize/2, doorY + arrowSize); doc.line(heightDimX, doorY + doorHeight, heightDimX - arrowSize/2, doorY + doorHeight - arrowSize); doc.line(heightDimX, doorY + doorHeight, heightDimX + arrowSize/2, doorY + doorHeight - arrowSize); // Height text (horizontal, centered in gap) doc.setFontSize(dimensionFontSize); doc.setFont(undefined, 'bold'); const doorHeightText = `${Utils.floatToFraction16(this.currentConfig.doorHeight)}"`; doc.text(doorHeightText, heightDimX, doorY + doorHeight/2 + 1, { align: 'center' }); } // Net opening dimensions below (scaled) const dimLineOffset = Math.max(10, 12 * scaleFactor); const dimLineY = doorY + doorHeight + dimLineOffset; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.5 * scaleFactor); doc.line(startX, dimLineY, startX + totalWidth, dimLineY); // End caps const capSize = Math.max(1.5, 2 * scaleFactor); doc.line(startX, dimLineY - capSize, startX, dimLineY + capSize); doc.line(startX + totalWidth, dimLineY - capSize, startX + totalWidth, dimLineY + capSize); // Net opening text doc.setFontSize(netOpeningFontSize); doc.setFont(undefined, 'bold'); const centerX = startX + (totalWidth / 2); const netOpeningTextOffset = Math.max(4, 5 * scaleFactor); doc.text(`${Utils.floatToFraction16(this.currentConfig.netOpeningWidth)}" (${Utils.inchesToFeetAndInches(this.currentConfig.netOpeningWidth)})`, centerX, dimLineY + netOpeningTextOffset, { align: 'center' }); // Height dimension on left (scaled) const heightDimOffset = Math.max(6, 8 * scaleFactor); const heightDimX = startX - heightDimOffset; const frameTop = doorY - frameTopPadding; const frameBottom = doorY + doorHeight + frameBottomPadding; // Draw dimension line in two parts with gap for text const heightTextGapSize = Math.max(4, 6 * scaleFactor); // Reduced gap size const textGapStart = frameTop + frameTotalHeight/2 - heightTextGapSize; const textGapEnd = frameTop + frameTotalHeight/2 + heightTextGapSize; // Top part of dimension line doc.line(heightDimX, frameTop, heightDimX, textGapStart); // Bottom part of dimension line doc.line(heightDimX, textGapEnd, heightDimX, frameBottom); // End caps const heightCapSize = Math.max(1.5, 2 * scaleFactor); doc.line(heightDimX - heightCapSize, frameTop, heightDimX + heightCapSize, frameTop); doc.line(heightDimX - heightCapSize, frameBottom, heightDimX + heightCapSize, frameBottom); // Height text (horizontal, centered in gap) doc.setFontSize(dimensionFontSize); doc.setFont(undefined, 'bold'); const netHeightText = `${Utils.floatToFraction16(this.currentConfig.netOpeningHeight)}"`; doc.text(netHeightText, heightDimX, frameTop + frameTotalHeight/2 + 1, { align: 'center' }); // No center mullion label if applicable (scaled) if (noCenterMullion) { doc.setFontSize(netOpeningFontSize); doc.setFont(undefined, 'bold'); doc.setTextColor(...accentRed); const mullionLabelText = `${numDoors} DOORS - NO CENTER MULLION`; const mullionLabelOffset = Math.max(8, 10 * scaleFactor); doc.text(mullionLabelText, 105, dimLineY + mullionLabelOffset, { align: 'center' }); } } drawDoorSwingDiagramPDF(doc, startY) { const primaryBlue = [43, 83, 151]; const accentRed = [217, 30, 24]; const darkGray = [51, 51, 51]; // Section header doc.setFillColor(...accentRed); doc.rect(10, startY, 190, 6, 'F'); doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.setTextColor(255, 255, 255); doc.text('DOOR SWING CONFIGURATION', 105, startY + 4, { align: 'center' }); // Swing type doc.setFontSize(10); doc.setFont(undefined, 'normal'); doc.setTextColor(0, 0, 0); doc.text(`Door Swing Configuration: ${this.currentConfig.doorSwing}`, 105, startY + 12, { align: 'center' }); // Calculate layout parameters (must match door layout exactly) const { numDoors, doorSwing, sections, noCenterMullion } = this.currentConfig; let sectionCounts = []; if (sections === "One section") { sectionCounts = [numDoors]; } else { const parts = sections.split(", "); sectionCounts = parts.map(part => parseInt(part.split(" ")[0])); } // Base dimensions (same as door layout) const baseDoorWidth = 15; const baseDoorGap = 2; const baseSectionGap = 3; const baseWallHeight = 8; const availableWidth = 185; // Usable PDF width // Calculate base total width without scaling (same logic as door layout) let baseTotalWidth = numDoors * baseDoorWidth + (numDoors - 1) * baseDoorGap; sectionCounts.forEach((count, i) => { if (i availableWidth) { scaleFactor = availableWidth / baseTotalWidth; // Ensure minimum scale for readability scaleFactor = Math.max(scaleFactor, 0.1); } // Apply scaling to all dimensions const doorWidth = baseDoorWidth * scaleFactor; const doorGap = baseDoorGap * scaleFactor; const sectionGap = baseSectionGap * scaleFactor; const wallHeight = baseWallHeight * scaleFactor; const swingRadius = doorWidth * 0.9; // Slightly less than door width for cleaner look // Calculate actual total width with scaling (must match door layout exactly) let totalWidth = numDoors * doorWidth + (numDoors - 1) * doorGap; sectionCounts.forEach((count, i) => { if (i { for (let i = 0; i < count; i++) { const doorLeftEdge = currentX; const doorRightEdge = currentX + doorWidth; const doorCenterX = currentX + doorWidth / 2; let hingeType = 'L'; if (doorSwing === 'Center Pull') { hingeType = doorNum % 2 === 1 ? 'L' : 'R'; } else if (doorSwing === 'All Left') { hingeType = 'L'; } else if (doorSwing === 'All Right') { hingeType = 'R'; } else if (doorSwing === 'Custom' && this.customSwingPattern) { hingeType = this.customSwingPattern[doorNum - 1] || 'L'; } // Draw door in open position (black vertical line from hinge point) - scaled doc.setDrawColor(0, 0, 0); doc.setLineWidth(Math.max(0.5, 1 * scaleFactor)); if (hingeType === 'L') { // Left hinge - line extends from left edge doc.line(doorLeftEdge, wallY + wallHeight, doorLeftEdge, wallY + wallHeight + swingRadius); } else { // Right hinge - line extends from right edge doc.line(doorRightEdge, wallY + wallHeight, doorRightEdge, wallY + wallHeight + swingRadius); } // Draw swing arc (red dashed quarter circle) - scaled doc.setLineWidth(Math.max(0.3, 0.5 * scaleFactor)); doc.setDrawColor(...accentRed); // Draw arc using small line segments with gaps for dashed effect const segments = Math.max(20, Math.round(40 * scaleFactor)); // Adjust segments based on scale let drawSegment = true; // Alternate between drawing and not drawing for (let j = 0; j < segments; j++) { if (drawSegment) { const angle1 = (j / segments) * (Math.PI / 2); const angle2 = ((j + 1) / segments) * (Math.PI / 2); if (hingeType === 'L') { // Arc from left edge sweeping right const x1 = doorLeftEdge + Math.sin(angle1) * swingRadius; const y1 = wallY + wallHeight + Math.cos(angle1) * swingRadius; const x2 = doorLeftEdge + Math.sin(angle2) * swingRadius; const y2 = wallY + wallHeight + Math.cos(angle2) * swingRadius; doc.line(x1, y1, x2, y2); } else { // Arc from right edge sweeping left const x1 = doorRightEdge - Math.sin(angle1) * swingRadius; const y1 = wallY + wallHeight + Math.cos(angle1) * swingRadius; const x2 = doorRightEdge - Math.sin(angle2) * swingRadius; const y2 = wallY + wallHeight + Math.cos(angle2) * swingRadius; doc.line(x1, y1, x2, y2); } } // Toggle drawing every 2 segments for dashed effect if (j % 2 === 1) drawSegment = !drawSegment; } // Door number below (scaled) doc.setFontSize(doorNumberFontSize); doc.setFont(undefined, 'bold'); doc.setTextColor(...darkGray); const doorNumberOffset = Math.max(3, 5 * scaleFactor); doc.text(doorNum.toString(), doorCenterX, wallY + wallHeight + swingRadius + doorNumberOffset, { align: 'center' }); // Hinge type above wall (scaled) doc.setFontSize(hingeTypeFontSize); const hingeTypeOffset = Math.max(1.5, 2 * scaleFactor); doc.text(hingeType, doorCenterX, wallY - hingeTypeOffset, { align: 'center' }); currentX += doorWidth; // Handle gaps between doors within the section (must match door layout exactly) if (i < count - 1) { currentX += doorGap; } doorNum++; } // Add section gap if not last section (must match door layout exactly) if (sectionIndex line.trim()); shipLines.forEach((line, index) => { if (index parseInt(part.split(" ")[0])); } // Base dimensions const baseDoorWidth = 15; const baseDoorGap = 0.5; const baseSectionGap = 3; const availableWidth = 190; // Calculate base total width let baseTotalWidth = numDoors * baseDoorWidth + (numDoors - 1) * baseDoorGap; sectionCounts.forEach((count, i) => { if (i availableWidth) { scaleFactor = availableWidth / baseTotalWidth; scaleFactor = Math.max(scaleFactor, 0.15); // Change this value to 0.25 or 0.28 for more aggressive scaling } // Scale-dependent dimensions and font sizes const boxHeight = Math.max(40, 50 * scaleFactor); const headerHeight = Math.max(5, 6 * scaleFactor); const headerFontSize = Math.max(7, 9 * scaleFactor); const labelFontSize = Math.max(6, 7 * scaleFactor); const lineSpacing = Math.max(6, 8 * scaleFactor); const optionLineSpacing = Math.max(5, 6 * scaleFactor); const columnOffset = Math.max(8, 10 * scaleFactor); const valueOffset = Math.max(2, 3 * scaleFactor); // Configuration Details section - Single box with 2 columns doc.setDrawColor(...primaryBlue); doc.setLineWidth(0.5 * scaleFactor); doc.rect(10, startY, 95, boxHeight, 'D'); doc.rect(105, startY, 95, boxHeight, 'D'); // Headers doc.setFillColor(...primaryBlue); doc.rect(10, startY, 95, headerHeight, 'F'); doc.rect(105, startY, 95, headerHeight, 'F'); doc.setFontSize(headerFontSize); doc.setFont(undefined, 'bold'); doc.setTextColor(255, 255, 255); const headerYPos = startY + (headerHeight * 0.7); // Adjust text position based on header height doc.text('CONFIGURATION DETAILS', 57.5, headerYPos, { align: 'center' }); doc.text('SELECTED OPTIONS', 152.5, headerYPos, { align: 'center' }); // Configuration details - 2 columns doc.setFontSize(labelFontSize); doc.setTextColor(...darkGray); let leftY = startY + headerHeight + (columnOffset * 0.5); // Left column - first half const configItems = [ { label: 'Frame Series:', value: this.currentConfig.series }, { label: 'Number of Doors:', value: this.currentConfig.numDoors.toString() }, { label: 'C/C Dimension:', value: this.getC2CDimension() }, { label: 'Door Size:', value: `${this.currentConfig.catalogSize} × ${this.currentConfig.doorHeightLabel}` }, { label: 'Net Opening:', value: `${Utils.floatToFraction16(this.currentConfig.netOpeningWidth)}" × ${Utils.floatToFraction16(this.currentConfig.netOpeningHeight)}"` }, { label: 'Frame Config:', value: this.getFrameConfigShort() } ]; // Split config items into 2 columns const midPoint = Math.ceil(configItems.length / 2); // Helper function to wrap text const wrapText = (text, maxWidth) => { const words = text.split(' '); const lines = []; let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; const textWidth = doc.getTextWidth(testLine); if (textWidth <= maxWidth) { currentLine = testLine; } else { if (currentLine) { lines.push(currentLine); currentLine = word; } else { lines.push(word); // Word is too long but we have to include it } } } if (currentLine) { lines.push(currentLine); } return lines; }; // Column width for text wrapping (accounting for margins) const columnWidth = 40; // Approximate width for each column // First column for (let i = 0; i < midPoint; i++) { doc.setFont(undefined, 'bold'); doc.text(configItems[i].label, 12, leftY); doc.setFont(undefined, 'normal'); // Handle text wrapping for long values (especially Frame Config) const wrappedLines = wrapText(configItems[i].value, columnWidth); for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) { doc.text(wrappedLines[lineIndex], 12, leftY + valueOffset + (lineIndex * (labelFontSize * 0.4))); } // Adjust spacing based on number of wrapped lines const extraLines = Math.max(0, wrappedLines.length - 1); leftY += lineSpacing + (extraLines * (labelFontSize * 0.4)); } // Second column leftY = startY + headerHeight + (columnOffset * 0.5); const secondColumnX = 55; for (let i = midPoint; i < configItems.length; i++) { doc.setFont(undefined, 'bold'); doc.text(configItems[i].label, secondColumnX, leftY); doc.setFont(undefined, 'normal'); // Handle text wrapping for long values const wrappedLines = wrapText(configItems[i].value, columnWidth); for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) { doc.text(wrappedLines[lineIndex], secondColumnX, leftY + valueOffset + (lineIndex * (labelFontSize * 0.4))); } // Adjust spacing based on number of wrapped lines const extraLines = Math.max(0, wrappedLines.length - 1); leftY += lineSpacing + (extraLines * (labelFontSize * 0.4)); } // Selected options - 2 columns let rightY = startY + headerHeight + (columnOffset * 0.5); const optionItems = [ { label: 'Application:', value: this.currentConfig.applicationType === 'cooler' ? 'Cooler' : 'Freezer' }, { label: 'Shelving:', value: this.getShelvingDescription() }, { label: 'Glass Package:', value: this.currentConfig.glassPackage === 'high_humidity' ? 'High Humidity' : 'Standard' }, { label: 'Door Swing:', value: this.currentConfig.doorSwing }, { label: 'Lighting:', value: this.currentConfig.lightingType === 'high_definition' ? 'HD LED (5yr)' : 'T8 LED (2yr warr.)' }, { label: 'Frame Finish:', value: this.getFrameFinishDescription() }, { label: 'Handle Type:', value: this.getHandleTypeDescription(this.currentConfig.handleType) }, { label: 'Locks:', value: this.currentConfig.locks === 'yes' ? 'Yes' : 'No' } ]; const optMidPoint = Math.ceil(optionItems.length / 2); // First column of options for (let i = 0; i < optMidPoint; i++) { doc.setFont(undefined, 'bold'); doc.text(optionItems[i].label, 107, rightY); doc.setFont(undefined, 'normal'); // Handle text wrapping for long option values const wrappedLines = wrapText(optionItems[i].value, columnWidth); for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) { doc.text(wrappedLines[lineIndex], 107, rightY + valueOffset + (lineIndex * (labelFontSize * 0.4))); } // Adjust spacing based on number of wrapped lines const extraLines = Math.max(0, wrappedLines.length - 1); rightY += optionLineSpacing + (extraLines * (labelFontSize * 0.4)); } // Second column of options rightY = startY + headerHeight + (columnOffset * 0.5); const optionsSecondColumnX = 152; for (let i = optMidPoint; i < optionItems.length; i++) { doc.setFont(undefined, 'bold'); doc.text(optionItems[i].label, optionsSecondColumnX, rightY); doc.setFont(undefined, 'normal'); // Handle text wrapping for long option values const wrappedLines = wrapText(optionItems[i].value, columnWidth); for (let lineIndex = 0; lineIndex w.charAt(0).toUpperCase() + w.slice(1) ).join(' '); } generateCombinedPDF(returnBlob = false) { const { jsPDF } = window.jspdf; const doc = new jsPDF(); const formData = this.getValidatedFormData(); if (!formData) return null; const quoteNumber = 'CDS-' + new Date().getTime().toString().slice(-8); const currentDate = new Date().toLocaleDateString(); // Add each configuration this.savedConfigurations.forEach((config, index) => { if (index > 0) { doc.addPage(); } // Temporarily set currentConfig const originalConfig = this.currentConfig; this.currentConfig = config; // Generate the page this.generatePDFPage(doc, quoteNumber, currentDate, index + 1, this.savedConfigurations.length); // Restore original config this.currentConfig = originalConfig; }); if (returnBlob) { return doc.output('blob'); } else { const timestamp = new Date().getTime(); const filename = `CDS_Configurations_${formData.customerCompany ? formData.customerCompany.replace(/\s+/g, '_') : formData.customerName.replace(/\s+/g, '_')}_${timestamp}.pdf`; doc.save(filename); } } generatePDFPage(doc, quoteNumber, currentDate, pageNum, totalPages) { const formData = this.getValidatedFormData(); if (!formData) return; const primaryBlue = [43, 83, 151]; const accentRed = [217, 30, 24]; const darkGray = [51, 51, 51]; const lightGray = [248, 248, 248]; // Header with blue background doc.setFillColor(...primaryBlue); doc.rect(0, 0, 210, 25, 'F'); // CDS Logo placeholder doc.setFillColor(255, 255, 255); doc.roundedRect(10, 7, 30, 11, 2, 2, 'F'); doc.setFontSize(12); doc.setTextColor(...primaryBlue); doc.setFont(undefined, 'bold'); doc.text('CDS', 25, 13.5, { align: 'center' }); doc.setFontSize(6); doc.setFont(undefined, 'normal'); doc.text('Commercial Display Systems', 25, 16, { align: 'center' }); // Title doc.setFontSize(14); doc.setFont(undefined, 'bold'); doc.setTextColor(255, 255, 255); doc.text('DOOR CONFIGURATION', 105, 15, { align: 'center' }); // Quote info in header doc.setFontSize(9); doc.setFont(undefined, 'normal'); const headerInfo = totalPages > 1 ? `#${quoteNumber} | Page ${pageNum} of ${totalPages} | ${currentDate}` : `#${quoteNumber} | ${currentDate}`; doc.text(headerInfo, 200, 15, { align: 'right' }); // Company info bar doc.setFillColor(245, 245, 245); doc.rect(0, 25, 210, 8, 'F'); doc.setFontSize(8); doc.setTextColor(...darkGray); doc.text('17341 Sierra Highway, Canyon Country, CA 91351 | www.cdsdoors.net | (800) 478-3790', 105, 29, { align: 'center' }); // Customer Info Section this.addCustomerInfoPDF(doc, 35, formData); // Configuration title const titleY = 65; const configTitle = `${this.currentConfig.numDoors}-Door ${this.currentConfig.series} Series Configuration`; doc.setFontSize(16); doc.setFont(undefined, 'bold'); doc.setTextColor(...primaryBlue); doc.text(configTitle, 105, titleY, { align: 'center' }); // Door Layout Section const layoutY = 75; this.drawDoorLayoutPDF(doc, layoutY); // Door Swing Section const swingY = 155; if (this.currentConfig.doorSwing && this.currentConfig.doorSwing !== 'None') { this.drawDoorSwingDiagramPDF(doc, swingY); } // Configuration Details const detailsY = this.currentConfig.doorSwing !== 'None' ? 210 : 160; this.addConfigurationDetailsPDF(doc, detailsY); // Footer doc.setFillColor(...primaryBlue); doc.rect(0, 280, 210, 17, 'F'); doc.setTextColor(255, 255, 255); doc.setFontSize(8); doc.setFont(undefined, 'normal'); doc.text('Commercial Display Systems • Professional Refrigeration Solutions', 105, 290, { align: 'center' }); } // ============================================== // SALES SUBMISSION // ============================================== async sendToSales() { if (!this.currentConfig && this.savedConfigurations.length === 0) { this.showWarningMessage('Please calculate a configuration first'); return; } const formData = this.getValidatedFormData(); if (!formData) return; // Check rate limiting if (!this.apiRateLimiter.canMakeRequest()) { this.showWarningMessage('Too many requests. Please wait a moment before trying again.'); return; } const config = Environment.getConfig(); if (!config.api.googleAppsScriptUrl || config.api.googleAppsScriptUrl === 'YOUR_GOOGLE_APPS_SCRIPT_WEB_APP_URL_HERE') { DebugUtils.log('Google Apps Script URL not configured, falling back to mailto'); this.sendToSalesMailto(); return; } try { this.showLoadingSpinner(); let pdfBlob; let pdfFilename; if (this.savedConfigurations.length > 0) { pdfBlob = this.generateCombinedPDF(true); pdfFilename = `CDS_Configurations_${formData.customerCompany.replace(/\s+/g, '_')}_${new Date().getTime()}.pdf`; } else { pdfBlob = this.generatePDF(true); pdfFilename = `CDS_Configuration_${formData.customerCompany.replace(/\s+/g, '_')}_${new Date().getTime()}.pdf`; } const pdfBase64 = await Utils.blobToBase64(pdfBlob); const requestData = { customerName: formData.customerName, customerEmail: formData.customerEmail, customerPhone: formData.customerPhone || 'Not provided', customerCompany: formData.customerCompany, shipToLocation: formData.shipToLocation || 'Not provided', configurationText: this.generateFullConfigurationText(), quoteNumber: 'CDS-' + new Date().getTime().toString().slice(-8), pdfFile: pdfBase64, pdfFilename: pdfFilename, attachments: [] }; for (let i = 0; i 0) { attachmentNotice = `\n\nATTACHMENTS:\n-----------\n`; attachmentNotice += `Please note: ${this.attachedFiles.length} file(s) need to be attached:\n`; this.attachedFiles.forEach((file, index) => { attachmentNotice += `${index + 1}. ${file.name} (${Utils.formatFileSize(file.size)})\n`; }); attachmentNotice += '\n[Note: Due to email client limitations, please manually attach the files and the generated PDF when the email opens]'; } const subject = `Door Configuration Request - ${this.currentConfig ? this.currentConfig.series : 'Multiple Configurations'} - ${new Date().toLocaleDateString()}`; const formData = this.getValidatedFormData(); if (!formData) return; const body = `Dear CDS Sales Team, Please find below our door configuration request: ${configText}${attachmentNotice} Best regards, ${formData.customerName}`; const mailtoLink = `mailto:hsoto@cdsdoors.net?cc=dfreiberg@cdsdoors.net&subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; if (this.savedConfigurations.length > 0) { this.generateCombinedPDF(); } else { this.generatePDF(); } if (this.attachedFiles.length > 0) { alert(`Please remember to:\n\n1. Attach the downloaded PDF\n2. Attach the following files:\n${this.attachedFiles.map(f => ' • ' + f.name).join('\n')}\n\nYour email client will open now.`); } else { alert('Please remember to attach the downloaded PDF to your email.\n\nYour email client will open now.'); } window.location.href = mailtoLink; setTimeout(() => { this.showSalesModal('success'); }, 1000); } catch (error) { this.showSalesModal('error'); } } generateFullConfigurationText() { const formData = this.getValidatedFormData(); if (!formData) return ''; const date = new Date().toLocaleDateString(); let text = `CDS DOOR CONFIGURATION REQUEST ===================================== Date: ${date} CUSTOMER INFORMATION: -------------------- Name: ${formData.customerName} Email: ${formData.customerEmail || 'Not provided'} Phone: ${formData.customerPhone || 'Not provided'} Company: ${formData.customerCompany} SHIPPING INFORMATION: -------------------- Ship To: ${formData.shipToLocation || 'Not provided'} `; if (this.savedConfigurations.length > 0) { text += 'MULTIPLE CONFIGURATIONS:\n'; text += '=======================\n\n'; this.savedConfigurations.forEach((config, index) => { text += `CONFIGURATION ${index + 1}:\n`; text += '----------------\n'; text += this.formatSingleConfiguration(config); text += '\n\n'; }); } else if (this.currentConfig) { text += 'CONFIGURATION DETAILS:\n'; text += '---------------------\n'; text += this.formatSingleConfiguration(this.currentConfig); } text += '\n====================================='; return text; } formatSingleConfiguration(config) { let text = `Frame Series: ${config.series} Number of Doors: ${config.numDoors} Door Dimensions: ${Utils.floatToFraction16(config.doorWidth)}" × ${Utils.floatToFraction16(config.doorHeight)}" Application: ${config.applicationType === 'cooler' ? 'Cooler' : 'Freezer'} NET OPENING DIMENSIONS: Width: ${Utils.floatToFraction16(config.netOpeningWidth)}" Height: ${Utils.floatToFraction16(config.netOpeningHeight)}"`; if (config.noCenterMullion) { const doorPairs = Math.floor(config.numDoors / 2); const mullionDeduct = doorPairs * 0.625; text += `\nNo Center Mullion: Yes (${doorPairs} pairs × 5/8" = ${Utils.floatToFraction16(mullionDeduct)}" deducted)`; } text += `\n\nDOOR CONFIGURATION: Handle Type: ${this.getHandleTypeDescription(config.handleType)} Door Swing: ${config.doorSwing || 'None'}`; if (config.swingPattern) { text += `\nSwing Pattern: ${config.swingPattern}`; } text += `\n\nADDITIONAL OPTIONS: Glass Package: ${config.glassPackage === 'high_humidity' ? 'High Humidity' : 'Standard'} Lighting: ${config.lightingType === 'high_definition' ? 'High Definition (5 yr warranty)' : 'Standard T8 LED (2 yr warranty)'} Shelving: ${config.shelving === 'yes' ? `${config.shelvingColor} - ${config.shelvingSize}` : 'None'} Frame Finish: ${config.frameFinish} Locks: ${config.locks === 'yes' ? 'Yes' : 'No'} SECTION CONFIGURATION: ${config.sections}`; if (config.notes) { text += `\n\nADDITIONAL NOTES: ${config.notes}`; } return text; } // ============================================== // 3D VIEWER FUNCTIONALITY // ============================================== show3DView() { if (!this.currentConfig) { this.showWarningMessage('Please calculate a configuration first'); return; } // Update 3D config from current configuration this.config3D.frameSeries = this.currentConfig.series; this.config3D.numDoors = this.currentConfig.numDoors; this.config3D.doorWidth = this.currentConfig.doorWidth; this.config3D.doorHeight = this.currentConfig.doorHeight; this.config3D.doorSwing = this.currentConfig.doorSwing; this.config3D.noCenterMullion = this.currentConfig.noCenterMullion; // Calculate proper clear openings based on frame series if (this.config3D.frameSeries === 'Legacy') { this.config3D.clearOpening = this.config3D.doorWidth - 1.4375; this.config3D.worstCaseClearOpening = this.config3D.doorWidth - 3; } else if (this.config3D.frameSeries === 'Advantage/Eco') { this.config3D.clearOpening = this.config3D.doorWidth - 0.8125; this.config3D.worstCaseClearOpening = this.config3D.doorWidth - 2.375; } else { this.config3D.clearOpening = this.config3D.doorWidth - 0.5; this.config3D.worstCaseClearOpening = this.config3D.doorWidth - 2.0625; } // Update shelf depth const shelvingSize = document.getElementById('shelvingSize').value; if (shelvingSize.includes('24')) this.config3D.shelfDepth = 24; else if (shelvingSize.includes('36')) this.config3D.shelfDepth = 36; else if (shelvingSize.includes('48')) this.config3D.shelfDepth = 48; else this.config3D.shelfDepth = 27; this.config3D.minServiceArea = this.config3D.shelfDepth + 10; // Update the configuration display in the viewer const elements = { frameSeries: document.getElementById('viewer-frame-series'), doorSize: document.getElementById('viewer-door-size'), numDoors: document.getElementById('viewer-num-doors'), shelfConfig: document.getElementById('viewer-shelf-config') }; if (elements.frameSeries) { elements.frameSeries.textContent = this.currentConfig.series + (this.currentConfig.series === 'Legacy' ? ' (T-Mullion)' : ''); } if (elements.doorSize) { elements.doorSize.textContent = this.currentConfig.catalogSize + ' Door'; } if (elements.numDoors) { elements.numDoors.textContent = this.currentConfig.numDoors + ' Doors'; } // Update shelf configuration display let shelfText = 'None'; if (this.currentConfig.shelving === 'yes') { const sizeText = this.currentConfig.shelvingSize.replace(/_/g, ' ').replace(/deep/i, 'Deep'); const sizeNumber = sizeText.match(/\d+/)[0]; shelfText = sizeNumber + '" Deep'; if (this.currentConfig.shelvingSize.includes('heavy_duty')) { shelfText += ' (Heavy Duty)'; } else { shelfText += ' (Standard)'; } } if (elements.shelfConfig) { elements.shelfConfig.textContent = shelfText; } // Update door swing configuration display const swingElement = document.getElementById('viewer-door-swing'); if (swingElement) { swingElement.textContent = this.currentConfig.doorSwing; } const modal = document.getElementById('viewer3DModal'); if (modal) { modal.style.display = 'block'; // Initialize 3D scene if not already done if (!this.scene3D) { setTimeout(() => { this.init3DScene(); this.animate3D(); }, 100); } else { // Update existing scene with new configuration this.createFrameSystem3D(); } } } close3DModal() { try { // Clean up existing 3D objects to prevent memory leaks if (this.frameSystem3D) { this.frameSystem3D.traverse((child) => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(material => material.dispose()); } else { child.material.dispose(); } } }); } // Clean up textures if (this.renderer3D) { this.renderer3D.dispose(); } const modal = document.getElementById('viewer3DModal'); if (modal) { modal.style.display = 'none'; } } catch (error) { DebugUtils.error('Error closing 3D modal', error); } } init3DScene() { try { const canvas = document.getElementById('viewer3D'); if (!canvas) { throw new Error('3D canvas element not found'); } // Check WebGL support if (!Environment.supportsWebGL) { throw new Error('WebGL not supported in this browser'); } const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) { throw new Error('WebGL context could not be created'); } // Scene this.scene3D = new THREE.Scene(); this.scene3D.background = new THREE.Color(0xf8f8f8); this.scene3D.fog = new THREE.Fog(0xf8f8f8, 50, 200); // Camera const aspect = window.innerWidth / window.innerHeight; this.camera3D = new THREE.PerspectiveCamera(AppConfig.viewer3D.camera.fov, aspect, AppConfig.viewer3D.camera.near, AppConfig.viewer3D.camera.far); const frameWidth = this.config3D.numDoors * this.config3D.doorWidth * 0.05; const cameraDistance = frameWidth * 2.5; this.camera3D.position.set(cameraDistance * 0.7, cameraDistance * 0.4, cameraDistance); this.camera3D.lookAt(0, 0, 0); // Renderer this.renderer3D = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); this.renderer3D.setSize(window.innerWidth, window.innerHeight); this.renderer3D.shadowMap.enabled = true; this.renderer3D.shadowMap.type = THREE.PCFSoftShadowMap; // Lighting this.setupLighting3D(); // Create frame system this.createFrameSystem3D(); // Controls this.setupControls3D(); DebugUtils.log('3D scene initialized successfully'); } catch (error) { DebugUtils.error('3D initialization failed', error); this.showWarningMessage('3D viewer not available: ' + error.message); // Disable 3D button const view3DBtn = document.getElementById('view3DBtn'); if (view3DBtn) { view3DBtn.disabled = true; view3DBtn.textContent = '3D Not Available'; } } } setupLighting3D() { const config = AppConfig.viewer3D.lighting; const ambientLight = new THREE.AmbientLight(config.ambient.color, config.ambient.intensity); this.scene3D.add(ambientLight); const mainLight = new THREE.DirectionalLight(config.directional.color, config.directional.intensity); mainLight.position.set(5, 10, 5); mainLight.castShadow = true; mainLight.shadow.camera.near = 0.1; mainLight.shadow.camera.far = 50; mainLight.shadow.camera.left = -15; mainLight.shadow.camera.right = 15; mainLight.shadow.camera.top = 15; mainLight.shadow.camera.bottom = -15; mainLight.shadow.mapSize.width = 2048; mainLight.shadow.mapSize.height = 2048; this.scene3D.add(mainLight); const fillLight = new THREE.DirectionalLight(config.directional.color, 0.3); fillLight.position.set(-5, 5, -5); this.scene3D.add(fillLight); const topLight = new THREE.DirectionalLight(config.directional.color, 0.2); topLight.position.set(0, 15, 0); this.scene3D.add(topLight); } createFrameSystem3D() { if (this.frameSystem3D) { this.scene3D.remove(this.frameSystem3D); } if (this.dimensionHelpers) { this.scene3D.remove(this.dimensionHelpers); } if (this.floorOutline) { this.scene3D.remove(this.floorOutline); } if (this.serviceAreaOutline) { this.scene3D.remove(this.serviceAreaOutline); } if (this.doorSwingArcs) { this.scene3D.remove(this.doorSwingArcs); this.doorSwingArcs = null; } this.doors3D = []; this.frameSystem3D = new THREE.Group(); const scale = 0.05; const totalWidth = this.config3D.numDoors * this.config3D.doorWidth * scale; const frameHeight = this.config3D.frameHeight * scale; const depth = this.config3D.coolerDepth * scale; // this.updateDimensionDisplay3D(totalWidth, frameHeight, depth); this.createFloorOutline3D(totalWidth, depth); this.createDoorFrame3D(totalWidth, frameHeight); this.createShelvingSystem3D(totalWidth, frameHeight, depth); this.createLEDLighting3D(totalWidth, frameHeight, depth); this.createDoors3D(totalWidth, frameHeight); this.createDimensionHelpers3D(totalWidth, frameHeight, depth); this.createServiceAreaOutline3D(totalWidth, depth); this.createDoorSwingArcs3D(totalWidth); // Update minimum service area when recreating frame system this.config3D.minServiceArea = this.config3D.shelfDepth + 10; this.scene3D.add(this.frameSystem3D); if (this.currentViewMode === '3d') { const frameWidth = this.config3D.numDoors * this.config3D.doorWidth * 0.05; const cameraDistance = frameWidth * 2.5; this.camera3D.position.set(cameraDistance * 0.7, cameraDistance * 0.4, cameraDistance); this.camera3D.lookAt(0, 0, 0); } this.setViewMode(this.currentViewMode); this.updateVisibility(); } createFloorOutline3D(width, depth) { this.floorOutline = new THREE.Group(); const lineMaterial = new THREE.LineDashedMaterial({ color: 0xcccccc, dashSize: 0.1, gapSize: 0.05 }); const serviceDepth = this.config3D.minServiceArea * 0.05; const points = [ new THREE.Vector3(-width/2, -2.08, 0), new THREE.Vector3(width/2, -2.08, 0), new THREE.Vector3(width/2, -2.08, -Math.max(depth, serviceDepth)), new THREE.Vector3(-width/2, -2.08, -Math.max(depth, serviceDepth)), new THREE.Vector3(-width/2, -2.08, 0) ]; const geometry = new THREE.BufferGeometry().setFromPoints(points); const outline = new THREE.Line(geometry, lineMaterial); outline.computeLineDistances(); this.floorOutline.add(outline); this.scene3D.add(this.floorOutline); } createDoorFrame3D(width, height) { const config = AppConfig.viewer3D.materials; const frameMaterial = new THREE.MeshStandardMaterial({ color: config.frame.color, metalness: config.frame.metalness, roughness: config.frame.roughness }); const frameDepth = 0.1; const topExtension = 0.1125; const bottomSill = 0.1125; const sidePostWidth = 0.1; const topRailGeometry = new THREE.BoxGeometry(width + sidePostWidth * 2, topExtension * 2, frameDepth); const topRail = new THREE.Mesh(topRailGeometry, frameMaterial); topRail.position.set(0, height/2 - topExtension, 0); topRail.castShadow = true; this.frameSystem3D.add(topRail); const bottomRailGeometry = new THREE.BoxGeometry(width + sidePostWidth * 2, bottomSill * 2, frameDepth); const bottomRail = new THREE.Mesh(bottomRailGeometry, frameMaterial); bottomRail.position.set(0, -height/2 + bottomSill, 0); bottomRail.castShadow = true; this.frameSystem3D.add(bottomRail); const clearOpeningHeight = height - (topExtension * 2) - (bottomSill * 2); const sidePostGeometry = new THREE.BoxGeometry(sidePostWidth, height, frameDepth); const leftPost = new THREE.Mesh(sidePostGeometry, frameMaterial); leftPost.position.set(-width/2 - sidePostWidth/2, 0, 0); leftPost.castShadow = true; this.frameSystem3D.add(leftPost); const rightPost = new THREE.Mesh(sidePostGeometry, frameMaterial); rightPost.position.set(width/2 + sidePostWidth/2, 0, 0); rightPost.castShadow = true; this.frameSystem3D.add(rightPost); const doorWidth = width / this.config3D.numDoors; const mullionMaterial = new THREE.MeshStandardMaterial({ color: config.frame.color, metalness: config.frame.metalness, roughness: config.frame.roughness }); for (let i = 1; i < this.config3D.numDoors; i++) { const xPos = -width/2 + i * doorWidth; // For No Center Mullion with Center Pull, skip mullions between L-R pairs only if (this.config3D.noCenterMullion && this.config3D.doorSwing === 'Center Pull') { // Determine swing types for adjacent doors const leftDoorHinge = ((i-1) + 1) % 2 === 1 ? 'L' : 'R'; // Previous door (door i-1) const rightDoorHinge = (i + 1) % 2 === 1 ? 'L' : 'R'; // Current door (door i) // Skip mullion if left door is L-hinge and right door is R-hinge (swinging away from each other) if (leftDoorHinge === 'L' && rightDoorHinge === 'R') { continue; } } if (this.config3D.frameSeries === 'Edge') { if (i % 2 === 0) { const mullion = new THREE.Mesh( new THREE.BoxGeometry(0.08, clearOpeningHeight, 0.08), mullionMaterial ); mullion.position.set(xPos, 0, 0); mullion.castShadow = true; this.frameSystem3D.add(mullion); } continue; } if (this.config3D.frameSeries === 'Legacy') { const mullionGroup = new THREE.Group(); const vertical = new THREE.Mesh( new THREE.BoxGeometry(0.08, clearOpeningHeight, 0.05), mullionMaterial ); vertical.position.set(xPos, 0, 0); vertical.castShadow = true; mullionGroup.add(vertical); const horizontal = new THREE.Mesh( new THREE.BoxGeometry(0.15, clearOpeningHeight, 0.02), mullionMaterial ); horizontal.position.set(xPos, 0, -0.035); horizontal.castShadow = true; mullionGroup.add(horizontal); this.frameSystem3D.add(mullionGroup); } else if (this.config3D.frameSeries === 'Advantage/Eco') { const mullion = new THREE.Mesh( new THREE.BoxGeometry(0.08, clearOpeningHeight, 0.08), mullionMaterial ); mullion.position.set(xPos, 0, 0); mullion.castShadow = true; this.frameSystem3D.add(mullion); } } } createShelvingSystem3D(width, height, depth) { const shelfGroup = new THREE.Group(); const postMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.7, roughness: 0.3 }); const bracketMaterial = new THREE.MeshStandardMaterial({ color: 0x999999, metalness: 0.5, roughness: 0.4 }); const wireMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd, metalness: 0.8, roughness: 0.2 }); const postWidth = 0.08; const postDepth = 0.06; const postHeight = height - 0.5; const mountBracketMaterial = new THREE.MeshStandardMaterial({ color: 0x777777, metalness: 0.6, roughness: 0.4 }); const doorWidth = width / this.config3D.numDoors; const frontPosts = []; const backPosts = []; // In createShelvingSystem3D method, modify the loop that calls createPostsAtPosition3D: if (this.config3D.frameSeries === 'Edge') { for (let i = 0; i 0 && i < this.config3D.numDoors && i % 2 !== 0) { continue; } this.createPostsAtPosition3D(xPos, i, shelfGroup, postMaterial, mountBracketMaterial, postWidth, postDepth, postHeight, height, depth, frontPosts, backPosts); } } else { // For Legacy and Advantage/Eco - ALWAYS create posts at every door position // This ensures single door shelving regardless of No Center Mullion setting for (let i = 0; i { if (this.config3D.frameSeries === 'Edge') { for (let pair = 0; pair < Math.ceil(this.config3D.numDoors / 2); pair++) { this.createEdgeShelfMesh(pair, width, depth, doorWidth, yPos, wireMaterial, shelfGroup); } } else { // For Legacy and Advantage/Eco - create individual shelf for each door for (let doorIndex = 0; doorIndex < this.config3D.numDoors; doorIndex++) { this.createSingleDoorShelfMesh(doorIndex, width, depth, doorWidth, yPos, wireMaterial, shelfGroup); } } }); this.frameSystem3D.add(shelfGroup); } createPostsAtPosition3D(xPos, index, shelfGroup, postMaterial, mountBracketMaterial, postWidth, postDepth, postHeight, height, depth, frontPosts, backPosts) { const frontTopBracket = new THREE.Group(); const bracketShape = new THREE.Shape(); bracketShape.moveTo(0, 0); bracketShape.lineTo(0.08, 0); bracketShape.lineTo(0.08, 0.03); bracketShape.lineTo(0.06, 0.03); bracketShape.lineTo(0.06, 0.08); bracketShape.lineTo(0.08, 0.08); bracketShape.lineTo(0.08, 0.11); bracketShape.lineTo(0, 0.11); bracketShape.lineTo(0, 0.08); bracketShape.lineTo(0.02, 0.08); bracketShape.lineTo(0.02, 0.03); bracketShape.lineTo(0, 0.03); const bracketExtrudeSettings = { depth: 0.06, bevelEnabled: false }; const bracketGeometry = new THREE.ExtrudeGeometry(bracketShape, bracketExtrudeSettings); const bracket = new THREE.Mesh(bracketGeometry, mountBracketMaterial); bracket.rotation.y = Math.PI / 2; bracket.rotation.z = Math.PI / 2; bracket.position.set(xPos - 0.03, height/2 - 0.25, -0.08); bracket.castShadow = true; frontTopBracket.add(bracket); const hookTab1 = new THREE.Mesh( new THREE.BoxGeometry(0.02, 0.04, 0.02), mountBracketMaterial ); hookTab1.position.set(xPos - 0.04, height/2 - 0.22, -0.02); frontTopBracket.add(hookTab1); const hookTab2 = new THREE.Mesh( new THREE.BoxGeometry(0.02, 0.04, 0.02), mountBracketMaterial ); hookTab2.position.set(xPos + 0.04, height/2 - 0.22, -0.02); frontTopBracket.add(hookTab2); shelfGroup.add(frontTopBracket); const frontBottomBracket = new THREE.Group(); const bottomBracket = bracket.clone(); bottomBracket.position.set(xPos - 0.03, -height/2 + 0.25, -0.08); frontBottomBracket.add(bottomBracket); const bottomHook1 = hookTab1.clone(); bottomHook1.position.set(xPos - 0.04, -height/2 + 0.28, -0.02); frontBottomBracket.add(bottomHook1); const bottomHook2 = hookTab2.clone(); bottomHook2.position.set(xPos + 0.04, -height/2 + 0.28, -0.02); frontBottomBracket.add(bottomHook2); shelfGroup.add(frontBottomBracket); const frontPostGeometry = new THREE.BoxGeometry(postWidth, postHeight, postDepth); const frontPost = new THREE.Mesh(frontPostGeometry, postMaterial); frontPost.position.set(xPos, -0.05, -0.12); frontPost.castShadow = true; shelfGroup.add(frontPost); frontPosts.push({mesh: frontPost, x: xPos}); const frontSlotGroup = new THREE.Group(); const slotCount = Math.floor(postHeight / 0.1); const slotSpacing = postHeight / slotCount; for (let s = 0; s < slotCount; s++) { const slotY = -postHeight/2 + s * slotSpacing + slotSpacing/2; const slot = new THREE.Mesh( new THREE.BoxGeometry(postWidth * 0.3, 0.025, 0.008), new THREE.MeshBasicMaterial({ color: 0x444444 }) ); slot.position.set(0, slotY, -postDepth/2 - 0.002); frontSlotGroup.add(slot); } frontSlotGroup.position.copy(frontPost.position); shelfGroup.add(frontSlotGroup); const backPost = new THREE.Mesh(frontPostGeometry, postMaterial); backPost.position.set(xPos, -0.05, -depth + 0.1); backPost.castShadow = true; shelfGroup.add(backPost); backPosts.push({mesh: backPost, x: xPos}); const backSlotGroup = new THREE.Group(); for (let s = 0; s < slotCount; s++) { const slotY = -postHeight/2 + s * slotSpacing + slotSpacing/2; const slot = new THREE.Mesh( new THREE.BoxGeometry(postWidth * 0.3, 0.025, 0.008), new THREE.MeshBasicMaterial({ color: 0x444444 }) ); slot.position.set(0, slotY, postDepth/2 + 0.002); backSlotGroup.add(slot); } backSlotGroup.position.copy(backPost.position); shelfGroup.add(backSlotGroup); } createEdgeShelfMesh(pair, width, depth, doorWidth, yPos, wireMaterial, shelfGroup) { const shelfMesh = new THREE.Group(); const startX = -width/2 + (pair * 2 * doorWidth); const endX = Math.min(startX + (2 * doorWidth), width/2); const shelfWidth = endX - startX; const shelfCenterX = (startX + endX) / 2; const frameThickness = 0.015; const frontRailGeometry = new THREE.CylinderGeometry(frameThickness, frameThickness, shelfWidth, 12); const frontRail = new THREE.Mesh(frontRailGeometry, wireMaterial); frontRail.rotation.z = Math.PI / 2; frontRail.position.set(shelfCenterX, yPos, -0.12); frontRail.castShadow = true; shelfMesh.add(frontRail); const backRail = new THREE.Mesh(frontRailGeometry, wireMaterial); backRail.rotation.z = Math.PI / 2; backRail.position.set(shelfCenterX, yPos, -depth + 0.1); backRail.castShadow = true; shelfMesh.add(backRail); const actualShelfDepth = Math.abs((-depth + 0.1) - (-0.12)); const leftRail = new THREE.Mesh( new THREE.CylinderGeometry(frameThickness * 0.8, frameThickness * 0.8, actualShelfDepth, 8), wireMaterial ); leftRail.rotation.x = Math.PI / 2; leftRail.position.set(startX, yPos, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(leftRail); const rightRail = new THREE.Mesh( new THREE.CylinderGeometry(frameThickness * 0.8, frameThickness * 0.8, actualShelfDepth, 8), wireMaterial ); rightRail.rotation.x = Math.PI / 2; rightRail.position.set(endX, yPos, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(rightRail); const centerX = shelfCenterX; const centerSupportRail = new THREE.Mesh( new THREE.CylinderGeometry(frameThickness * 0.8, frameThickness * 0.8, actualShelfDepth, 8), wireMaterial ); centerSupportRail.rotation.x = Math.PI / 2; centerSupportRail.position.set(centerX, yPos, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(centerSupportRail); // Add wire grid and prongs this.addShelfWires(shelfMesh, startX, shelfWidth, shelfCenterX, actualShelfDepth, yPos, depth, wireMaterial); this.addShelfProgs(shelfMesh, [startX, centerX, endX], yPos, depth, wireMaterial); shelfGroup.add(shelfMesh); } createStandardShelfMesh(width, depth, doorWidth, yPos, wireMaterial, shelfGroup) { const shelfMesh = new THREE.Group(); const frameThickness = 0.015; const frontRailGeometry = new THREE.CylinderGeometry(frameThickness, frameThickness, width, 12); const frontRail = new THREE.Mesh(frontRailGeometry, wireMaterial); frontRail.rotation.z = Math.PI / 2; frontRail.position.set(0, yPos, -0.12); frontRail.castShadow = true; shelfMesh.add(frontRail); const backRail = new THREE.Mesh(frontRailGeometry, wireMaterial); backRail.rotation.z = Math.PI / 2; backRail.position.set(0, yPos, -depth + 0.1); backRail.castShadow = true; shelfMesh.add(backRail); const actualShelfDepth = Math.abs((-depth + 0.1) - (-0.12)); const positions = []; for (let i = 0; i <= this.config3D.numDoors; i++) { const xPos = -width/2 + i * doorWidth; positions.push(xPos); const sideRail = new THREE.Mesh( new THREE.CylinderGeometry(frameThickness * 0.8, frameThickness * 0.8, actualShelfDepth, 8), wireMaterial ); sideRail.rotation.x = Math.PI / 2; sideRail.position.set(xPos, yPos, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(sideRail); } // Add wire grid and prongs this.addShelfWires(shelfMesh, -width/2, width, 0, actualShelfDepth, yPos, depth, wireMaterial); this.addShelfProgs(shelfMesh, positions, yPos, depth, wireMaterial); shelfGroup.add(shelfMesh); } createSingleDoorShelfMesh(doorIndex, width, depth, doorWidth, yPos, wireMaterial, shelfGroup) { const shelfMesh = new THREE.Group(); const startX = -width/2 + (doorIndex * doorWidth); const endX = startX + doorWidth; const shelfCenterX = (startX + endX) / 2; const frameThickness = 0.015; // Front rail for this door const frontRailGeometry = new THREE.CylinderGeometry(frameThickness, frameThickness, doorWidth, 12); const frontRail = new THREE.Mesh(frontRailGeometry, wireMaterial); frontRail.rotation.z = Math.PI / 2; frontRail.position.set(shelfCenterX, yPos, -0.12); frontRail.castShadow = true; shelfMesh.add(frontRail); // Back rail for this door const backRail = new THREE.Mesh(frontRailGeometry, wireMaterial); backRail.rotation.z = Math.PI / 2; backRail.position.set(shelfCenterX, yPos, -depth + 0.1); backRail.castShadow = true; shelfMesh.add(backRail); const actualShelfDepth = Math.abs((-depth + 0.1) - (-0.12)); // Left side rail const leftRail = new THREE.Mesh( new THREE.CylinderGeometry(frameThickness * 0.8, frameThickness * 0.8, actualShelfDepth, 8), wireMaterial ); leftRail.rotation.x = Math.PI / 2; leftRail.position.set(startX, yPos, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(leftRail); // Right side rail const rightRail = new THREE.Mesh( new THREE.CylinderGeometry(frameThickness * 0.8, frameThickness * 0.8, actualShelfDepth, 8), wireMaterial ); rightRail.rotation.x = Math.PI / 2; rightRail.position.set(endX, yPos, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(rightRail); // Add wire grid and prongs this.addShelfWires(shelfMesh, startX, doorWidth, shelfCenterX, actualShelfDepth, yPos, depth, wireMaterial); this.addShelfProgs(shelfMesh, [startX, endX], yPos, depth, wireMaterial); shelfGroup.add(shelfMesh); } addShelfWires(shelfMesh, startX, shelfWidth, shelfCenterX, actualShelfDepth, yPos, depth, wireMaterial) { const wireThickness = 0.004; const wireSpacing = 0.1; const numLongWires = Math.floor(shelfWidth / wireSpacing); for (let w = 1; w < numLongWires; w++) { const wireX = startX + w * wireSpacing; const wire = new THREE.Mesh( new THREE.CylinderGeometry(wireThickness, wireThickness, actualShelfDepth, 8), wireMaterial ); wire.rotation.x = Math.PI / 2; wire.position.set(wireX, yPos - 0.005, (-0.12 + (-depth + 0.1)) / 2); shelfMesh.add(wire); } const numCrossWires = Math.floor(actualShelfDepth / wireSpacing); for (let w = 1; w { const frontProng = new THREE.Mesh( new THREE.BoxGeometry(0.04, 0.025, 0.05), wireMaterial ); frontProng.position.set(xPos, yPos - 0.01, -0.12); shelfMesh.add(frontProng); const backProng = new THREE.Mesh( new THREE.BoxGeometry(0.04, 0.025, 0.05), wireMaterial ); backProng.position.set(xPos, yPos - 0.01, -depth + 0.1); shelfMesh.add(backProng); }); } createLEDLighting3D(width, height, depth) { const ledGroup = new THREE.Group(); const ledMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.5 }); const housingMaterial = new THREE.MeshStandardMaterial({ color: 0xeeeeee, metalness: 0.1, roughness: 0.5 }); const stripWidth = 0.06; const stripHeight = height * 0.85; const stripDepth = 0.03; const positions = []; positions.push(-width/2 - 0.05); positions.push(width/2 + 0.05); // In createLEDLighting3D method, modify the mullion position logic: // In createLEDLighting3D method, modify the mullion position logic: const doorWidth = width / this.config3D.numDoors; for (let i = 1; i { const housing = new THREE.Mesh( new THREE.BoxGeometry(stripWidth, stripHeight, stripDepth), housingMaterial ); housing.position.set(xPos, 0, -0.08); housing.castShadow = true; ledGroup.add(housing); const ledStrip = new THREE.Mesh( new THREE.BoxGeometry(stripWidth * 0.7, stripHeight * 0.95, 0.01), ledMaterial ); ledStrip.position.set(xPos, 0, -0.06); ledGroup.add(ledStrip); const pointLight = new THREE.PointLight(0xffffff, 0.2, 2); pointLight.position.set(xPos, 0, -0.05); ledGroup.add(pointLight); }); this.frameSystem3D.add(ledGroup); } createDoors3D(totalWidth, height) { const numDoors = this.config3D.numDoors; const topExtension = 0.1125; const bottomSill = 0.1125; const clearOpeningHeight = height - (topExtension * 2) - (bottomSill * 2); const doorHeight = clearOpeningHeight - 0.05; // Different gap sizes for different door swing combinations const gapLR = 0.063; // Small gap between L-R doors (where mullions remain) const gapRL = 0.063; // Larger gap between R-L doors (where mullions are removed) // Calculate total gap space needed let totalGapSpace = 0; if (this.config3D.noCenterMullion && this.config3D.doorSwing === 'Center Pull') { for (let i = 0; i < numDoors - 1; i++) { const leftDoorHinge = (i + 1) % 2 === 1 ? 'L' : 'R'; const rightDoorHinge = (i + 2) % 2 === 1 ? 'L' : 'R'; if (leftDoorHinge === 'L' && rightDoorHinge === 'R') { totalGapSpace += gapLR; // Small gap for L-R } else { totalGapSpace += gapRL; // Large gap for R-L } } } else { // Standard spacing for all other configurations totalGapSpace = gapLR * (numDoors - 1); } const availableWidth = totalWidth - totalGapSpace; const adjustedDoorWidth = availableWidth / numDoors; const config = AppConfig.viewer3D.materials; const glassMaterial = new THREE.MeshPhysicalMaterial({ color: config.glass.color, transparent: true, opacity: config.glass.opacity, roughness: config.glass.roughness, side: THREE.DoubleSide }); const frameMaterial = new THREE.MeshStandardMaterial({ color: 0x2a2a2a, metalness: 0.3, roughness: 0.6 }); const handleMaterial = new THREE.MeshStandardMaterial({ color: config.handle.color, metalness: config.handle.metalness, roughness: config.handle.roughness }); let currentXPosition = -totalWidth/2 + adjustedDoorWidth/2; for (let i = 0; i < numDoors; i++) { const doorGroup = new THREE.Group(); doorGroup.name = `door_${i}`; let hingeType = 'L'; if (this.config3D.doorSwing === 'Center Pull') { hingeType = (i + 1) % 2 === 1 ? 'L' : 'R'; } else if (this.config3D.doorSwing === 'All Left') { hingeType = 'L'; } else if (this.config3D.doorSwing === 'All Right') { hingeType = 'R'; } else if (this.config3D.doorSwing === 'Custom' && this.customSwingPattern) { hingeType = this.customSwingPattern[i] || 'L'; } else if (this.config3D.frameSeries === 'Edge') { hingeType = i % 2 === 0 ? 'L' : 'R'; } else { hingeType = 'L'; } const frameThickness = 0.08; const frameDepth = 0.05; const glass = new THREE.Mesh( new THREE.BoxGeometry(adjustedDoorWidth - frameThickness * 1.5, doorHeight - frameThickness * 2, 0.02), glassMaterial ); glass.receiveShadow = true; const frameGroup = new THREE.Group(); const topRail = new THREE.Mesh( new THREE.BoxGeometry(adjustedDoorWidth - 0.02, frameThickness, frameDepth), frameMaterial ); topRail.position.y = doorHeight/2 - frameThickness/2; topRail.castShadow = true; frameGroup.add(topRail); const bottomRail = new THREE.Mesh( new THREE.BoxGeometry(adjustedDoorWidth - 0.02, frameThickness, frameDepth), frameMaterial ); bottomRail.position.y = -doorHeight/2 + frameThickness/2; bottomRail.castShadow = true; frameGroup.add(bottomRail); const leftStile = new THREE.Mesh( new THREE.BoxGeometry(frameThickness * 0.8, doorHeight, frameDepth), frameMaterial ); leftStile.position.x = -adjustedDoorWidth/2 + frameThickness/2; leftStile.castShadow = true; frameGroup.add(leftStile); const rightStile = new THREE.Mesh( new THREE.BoxGeometry(frameThickness * 0.8, doorHeight, frameDepth), frameMaterial ); rightStile.position.x = adjustedDoorWidth/2 - frameThickness/2; rightStile.castShadow = true; frameGroup.add(rightStile); const handleGroup = new THREE.Group(); const handleLength = doorHeight * 0.15; const handleDiameter = 0.02; const handleStandoff = 0.03; const handleGeometry = new THREE.CylinderGeometry( handleDiameter/2, handleDiameter/2, handleLength, 12 ); const handleBar = new THREE.Mesh(handleGeometry, handleMaterial); handleBar.rotation.y = Math.PI / 2; handleBar.castShadow = true; handleGroup.add(handleBar); const bracketGeometry = new THREE.BoxGeometry(0.025, 0.015, 0.04); const topBracket = new THREE.Mesh(bracketGeometry, handleMaterial); topBracket.position.set(0, handleLength/2, -handleStandoff/2); topBracket.castShadow = true; handleGroup.add(topBracket); const bottomBracket = new THREE.Mesh(bracketGeometry, handleMaterial); bottomBracket.position.set(0, -handleLength/2, -handleStandoff/2); bottomBracket.castShadow = true; handleGroup.add(bottomBracket); if (hingeType === 'L') { handleGroup.position.x = adjustedDoorWidth/2 - frameThickness/2; handleGroup.position.y = 0; handleGroup.position.z = frameDepth/2 + handleStandoff; } else { handleGroup.position.x = -adjustedDoorWidth/2 + frameThickness/2; handleGroup.position.y = 0; handleGroup.position.z = frameDepth/2 + handleStandoff; } if (this.config3D.frameSeries === 'Edge' && hingeType === 'R') { const sealMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.1, roughness: 0.9 }); const centerSeal = new THREE.Mesh( new THREE.BoxGeometry(0.02, doorHeight, 0.03), sealMaterial ); centerSeal.position.x = -adjustedDoorWidth/2 + 0.01; centerSeal.position.z = frameDepth/2; frameGroup.add(centerSeal); } const doorPivot = new THREE.Group(); doorPivot.add(glass); doorPivot.add(frameGroup); doorPivot.add(handleGroup); if (hingeType === 'L') { doorPivot.position.x = -adjustedDoorWidth/2; glass.position.x = adjustedDoorWidth/2; frameGroup.position.x = adjustedDoorWidth/2; handleGroup.position.x += adjustedDoorWidth/2; } else { doorPivot.position.x = adjustedDoorWidth/2; glass.position.x = -adjustedDoorWidth/2; frameGroup.position.x = -adjustedDoorWidth/2; handleGroup.position.x -= adjustedDoorWidth/2; } doorPivot.userData = { hingeType: hingeType, isOpen: false, targetRotation: 0, currentRotation: 0 }; doorGroup.add(doorPivot); doorGroup.userData.doorPivot = doorPivot; doorGroup.userData.clickable = true; doorGroup.position.x = currentXPosition; doorGroup.position.y = 0; doorGroup.position.z = 0.02; // Calculate next door position with appropriate gap currentXPosition += adjustedDoorWidth; if (i { const doorPivot = doorGroup.userData.doorPivot; const hingeType = doorPivot.userData.hingeType; const doorX = -width/2 + doorWidth/2 + i * doorWidth; const hingeX = hingeType === 'L' ? doorX - doorWidth/2 : doorX + doorWidth/2; const startAngle = hingeType === 'L' ? 0 : Math.PI/2; const endAngle = hingeType === 'L' ? Math.PI/2 : Math.PI; const curve = new THREE.EllipseCurve( hingeX, 0, swingRadius, swingRadius, startAngle, endAngle, false, 0 ); const points = curve.getPoints(40); const arcGeometry = new THREE.BufferGeometry().setFromPoints( points.map(p => new THREE.Vector3(p.x, -2.08, p.y)) ); const arc = new THREE.Line(arcGeometry, arcMaterial); arc.computeLineDistances(); this.doorSwingArcs.add(arc); maxSwingZ = Math.max(maxSwingZ, swingRadius); const hingeMarker = new THREE.Mesh( new THREE.CircleGeometry(0.05, 16), new THREE.MeshBasicMaterial({ color: 0x0066cc, opacity: 0.6, transparent: true }) ); hingeMarker.position.set(hingeX, -2.075, 0); hingeMarker.rotation.x = -Math.PI/2; this.doorSwingArcs.add(hingeMarker); }); // Add dimension labels for opened doors this.addSwingDimensions(width, doorWidth, swingRadius); this.scene3D.add(this.doorSwingArcs); } addSwingDimensions(width, doorWidth, swingRadius) { // ADD THIS CHECK if (!swingRadius || swingRadius { const doorPivot = doorGroup.userData.doorPivot; if (doorPivot && Math.abs(doorPivot.rotation.y) > 0.1) { hasOpenDoor = true; } }); if (!hasOpenDoor) { console.log('No open doors detected - skipping dimension display'); return; } const dimMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 }); console.log('addSwingDimensions called:', { doorSwing: this.config3D.doorSwing, hasOpenDoor: hasOpenDoor, doorsCount: this.doors3D.length, doorStates: this.doors3D.map(d => d.userData.doorPivot?.userData?.isOpen) }); // Show dimensions for the first door const firstDoor = this.doors3D[0]; if (!firstDoor) return; const doorPivot = firstDoor.userData.doorPivot; const userData = doorPivot.userData; const hingeType = userData.hingeType; const doorX = -width/2 + doorWidth/2; const hingeX = hingeType === 'L' ? doorX - doorWidth/2 : doorX + doorWidth/2; const doorWidthInches = this.config3D.doorWidth; // Get the actual door width based on catalog size let actualDoorWidth; if (this.config3D.frameSeries === 'Legacy') { actualDoorWidth = doorWidthInches === 23 ? 21.75 : doorWidthInches === 26 ? 25.5 : doorWidthInches === 28 ? 27.5 : doorWidthInches === 30 ? 29.0 : doorWidthInches; } else if (this.config3D.frameSeries === 'Advantage/Eco') { actualDoorWidth = doorWidthInches === 24 ? 22.375 : doorWidthInches === 26 ? 26.125 : doorWidthInches === 28 ? 28.125 : doorWidthInches === 30 ? 29.625 : doorWidthInches; } else { // Edge actualDoorWidth = doorWidthInches; // Edge uses nominal sizes } let c2cSpacing; if (this.config3D.frameSeries === 'Legacy') { c2cSpacing = this.config3D.doorWidth + 1.25; } else if (this.config3D.frameSeries === 'Advantage/Eco') { c2cSpacing = this.config3D.doorWidth + 0.625; } else { c2cSpacing = null; // Don't show C2C for Edge } // Clear opening calculations based on the reference sheet let nominalClearOpening, worstCaseClearOpening; // These values are from the clear opening reference sheet if (this.config3D.frameSeries === 'Legacy') { // Based on the pattern from the reference sheet if (doorWidthInches === 21.75) { // Changed from 23 nominalClearOpening = 18.5; // From reference worstCaseClearOpening = 16.875; // 18.5 - 1.625 } else if (doorWidthInches === 25.5) { // Changed from 26 nominalClearOpening = 22.3125; // From reference (22 5/16") worstCaseClearOpening = 20.6875; // 22.3125 - 1.625 } else if (doorWidthInches === 27.5) { // Changed from 28 nominalClearOpening = 24.25; // From reference (24 1/4") worstCaseClearOpening = 22.625; // 24.25 - 1.625 } else if (doorWidthInches === 29.0) { // Changed from 30 nominalClearOpening = 25.8125; // From reference (25 13/16") worstCaseClearOpening = 24.1875; // 25.8125 - 1.625 } } else if (this.config3D.frameSeries === 'Advantage/Eco') { nominalClearOpening = this.config3D.clearOpening; worstCaseClearOpening = this.config3D.worstCaseClearOpening; } else { // Edge series nominalClearOpening = this.config3D.clearOpening; worstCaseClearOpening = this.config3D.worstCaseClearOpening; } // Add dimension lines and labels this.addClearOpeningDimensions(hingeX, hingeType, doorWidth, swingRadius, c2cSpacing, nominalClearOpening, worstCaseClearOpening, dimMaterial); } addClearOpeningDimensions(hingeX, hingeType, doorWidth, swingRadius, c2cSpacing, nominalClearOpening, worstCaseClearOpening, dimMaterial) { // C2C dimension (this already works) if (c2cSpacing !== null) { const c2cStart = hingeX; const c2cEnd = hingeType === 'L' ? hingeX + (doorWidth + 0.0625) : hingeX - (doorWidth + 0.0625); // C2C dimension lines const extLine1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(c2cStart, -2.08, swingRadius * 2.0), new THREE.Vector3(c2cStart, -2.08, swingRadius * 2.2) ]); this.doorSwingArcs.add(new THREE.Line(extLine1, dimMaterial)); const extLine2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(c2cEnd, -2.08, swingRadius * 2.0), new THREE.Vector3(c2cEnd, -2.08, swingRadius * 2.2) ]); this.doorSwingArcs.add(new THREE.Line(extLine2, dimMaterial)); const c2cDimLine = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(c2cStart, -2.08, swingRadius * 2.1), new THREE.Vector3(c2cEnd, -2.08, swingRadius * 2.1) ]); this.doorSwingArcs.add(new THREE.Line(c2cDimLine, dimMaterial)); this.addDimensionArrowsAndLabels(c2cStart, c2cEnd, swingRadius, c2cSpacing, dimMaterial); } // NOMINAL CLEAR OPENING - USE THE SAME METHOD THAT WORKS FOR C2C const scale = 0.05; const nominalClearStart = hingeX; const nominalClearEnd = hingeType === 'L' ? hingeX + (nominalClearOpening * scale) : hingeX - (nominalClearOpening * scale); // Extension lines const nomExtLine1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(nominalClearStart, -2.08, swingRadius * 1.6), new THREE.Vector3(nominalClearStart, -2.08, swingRadius * 1.8) ]); this.doorSwingArcs.add(new THREE.Line(nomExtLine1, dimMaterial)); const nomExtLine2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(nominalClearEnd, -2.08, swingRadius * 1.6), new THREE.Vector3(nominalClearEnd, -2.08, swingRadius * 1.8) ]); this.doorSwingArcs.add(new THREE.Line(nomExtLine2, dimMaterial)); // Dimension line const nomDimLine = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(nominalClearStart, -2.08, swingRadius * 1.7), new THREE.Vector3(nominalClearEnd, -2.08, swingRadius * 1.7) ]); this.doorSwingArcs.add(new THREE.Line(nomDimLine, dimMaterial)); // Arrows and labels - EXACTLY LIKE C2C const arrowSize = 0.05; const nomArrow1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(nominalClearStart + arrowSize, -2.08, swingRadius * 1.7 - arrowSize/2), new THREE.Vector3(nominalClearStart, -2.08, swingRadius * 1.7), new THREE.Vector3(nominalClearStart + arrowSize, -2.08, swingRadius * 1.7 + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(nomArrow1, new THREE.LineBasicMaterial({ color: 0x000000 }))); const nomArrow2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(nominalClearEnd - arrowSize, -2.08, swingRadius * 1.7 - arrowSize/2), new THREE.Vector3(nominalClearEnd, -2.08, swingRadius * 1.7), new THREE.Vector3(nominalClearEnd - arrowSize, -2.08, swingRadius * 1.7 + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(nomArrow2, new THREE.LineBasicMaterial({ color: 0x000000 }))); const nomCenterX = (nominalClearStart + nominalClearEnd) / 2; const nomLabel = this.createTextSprite3D(`${Utils.floatToFraction16(nominalClearOpening)}"`, { fontSize: 24, color: '#0066CC', backgroundColor: 'rgba(255, 255, 255, 0.95)' }); nomLabel.position.set(nomCenterX, -2.05, swingRadius * 1.7 + 0.15); this.doorSwingArcs.add(nomLabel); const nomSubLabel = this.createTextSprite3D(`NOMINAL CLEAR OPENING`, { fontSize: 20, color: '#0066CC', backgroundColor: 'rgba(255, 255, 255, 0.95)' }); nomSubLabel.position.set(nomCenterX, -2.05, swingRadius * 1.7 + 0.35); this.doorSwingArcs.add(nomSubLabel); // WORST CASE CLEAR OPENING - SAME METHOD const worstClearStart = hingeX; const worstClearEnd = hingeType === 'L' ? hingeX + (worstCaseClearOpening * scale) : hingeX - (worstCaseClearOpening * scale); // Extension lines const worstExtLine1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(worstClearStart, -2.08, swingRadius * 1.2), new THREE.Vector3(worstClearStart, -2.08, swingRadius * 1.4) ]); this.doorSwingArcs.add(new THREE.Line(worstExtLine1, dimMaterial)); const worstExtLine2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(worstClearEnd, -2.08, swingRadius * 1.2), new THREE.Vector3(worstClearEnd, -2.08, swingRadius * 1.4) ]); this.doorSwingArcs.add(new THREE.Line(worstExtLine2, dimMaterial)); // Dimension line const worstDimLine = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(worstClearStart, -2.08, swingRadius * 1.3), new THREE.Vector3(worstClearEnd, -2.08, swingRadius * 1.3) ]); this.doorSwingArcs.add(new THREE.Line(worstDimLine, dimMaterial)); // Arrows const worstArrow1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(worstClearStart + arrowSize, -2.08, swingRadius * 1.3 - arrowSize/2), new THREE.Vector3(worstClearStart, -2.08, swingRadius * 1.3), new THREE.Vector3(worstClearStart + arrowSize, -2.08, swingRadius * 1.3 + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(worstArrow1, new THREE.LineBasicMaterial({ color: 0x000000 }))); const worstArrow2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(worstClearEnd - arrowSize, -2.08, swingRadius * 1.3 - arrowSize/2), new THREE.Vector3(worstClearEnd, -2.08, swingRadius * 1.3), new THREE.Vector3(worstClearEnd - arrowSize, -2.08, swingRadius * 1.3 + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(worstArrow2, new THREE.LineBasicMaterial({ color: 0x000000 }))); const worstCenterX = (worstClearStart + worstClearEnd) / 2; const worstLabel = this.createTextSprite3D(`${Utils.floatToFraction16(worstCaseClearOpening)}"`, { fontSize: 24, color: '#CC0000', backgroundColor: 'rgba(255, 255, 255, 0.95)' }); worstLabel.position.set(worstCenterX, -2.05, swingRadius * 1.3 + 0.15); this.doorSwingArcs.add(worstLabel); const worstSubLabel = this.createTextSprite3D(`WORST CASE 87°`, { fontSize: 20, color: '#CC0000', backgroundColor: 'rgba(255, 255, 255, 0.95)' }); worstSubLabel.position.set(worstCenterX, -2.05, swingRadius * 1.3 + 0.35); this.doorSwingArcs.add(worstSubLabel); } addDimensionArrowsAndLabels(start, end, swingRadius, doorWidth, dimMaterial) { const arrowSize = 0.05; const netArrow1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(start + arrowSize, -2.08, swingRadius * 2.1 - arrowSize/2), new THREE.Vector3(start, -2.08, swingRadius * 2.1), new THREE.Vector3(start + arrowSize, -2.08, swingRadius * 2.1 + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(netArrow1, new THREE.LineBasicMaterial({ color: 0x000000 }))); const netArrow2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(end - arrowSize, -2.08, swingRadius * 1.3 - arrowSize/2), new THREE.Vector3(end, -2.08, swingRadius * 1.3), new THREE.Vector3(end - arrowSize, -2.08, swingRadius * 1.3 + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(netArrow2, new THREE.LineBasicMaterial({ color: 0x000000 }))); const netCenterX = (start + end) / 2; const netLabel = this.createTextSprite3D(`${Utils.floatToFraction16(doorWidth)}"`, { fontSize: 24, color: '#000000', backgroundColor: 'rgba(255, 255, 255, 0.95)' }); netLabel.position.set(netCenterX, -2.05, swingRadius * 2.1 + 0.15); this.doorSwingArcs.add(netLabel); const netSubLabel = this.createTextSprite3D(`Center to Center`, { fontSize: 20, color: '#666666', backgroundColor: 'rgba(255, 255, 255, 0.95)' }); netSubLabel.position.set(netCenterX, -2.05, swingRadius * 2.1 + 0.35); this.doorSwingArcs.add(netSubLabel); } addClearanceLabels(hingeX, hingeType, swingRadius, worstCaseClearOpening, nominalClearOpening, dimMaterial) { const scale = 0.05; // Worst case clearance const worstClearStart = hingeX; const worstClearEnd = hingeType === 'L' ? hingeX + (worstCaseClearOpening * scale) : hingeX - (worstCaseClearOpening * scale); this.addClearanceDimension(worstClearStart, worstClearEnd, swingRadius * 1.6, worstCaseClearOpening, 'WORST CASE 87°', '#CC0000', dimMaterial); // Nominal clearance const nominalClearStart = hingeX; const nominalClearEnd = hingeType === 'L' ? hingeX + (nominalClearOpening * scale) : hingeX - (nominalClearOpening * scale); this.addClearanceDimension(nominalClearStart, nominalClearEnd, swingRadius * 1.9, nominalClearOpening, 'NOMINAL', '#0066CC', dimMaterial); } addClearanceDimension(start, end, yOffset, openingValue, label, color, dimMaterial) { // Extension lines const extLine1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(start, -2.08, yOffset - 0.1), new THREE.Vector3(start, -2.08, yOffset + 0.1) ]); this.doorSwingArcs.add(new THREE.Line(extLine1, dimMaterial)); const extLine2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(end, -2.08, yOffset - 0.1), new THREE.Vector3(end, -2.08, yOffset + 0.1) ]); this.doorSwingArcs.add(new THREE.Line(extLine2, dimMaterial)); // Main dimension line const dimLine = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(start, -2.08, yOffset), new THREE.Vector3(end, -2.08, yOffset) ]); this.doorSwingArcs.add(new THREE.Line(dimLine, dimMaterial)); // Arrows const arrowSize = 0.05; const arrow1 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(start + arrowSize, -2.08, yOffset - arrowSize/2), new THREE.Vector3(start, -2.08, yOffset), new THREE.Vector3(start + arrowSize, -2.08, yOffset + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(arrow1, dimMaterial)); const arrow2 = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(end - arrowSize, -2.08, yOffset - arrowSize/2), new THREE.Vector3(end, -2.08, yOffset), new THREE.Vector3(end - arrowSize, -2.08, yOffset + arrowSize/2) ]); this.doorSwingArcs.add(new THREE.Line(arrow2, dimMaterial)); // Labels - position them below the door area const centerX = (start + end) / 2; // Format the opening value as fraction const formattedValue = `${Utils.floatToFraction16(openingValue)}"`; const label1 = this.createTextSprite3D(formattedValue, { fontSize: 22, color: color, backgroundColor: 'rgba(255, 255, 255, 0.95)' }); label1.position.set(centerX, -2.05, yOffset + 0.2); this.doorSwingArcs.add(label1); const label2 = this.createTextSprite3D(label, { fontSize: 16, color: color, backgroundColor: 'rgba(255, 255, 255, 0.95)' }); label2.position.set(centerX, -2.05, yOffset + 0.4); this.doorSwingArcs.add(label2); } createTextSprite3D(text, options = {}) { const { fontSize = 32, color = '#000000', backgroundColor = 'rgba(255, 255, 255, 0.8)' } = options; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const lines = text.split('\n'); context.font = `bold ${fontSize}px Arial`; let maxWidth = 0; lines.forEach(line => { const width = context.measureText(line).width; if (width > maxWidth) maxWidth = width; }); const lineHeight = fontSize * 1.2; canvas.width = maxWidth + 20; canvas.height = lines.length * lineHeight + 20; context.fillStyle = backgroundColor; context.fillRect(0, 0, canvas.width, canvas.height); context.font = `bold ${fontSize}px Arial`; context.fillStyle = color; context.textAlign = 'center'; context.textBaseline = 'middle'; lines.forEach((line, index) => { const y = (index + 0.5) * lineHeight + 10; context.fillText(line, canvas.width/2, y); }); const texture = new THREE.CanvasTexture(canvas); const spriteMaterial = new THREE.SpriteMaterial({ map: texture, depthTest: false, depthWrite: false }); const sprite = new THREE.Sprite(spriteMaterial); const scale = 0.005; sprite.scale.set(canvas.width * scale, canvas.height * scale, 1); return sprite; } updateDimensionDisplay3D(width, height, depth) { const scale = 0.05; const widthInches = Math.round(width / scale); const heightInches = Math.round(height / scale * 10) / 10; const depthInches = Math.round(depth / scale); const elements = { frameWidth: document.getElementById('frameWidth'), frameHeight: document.getElementById('frameHeight'), frameDepth: document.getElementById('frameDepth'), shelfDepthValue: document.getElementById('shelfDepthValue'), serviceClearance: document.getElementById('serviceClearance'), frameWidthNote: document.getElementById('frameWidthNote') }; if (elements.frameWidth) { elements.frameWidth.textContent = `${widthInches}"`; if (elements.frameWidthNote) { elements.frameWidthNote.textContent = `${this.config3D.numDoors} doors × ${this.config3D.doorWidth}" wide`; } } if (elements.frameHeight) { elements.frameHeight.textContent = `${heightInches}"`; } if (elements.frameDepth) { elements.frameDepth.textContent = `${depthInches}"`; } if (elements.shelfDepthValue) { elements.shelfDepthValue.textContent = `${this.config3D.shelfDepth}"`; } if (elements.serviceClearance) { elements.serviceClearance.textContent = `${this.config3D.minServiceArea}"`; } } setViewMode(mode, button) { this.currentViewMode = mode; if (button) { document.querySelectorAll('.view-btn').forEach(btn => { btn.classList.remove('active'); }); button.classList.add('active'); } if (this.frameSystem3D) { this.frameSystem3D.rotation.x = 0; this.frameSystem3D.rotation.y = 0; } if (!this.camera3D) return; const aspect = window.innerWidth / window.innerHeight; const floatingButton = document.getElementById('floatingShelfButton'); switch(mode) { case 'front': this.camera3D = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000); this.camera3D.position.set(0, 0, 15); this.camera3D.lookAt(0, 0, 0); this.targetRotationX = 0; this.targetRotationY = 0; if (floatingButton) { floatingButton.classList.remove('visible'); } break; case 'side': this.camera3D = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000); this.camera3D.position.set(15, 0, -2); this.camera3D.lookAt(0, 0, -2); this.targetRotationX = 0; this.targetRotationY = 0; if (floatingButton) { floatingButton.classList.remove('visible'); } break; case 'top': const frameWidth = this.config3D.numDoors * this.config3D.doorWidth * 0.05; const frameDepth = this.config3D.coolerDepth * 0.05; const serviceDepth = this.config3D.minServiceArea * 0.05; const doorWidthInches = this.config3D.doorWidth; const swingRadiusInches = doorWidthInches + 0.8; const swingRadius = swingRadiusInches * 0.05; const totalWidth = frameWidth + (swingRadius * 2) + 2; const totalDepth = frameDepth + serviceDepth + swingRadius + 3; const viewSize = Math.max(totalWidth, totalDepth) * 1.0; const viewHeight = viewSize; const viewWidth = viewSize * aspect; this.camera3D = new THREE.OrthographicCamera( -viewWidth / 2, viewWidth / 2, viewHeight / 2, -viewHeight / 2, 1, 1000 ); const centerZ = -frameDepth / 3; this.camera3D.position.set(0, 50, centerZ); this.camera3D.lookAt(0, 0, centerZ); this.camera3D.updateProjectionMatrix(); this.targetRotationX = 0; this.targetRotationY = 0; if (this.doorSwingArcs) { this.doorSwingArcs.visible = true; } if (floatingButton) { floatingButton.classList.add('visible'); } this.updateVisibility(); break; case '3d': default: this.camera3D = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000); this.camera3D.position.set(10, 6, 15); this.camera3D.lookAt(0, 0, 0); if (this.doorSwingArcs) { this.doorSwingArcs.visible = false; } if (floatingButton) { floatingButton.classList.remove('visible'); } break; } this.camera3D.updateProjectionMatrix(); this.updateVisibility(); } updateVisibility() { if (this.floorOutline) { this.floorOutline.visible = true; } if (this.dimensionHelpers) { this.dimensionHelpers.visible = (this.currentViewMode === 'side' || this.currentViewMode === 'top'); } if (this.serviceAreaOutline) { this.serviceAreaOutline.visible = (this.currentViewMode === 'top' || this.currentViewMode === 'side'); } if (this.doorSwingArcs) { this.doorSwingArcs.visible = this.currentViewMode === 'top'; } } setupControls3D() { const canvas = this.renderer3D.domElement; let mouseDown = false; let mouseX = 0; let mouseY = 0; let mouseButton = 0; canvas.addEventListener('mousedown', (e) => { mouseDown = true; mouseX = e.clientX; mouseY = e.clientY; mouseButton = e.button; }); canvas.addEventListener('mouseup', () => { mouseDown = false; }); canvas.addEventListener('mousemove', (e) => { if (!mouseDown) return; const deltaX = e.clientX - mouseX; const deltaY = e.clientY - mouseY; if (mouseButton === 0 && this.currentViewMode === '3d') { this.targetRotationY += deltaX * 0.01; this.targetRotationX += deltaY * 0.01; this.targetRotationX = Math.max(-Math.PI/3, Math.min(Math.PI/3, this.targetRotationX)); } else if (mouseButton === 2) { const panSpeed = 0.01; this.camera3D.position.x -= deltaX * panSpeed; this.camera3D.position.y += deltaY * panSpeed; this.camera3D.lookAt(0, 0, 0); } mouseX = e.clientX; mouseY = e.clientY; }); canvas.addEventListener('contextmenu', (e) => { e.preventDefault(); }); canvas.addEventListener('wheel', (e) => { e.preventDefault(); const scale = e.deltaY > 0 ? 1.1 : 0.9; const minDist = 5; const maxDist = 50; const currentDist = this.camera3D.position.length(); if ((scale > 1 && currentDist < maxDist) || (scale minDist)) { this.camera3D.position.multiplyScalar(scale); if (this.camera3D.isOrthographicCamera) { this.camera3D.left *= scale; this.camera3D.right *= scale; this.camera3D.top *= scale; this.camera3D.bottom *= scale; this.camera3D.updateProjectionMatrix(); } } }); canvas.addEventListener('click', (e) => this.handleDoorClick3D(e)); } handleDoorClick3D(event) { if (this.currentViewMode === 'top' || this.currentViewMode === 'side') return; const mouse = new THREE.Vector2(); const raycaster = new THREE.Raycaster(); const rect = this.renderer3D.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, this.camera3D); const doorMeshes = []; this.doors3D.forEach(doorGroup => { doorGroup.traverse(child => { if (child instanceof THREE.Mesh) { doorMeshes.push(child); } }); }); const intersects = raycaster.intersectObjects(doorMeshes, false); if (intersects.length > 0) { let clickedObject = intersects[0].object; for (let i = 0; i { if (child === clickedObject) { found = true; } }); if (found) { this.toggleSingleDoor3D(i); break; } } } } toggleSingleDoor3D(index) { const doorGroup = this.doors3D[index]; if (!doorGroup) return; const doorPivot = doorGroup.userData.doorPivot; const userData = doorPivot.userData; userData.isOpen = !userData.isOpen; userData.targetRotation = userData.isOpen ? (userData.hingeType === 'L' ? -Math.PI/2 : Math.PI/2) : 0; this.animateDoor3D(doorPivot); } animateDoor3D(doorPivot) { const userData = doorPivot.userData; const animateDoorRotation = () => { const diff = userData.targetRotation - userData.currentRotation; if (Math.abs(diff) > 0.01) { userData.currentRotation += diff * 0.15; doorPivot.rotation.y = userData.currentRotation; requestAnimationFrame(animateDoorRotation); } else { doorPivot.rotation.y = userData.targetRotation; userData.currentRotation = userData.targetRotation; // Refresh door swing arcs and dimensions when animation completes if (this.currentViewMode === 'top') { // Small delay to ensure all door states are updated setTimeout(() => { if (this.doorSwingArcs) { this.scene3D.remove(this.doorSwingArcs); } const totalWidth = this.config3D.numDoors * this.config3D.doorWidth * 0.05; this.createDoorSwingArcs3D(totalWidth); this.doorSwingArcs.visible = true; // Force update of visibility this.updateVisibility(); if (this.renderer3D && this.scene3D && this.camera3D) { this.renderer3D.render(this.scene3D, this.camera3D); } }, 50); } } }; animateDoorRotation(); } animateAllDoors() { this.doorsOpen = !this.doorsOpen; this.doors3D.forEach((doorGroup, i) => { const doorPivot = doorGroup.userData.doorPivot; const userData = doorPivot.userData; userData.isOpen = this.doorsOpen; userData.targetRotation = this.doorsOpen ? (userData.hingeType === 'L' ? -Math.PI/2 : Math.PI/2) : 0; setTimeout(() => { this.animateDoor3D(doorPivot); }, i * 100); }); // Force refresh of dimensions after all doors finish animating if (this.currentViewMode === 'top') { setTimeout(() => { if (this.doorSwingArcs) { this.scene3D.remove(this.doorSwingArcs); } const totalWidth = this.config3D.numDoors * this.config3D.doorWidth * 0.05; this.createDoorSwingArcs3D(totalWidth); this.doorSwingArcs.visible = true; // Force update of visibility this.updateVisibility(); }, this.doors3D.length * 100 + 1000); } } resetView() { const aspect = window.innerWidth / window.innerHeight; this.camera3D = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000); this.camera3D.position.set(10, 6, 15); this.camera3D.lookAt(0, 0, 0); this.targetRotationX = 0; this.targetRotationY = 0; this.scene3D.background = new THREE.Color(0xf8f8f8); this.scene3D.fog = new THREE.Fog(0xf8f8f8, 50, 200); this.doorsOpen = false; this.doors3D.forEach(doorGroup => { const doorPivot = doorGroup.userData.doorPivot; const userData = doorPivot.userData; userData.isOpen = false; userData.targetRotation = 0; this.animateDoor3D(doorPivot); }); } onWindowResize3D() { if (!this.camera3D || !this.renderer3D) return; const container = document.getElementById('viewer3DContainer'); if (!container) return; const width = container.clientWidth; const height = container.clientHeight; this.camera3D.aspect = width / height; this.camera3D.updateProjectionMatrix(); this.renderer3D.setSize(width, height); } animate3D() { requestAnimationFrame(() => this.animate3D()); if (this.frameSystem3D && this.currentViewMode === '3d') { this.frameSystem3D.rotation.y += (this.targetRotationY - this.frameSystem3D.rotation.y) * 0.08; this.frameSystem3D.rotation.x += (this.targetRotationX - this.frameSystem3D.rotation.x) * 0.08; } if (this.renderer3D && this.scene3D && this.camera3D) { this.renderer3D.render(this.scene3D, this.camera3D); } } // ============================================== // PERFORMANCE MONITORING & TESTING // ============================================== static runBasicTests() { if (!Environment.isDevelopment) return; DebugUtils.log('Running basic application tests...'); try { // Test calculation engine const calculator = new DoorCalculator(); const legacyWidth = calculator.calculateLegacyWidth(21.75, 4); console.assert(legacyWidth === 92.75, 'Legacy width calculation failed'); const advantageWidth = calculator.calculateAdvantageWidth(22.375, 4); console.assert(advantageWidth === 94.125, 'Advantage width calculation failed'); // Test input validation console.assert(InputValidator.validateEmail('test@example.com'), 'Email validation failed'); console.assert(!InputValidator.validateEmail('invalid-email'), 'Email validation should fail'); const num = InputValidator.validateNumericInput('5', 1, 10); console.assert(num === 5, 'Numeric validation failed'); DebugUtils.log('✓ All basic tests passed'); } catch (error) { DebugUtils.error('✗ Basic tests failed', error); } } static initPerformanceMonitoring() { if (!Environment.isDevelopment) return; // Monitor long tasks if ('PerformanceObserver' in window) { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.duration > 50) { DebugUtils.log(`Long task detected: ${entry.duration}ms`); } } }); observer.observe({entryTypes: ['longtask']}); } // Monitor memory usage if (performance.memory) { setInterval(() => { const memory = performance.memory; DebugUtils.log('Memory usage:', { used: Math.round(memory.usedJSHeapSize / 1024 / 1024), total: Math.round(memory.totalJSHeapSize / 1024 / 1024), limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024) }); }, 30000); } } } // ============================================== // GLOBAL APPLICATION INSTANCE // ============================================== // Create global application instance const CDSApp = new CDSApplication(); // Make methods available globally for onclick handlers window.CDSApp = CDSApp; // ============================================== // APPLICATION INITIALIZATION // ============================================== // ============================================== // APPLICATION INITIALIZATION // ============================================== // Initialize debugging tools and performance monitoring document.addEventListener('DOMContentLoaded', () => { try { CDSApplication.runBasicTests(); CDSApplication.initPerformanceMonitoring(); DebugUtils.log('CDS Application loaded successfully'); } catch (error) { DebugUtils.error('Failed to initialize debugging tools', error); } }); // Handle page visibility for performance document.addEventListener('visibilitychange', () => { if (document.hidden) { DebugUtils.log('Page hidden - pausing non-critical operations'); } else { DebugUtils.log('Page visible - resuming operations'); } }); // Handle errors globally window.addEventListener('error', (event) => { DebugUtils.error('Global error caught', { message: event.message, filename: event.filename, line: event.lineno, column: event.colno, error: event.error }); }); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { DebugUtils.error('Unhandled promise rejection', event.reason); });