document.addEventListener('DOMContentLoaded', () => { // --- DOM Elements --- const uploadForm = document.getElementById('upload-form'); const binDefinitionForm = document.getElementById('bin-definition-form'); const uploadSection = document.getElementById('upload-section'); const binDefinitionSection = document.getElementById('bin-definition-section'); const editorControlsSection = document.getElementById('editor-controls-section'); const editorSection = document.getElementById('editor-section'); const welcomeMessage = document.getElementById('welcome-message'); const uploadStatus = document.getElementById('upload-status'); const binStatus = document.getElementById('bin-status'); const editorStatus = document.getElementById('editor-status'); const binMapContainer = document.getElementById('bin-map-container'); const canvas = document.getElementById('wafer-map-canvas'); const ctx = canvas.getContext('2d'); const updateSelectionBtn = document.getElementById('update-selection-btn'); const resetViewBtn = document.getElementById('reset-view-btn'); const rotateBtn = document.getElementById('rotate-btn'); const binCountsContainer = document.getElementById('bin-counts'); // --- State Management --- let waferMap = []; let mapRows = 0; let mapCols = 0; let selection = new Set(); // --- Canvas & View State --- const BIN_COLORS = { 2: '#28a745', 1: '#dc3545', 0: '#6c757d', '-1': '#3e3e42' }; const SELECTION_COLOR = 'rgba(0, 123, 255, 0.5)'; let transform = { x: 0, y: 0, scale: 1 }; let isPanning = false; let isSelecting = false; let panStart = { x: 0, y: 0 }; let selectionStart = { x: 0, y: 0 }; let selectionEnd = { x: 0, y: 0 }; // --- UI/UX Functions --- function setStatus(element, message, isError = false) { element.textContent = message; element.className = 'status'; if (message) { element.classList.add(isError ? 'error' : 'success'); } } function showSpinner(button, show = true) { const spinner = button.querySelector('.spinner-border'); const btnText = button.querySelector('.btn-text'); if (spinner) { spinner.style.display = show ? 'inline-block' : 'none'; if (btnText) btnText.style.display = show ? 'none' : 'inline'; button.disabled = show; } } function switchCard(from, to) { if(from) from.classList.remove('active'); if(to) to.classList.add('active'); } // --- Canvas & Data Functions --- function worldToScreen(x, y) { return { x: x * transform.scale + transform.x, y: y * transform.scale + transform.y }; } function screenToWorld(x, y) { return { x: (x - transform.x) / transform.scale, y: (y - transform.y) / transform.scale }; } function getDieAtScreen(x, y) { const worldPos = screenToWorld(x, y); const col = Math.floor(worldPos.x / 10); const row = Math.floor(worldPos.y / 10); if (row >= 0 && row < mapRows && col >= 0 && col < mapCols) { return { row, col }; } return null; } function resetTransform() { const dieSize = 10; const container = canvas.parentElement; canvas.width = container.clientWidth; canvas.height = container.clientHeight; const scaleX = canvas.width / (mapCols * dieSize); const scaleY = canvas.height / (mapRows * dieSize); transform.scale = Math.min(scaleX, scaleY) * 0.9; const mapRenderWidth = mapCols * dieSize * transform.scale; const mapRenderHeight = mapRows * dieSize * transform.scale; transform.x = (canvas.width - mapRenderWidth) / 2; transform.y = (canvas.height - mapRenderHeight) / 2; } function updateBinCounts() { const counts = { 2: 0, 1: 0, 0: 0, '-1': 0 }; let total = 0; for (let r = 0; r < mapRows; r++) { for (let c = 0; c < mapCols; c++) { const bin = waferMap[r][c]; if (bin in counts) counts[bin]++; if (bin != -1) total++; } } binCountsContainer.innerHTML = `
Total Dies: ${total}
Good: ${counts[2]}
NG: ${counts[1]}
Dummy: ${counts[0]}
`; } // --- Rendering Engine --- function draw() { requestAnimationFrame(() => { const dieSize = 10; const minScaleForGrid = 4; const maxScaleForGrid = 10; let gridAlpha = (transform.scale - minScaleForGrid) / (maxScaleForGrid - minScaleForGrid); gridAlpha = Math.min(1, Math.max(0, gridAlpha)); // Dynamically set canvas background ctx.clearRect(0, 0, canvas.width, canvas.height); if (gridAlpha > 0) { ctx.fillStyle = `rgba(240, 240, 240, ${gridAlpha})`; ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.save(); ctx.translate(transform.x, transform.y); ctx.scale(transform.scale, transform.scale); // Draw dies for (let r = 0; r < mapRows; r++) { for (let c = 0; c < mapCols; c++) { const bin = waferMap[r][c]; ctx.fillStyle = BIN_COLORS[bin] || '#ffffff'; ctx.fillRect(c * dieSize, r * dieSize, dieSize, dieSize); } } // Draw grid lines if (gridAlpha > 0) { ctx.strokeStyle = `rgba(0, 0, 0, ${gridAlpha})`; ctx.lineWidth = 1 / transform.scale; for (let r = 0; r <= mapRows; r++) { ctx.beginPath(); ctx.moveTo(0, r * dieSize); ctx.lineTo(mapCols * dieSize, r * dieSize); ctx.stroke(); } for (let c = 0; c <= mapCols; c++) { ctx.beginPath(); ctx.moveTo(c * dieSize, 0); ctx.lineTo(c * dieSize, mapRows * dieSize); ctx.stroke(); } } // Draw selection highlights ctx.fillStyle = SELECTION_COLOR; selection.forEach(key => { const [r, c] = key.split(',').map(Number); ctx.fillRect(c * dieSize, r * dieSize, dieSize, dieSize); }); ctx.restore(); // Draw selection rectangle (in screen space) if (isSelecting) { ctx.fillStyle = 'rgba(0, 123, 255, 0.2)'; ctx.strokeStyle = 'rgba(0, 123, 255, 0.8)'; ctx.lineWidth = 1; const rect = { x: Math.min(selectionStart.x, selectionEnd.x), y: Math.min(selectionStart.y, selectionEnd.y), w: Math.abs(selectionStart.x - selectionEnd.x), h: Math.abs(selectionStart.y - selectionEnd.y) }; ctx.fillRect(rect.x, rect.y, rect.w, rect.h); ctx.strokeRect(rect.x, rect.y, rect.w, rect.h); } }); } // --- Event Handlers --- uploadForm.addEventListener('submit', async (e) => { e.preventDefault(); const btn = e.submitter; showSpinner(btn, true); setStatus(uploadStatus, ''); const formData = new FormData(uploadForm); try { const response = await fetch('/upload', { method: 'POST', body: formData }); const result = await response.json(); if (!response.ok) throw new Error(result.error); displayBinDefinition(result.unique_chars); switchCard(uploadSection, binDefinitionSection); } catch (error) { setStatus(uploadStatus, `Error: ${error.message}`, true); } finally { showSpinner(btn, false); } }); function displayBinDefinition(chars) { binMapContainer.innerHTML = ''; chars.forEach(char => { const item = document.createElement('div'); item.className = 'bin-item'; item.innerHTML = ` `; binMapContainer.appendChild(item); }); } binDefinitionForm.addEventListener('submit', async (e) => { e.preventDefault(); setStatus(binStatus, ''); const formData = new FormData(binDefinitionForm); const charMap = Object.fromEntries(formData.entries()); try { const response = await fetch('/generate_map', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ char_map: charMap }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error); waferMap = result.wafer_map; mapRows = result.rows; mapCols = result.cols; welcomeMessage.style.display = 'none'; editorSection.style.display = 'flex'; resetTransform(); updateBinCounts(); draw(); switchCard(binDefinitionSection, editorControlsSection); } catch (error) { setStatus(binStatus, `Error: ${error.message}`, true); } }); canvas.addEventListener('wheel', (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const mouse = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const worldPos = screenToWorld(mouse.x, mouse.y); const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1; transform.scale *= zoomFactor; const newWorldPos = screenToWorld(mouse.x, mouse.y); transform.x += (newWorldPos.x - worldPos.x) * transform.scale; transform.y += (newWorldPos.y - worldPos.y) * transform.scale; draw(); }); canvas.addEventListener('mousedown', (e) => { if (e.shiftKey) { isSelecting = true; selectionStart = { x: e.offsetX, y: e.offsetY }; selectionEnd = selectionStart; } else { isPanning = true; panStart = { x: e.clientX, y: e.clientY }; } }); canvas.addEventListener('mousemove', (e) => { if (isPanning) { const dx = e.clientX - panStart.x; const dy = e.clientY - panStart.y; transform.x += dx; transform.y += dy; panStart = { x: e.clientX, y: e.clientY }; draw(); } else if (isSelecting) { selectionEnd = { x: e.offsetX, y: e.offsetY }; draw(); } }); window.addEventListener('mouseup', (e) => { if (isPanning) isPanning = false; if (isSelecting) { isSelecting = false; const startWorld = screenToWorld(selectionStart.x, selectionStart.y); const endWorld = screenToWorld(selectionEnd.x, selectionEnd.y); const startCol = Math.floor(Math.min(startWorld.x, endWorld.x) / 10); const endCol = Math.floor(Math.max(startWorld.x, endWorld.x) / 10); const startRow = Math.floor(Math.min(startWorld.y, endWorld.y) / 10); const endRow = Math.floor(Math.max(startWorld.y, endWorld.y) / 10); if (!e.ctrlKey) selection.clear(); for (let r = startRow; r <= endRow; r++) { for (let c = startCol; c <= endCol; c++) { if (r >= 0 && r < mapRows && c >= 0 && c < mapCols) { const key = `${r},${c}`; if (selection.has(key)) selection.delete(key); else selection.add(key); } } } setStatus(editorStatus, `${selection.size} die(s) selected.`); draw(); } }); canvas.addEventListener('click', (e) => { const wasDrag = Math.abs(e.clientX - panStart.x) > 2 || Math.abs(e.clientY - panStart.y) > 2; if (e.shiftKey || (isPanning && wasDrag)) return; const die = getDieAtScreen(e.offsetX, e.offsetY); if (die) { if (!e.ctrlKey) selection.clear(); const key = `${die.row},${die.col}`; if (selection.has(key)) selection.delete(key); else selection.add(key); setStatus(editorStatus, `${selection.size} die(s) selected.`); draw(); } }); updateSelectionBtn.addEventListener('click', async () => { if (selection.size === 0) { setStatus(editorStatus, 'No dies selected to update.', true); return; } const binType = document.getElementById('bin-type-selector').value; const diesToUpdate = Array.from(selection).map(key => key.split(',').map(Number)); setStatus(editorStatus, `Updating ${selection.size} die(s)...`); try { const response = await fetch('/update_map', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dies: diesToUpdate, bin_type: binType }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error); waferMap = result.wafer_map; selection.clear(); updateBinCounts(); setStatus(editorStatus, 'Update successful.'); draw(); } catch (error) { setStatus(editorStatus, `Error: ${error.message}`, true); } }); resetViewBtn.addEventListener('click', () => { resetTransform(); draw(); }); rotateBtn.addEventListener('click', async () => { setStatus(editorStatus, 'Rotating map...'); try { const response = await fetch('/rotate_map', { method: 'POST' }); const result = await response.json(); if (!response.ok) throw new Error(result.error); waferMap = result.wafer_map; mapRows = result.rows; mapCols = result.cols; selection.clear(); resetTransform(); updateBinCounts(); draw(); setStatus(editorStatus, 'Map rotated successfully.'); } catch (error) { setStatus(editorStatus, `Error: ${error.message}`, true); } }); new ResizeObserver(() => { if (mapRows > 0) { // Only resize if there is a map resetTransform(); draw(); } }).observe(canvas.parentElement); });