pcb-to-stencil/static/preview.html

1032 lines
39 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;
}
/* Auto-Align Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
}
.modal-content {
background-color: #fefefe;
padding: 24px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.close-modal {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
margin-top: -5px;
}
.close-modal:hover {
color: black;
}
.fp-item {
padding: 10px;
border: 1px solid #e5e7eb;
margin-bottom: 5px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.fp-item:hover {
background-color: #f9fafb;
}
.fp-item.selected {
background-color: #eff6ff;
border-color: #3b82f6;
}
</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>
<hr style="margin: 1.5rem 0 1rem; border: 0; border-top: 1px solid #e5e7eb;">
<div style="text-align: center;">
<button type="button" class="btn-preset" id="btnOpenAutoAlign"
style="font-size: 0.85rem; padding: 0.5rem 1rem; border-color: #8b5cf6; color: #7c3aed; background: #f5f3ff;">
Auto-Align USB Port</button>
</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">
<div style="display: flex; gap: 10px; margin-top: 15px;">
<a href="/" class="btn secondary"
style="flex: 1; text-align: center; text-decoration: none; padding: 10px; border-radius: 4px; border: 1px solid var(--border-color); color: var(--text-color);">Go
Back</a>
<button type="submit" class="submit-btn" style="flex: 1; margin-top: 0;">Generate</button>
</div>
</form>
</div>
<!-- Auto-Align Modal -->
<div id="autoAlignModal" class="modal">
<div class="modal-content">
<span class="close-modal" id="closeModal">&times;</span>
<h2 style="margin-top:0; font-size: 1.25rem; font-weight: 600; color: #111827;">Auto-Align USB Port</h2>
<p style="font-size:0.85rem; color:#6b7280; margin-bottom: 1rem;">Upload your **Fabrication** Gerbers (e.g.,
F.Fab,
B.Fab) to visually select your USB-C footprint on the board canvas.<br><strong
style="color:#ef4444">Note: You must upload the Fab layer.</strong> Copper, Mask, and Edge Cuts do
not contain footprint component data!</p>
<form id="autoAlignForm">
<div style="margin-bottom: 1rem;">
<input type="file" id="autoAlignGerbers" name="gerbers" multiple accept=".gbr" required
style="font-size: 0.8rem;">
</div>
<button type="button" id="btnUploadFootprints" class="btn-small">Upload & Align</button>
</form>
<div id="autoAlignStatus" style="margin-top:0.75rem; font-size:0.85rem; color:#4b5563;"></div>
</div>
</div>
<!-- Floating Align Instructions -->
<div id="alignInstructions"
style="display:none; position:fixed; top:20px; left:50%; transform:translateX(-50%); background:#2563eb; color:white; padding:12px 24px; border-radius:30px; font-weight:600; box-shadow:0 10px 15px -3px rgba(0,0,0,0.1); z-index:1000; align-items:center; gap:12px;">
<span id="alignInstructionsText">Select the USB-C footprint</span>
<button id="btnCancelAlign"
style="background:rgba(255,255,255,0.2); border:none; color:white; padding:4px 10px; border-radius:4px; cursor:pointer;">Cancel</button>
</div>
<script>
var sideCutouts = [];
var currentSide = 1;
var dragStart = null;
var dragCurrent = null;
// Visual Alignment Globals
var alignMode = null; // 'SELECT_FOOTPRINT' | 'SELECT_EDGE'
var footprintsData = [];
var hoverFootprint = null;
var selectedFootprint = null;
var hoverEdge = null; // 'top'|'bottom'|'left'|'right'
var fabImg = new Image();
var imgLoaded = false;
fabImg.onload = function () {
imgLoaded = true;
drawBoardWithLabels();
};
// Board dimensions from server
var boardInfo = {{.BoardInfoJSON }};
var sessionId = '{{.SessionID}}';
document.getElementById('sessionId').value = sessionId;
var sides = boardInfo.sides || [];
if (sides.length === 0) {
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, scale: scale };
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, boardCanvas.width, boardCanvas.height);
ctx.drawImage(boardImg, x, y, w, h);
if (alignMode && imgLoaded) {
ctx.globalAlpha = 0.5;
ctx.drawImage(fabImg, x, y, w, h);
ctx.globalAlpha = 1.0;
}
// Draw visual alignment overlays
if (alignMode) {
var pxFromMmX = function (mmX) { return x + (mmX - boardInfo.minX) * boardInfo.dpi / 25.4 * scale; };
var pxFromMmY = function (mmY) { return y + (boardInfo.maxY - mmY) * boardInfo.dpi / 25.4 * scale; };
if (alignMode === 'SELECT_FOOTPRINT') {
footprintsData.forEach(function (fp) {
var px1 = pxFromMmX(fp.minX);
var py1 = pxFromMmY(fp.maxY); // Y is flipped
var px2 = pxFromMmX(fp.maxX);
var py2 = pxFromMmY(fp.minY);
var fw = px2 - px1;
var fh = py2 - py1;
ctx.beginPath();
ctx.rect(px1, py1, fw, fh);
if (hoverFootprint && hoverFootprint.name === fp.name && hoverFootprint.centerX === fp.centerX) {
ctx.fillStyle = 'rgba(59, 130, 246, 0.4)';
ctx.fill();
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
} else {
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 1;
}
ctx.stroke();
});
} else if (alignMode === 'SELECT_EDGE' && selectedFootprint) {
var px1 = pxFromMmX(selectedFootprint.minX);
var py1 = pxFromMmY(selectedFootprint.maxY);
var px2 = pxFromMmX(selectedFootprint.maxX);
var py2 = pxFromMmY(selectedFootprint.minY);
// Draw base box
ctx.strokeStyle = 'rgba(59, 130, 246, 0.5)';
ctx.lineWidth = 1;
ctx.strokeRect(px1, py1, px2 - px1, py2 - py1);
// Draw 4 edges
var edges = [
{ id: 'top', x1: px1, y1: py1, x2: px2, y2: py1 },
{ id: 'bottom', x1: px1, y1: py2, x2: px2, y2: py2 },
{ id: 'left', x1: px1, y1: py1, x2: px1, y2: py2 },
{ id: 'right', x1: px2, y1: py1, x2: px2, y2: py2 }
];
edges.forEach(function (e) {
ctx.beginPath();
ctx.moveTo(e.x1, e.y1);
ctx.lineTo(e.x2, e.y2);
if (hoverEdge === e.id) {
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 4;
} else {
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
}
ctx.stroke();
});
}
}
// 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;
if (side.startX !== undefined) {
var px1 = x + (side.startX - boardInfo.minX) * boardInfo.dpi / 25.4 * scale;
var py1 = y + (boardInfo.maxY - side.startY) * boardInfo.dpi / 25.4 * scale;
var px2 = x + (side.endX - boardInfo.minX) * boardInfo.dpi / 25.4 * scale;
var py2 = y + (boardInfo.maxY - side.endY) * boardInfo.dpi / 25.4 * scale;
ctx.beginPath();
ctx.moveTo(px1, py1);
ctx.lineTo(px2, py2);
ctx.stroke();
var midX_mm = (side.startX + side.endX) / 2;
var midY_mm = (side.startY + side.endY) / 2;
var imgPxX = (midX_mm - boardInfo.minX) * boardInfo.dpi / 25.4;
var imgPxY = (boardInfo.maxY - midY_mm) * boardInfo.dpi / 25.4;
var cx = x + imgPxX * scale;
var cy = y + imgPxY * scale;
var nx = Math.cos(side.angle);
var ny = -Math.sin(side.angle); // -sin because canvas Y is flipped
lx = cx + nx * labelPad;
ly = cy + ny * labelPad;
} else {
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...';
});
// --- Auto-Align Logic ---
var autoAlignModal = document.getElementById('autoAlignModal');
document.getElementById('btnOpenAutoAlign').addEventListener('click', function () {
autoAlignModal.style.display = 'flex';
});
document.getElementById('closeModal').addEventListener('click', function () {
autoAlignModal.style.display = 'none';
});
document.getElementById('btnCancelAlign').addEventListener('click', function () {
alignMode = null;
document.getElementById('alignInstructions').style.display = 'none';
drawBoardWithLabels();
});
document.getElementById('btnUploadFootprints').addEventListener('click', function () {
var files = document.getElementById('autoAlignGerbers').files;
if (files.length === 0) return;
var fd = new FormData();
for (var i = 0; i < files.length; i++) {
fd.append('gerbers', files[i]);
}
fd.append('sessionId', sessionId);
document.getElementById('autoAlignStatus').textContent = 'Processing files...';
fetch('/upload-footprints', {
method: 'POST',
body: fd
}).then(r => r.json()).then(data => {
if (!data || data.length === 0) {
document.getElementById('autoAlignStatus').textContent = 'No matching footprints found.';
return;
}
footprintsData = data;
autoAlignModal.style.display = 'none';
// Switch to visual selection mode
alignMode = 'SELECT_FOOTPRINT';
hoverFootprint = null;
selectedFootprint = null;
hoverEdge = null;
document.getElementById('alignInstructions').style.display = 'flex';
document.getElementById('alignInstructionsText').textContent = 'Select the USB-C footprint on the board';
// Fetch the rendered composite fab layer
fabImg.src = '/fab-image/' + sessionId + '?t=' + Date.now();
drawBoardWithLabels();
}).catch(e => {
document.getElementById('autoAlignStatus').textContent = 'Error: ' + e;
});
});
// Interactive Canvas Event Listeners
boardCanvas.addEventListener('mousemove', function (e) {
if (!alignMode) return;
var rect = boardCanvas.getBoundingClientRect();
var pxX = (e.clientX - rect.left) * (boardCanvas.width / rect.width);
var pxY = (e.clientY - rect.top) * (boardCanvas.height / rect.height);
// Convert canvas px to mm
var scale = boardRect.scale;
var mmX = boardInfo.minX + (pxX - boardRect.x) / scale * 25.4 / boardInfo.dpi;
var mmY = boardInfo.maxY - (pxY - boardRect.y) / scale * 25.4 / boardInfo.dpi;
if (alignMode === 'SELECT_FOOTPRINT') {
hoverFootprint = null;
for (var i = 0; i < footprintsData.length; i++) {
var fp = footprintsData[i];
if (mmX >= fp.minX && mmX <= fp.maxX && mmY >= fp.minY && mmY <= fp.maxY) {
hoverFootprint = fp;
break;
}
}
drawBoardWithLabels();
} else if (alignMode === 'SELECT_EDGE' && selectedFootprint) {
hoverEdge = null;
var dists = [
{ id: 'top', d: Math.abs(mmY - selectedFootprint.maxY) },
{ id: 'bottom', d: Math.abs(mmY - selectedFootprint.minY) },
{ id: 'left', d: Math.abs(mmX - selectedFootprint.minX) },
{ id: 'right', d: Math.abs(mmX - selectedFootprint.maxX) }
];
dists.sort(function (a, b) { return a.d - b.d; });
if (dists[0].d < 3.0) { // snap tolerance 3mm
hoverEdge = dists[0].id;
}
drawBoardWithLabels();
}
});
boardCanvas.addEventListener('click', function (e) {
if (!alignMode) return;
if (alignMode === 'SELECT_FOOTPRINT' && hoverFootprint) {
selectedFootprint = hoverFootprint;
alignMode = 'SELECT_EDGE';
document.getElementById('alignInstructionsText').textContent = 'Select the outermost edge of the connector lip';
drawBoardWithLabels();
} else if (alignMode === 'SELECT_EDGE' && hoverEdge && selectedFootprint) {
applyAlignment(selectedFootprint, hoverEdge);
alignMode = null;
document.getElementById('alignInstructions').style.display = 'none';
drawBoardWithLabels();
}
});
function applyAlignment(fp, lip) {
var bx = fp.centerX;
var by = fp.centerY;
if (lip === 'top') by = fp.maxY;
else if (lip === 'bottom') by = fp.minY;
else if (lip === 'left') bx = fp.minX;
else if (lip === 'right') bx = fp.maxX;
// Find closest board side
var closestSide = null;
var minDist = Infinity;
var bestPosX = 0;
sides.forEach(function (s) {
if (s.startX !== undefined) {
var dx = s.endX - s.startX;
var dy = s.endY - s.startY;
var lenSq = dx * dx + dy * dy;
if (lenSq > 0) {
var t = ((bx - s.startX) * dx + (by - s.startY) * dy) / lenSq;
t = Math.max(0, Math.min(1, t));
var rx = s.startX + t * dx;
var ry = s.startY + t * dy;
var dist = Math.sqrt(Math.pow(bx - rx, 2) + Math.pow(by - ry, 2));
if (dist < minDist) {
minDist = dist;
closestSide = s;
bestPosX = t * s.length;
}
}
}
});
if (closestSide) {
// Set typical USB-C parameters
document.getElementById('cutW').value = '9.00';
document.getElementById('cutH').value = '3.50';
document.getElementById('cutR').value = '1.30';
// Center based on projected posX
var cutX = bestPosX - (9.0 / 2);
document.getElementById('cutX').value = cutX.toFixed(2);
document.getElementById('cutY').value = '0.00';
currentSide = closestSide.num;
document.getElementById('btnAddCutout').click();
// Activate the correct side tab
document.querySelector('.face-tab[data-side="' + closestSide.num + '"]').click();
}
}
</script>
</body>
</html>