Files
wafer_map_webui/static/js/script.js
2025-07-17 15:56:04 +08:00

406 lines
15 KiB
JavaScript

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 = `
<div class="count-item">Total Dies: <span>${total}</span></div>
<div class="count-item">Good: <span style="background-color:${BIN_COLORS[2]}">${counts[2]}</span></div>
<div class="count-item">NG: <span style="background-color:${BIN_COLORS[1]}">${counts[1]}</span></div>
<div class="count-item">Dummy: <span style="background-color:${BIN_COLORS[0]}">${counts[0]}</span></div>
`;
}
// --- 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 = `
<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);
});
}
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);
});