document.addEventListener('DOMContentLoaded', () => { // --- DOM Elements --- const uploadForm = document.getElementById('upload-form'); const binDefinitionForm = document.getElementById('bin-definition-form'); const fileInput = document.getElementById('file-input'); const uploadSection = document.getElementById('upload-section'); const binDefinitionSection = document.getElementById('bin-definition-section'); const comparisonControlsSection = document.getElementById('comparison-controls-section'); const viewerSection = document.getElementById('viewer-section'); const comparisonResultSection = document.getElementById('comparison-result-section'); const welcomeMessage = document.getElementById('welcome-message'); const uploadStatus = document.getElementById('upload-status'); const binStatus = document.getElementById('bin-status'); const compareStatus = document.getElementById('compare-status'); const binMapContainer = document.getElementById('bin-map-container'); const referenceSetterContainer = document.getElementById('reference-setter-container'); const mapSelector = document.getElementById('map-selector'); const rotateMapBtn = document.getElementById('rotate-map-btn'); const compareBtn = document.getElementById('compare-btn'); const backToViewerBtn = document.getElementById('back-to-viewer-btn'); const canvas = document.getElementById('wafer-map-canvas'); const ctx = canvas.getContext('2d'); const resultCanvas = document.getElementById('result-map-canvas'); const resultCtx = resultCanvas.getContext('2d'); const resultLegendContainer = document.getElementById('result-legend'); const tooltip = document.getElementById('map-tooltip'); // --- State Management --- let sessionMaps = {}; let activeMapFid = null; let comparisonData = {}; // To store result_map, detailed_grid, etc. // --- Canvas & View State --- const BIN_CODE_MAP = { 2: 'Good', 1: 'NG', 0: 'Dummy', '-1': 'Ignore', '-10': 'N/A' }; const BIN_COLORS = { 2: '#28a745', 1: '#dc3545', 0: '#6c757d', '-1': '#3e3e42' }; const REFERENCE_COLOR = 'rgba(255, 193, 7, 1)'; let transform = { x: 0, y: 0, scale: 1 }; let isPanning = false; let panStart = { x: 0, y: 0 }; let resultTransform = { x: 0, y: 0, scale: 1 }; let isResultPanning = false; let resultPanStart = { x: 0, y: 0 }; let highlightedDie = null; // --- UI Update Functions --- function setStatus(element, message, isError = false) { element.textContent = message; element.className = 'status'; if (message) { element.classList.add(isError ? 'error' : 'success'); } } function switchCard(from, to) { if(from) from.classList.remove('active'); if(to) to.classList.add('active'); } function updateReferenceUI() { referenceSetterContainer.innerHTML = ''; Object.keys(sessionMaps).forEach(fid => { const map = sessionMaps[fid]; const hasRef = map.reference !== null; const refText = hasRef ? `(${map.reference.join(', ')})` : 'Not Set'; const item = document.createElement('div'); item.className = `reference-item ${hasRef ? 'has-ref' : ''}`; item.id = `ref-item-${fid}`; item.innerHTML = ` ${map.filename}
${refText}
`; referenceSetterContainer.appendChild(item); }); checkAllReferencesSet(); } function checkAllReferencesSet() { const allSet = Object.values(sessionMaps).every(m => m.reference !== null); compareBtn.disabled = !allSet; if (allSet) { setStatus(compareStatus, 'All references set. Ready to compare.'); } else { setStatus(compareStatus, 'Please set a reference point for each map.'); } } // --- Canvas & Drawing --- function worldToScreen(x, y, t) { return { x: x * t.scale + t.x, y: y * t.scale + t.y }; } function screenToWorld(x, y, t) { return { x: (x - t.x) / t.scale, y: (y - t.y) / t.scale }; } function resetTransform(map, t, targetCanvas) { const dieSize = 10; const container = targetCanvas.parentElement; targetCanvas.width = container.clientWidth; targetCanvas.height = container.clientHeight; const scaleX = targetCanvas.width / (map.cols * dieSize); const scaleY = targetCanvas.height / (map.rows * dieSize); t.scale = Math.min(scaleX, scaleY) * 0.9; const mapRenderWidth = map.cols * dieSize * t.scale; const mapRenderHeight = map.rows * dieSize * t.scale; t.x = (targetCanvas.width - mapRenderWidth) / 2; t.y = (targetCanvas.height - mapRenderHeight) / 2; } function drawActiveMap() { if (!activeMapFid || !sessionMaps[activeMapFid] || !sessionMaps[activeMapFid].wafer_map) return; const map = sessionMaps[activeMapFid]; requestAnimationFrame(() => { const dieSize = 10; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(transform.x, transform.y); ctx.scale(transform.scale, transform.scale); for (let r = 0; r < map.rows; r++) { for (let c = 0; c < map.cols; c++) { const bin = map.wafer_map[r][c]; ctx.fillStyle = BIN_COLORS[bin] || '#ffffff'; ctx.fillRect(c * dieSize, r * dieSize, dieSize, dieSize); } } if (map.reference) { const [r, c] = map.reference; ctx.strokeStyle = REFERENCE_COLOR; ctx.lineWidth = 2 / transform.scale; ctx.strokeRect(c * dieSize - 1, r * dieSize - 1, dieSize + 2, dieSize + 2); } ctx.restore(); }); } function drawResultMap() { const { rows, cols, result_map, legend } = comparisonData; requestAnimationFrame(() => { const dieSize = 10; resultCtx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); resultCtx.save(); resultCtx.translate(resultTransform.x, resultTransform.y); resultCtx.scale(resultTransform.scale, resultTransform.scale); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const code = result_map[r][c]; resultCtx.fillStyle = legend[code] ? legend[code].color : '#ffffff'; resultCtx.fillRect(c * dieSize, r * dieSize, dieSize, dieSize); } } // Draw highlight on hovered die if (highlightedDie) { const { r, c } = highlightedDie; resultCtx.strokeStyle = REFERENCE_COLOR; resultCtx.lineWidth = 2 / resultTransform.scale; resultCtx.strokeRect(c * dieSize, r * dieSize, dieSize, dieSize); } resultCtx.restore(); }); } // --- Event Handlers --- uploadForm.addEventListener('submit', async (e) => { e.preventDefault(); setStatus(uploadStatus, ''); if (fileInput.files.length < 2) { setStatus(uploadStatus, 'Please select at least two files for comparison.', true); return; } 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); binMapContainer.innerHTML = ''; result.unique_chars.forEach(char => { const item = document.createElement('div'); item.className = 'bin-item'; item.innerHTML = ``; binMapContainer.appendChild(item); }); mapSelector.innerHTML = ''; sessionMaps = {}; // Clear previous session Object.keys(result.maps).forEach(fid => { sessionMaps[fid] = { filename: result.maps[fid], wafer_map: null, reference: null }; const option = document.createElement('option'); option.value = fid; option.textContent = result.maps[fid]; mapSelector.appendChild(option); }); activeMapFid = mapSelector.value; switchCard(uploadSection, binDefinitionSection); } catch (error) { setStatus(uploadStatus, `Error: ${error.message}`, true); } }); 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); Object.keys(result.maps_data).forEach(fid => { sessionMaps[fid] = { ...sessionMaps[fid], ...result.maps_data[fid] }; }); welcomeMessage.style.display = 'none'; viewerSection.style.display = 'flex'; resetTransform(sessionMaps[activeMapFid], transform, canvas); drawActiveMap(); updateReferenceUI(); switchCard(binDefinitionSection, comparisonControlsSection); } catch (error) { setStatus(binStatus, `Error: ${error.message}`, true); } }); mapSelector.addEventListener('change', (e) => { activeMapFid = e.target.value; resetTransform(sessionMaps[activeMapFid], transform, canvas); drawActiveMap(); }); rotateMapBtn.addEventListener('click', async () => { if (!activeMapFid) return; try { const response = await fetch('/compare/rotate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fid: activeMapFid }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error); sessionMaps[activeMapFid].wafer_map = result.wafer_map; sessionMaps[activeMapFid].rows = result.rows; sessionMaps[activeMapFid].cols = result.cols; sessionMaps[activeMapFid].reference = null; resetTransform(sessionMaps[activeMapFid], transform, canvas); drawActiveMap(); updateReferenceUI(); } catch (error) { setStatus(compareStatus, `Error rotating map: ${error.message}`, true); } }); referenceSetterContainer.addEventListener('click', async (e) => { if (e.target.classList.contains('clear-ref-btn')) { const fid = e.target.dataset.fid; try { const response = await fetch('/compare/clear_reference', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fid: fid }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error); sessionMaps[fid].reference = null; updateReferenceUI(); if (fid === activeMapFid) drawActiveMap(); } catch (error) { setStatus(compareStatus, `Error clearing reference: ${error.message}`, true); } } }); canvas.addEventListener('click', async (e) => { const wasDrag = Math.abs(e.clientX - panStart.x) > 2 || Math.abs(e.clientY - panStart.y) > 2; if ((isPanning && wasDrag) || !activeMapFid) return; const worldPos = screenToWorld(e.offsetX, e.offsetY, transform); const col = Math.floor(worldPos.x / 10); const row = Math.floor(worldPos.y / 10); const map = sessionMaps[activeMapFid]; if (row >= 0 && row < map.rows && col >= 0 && col < map.cols) { const reference = [row, col]; try { const response = await fetch('/set_reference', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fid: activeMapFid, reference: reference }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error); sessionMaps[activeMapFid].reference = result.reference; updateReferenceUI(); drawActiveMap(); } catch (error) { setStatus(compareStatus, `Error: ${error.message}`, true); } } }); compareBtn.addEventListener('click', async () => { setStatus(compareStatus, 'Running comparison...'); try { const response = await fetch('/run_comparison', { method: 'POST' }); const result = await response.json(); if (!response.ok) throw new Error(result.error); comparisonData = result; viewerSection.style.display = 'none'; comparisonResultSection.style.display = 'flex'; resetTransform(comparisonData, resultTransform, resultCanvas); drawResultMap(); resultLegendContainer.innerHTML = '

Legend

'; for (const code in result.legend) { const item = result.legend[code]; const legendItem = document.createElement('div'); legendItem.className = 'count-item'; legendItem.innerHTML = `${item.label}: `; resultLegendContainer.appendChild(legendItem); } } catch (error) { setStatus(compareStatus, `Error: ${error.message}`, true); } }); backToViewerBtn.addEventListener('click', () => { comparisonResultSection.style.display = 'none'; viewerSection.style.display = 'flex'; }); // --- Canvas Pan & Zoom --- function setupCanvasEvents(targetCanvas, t, panFlagSetter, drawFn) { targetCanvas.addEventListener('wheel', (e) => { e.preventDefault(); const rect = targetCanvas.getBoundingClientRect(); const mouse = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const worldPos = screenToWorld(mouse.x, mouse.y, t); const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1; t.scale *= zoomFactor; const newWorldPos = screenToWorld(mouse.x, mouse.y, t); t.x += (newWorldPos.x - worldPos.x) * t.scale; t.y += (newWorldPos.y - worldPos.y) * t.scale; drawFn(); }); targetCanvas.addEventListener('mousedown', (e) => { panFlagSetter(true, e); }); targetCanvas.addEventListener('mousemove', (e) => { panFlagSetter(false, e, true, drawFn); }); } setupCanvasEvents(canvas, transform, (val, e) => { isPanning = val; if(val) panStart = { x: e.clientX, y: e.clientY }; }, drawActiveMap); setupCanvasEvents(resultCanvas, resultTransform, (val, e) => { isResultPanning = val; if(val) resultPanStart = { x: e.clientX, y: e.clientY }; }, drawResultMap); window.addEventListener('mouseup', () => { isPanning = false; isResultPanning = false; }); window.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 }; drawActiveMap(); } if (isResultPanning) { const dx = e.clientX - resultPanStart.x; const dy = e.clientY - resultPanStart.y; resultTransform.x += dx; resultTransform.y += dy; resultPanStart = { x: e.clientX, y: e.clientY }; drawResultMap(); } }); // --- Tooltip Logic --- resultCanvas.addEventListener('mousemove', (e) => { if (!comparisonData.detailed_grid || isResultPanning) return; const rect = resultCanvas.getBoundingClientRect(); const mouse = { x: e.clientX - rect.left, y: e.clientY - rect.top }; const worldPos = screenToWorld(mouse.x, mouse.y, resultTransform); const c = Math.floor(worldPos.x / 10); const r = Math.floor(worldPos.y / 10); if (r >= 0 && r < comparisonData.rows && c >= 0 && c < comparisonData.cols) { highlightedDie = { r, c }; const dieData = comparisonData.detailed_grid[r][c]; let tooltipContent = `Coord: (${r}, ${c})\n-------------------\n`; tooltipContent += comparisonData.map_names.map((name, i) => { const binCode = dieData[i]; return `${name}: ${BIN_CODE_MAP[binCode] || 'Unknown'}`; }).join('\n'); tooltip.style.display = 'block'; tooltip.style.opacity = '1'; tooltip.textContent = tooltipContent; // Position tooltip const tooltipRect = tooltip.getBoundingClientRect(); let left = e.clientX + 15; let top = e.clientY + 15; if (left + tooltipRect.width > window.innerWidth) { left = e.clientX - tooltipRect.width - 15; } if (top + tooltipRect.height > window.innerHeight) { top = e.clientY - tooltipRect.height - 15; } tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; } else { highlightedDie = null; tooltip.style.display = 'none'; tooltip.style.opacity = '0'; } drawResultMap(); }); resultCanvas.addEventListener('mouseleave', () => { highlightedDie = null; tooltip.style.display = 'none'; tooltip.style.opacity = '0'; drawResultMap(); }); new ResizeObserver(() => { if (activeMapFid) { resetTransform(sessionMaps[activeMapFid], transform, canvas); drawActiveMap(); } }).observe(canvas.parentElement); new ResizeObserver(() => { if (comparisonData.rows) { resetTransform(comparisonData, resultTransform, resultCanvas); drawResultMap(); } }).observe(resultCanvas.parentElement); });