Files
wafer_map_webui/static/js/compare.js
beabigegg 9f7040ece9 ver 2
2025-07-30 11:24:58 +08:00

431 lines
19 KiB
JavaScript
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.

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 = `
<span class="filename" title="${map.filename}">${map.filename}</span>
<div class="ref-details">
<span class="ref-status ${hasRef ? 'set' : ''}">${refText}</span>
<button class="clear-ref-btn" data-fid="${fid}" title="Clear reference">×</button>
</div>
`;
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 = `<label>'${char}':</label><select name="${char}"><option value="good">Good</option><option value="ng">NG</option><option value="dummy">Dummy</option><option value="ignore">Ignore</option></select>`;
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 = '<h3>Legend</h3>';
for (const code in result.legend) {
const item = result.legend[code];
const legendItem = document.createElement('div');
legendItem.className = 'count-item';
legendItem.innerHTML = `${item.label}: <span style="background-color:${item.color}"></span>`;
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);
});