499 lines
17 KiB
HTML
499 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Enclosure Preview — PCB Tools</title>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<style>
|
||
.preview-container {
|
||
max-width: 800px;
|
||
}
|
||
|
||
.board-canvas-wrap {
|
||
background: #1a1a2e;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 1rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.board-canvas-wrap canvas {
|
||
max-width: 100%;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.option-group {
|
||
margin-bottom: 1rem;
|
||
padding: 0.75rem;
|
||
background: #f9fafb;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.option-group label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
font-size: 0.9rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.option-group input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
accent-color: var(--primary);
|
||
}
|
||
|
||
.option-hint {
|
||
font-size: 0.75rem;
|
||
color: #6b7280;
|
||
margin-left: 26px;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.side-editor {
|
||
display: none;
|
||
margin-top: 1rem;
|
||
padding: 1rem;
|
||
background: #f0f1f5;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.side-editor.active {
|
||
display: block;
|
||
}
|
||
|
||
.face-tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.face-tab {
|
||
flex: 1;
|
||
padding: 0.4rem;
|
||
text-align: center;
|
||
border: 1px solid var(--border);
|
||
background: white;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.face-tab:first-child {
|
||
border-radius: 4px 0 0 4px;
|
||
}
|
||
|
||
.face-tab:last-child {
|
||
border-radius: 0 4px 4px 0;
|
||
}
|
||
|
||
.face-tab.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.side-canvas-wrap {
|
||
background: #e5e7eb;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
margin-bottom: 0.75rem;
|
||
position: relative;
|
||
cursor: crosshair;
|
||
}
|
||
|
||
.side-canvas-wrap canvas {
|
||
width: 100%;
|
||
display: block;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.coord-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.coord-row label {
|
||
font-size: 0.75rem;
|
||
margin-bottom: 0.2rem;
|
||
}
|
||
|
||
.coord-row input {
|
||
font-size: 0.85rem;
|
||
padding: 0.3rem;
|
||
}
|
||
|
||
.cutout-list {
|
||
max-height: 120px;
|
||
overflow-y: auto;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.cutout-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.3rem 0.5rem;
|
||
background: white;
|
||
border-radius: 4px;
|
||
margin-bottom: 0.3rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.cutout-item button {
|
||
background: #ef4444;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 0.15rem 0.4rem;
|
||
cursor: pointer;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.btn-row {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 0.4rem 0.8rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
background: white;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.btn-small:hover {
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.unit-note {
|
||
font-size: 0.7rem;
|
||
color: #9ca3af;
|
||
text-align: right;
|
||
margin-top: 0.2rem;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container preview-container">
|
||
<h1>Enclosure Preview</h1>
|
||
|
||
<!-- Top-down board view -->
|
||
<div class="board-canvas-wrap">
|
||
<canvas id="boardCanvas" width="600" height="400"></canvas>
|
||
</div>
|
||
|
||
<!-- Options -->
|
||
<div class="option-group">
|
||
<label>
|
||
<input type="checkbox" id="optConform" checked>
|
||
Conform to edge cuts
|
||
</label>
|
||
<div class="option-hint">Enclosure walls follow the board outline shape instead of a rectangular box.</div>
|
||
</div>
|
||
|
||
<div class="option-group">
|
||
<label>
|
||
<input type="checkbox" id="optSideCutout">
|
||
Add side cutout (USB-C, connectors)
|
||
</label>
|
||
<div class="option-hint">Place rounded-rectangle cutouts on enclosure side walls.</div>
|
||
</div>
|
||
|
||
<!-- Side cutout editor -->
|
||
<div class="side-editor" id="sideEditor">
|
||
<div class="face-tabs" id="faceTabs">
|
||
<div class="face-tab active" data-face="north">North</div>
|
||
<div class="face-tab" data-face="east">East</div>
|
||
<div class="face-tab" data-face="south">South</div>
|
||
<div class="face-tab" data-face="west">West</div>
|
||
</div>
|
||
|
||
<div class="side-canvas-wrap" id="sideCanvasWrap">
|
||
<canvas id="sideCanvas" width="700" height="200"></canvas>
|
||
</div>
|
||
|
||
<div class="coord-row">
|
||
<div class="form-group">
|
||
<label for="cutX">X (mm)</label>
|
||
<input type="number" id="cutX" value="0" step="0.01">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="cutY">Y (mm)</label>
|
||
<input type="number" id="cutY" value="0" step="0.01">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="cutW">Width (mm)</label>
|
||
<input type="number" id="cutW" value="9.0" step="0.01">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="cutH">Height (mm)</label>
|
||
<input type="number" id="cutH" value="3.5" step="0.01">
|
||
</div>
|
||
</div>
|
||
<div class="coord-row">
|
||
<div class="form-group">
|
||
<label for="cutR">Corner Radius (mm)</label>
|
||
<input type="number" id="cutR" value="0.8" step="0.01">
|
||
</div>
|
||
<div class="form-group"></div>
|
||
<div class="form-group"></div>
|
||
<div class="form-group">
|
||
<div class="unit-note">All values in mm (0.01mm precision)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn-small" id="btnAddCutout">+ Add Cutout</button>
|
||
</div>
|
||
|
||
<div class="cutout-list" id="cutoutList"></div>
|
||
</div>
|
||
|
||
<form id="generateForm" method="POST" action="/generate-enclosure">
|
||
<input type="hidden" name="sessionId" id="sessionId">
|
||
<input type="hidden" name="sideCutouts" id="sideCutoutsInput">
|
||
<input type="hidden" name="conformToEdge" id="conformInput" value="true">
|
||
<button type="submit" class="submit-btn">Generate Enclosure</button>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
// Session data loaded from server
|
||
var sessionData = null;
|
||
var sideCutouts = [];
|
||
var currentFace = 'north';
|
||
var dragStart = null;
|
||
var dragCurrent = null;
|
||
|
||
// Board dimensions from session (set by server-rendered JSON)
|
||
var boardInfo = {{.BoardInfoJSON }};
|
||
var sessionId = '{{.SessionID}}';
|
||
|
||
document.getElementById('sessionId').value = sessionId;
|
||
|
||
// Initialize board canvas
|
||
var boardCanvas = document.getElementById('boardCanvas');
|
||
var boardCtx = boardCanvas.getContext('2d');
|
||
|
||
// Load and draw the board preview image
|
||
var boardImg = new Image();
|
||
boardImg.onload = function () {
|
||
var scale = Math.min(boardCanvas.width / boardImg.width, boardCanvas.height / boardImg.height);
|
||
var w = boardImg.width * scale;
|
||
var h = boardImg.height * scale;
|
||
var x = (boardCanvas.width - w) / 2;
|
||
var y = (boardCanvas.height - h) / 2;
|
||
boardCtx.fillStyle = '#1a1a2e';
|
||
boardCtx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
|
||
boardCtx.drawImage(boardImg, x, y, w, h);
|
||
};
|
||
boardImg.src = '/preview-image/' + sessionId;
|
||
|
||
// Side cutout checkbox toggle
|
||
document.getElementById('optSideCutout').addEventListener('change', function () {
|
||
document.getElementById('sideEditor').classList.toggle('active', this.checked);
|
||
if (this.checked) drawSideFace();
|
||
});
|
||
|
||
// Conform checkbox
|
||
document.getElementById('optConform').addEventListener('change', function () {
|
||
document.getElementById('conformInput').value = this.checked ? 'true' : 'false';
|
||
});
|
||
|
||
// Face tabs
|
||
document.querySelectorAll('.face-tab').forEach(function (tab) {
|
||
tab.addEventListener('click', function () {
|
||
document.querySelectorAll('.face-tab').forEach(function (t) { t.classList.remove('active'); });
|
||
tab.classList.add('active');
|
||
currentFace = tab.dataset.face;
|
||
drawSideFace();
|
||
});
|
||
});
|
||
|
||
// Get face dimensions in mm
|
||
function getFaceDims() {
|
||
var info = boardInfo;
|
||
if (currentFace === 'north' || currentFace === 'south') {
|
||
return { width: info.boardW, height: info.totalH };
|
||
} else {
|
||
return { width: info.boardH, height: info.totalH };
|
||
}
|
||
}
|
||
|
||
// Draw side face
|
||
function drawSideFace() {
|
||
var canvas = document.getElementById('sideCanvas');
|
||
var ctx = canvas.getContext('2d');
|
||
var dims = getFaceDims();
|
||
|
||
// Scale to fit canvas
|
||
var scaleX = (canvas.width - 20) / dims.width;
|
||
var scaleY = (canvas.height - 20) / dims.height;
|
||
var scale = Math.min(scaleX, scaleY);
|
||
|
||
var offX = (canvas.width - dims.width * scale) / 2;
|
||
var offY = (canvas.height - dims.height * scale) / 2;
|
||
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Draw wall face
|
||
ctx.fillStyle = '#d1d5db';
|
||
ctx.strokeStyle = '#6b7280';
|
||
ctx.lineWidth = 1;
|
||
ctx.fillRect(offX, offY, dims.width * scale, dims.height * scale);
|
||
ctx.strokeRect(offX, offY, dims.width * scale, dims.height * scale);
|
||
|
||
// Draw existing cutouts for this face
|
||
ctx.fillStyle = '#1a1a2e';
|
||
sideCutouts.forEach(function (c) {
|
||
if (c.face !== currentFace) return;
|
||
drawRoundedRect(ctx, offX + c.x * scale, offY + (dims.height - c.y - c.h) * scale,
|
||
c.w * scale, c.h * scale, c.r * scale);
|
||
});
|
||
|
||
// Draw drag preview
|
||
if (dragStart && dragCurrent) {
|
||
ctx.fillStyle = 'rgba(37, 99, 235, 0.3)';
|
||
ctx.strokeStyle = 'var(--primary)';
|
||
var x1 = Math.min(dragStart.x, dragCurrent.x);
|
||
var y1 = Math.min(dragStart.y, dragCurrent.y);
|
||
var w = Math.abs(dragCurrent.x - dragStart.x);
|
||
var h = Math.abs(dragCurrent.y - dragStart.y);
|
||
ctx.fillRect(x1, y1, w, h);
|
||
ctx.strokeRect(x1, y1, w, h);
|
||
}
|
||
|
||
// Draw mm grid labels
|
||
ctx.fillStyle = '#9ca3af';
|
||
ctx.font = '10px sans-serif';
|
||
ctx.textAlign = 'center';
|
||
var step = Math.ceil(dims.width / 10);
|
||
for (var mm = 0; mm <= dims.width; mm += step) {
|
||
var px = offX + mm * scale;
|
||
ctx.fillText(mm + '', px, canvas.height - 2);
|
||
}
|
||
}
|
||
|
||
function drawRoundedRect(ctx, x, y, w, h, r) {
|
||
r = Math.min(r, w / 2, h / 2);
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + r, y);
|
||
ctx.lineTo(x + w - r, y);
|
||
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||
ctx.lineTo(x + w, y + h - r);
|
||
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||
ctx.lineTo(x + r, y + h);
|
||
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||
ctx.lineTo(x, y + r);
|
||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
}
|
||
|
||
// Mouse drag on side canvas
|
||
var sideCanvas = document.getElementById('sideCanvas');
|
||
sideCanvas.addEventListener('mousedown', function (e) {
|
||
var rect = sideCanvas.getBoundingClientRect();
|
||
dragStart = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||
dragCurrent = null;
|
||
});
|
||
sideCanvas.addEventListener('mousemove', function (e) {
|
||
if (!dragStart) return;
|
||
var rect = sideCanvas.getBoundingClientRect();
|
||
dragCurrent = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||
drawSideFace();
|
||
});
|
||
sideCanvas.addEventListener('mouseup', function (e) {
|
||
if (!dragStart || !dragCurrent) { dragStart = null; return; }
|
||
var rect = sideCanvas.getBoundingClientRect();
|
||
dragCurrent = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||
|
||
// Convert pixel coords to mm
|
||
var dims = getFaceDims();
|
||
var scaleX = (sideCanvas.width - 20) / dims.width;
|
||
var scaleY = (sideCanvas.height - 20) / dims.height;
|
||
var scale = Math.min(scaleX, scaleY);
|
||
var offX = (sideCanvas.width - dims.width * scale) / 2;
|
||
var offY = (sideCanvas.height - dims.height * scale) / 2;
|
||
|
||
var x1 = Math.min(dragStart.x, dragCurrent.x);
|
||
var y1 = Math.min(dragStart.y, dragCurrent.y);
|
||
var w = Math.abs(dragCurrent.x - dragStart.x);
|
||
var h = Math.abs(dragCurrent.y - dragStart.y);
|
||
|
||
var mmX = (x1 - offX) / scale;
|
||
var mmY = dims.height - (y1 + h - offY) / scale;
|
||
var mmW = w / scale;
|
||
var mmH = h / scale;
|
||
|
||
if (mmW > 0.5 && mmH > 0.5) {
|
||
document.getElementById('cutX').value = mmX.toFixed(2);
|
||
document.getElementById('cutY').value = mmY.toFixed(2);
|
||
document.getElementById('cutW').value = mmW.toFixed(2);
|
||
document.getElementById('cutH').value = mmH.toFixed(2);
|
||
}
|
||
|
||
dragStart = null;
|
||
dragCurrent = null;
|
||
drawSideFace();
|
||
});
|
||
|
||
// Add cutout button
|
||
document.getElementById('btnAddCutout').addEventListener('click', function () {
|
||
var c = {
|
||
face: currentFace,
|
||
x: parseFloat(document.getElementById('cutX').value) || 0,
|
||
y: parseFloat(document.getElementById('cutY').value) || 0,
|
||
w: parseFloat(document.getElementById('cutW').value) || 9,
|
||
h: parseFloat(document.getElementById('cutH').value) || 3.5,
|
||
r: parseFloat(document.getElementById('cutR').value) || 0.8
|
||
};
|
||
sideCutouts.push(c);
|
||
updateCutoutList();
|
||
drawSideFace();
|
||
});
|
||
|
||
function updateCutoutList() {
|
||
var list = document.getElementById('cutoutList');
|
||
list.innerHTML = '';
|
||
sideCutouts.forEach(function (c, i) {
|
||
var div = document.createElement('div');
|
||
div.className = 'cutout-item';
|
||
div.innerHTML = '<span>' + c.face.toUpperCase() + ': ' +
|
||
c.w.toFixed(1) + '×' + c.h.toFixed(1) + 'mm at (' +
|
||
c.x.toFixed(1) + ',' + c.y.toFixed(1) + ') r=' + c.r.toFixed(1) +
|
||
'</span><button onclick="removeCutout(' + i + ')">✕</button>';
|
||
list.appendChild(div);
|
||
});
|
||
// Update hidden form field
|
||
document.getElementById('sideCutoutsInput').value = JSON.stringify(sideCutouts);
|
||
}
|
||
|
||
window.removeCutout = function (i) {
|
||
sideCutouts.splice(i, 1);
|
||
updateCutoutList();
|
||
drawSideFace();
|
||
};
|
||
|
||
// Form submit
|
||
document.getElementById('generateForm').addEventListener('submit', function () {
|
||
document.getElementById('sideCutoutsInput').value = JSON.stringify(sideCutouts);
|
||
var btn = this.querySelector('.submit-btn');
|
||
btn.disabled = true;
|
||
btn.innerText = 'Generating...';
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |