FIRST_UPLOAD
This commit is contained in:
310
static/css/style.css
Normal file
310
static/css/style.css
Normal file
@@ -0,0 +1,310 @@
|
||||
/* --- CSS Variables for Theming --- */
|
||||
:root {
|
||||
--font-family: 'Poppins', sans-serif;
|
||||
--bg-color: #1e1e1e;
|
||||
--sidebar-bg: #252526;
|
||||
--card-bg: #2d2d30;
|
||||
--text-color: #d4d4d4;
|
||||
--text-color-dark: #888;
|
||||
--primary-color: #007acc;
|
||||
--primary-color-hover: #009bff;
|
||||
--secondary-color: #3e3e42;
|
||||
--secondary-color-hover: #505054;
|
||||
--border-color: #444444;
|
||||
--success-color: #28a745;
|
||||
--error-color: #dc3545;
|
||||
--shadow-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* --- General Styles & Reset --- */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
h1 { font-size: 24px; }
|
||||
h2 { font-size: 16px; }
|
||||
|
||||
/* --- App Layout --- */
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 320px;
|
||||
background-color: var(--sidebar-bg);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* --- Card System --- */
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
|
||||
.card.active {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* --- Forms & Controls --- */
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
input[type="file"]::file-selector-button {
|
||||
font-family: var(--font-family);
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
input[type="file"]::file-selector-button:hover {
|
||||
background-color: var(--secondary-color-hover);
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.button-primary, .button-secondary {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
.button-primary:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px var(--shadow-color);
|
||||
}
|
||||
.button-primary:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
.button-secondary:hover {
|
||||
background-color: var(--secondary-color-hover);
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.control-group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.control-group-grid .button-primary,
|
||||
.control-group-grid .button-secondary {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-dark);
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
.hint-text strong {
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
/* --- Bin Definition --- */
|
||||
#bin-map-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.bin-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
align-items: center;
|
||||
}
|
||||
.bin-item label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* --- Main Editor Area --- */
|
||||
#editor-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#welcome-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: var(--text-color-dark);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.counts-container {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.count-item {
|
||||
font-weight: 600;
|
||||
}
|
||||
.count-item span {
|
||||
font-weight: 400;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
min-width: 30px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#map-container {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--card-bg);
|
||||
}
|
||||
#map-container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
#wafer-map-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* --- Status & Spinner --- */
|
||||
.status {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status.success {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
display: block;
|
||||
}
|
||||
.status.error {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spinner-border .75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
406
static/js/script.js
Normal file
406
static/js/script.js
Normal file
@@ -0,0 +1,406 @@
|
||||
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);
|
||||
});
|
Reference in New Issue
Block a user