pcb-to-stencil/static/preview.html

499 lines
17 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;
}
.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>