test21 / index.html
MarkTheArtist's picture
Add 3 files
9274e29 verified
<!DOCTYPE html>
<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