pcb-to-stencil/static/preview.html

660 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
flex-wrap: wrap;
}
.face-tab {
flex: 1;
min-width: 60px;
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;
}
.preset-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.btn-preset {
padding: 0.35rem 0.7rem;
border: 1px solid #3b82f6;
border-radius: 4px;
background: #eff6ff;
color: #1d4ed8;
cursor: pointer;
font-size: 0.75rem;
font-weight: 500;
}
.btn-preset:hover {
background: #dbeafe;
}
.coord-field {
display: flex;
flex-direction: column;
}
.coord-label-row {
display: flex;
align-items: center;
gap: 0.3rem;
margin-bottom: 0.2rem;
}
.btn-center {
padding: 0.1rem 0.35rem;
border: 1px solid #d1d5db;
border-radius: 3px;
background: #f9fafb;
cursor: pointer;
font-size: 0.65rem;
color: #6b7280;
line-height: 1;
}
.btn-center:hover {
background: #e5e7eb;
color: #374151;
}
.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 with numbered side labels -->
<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>
<div class="side-canvas-wrap" id="sideCanvasWrap">
<canvas id="sideCanvas" width="700" height="200"></canvas>
</div>
<div class="preset-row">
<button type="button" class="btn-preset" id="btnPresetUSBC">⚡ USB-C (9 × 3.26mm r=1.3)</button>
</div>
<div class="coord-row">
<div class="coord-field">
<div class="coord-label-row">
<label for="cutX">X (mm)</label>
<button type="button" class="btn-center" id="btnCenterX" title="Center horizontally">
center</button>
</div>
<input type="number" id="cutX" value="0" step="0.01">
</div>
<div class="coord-field">
<div class="coord-label-row">
<label for="cutY">Y (mm)</label>
<button type="button" class="btn-center" id="btnCenterY" title="Center vertically">
center</button>
</div>
<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="1.3" 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>
var sideCutouts = [];
var currentSide = 1;
var dragStart = null;
var dragCurrent = null;
// Board dimensions from server
var boardInfo = {{.BoardInfoJSON }};
var sessionId = '{{.SessionID}}';
document.getElementById('sessionId').value = sessionId;
// Define sides as numbered segments (clockwise from top)
// For rectangular boards: Side 1=top, 2=right, 3=bottom, 4=left
// Future: server could pass actual polygon segments for irregular boards
var sides = [
{ num: 1, label: 'Side 1 (Top)', length: boardInfo.boardW, pos: 'top' },
{ num: 2, label: 'Side 2 (Right)', length: boardInfo.boardH, pos: 'right' },
{ num: 3, label: 'Side 3 (Bottom)', length: boardInfo.boardW, pos: 'bottom' },
{ num: 4, label: 'Side 4 (Left)', length: boardInfo.boardH, pos: 'left' }
];
// Build side tabs dynamically
var tabsContainer = document.getElementById('faceTabs');
sides.forEach(function (side, i) {
var tab = document.createElement('div');
tab.className = 'face-tab' + (i === 0 ? ' active' : '');
tab.dataset.side = side.num;
tab.textContent = 'Side ' + side.num;
tab.addEventListener('click', function () {
document.querySelectorAll('.face-tab').forEach(function (t) { t.classList.remove('active'); });
tab.classList.add('active');
currentSide = side.num;
drawSideFace();
});
tabsContainer.appendChild(tab);
});
// Colors for side labels
var sideColors = ['#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
// Initialize board canvas
var boardCanvas = document.getElementById('boardCanvas');
var boardCtx = boardCanvas.getContext('2d');
// Board image position (set after load for label drawing)
var boardRect = { x: 0, y: 0, w: 0, h: 0 };
var boardImg = new Image();
boardImg.onload = function () {
drawBoardWithLabels();
};
boardImg.src = '/preview-image/' + sessionId;
function drawBoardWithLabels() {
var ctx = boardCtx;
var scale = Math.min(boardCanvas.width / boardImg.width, boardCanvas.height / boardImg.height) * 0.75;
var w = boardImg.width * scale;
var h = boardImg.height * scale;
var x = (boardCanvas.width - w) / 2;
var y = (boardCanvas.height - h) / 2;
boardRect = { x: x, y: y, w: w, h: h };
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
ctx.drawImage(boardImg, x, y, w, h);
// Draw numbered side labels around the board
ctx.font = 'bold 13px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var labelPad = 18;
sides.forEach(function (side) {
var color = sideColors[(side.num - 1) % sideColors.length];
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
var lx, ly;
switch (side.pos) {
case 'top':
lx = x + w / 2; ly = y - labelPad;
ctx.beginPath(); ctx.moveTo(x, y - 1); ctx.lineTo(x + w, y - 1); ctx.stroke();
break;
case 'right':
lx = x + w + labelPad; ly = y + h / 2;
ctx.beginPath(); ctx.moveTo(x + w + 1, y); ctx.lineTo(x + w + 1, y + h); ctx.stroke();
break;
case 'bottom':
lx = x + w / 2; ly = y + h + labelPad;
ctx.beginPath(); ctx.moveTo(x, y + h + 1); ctx.lineTo(x + w, y + h + 1); ctx.stroke();
break;
case 'left':
lx = x - labelPad; ly = y + h / 2;
ctx.beginPath(); ctx.moveTo(x - 1, y); ctx.lineTo(x - 1, y + h); ctx.stroke();
break;
}
// Draw circled number
ctx.beginPath();
ctx.arc(lx, ly, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.fillText(side.num, lx, ly + 1);
});
}
// 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';
});
// Get face dimensions in mm for current side
function getFaceDims() {
var side = sides.find(function (s) { return s.num === currentSide; });
return { width: side ? side.length : boardInfo.boardW, height: boardInfo.totalH };
}
// Draw side face
function drawSideFace() {
var canvas = document.getElementById('sideCanvas');
var ctx = canvas.getContext('2d');
var dims = getFaceDims();
var side = sides.find(function (s) { return s.num === currentSide; });
var scaleX = (canvas.width - 40) / dims.width;
var scaleY = (canvas.height - 30) / dims.height;
var scale = Math.min(scaleX, scaleY);
var offX = (canvas.width - dims.width * scale) / 2;
var offY = 10;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw wall face
ctx.fillStyle = '#d1d5db';
ctx.strokeStyle = sideColors[(currentSide - 1) % sideColors.length];
ctx.lineWidth = 2;
ctx.fillRect(offX, offY, dims.width * scale, dims.height * scale);
ctx.strokeRect(offX, offY, dims.width * scale, dims.height * scale);
// Side label
ctx.fillStyle = sideColors[(currentSide - 1) % sideColors.length];
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(side ? side.label : 'Side ' + currentSide, offX, offY - 2);
// Draw existing cutouts for this side
ctx.fillStyle = '#1a1a2e';
sideCutouts.forEach(function (c) {
if (c.side !== currentSide) 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 = '#2563eb';
ctx.lineWidth = 1;
var x1 = Math.min(dragStart.x, dragCurrent.x);
var y1 = Math.min(dragStart.y, dragCurrent.y);
var dw = Math.abs(dragCurrent.x - dragStart.x);
var dh = Math.abs(dragCurrent.y - dragStart.y);
ctx.fillRect(x1, y1, dw, dh);
ctx.strokeRect(x1, y1, dw, dh);
}
// 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, offY + dims.height * scale + 14);
}
}
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();
var sx = (e.clientX - rect.left) * (sideCanvas.width / rect.width);
var sy = (e.clientY - rect.top) * (sideCanvas.height / rect.height);
dragStart = { x: sx, y: sy };
dragCurrent = null;
});
sideCanvas.addEventListener('mousemove', function (e) {
if (!dragStart) return;
var rect = sideCanvas.getBoundingClientRect();
dragCurrent = {
x: (e.clientX - rect.left) * (sideCanvas.width / rect.width),
y: (e.clientY - rect.top) * (sideCanvas.height / rect.height)
};
drawSideFace();
});
sideCanvas.addEventListener('mouseup', function (e) {
if (!dragStart || !dragCurrent) { dragStart = null; return; }
var rect = sideCanvas.getBoundingClientRect();
dragCurrent = {
x: (e.clientX - rect.left) * (sideCanvas.width / rect.width),
y: (e.clientY - rect.top) * (sideCanvas.height / rect.height)
};
var dims = getFaceDims();
var scaleX = (sideCanvas.width - 40) / dims.width;
var scaleY = (sideCanvas.height - 30) / dims.height;
var scale = Math.min(scaleX, scaleY);
var offX = (sideCanvas.width - dims.width * scale) / 2;
var offY = 10;
var x1 = Math.min(dragStart.x, dragCurrent.x);
var y1 = Math.min(dragStart.y, dragCurrent.y);
var dw = Math.abs(dragCurrent.x - dragStart.x);
var dh = Math.abs(dragCurrent.y - dragStart.y);
var mmX = (x1 - offX) / scale;
var mmY = dims.height - (y1 + dh - offY) / scale;
var mmW = dw / scale;
var mmH = dh / 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();
});
// USB-C Preset button
document.getElementById('btnPresetUSBC').addEventListener('click', function () {
document.getElementById('cutW').value = '9';
document.getElementById('cutH').value = '3.26';
document.getElementById('cutR').value = '1.3';
drawSideFace();
});
// Horizontal center button (centers X along face width)
document.getElementById('btnCenterX').addEventListener('click', function () {
var dims = getFaceDims();
var w = parseFloat(document.getElementById('cutW').value) || 0;
var x = (dims.width - w) / 2;
document.getElementById('cutX').value = x.toFixed(2);
drawSideFace();
});
// Vertical center button (centers Y along face height)
document.getElementById('btnCenterY').addEventListener('click', function () {
var dims = getFaceDims();
var h = parseFloat(document.getElementById('cutH').value) || 0;
var y = (dims.height - h) / 2;
document.getElementById('cutY').value = y.toFixed(2);
drawSideFace();
});
// Add cutout button
document.getElementById('btnAddCutout').addEventListener('click', function () {
var c = {
side: currentSide,
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) || 1.3
};
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';
var color = sideColors[(c.side - 1) % sideColors.length];
div.innerHTML = '<span style="color:' + color + ';font-weight:600;">Side ' + c.side + '</span>&nbsp; ' +
c.w.toFixed(1) + '×' + c.h.toFixed(1) + 'mm at (' +
c.x.toFixed(1) + ',' + c.y.toFixed(1) + ') r=' + c.r.toFixed(1) +
'<button onclick="removeCutout(' + i + ')">✕</button>';
list.appendChild(div);
});
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>