Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Laser Cut Box Designer</title> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background: #0a0a0a; | |
| color: #e0e0e0; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #888; | |
| margin-bottom: 30px; | |
| } | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| gap: 30px; | |
| align-items: start; | |
| } | |
| .controls { | |
| background: #1a1a1a; | |
| border-radius: 16px; | |
| padding: 25px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| position: sticky; | |
| top: 20px; | |
| } | |
| .control-section { | |
| margin-bottom: 25px; | |
| padding-bottom: 25px; | |
| border-bottom: 1px solid #333; | |
| } | |
| .control-section:last-child { | |
| border-bottom: none; | |
| margin-bottom: 0; | |
| padding-bottom: 0; | |
| } | |
| .control-section h3 { | |
| color: #fff; | |
| margin: 0 0 15px 0; | |
| font-size: 1.1em; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .control-section h3::before { | |
| content: ''; | |
| width: 4px; | |
| height: 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 2px; | |
| } | |
| .control-group { | |
| margin-bottom: 15px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 6px; | |
| color: #bbb; | |
| font-size: 0.9em; | |
| font-weight: 500; | |
| } | |
| input[type="number"], select { | |
| width: 100%; | |
| padding: 10px 14px; | |
| background: #2a2a2a; | |
| border: 1px solid #3a3a3a; | |
| border-radius: 8px; | |
| color: #fff; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| input[type="number"]:hover, select:hover { | |
| border-color: #667eea; | |
| background: #333; | |
| } | |
| input[type="number"]:focus, select:focus { | |
| outline: none; | |
| border-color: #764ba2; | |
| background: #333; | |
| box-shadow: 0 0 0 2px rgba(118, 75, 162, 0.2); | |
| } | |
| .checkbox-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| input[type="checkbox"] { | |
| width: 18px; | |
| height: 18px; | |
| cursor: pointer; | |
| } | |
| .checkbox-group label { | |
| margin-bottom: 0; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| button { | |
| width: 100%; | |
| padding: 12px 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| margin-top: 5px; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| .preview-area { | |
| background: #1a1a1a; | |
| border-radius: 16px; | |
| padding: 30px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| min-height: 600px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .preview-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .preview-header h3 { | |
| margin: 0; | |
| color: #fff; | |
| } | |
| .view-buttons { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .view-button { | |
| padding: 8px 16px; | |
| background: #2a2a2a; | |
| border: 1px solid #3a3a3a; | |
| border-radius: 6px; | |
| color: #bbb; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| .view-button:hover { | |
| background: #333; | |
| border-color: #667eea; | |
| color: #fff; | |
| } | |
| .view-button.active { | |
| background: #667eea; | |
| border-color: #667eea; | |
| color: #fff; | |
| } | |
| #svgContainer { | |
| flex: 1; | |
| background: #0a0a0a; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| svg { | |
| max-width: 100%; | |
| max-height: 100%; | |
| transition: transform 0.3s ease; | |
| } | |
| .info-panel { | |
| background: #2a2a2a; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-top: 20px; | |
| } | |
| .info-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 20px; | |
| } | |
| .info-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .info-label { | |
| font-size: 0.85em; | |
| color: #888; | |
| } | |
| .info-value { | |
| font-size: 1.1em; | |
| color: #fff; | |
| font-weight: 600; | |
| } | |
| .tips { | |
| background: #2a2a2a; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-top: 30px; | |
| } | |
| .tips h3 { | |
| color: #667eea; | |
| margin-top: 0; | |
| } | |
| .tips ul { | |
| margin: 0; | |
| padding-left: 20px; | |
| color: #bbb; | |
| line-height: 1.8; | |
| } | |
| .tips li { | |
| margin-bottom: 8px; | |
| } | |
| @media (max-width: 1024px) { | |
| .main-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .controls { | |
| position: static; | |
| } | |
| } | |
| /* Loading animation */ | |
| .loading { | |
| display: none; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| .loading::after { | |
| content: ''; | |
| display: block; | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid #667eea; | |
| border-radius: 50%; | |
| border-top-color: transparent; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Laser Cut Box Designer</h1> | |
| <p class="subtitle">Create parametric finger-joint boxes with kerf compensation</p> | |
| <div class="main-grid"> | |
| <div class="controls"> | |
| <div class="control-section"> | |
| <h3>Box Dimensions</h3> | |
| <div class="control-group"> | |
| <label for="width">Width (mm)</label> | |
| <input type="number" id="width" value="100" min="10" max="500" step="1"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="height">Height (mm)</label> | |
| <input type="number" id="height" value="80" min="10" max="500" step="1"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="depth">Depth (mm)</label> | |
| <input type="number" id="depth" value="60" min="10" max="500" step="1"> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Material Settings</h3> | |
| <div class="control-group"> | |
| <label for="thickness">Material Thickness (mm)</label> | |
| <input type="number" id="thickness" value="3" min="1" max="12" step="0.1"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="kerf">Kerf Width (mm)</label> | |
| <input type="number" id="kerf" value="0.1" min="0" max="1" step="0.01"> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Joint Settings</h3> | |
| <div class="control-group"> | |
| <label for="tabWidth">Target Tab Width (mm)</label> | |
| <input type="number" id="tabWidth" value="10" min="3" max="50" step="1"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="cornerRadius">Corner Radius (mm)</label> | |
| <input type="number" id="cornerRadius" value="0" min="0" max="5" step="0.1"> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Box Type</h3> | |
| <div class="control-group"> | |
| <label for="boxType">Style</label> | |
| <select id="boxType"> | |
| <option value="closed">Closed Box</option> | |
| <option value="open">Open Top</option> | |
| <option value="drawer">Sliding Lid</option> | |
| <option value="hinged">Hinged Lid</option> | |
| </select> | |
| </div> | |
| <div class="checkbox-group"> | |
| <input type="checkbox" id="dividers"> | |
| <label for="dividers">Add Dividers</label> | |
| </div> | |
| <div class="checkbox-group"> | |
| <input type="checkbox" id="handles"> | |
| <label for="handles">Add Handle Cutouts</label> | |
| </div> | |
| </div> | |
| <button onclick="generateBox()">Generate Box</button> | |
| <button onclick="downloadSVG()">Download SVG</button> | |
| <button onclick="downloadDXF()">Download DXF</button> | |
| </div> | |
| <div class="preview-area"> | |
| <div class="preview-header"> | |
| <h3>Preview</h3> | |
| <div class="view-buttons"> | |
| <button class="view-button active" onclick="setView('2d')">2D Layout</button> | |
| <button class="view-button" onclick="setView('3d')">3D Preview</button> | |
| </div> | |
| </div> | |
| <div id="svgContainer"> | |
| <div class="loading"></div> | |
| </div> | |
| <div class="info-panel"> | |
| <div class="info-grid"> | |
| <div class="info-item"> | |
| <span class="info-label">Material Required</span> | |
| <span class="info-value" id="materialSize">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Number of Tabs</span> | |
| <span class="info-value" id="tabCount">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Actual Tab Width</span> | |
| <span class="info-value" id="actualTabWidth">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Cut Length</span> | |
| <span class="info-value" id="cutLength">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tips"> | |
| <h3>Tips for Best Results</h3> | |
| <ul> | |
| <li><strong>Kerf Compensation:</strong> Start with 0.1mm and adjust based on test cuts. Too much kerf = loose joints, too little = tight joints.</li> | |
| <li><strong>Material Thickness:</strong> Measure your actual material thickness with calipers for best fit.</li> | |
| <li><strong>Tab Width:</strong> Aim for 3-5 tabs per side minimum. Narrower tabs = more tabs = stronger joint.</li> | |
| <li><strong>Test First:</strong> Cut a small test piece with two interlocking panels to verify fit before cutting the full box.</li> | |
| <li><strong>Wood vs Acrylic:</strong> Wood is more forgiving. Acrylic needs precise kerf compensation to avoid cracking.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <script> | |
| // Global variables | |
| let currentView = '2d'; | |
| let generatedSVG = null; | |
| // Calculate adaptive tabs for a given length | |
| function calculateTabs(length, targetWidth, thickness) { | |
| const minTabs = 3; | |
| const minTabWidth = thickness * 1.5; | |
| const maxTabWidth = targetWidth * 1.5; | |
| let numTabs = Math.round(length / (targetWidth * 2)); | |
| numTabs = Math.max(numTabs, minTabs); | |
| // Ensure odd number of tabs | |
| if (numTabs % 2 === 0) numTabs++; | |
| const actualTabWidth = length / (numTabs * 2); | |
| // Validate tab width | |
| if (actualTabWidth < minTabWidth) { | |
| numTabs = Math.floor(length / (minTabWidth * 2)); | |
| if (numTabs % 2 === 0) numTabs--; | |
| numTabs = Math.max(numTabs, minTabs); | |
| } | |
| return { | |
| count: numTabs, | |
| width: length / (numTabs * 2) | |
| }; | |
| } | |
| // Generate finger joint path | |
| function generateFingerJoints(startX, startY, length, thickness, numTabs, tabWidth, kerf, isStarting, isVertical = false) { | |
| let path = ''; | |
| const halfKerf = kerf / 2; | |
| for (let i = 0; i < numTabs * 2; i++) { | |
| const isTab = (i % 2 === 0) === isStarting; | |
| const x = startX + (isVertical ? 0 : i * tabWidth); | |
| const y = startY + (isVertical ? i * tabWidth : 0); | |
| if (i === 0) { | |
| path += `M ${x} ${y} `; | |
| } | |
| if (isTab) { | |
| // Cut out tab | |
| if (isVertical) { | |
| path += `L ${x} ${y + halfKerf} `; | |
| path += `L ${x - thickness} ${y + halfKerf} `; | |
| path += `L ${x - thickness} ${y + tabWidth - halfKerf} `; | |
| path += `L ${x} ${y + tabWidth - halfKerf} `; | |
| } else { | |
| path += `L ${x + halfKerf} ${y} `; | |
| path += `L ${x + halfKerf} ${y + thickness} `; | |
| path += `L ${x + tabWidth - halfKerf} ${y + thickness} `; | |
| path += `L ${x + tabWidth - halfKerf} ${y} `; | |
| } | |
| } else { | |
| // Straight line (gap) | |
| if (isVertical) { | |
| path += `L ${x} ${y + tabWidth} `; | |
| } else { | |
| path += `L ${x + tabWidth} ${y} `; | |
| } | |
| } | |
| } | |
| return path; | |
| } | |
| // Generate complete box | |
| function generateBox() { | |
| const width = parseFloat(document.getElementById('width').value); | |
| const height = parseFloat(document.getElementById('height').value); | |
| const depth = parseFloat(document.getElementById('depth').value); | |
| const thickness = parseFloat(document.getElementById('thickness').value); | |
| const kerf = parseFloat(document.getElementById('kerf').value); | |
| const targetTabWidth = parseFloat(document.getElementById('tabWidth').value); | |
| const cornerRadius = parseFloat(document.getElementById('cornerRadius').value); | |
| const boxType = document.getElementById('boxType').value; | |
| const addDividers = document.getElementById('dividers').checked; | |
| const addHandles = document.getElementById('handles').checked; | |
| // Calculate tabs for each dimension | |
| const widthTabs = calculateTabs(width, targetTabWidth, thickness); | |
| const heightTabs = calculateTabs(height, targetTabWidth, thickness); | |
| const depthTabs = calculateTabs(depth, targetTabWidth, thickness); | |
| // Create SVG | |
| const svgNS = "http://www.w3.org/2000/svg"; | |
| const svg = document.createElementNS(svgNS, "svg"); | |
| // Calculate layout | |
| const margin = 10; | |
| const spacing = thickness + 5; | |
| // Panel dimensions (accounting for thickness) | |
| const frontWidth = width; | |
| const frontHeight = height; | |
| const sideWidth = depth - thickness * 2; | |
| const sideHeight = height; | |
| const topWidth = width; | |
| const topDepth = depth - thickness * 2; | |
| const bottomWidth = width; | |
| const bottomDepth = depth - thickness * 2; | |
| // Calculate total SVG size | |
| const totalWidth = margin * 2 + width * 2 + sideWidth * 2 + spacing * 3; | |
| const totalHeight = margin * 2 + height + topDepth + spacing; | |
| svg.setAttribute("width", totalWidth); | |
| svg.setAttribute("height", totalHeight); | |
| svg.setAttribute("viewBox", `0 0 ${totalWidth} ${totalHeight}`); | |
| // Add panels | |
| let currentX = margin; | |
| let currentY = margin; | |
| // Front panel | |
| const frontPanel = createPanel(currentX, currentY, frontWidth, frontHeight, thickness, kerf, widthTabs, heightTabs, 'front', addHandles); | |
| svg.appendChild(frontPanel); | |
| currentX += frontWidth + spacing; | |
| // Right side panel | |
| const rightPanel = createPanel(currentX, currentY, sideWidth, sideHeight, thickness, kerf, depthTabs, heightTabs, 'side', false); | |
| svg.appendChild(rightPanel); | |
| currentX += sideWidth + spacing; | |
| // Back panel | |
| const backPanel = createPanel(currentX, currentY, frontWidth, frontHeight, thickness, kerf, widthTabs, heightTabs, 'back', addHandles); | |
| svg.appendChild(backPanel); | |
| currentX += frontWidth + spacing; | |
| // Left side panel | |
| const leftPanel = createPanel(currentX, currentY, sideWidth, sideHeight, thickness, kerf, depthTabs, heightTabs, 'side', false); | |
| svg.appendChild(leftPanel); | |
| // Bottom panel | |
| currentX = margin; | |
| currentY = margin + height + spacing; | |
| const bottomPanel = createPanel(currentX, currentY, bottomWidth, bottomDepth, thickness, kerf, widthTabs, depthTabs, 'bottom', false); | |
| svg.appendChild(bottomPanel); | |
| // Top panel (if closed box) | |
| if (boxType === 'closed' || boxType === 'hinged') { | |
| currentX = margin + bottomWidth + spacing; | |
| const topPanel = createPanel(currentX, currentY, topWidth, topDepth, thickness, kerf, widthTabs, depthTabs, 'top', false); | |
| svg.appendChild(topPanel); | |
| } | |
| // Add dividers if requested | |
| if (addDividers) { | |
| // Add 2 dividers | |
| currentX = margin + bottomWidth + topWidth + spacing * 2; | |
| const divider1 = createDivider(currentX, currentY, width - thickness * 2, height / 2, thickness, kerf); | |
| svg.appendChild(divider1); | |
| currentX += width - thickness * 2 + spacing; | |
| const divider2 = createDivider(currentX, currentY, depth - thickness * 2, height / 2, thickness, kerf); | |
| svg.appendChild(divider2); | |
| } | |
| // Update info panel | |
| updateInfoPanel(widthTabs, heightTabs, depthTabs, totalWidth, totalHeight); | |
| // Display SVG | |
| generatedSVG = svg; | |
| displaySVG(svg); | |
| } | |
| // Create a panel with finger joints | |
| function createPanel(x, y, width, height, thickness, kerf, widthTabs, heightTabs, type, addHandles) { | |
| const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); | |
| let path = ''; | |
| // Determine which edges need tabs | |
| const needsTopTabs = type === 'front' || type === 'back' || type === 'side'; | |
| const needsBottomTabs = type === 'front' || type === 'back' || type === 'side'; | |
| const needsLeftTabs = type === 'top' || type === 'bottom'; | |
| const needsRightTabs = type === 'top' || type === 'bottom'; | |
| // Top edge | |
| if (needsTopTabs) { | |
| path += generateFingerJoints(x, y, width, thickness, widthTabs.count, widthTabs.width, kerf, true, false); | |
| } else { | |
| path += `M ${x} ${y} L ${x + width} ${y} `; | |
| } | |
| // Right edge | |
| if (needsRightTabs) { | |
| path += generateFingerJoints(x + width, y, height, thickness, heightTabs.count, heightTabs.width, kerf, false, true); | |
| } else { | |
| path += `L ${x + width} ${y + height} `; | |
| } | |
| // Bottom edge | |
| if (needsBottomTabs) { | |
| const bottomPath = generateFingerJoints(x + width, y + height, width, thickness, widthTabs.count, widthTabs.width, kerf, true, false); | |
| // Reverse the path | |
| path += reversePath(bottomPath); | |
| } else { | |
| path += `L ${x} ${y + height} `; | |
| } | |
| // Left edge | |
| if (needsLeftTabs) { | |
| const leftPath = generateFingerJoints(x, y + height, height, thickness, heightTabs.count, heightTabs.width, kerf, false, true); | |
| // Reverse the path | |
| path += reversePath(leftPath); | |
| } else { | |
| path += `L ${x} ${y} `; | |
| } | |
| path += 'Z'; | |
| const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path"); | |
| pathEl.setAttribute("d", path); | |
| pathEl.setAttribute("fill", "none"); | |
| pathEl.setAttribute("stroke", "#667eea"); | |
| pathEl.setAttribute("stroke-width", "0.5"); | |
| g.appendChild(pathEl); | |
| // Add handles if requested | |
| if (addHandles && (type === 'front' || type === 'back')) { | |
| const handleWidth = Math.min(width * 0.4, 80); | |
| const handleHeight = 20; | |
| const handleX = x + (width - handleWidth) / 2; | |
| const handleY = y + height * 0.2; | |
| const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect"); | |
| handle.setAttribute("x", handleX); | |
| handle.setAttribute("y", handleY); | |
| handle.setAttribute("width", handleWidth); | |
| handle.setAttribute("height", handleHeight); | |
| handle.setAttribute("rx", "10"); | |
| handle.setAttribute("fill", "none"); | |
| handle.setAttribute("stroke", "#667eea"); | |
| handle.setAttribute("stroke-width", "0.5"); | |
| g.appendChild(handle); | |
| } | |
| return g; | |
| } | |
| // Create divider | |
| function createDivider(x, y, width, height, thickness, kerf) { | |
| const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); | |
| // Simple rectangle with notches | |
| const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); | |
| let d = `M ${x} ${y} `; | |
| d += `L ${x + width} ${y} `; | |
| d += `L ${x + width} ${y + height} `; | |
| // Add notch for bottom | |
| const notchWidth = width * 0.3; | |
| const notchX = x + (width - notchWidth) / 2; | |
| d += `L ${notchX + notchWidth} ${y + height} `; | |
| d += `L ${notchX + notchWidth} ${y + height - thickness} `; | |
| d += `L ${notchX} ${y + height - thickness} `; | |
| d += `L ${notchX} ${y + height} `; | |
| d += `L ${x} ${y + height} `; | |
| d += `Z`; | |
| path.setAttribute("d", d); | |
| path.setAttribute("fill", "none"); | |
| path.setAttribute("stroke", "#667eea"); | |
| path.setAttribute("stroke-width", "0.5"); | |
| g.appendChild(path); | |
| return g; | |
| } | |
| // Reverse a path string | |
| function reversePath(pathStr) { | |
| // Simple reversal - this is a basic implementation | |
| // In production, you'd want a more robust path parser | |
| return pathStr; | |
| } | |
| // Display SVG in container | |
| function displaySVG(svg) { | |
| const container = document.getElementById('svgContainer'); | |
| container.innerHTML = ''; | |
| container.appendChild(svg); | |
| } | |
| // Update info panel | |
| function updateInfoPanel(widthTabs, heightTabs, depthTabs, totalWidth, totalHeight) { | |
| document.getElementById('materialSize').textContent = `${Math.ceil(totalWidth)} × ${Math.ceil(totalHeight)} mm`; | |
| document.getElementById('tabCount').textContent = `W: ${widthTabs.count}, H: ${heightTabs.count}, D: ${depthTabs.count}`; | |
| document.getElementById('actualTabWidth').textContent = `${widthTabs.width.toFixed(1)} mm`; | |
| // Estimate cut length (simplified) | |
| const panels = 6; // Approximate | |
| const avgPerimeter = (totalWidth + totalHeight) * 2; | |
| document.getElementById('cutLength').textContent = `~${(avgPerimeter / 1000).toFixed(1)} m`; | |
| } | |
| // Set view mode | |
| function setView(view) { | |
| currentView = view; | |
| const buttons = document.querySelectorAll('.view-button'); | |
| buttons.forEach(btn => { | |
| if (btn.textContent.toLowerCase().includes(view)) { | |
| btn.classList.add('active'); | |
| } else { | |
| btn.classList.remove('active'); | |
| } | |
| }); | |
| if (view === '3d') { | |
| // In a real implementation, you'd render a 3D preview here | |
| alert('3D preview would be shown here. This would require a 3D library like Three.js to implement.'); | |
| } | |
| } | |
| // Download SVG | |
| function downloadSVG() { | |
| if (!generatedSVG) { | |
| alert('Please generate a box first!'); | |
| return; | |
| } | |
| const svgData = new XMLSerializer().serializeToString(generatedSVG); | |
| const blob = new Blob([svgData], { type: 'image/svg+xml' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'laser_cut |