ver 2
This commit is contained in:
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1,2 +1,21 @@
|
||||
__pycache__/wafer_processor.cpython-313.pyc
|
||||
__pycache__/wafer_processor.cpython-310.pyc
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Application-specific
|
||||
/static/uploads/
|
||||
/static/outputs/
|
||||
|
||||
# IDE / Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS-specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
59
README.md
59
README.md
@@ -1,10 +1,12 @@
|
||||
# Wafer Map Pro
|
||||
|
||||
Wafer Map Pro is a modern, web-based tool designed for the visualization and interactive editing of semiconductor wafer maps. Built with a Python Flask backend and a dynamic HTML5 Canvas frontend, it provides a fluid and intuitive user experience for engineers and technicians working with wafer data.
|
||||
Wafer Map Pro is a modern, web-based tool designed for the visualization, interactive editing, and comparison of semiconductor wafer maps. Built with a Python Flask backend and a dynamic HTML5 Canvas frontend, it provides a fluid and intuitive user experience for engineers and technicians working with wafer data.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🗺️ Wafer Map Editor
|
||||
- **Interactive Canvas Renderer**: The wafer map is rendered on an HTML5 Canvas, allowing for smooth and efficient handling of large datasets.
|
||||
- **Dynamic Pan & Zoom**: Effortlessly navigate the wafer map by clicking and dragging to pan, and using the mouse wheel to zoom in on areas of interest.
|
||||
- **Advanced Selection Tools**:
|
||||
@@ -12,15 +14,23 @@ Wafer Map Pro is a modern, web-based tool designed for the visualization and int
|
||||
- **Multi-Select**: Hold `Ctrl` while clicking to select multiple, non-contiguous dies.
|
||||
- **Region/Box Select**: Hold `Shift` and drag the mouse to select a rectangular area of dies.
|
||||
- **Bin Code Editing**: Instantly change the bin code (`Good`, `NG`, `Dummy`, `Ignore`) of any selected die or region.
|
||||
- **Map Transformation**: Rotate the entire wafer map 90 degrees clockwise with a single click.
|
||||
- **Map Transformation**:
|
||||
- **Rotate**: Rotate the entire wafer map 90 degrees clockwise with a single click.
|
||||
- **Inset**: Automatically change the bin code of the outer N layers of dies.
|
||||
- **History Control**:
|
||||
- **Undo/Redo**: Step backward and forward through your edits.
|
||||
- **Reset**: Revert the map to its original state.
|
||||
- **Real-Time Data Analysis**: A persistent display shows the live count of each bin code, updating automatically after every edit.
|
||||
- **File Handling**:
|
||||
- Upload wafer maps from simple `.txt` files.
|
||||
- Download the edited wafer map, preserving its structure.
|
||||
- **Modern UI/UX**:
|
||||
- A sleek dark mode theme reduces eye strain.
|
||||
- A professional sidebar layout keeps controls organized and the map view unobstructed.
|
||||
- Smooth animations and transitions provide a polished user experience.
|
||||
|
||||
### 🔬 Wafer Map Comparator
|
||||
- **Multi-Map Upload**: Upload two or more wafer maps for side-by-side comparison.
|
||||
- **Reference Point Alignment**: Set a reference die on each map to ensure accurate alignment.
|
||||
- **Visual Comparison**: The tool generates a color-coded result map highlighting matches, mismatches, and areas where data is missing or partial.
|
||||
- **Detailed Inspection**: Hover over any die on the result map to see a detailed tooltip showing the bin status from each of the source maps.
|
||||
- **Independent Map Rotation**: Rotate individual maps within the comparison view to align them correctly before running the comparison.
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +39,7 @@ Wafer Map Pro is a modern, web-based tool designed for the visualization and int
|
||||
- **Backend**:
|
||||
- **Python 3**
|
||||
- **Flask**: A lightweight web framework for serving the application and handling API requests.
|
||||
- **NumPy**: Used for efficient numerical operations and array manipulations (e.g., map rotation).
|
||||
- **NumPy**: Used for efficient numerical operations and array manipulations (e.g., map rotation, inset, and comparison logic).
|
||||
- **Frontend**:
|
||||
- **HTML5**
|
||||
- **CSS3**: For modern styling, animations, and the dark theme.
|
||||
@@ -75,30 +85,27 @@ Follow these instructions to get a copy of the project up and running on your lo
|
||||
python app.py
|
||||
```
|
||||
|
||||
5. **Open your web browser** and navigate to the following address:
|
||||
[http://127.0.0.1:5000](http://127.0.0.1:5000)
|
||||
5. **Open your web browser** and navigate to the following addresses:
|
||||
- **Editor**: [http://127.0.0.1:5000](http://127.0.0.1:5000)
|
||||
- **Comparator**: [http://127.0.0.1:5000/compare](http://127.0.0.1:5000/compare)
|
||||
|
||||
---
|
||||
|
||||
## 📖 How to Use
|
||||
|
||||
1. **Step 1: Upload File**
|
||||
- Click the file input area or drag and drop a `.txt` file containing your wafer map data.
|
||||
- Click "Upload & Analyze".
|
||||
### Wafer Map Editor
|
||||
1. **Upload File**: Drag and drop a `.txt` file containing your wafer map data, or click to select a file.
|
||||
2. **Define Bin Codes**: Assign a bin type (`Good`, `NG`, `Dummy`, or `Ignore`) to each unique character found in your file.
|
||||
3. **Generate Map**: Click "Generate Map" to view the wafer.
|
||||
4. **Interact and Edit**: Use the controls to Pan, Zoom, Select, Edit, Rotate, and Inset the map.
|
||||
5. **Download**: Click "Download .txt" to save your changes.
|
||||
|
||||
2. **Step 2: Define Bin Codes**
|
||||
- The application will automatically detect all unique characters in your file.
|
||||
- For each character, use the dropdown menu to assign its corresponding bin type (`Good`, `NG`, `Dummy`, or `Ignore`).
|
||||
- Click "Generate Map".
|
||||
|
||||
3. **Step 3: Interact and Edit**
|
||||
- Your wafer map will be displayed in the main view.
|
||||
- Use the controls to **Pan**, **Zoom**, **Select**, **Edit**, and **Rotate** the map.
|
||||
- The bin code counts will update in real-time.
|
||||
- The hint `Pro-tip: Hold Shift and drag to select a region.` is provided for quick reference.
|
||||
|
||||
4. **Step 4: Download**
|
||||
- Once you have finished editing, click the "Download .txt" button to save your work. A new text file with the updated map will be downloaded.
|
||||
### Wafer Map Comparator
|
||||
1. **Navigate**: Go to the "Compare" page.
|
||||
2. **Upload Files**: Select two or more `.txt` wafer map files.
|
||||
3. **Define Bin Codes**: Assign bin types for all unique characters across all files.
|
||||
4. **Set Reference Points**: For each map, click on a die to set it as the alignment reference. You can rotate individual maps if needed.
|
||||
5. **Run Comparison**: Click "Run Comparison" to see the result map. Hover over dies for detailed information.
|
||||
|
||||
---
|
||||
|
||||
@@ -115,4 +122,4 @@ The application expects a simple text file where each character represents a die
|
||||
11222211
|
||||
112211
|
||||
1111
|
||||
```
|
||||
```
|
88
USER_MANUAL.md
Normal file
88
USER_MANUAL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Wafer Map Pro - User Manual
|
||||
|
||||
Welcome to the Wafer Map Pro User Manual. This guide provides detailed instructions on how to use both the **Wafer Map Editor** and the **Wafer Map Comparator**.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Wafer Map Editor
|
||||
|
||||
The Editor allows you to visualize, edit, and analyze a single wafer map file.
|
||||
|
||||
### 1.1 - Getting Started
|
||||
|
||||
1. **Navigate to the Editor**: Open your web browser and go to the root URL (e.g., `http://127.0.0.1:5000`).
|
||||
2. **Upload a File**:
|
||||
- Drag and drop your `.txt` wafer map file onto the designated area.
|
||||
- Alternatively, click the area to open a file selection dialog.
|
||||
3. **Define Bin Codes**:
|
||||
- After uploading, the application automatically detects all unique characters in your file.
|
||||
- For each character, use the dropdown menu to assign its bin type. The available types are:
|
||||
- **Good**: Represents a functional die.
|
||||
- **NG** (No Good): Represents a failed die.
|
||||
- **Dummy**: Represents a non-functional die used for structural purposes.
|
||||
- **Ignore**: Represents areas outside the wafer boundary. These will not be rendered on the map.
|
||||
4. **Generate Map**: Once all characters are defined, click the **"Generate Map"** button. Your wafer map will be rendered on the canvas.
|
||||
|
||||
### 1.2 - Interacting with the Map
|
||||
|
||||
- **Pan**: Click and hold the left mouse button anywhere on the map and drag to move it around.
|
||||
- **Zoom**: Use the mouse wheel to zoom in and out. The zoom is centered on your mouse cursor's position.
|
||||
|
||||
### 1.3 - Editing Tools
|
||||
|
||||
#### Selection
|
||||
- **Single Select**: Click on any single die to select it. It will be highlighted.
|
||||
- **Multi-Select**: Hold down the `Ctrl` key and click on multiple dies to select them individually.
|
||||
- **Region Select (Box Select)**: Hold down the `Shift` key, then click and drag the mouse to draw a rectangle. All dies within the rectangle will be selected.
|
||||
|
||||
#### Modifying Bins
|
||||
- Once you have one or more dies selected, use the **"Edit Bin Code"** buttons (`Good`, `NG`, `Dummy`) on the sidebar to change the bin code of all selected dies.
|
||||
|
||||
### 1.4 - Transformation Tools
|
||||
|
||||
- **Rotate**: Click the **"Rotate 90°"** button to rotate the entire wafer map 90 degrees clockwise.
|
||||
- **Inset**:
|
||||
1. Click the **"Inset"** button.
|
||||
2. A dialog will appear. Enter the number of **layers** you want to inset from the edge.
|
||||
3. Select the **"From Bin"** (the bin type you want to change) and the **"To Bin"** (the bin type you want to change to).
|
||||
4. Click **"Apply Inset"**. The tool will identify the outer N layers of the specified "From Bin" and change them to the "To Bin".
|
||||
|
||||
### 1.5 - History and Data
|
||||
|
||||
- **Undo/Redo**: Use the **"Undo"** and **"Redo"** buttons to step through your changes.
|
||||
- **Reset**: Click **"Reset"** to revert the map to its original, freshly-loaded state.
|
||||
- **Bin Code Counts**: The sidebar displays a real-time count of each bin type on the map. This updates automatically after every edit.
|
||||
- **Download**: When you are finished, click the **"Download .txt"** button to save the modified wafer map to a new `.txt` file.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Wafer Map Comparator
|
||||
|
||||
The Comparator tool allows you to align and compare multiple wafer maps to find differences.
|
||||
|
||||
### 2.1 - Getting Started
|
||||
|
||||
1. **Navigate to the Comparator**: Open your web browser and go to the `/compare` URL (e.g., `http://127.0.0.1:5000/compare`).
|
||||
2. **Upload Files**: Click the upload area and select two or more `.txt` wafer map files for comparison.
|
||||
3. **Define Bin Codes**: Just like in the editor, assign a bin type for each unique character found across all uploaded files.
|
||||
4. **Generate Maps**: Click **"Generate Maps"**. The individual maps will be displayed side-by-side.
|
||||
|
||||
### 2.2 - Alignment and Comparison
|
||||
|
||||
1. **Set Reference Points**: For each map, you must set a reference point for alignment. Click on a die that should correspond to the same location on the other maps. The selected reference die will be marked.
|
||||
- If a map is oriented differently, use the **"Rotate"** button beneath it to align it correctly *before* setting the reference point.
|
||||
- To change a reference point, simply click a different die. To clear it, use the **"Clear Reference"** button.
|
||||
2. **Run Comparison**: Once a reference point is set for all maps, click the **"Run Comparison"** button.
|
||||
|
||||
### 2.3 - Interpreting the Results
|
||||
|
||||
A new, combined map will be generated with the following color code:
|
||||
|
||||
- **Green (Match - Good)**: All maps have a "Good" die at this location.
|
||||
- **Blue (Match - NG)**: All maps have an "NG" die at this location.
|
||||
- **Gray (Match - Dummy)**: All maps have a "Dummy" die at this location.
|
||||
- **Red (Mismatch)**: The maps have different bin codes (e.g., one is Good, another is NG) at this location.
|
||||
- **Yellow (Partial Data)**: One or more maps have data at this location, but at least one map does not (it's outside its boundary).
|
||||
- **Dark Gray (No Data)**: This location is outside the boundaries of all aligned maps.
|
||||
|
||||
**Detailed View**: Hover your mouse over any die on the result map to see a tooltip. This tooltip shows the filename and the specific bin status for each map at that exact location, making it easy to identify the source of any mismatches or partial data.
|
344
app.py
344
app.py
@@ -35,17 +35,66 @@ def index():
|
||||
resp.set_cookie('session_id', session_id)
|
||||
return resp
|
||||
|
||||
@app.route('/compare')
|
||||
def compare_index():
|
||||
"""Serves the comparison HTML page."""
|
||||
session_id = get_session_id()
|
||||
resp = make_response(render_template('compare.html'))
|
||||
resp.set_cookie('session_id', session_id)
|
||||
# Clean up previous comparison data if any
|
||||
if session_id in session_data and 'maps' in session_data[session_id]:
|
||||
del session_data[session_id]['maps']
|
||||
return resp
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload_file():
|
||||
"""Handles file upload, saves it, and returns unique characters."""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({"error": "No file part"}), 400
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({"error": "No selected file"}), 400
|
||||
"""
|
||||
Handles both single file upload for the editor and multiple file uploads for comparison.
|
||||
It distinguishes between them by checking the 'name' attribute of the file input.
|
||||
"""
|
||||
session_id = get_session_id()
|
||||
|
||||
# Case 1: Multiple files from comparison page (input name="files")
|
||||
if 'files' in request.files:
|
||||
files = request.files.getlist('files')
|
||||
if not files or files[0].filename == '':
|
||||
return jsonify({"error": "No files selected for comparison."}), 400
|
||||
|
||||
session_data[session_id] = {'maps': {}}
|
||||
all_unique_chars = set()
|
||||
|
||||
for i, file in enumerate(files):
|
||||
fid = f"map_{i}"
|
||||
filename = f"{session_id}_{fid}_{file.filename}"
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
file.save(filepath)
|
||||
|
||||
unique_chars = wp.get_unique_chars(filepath)
|
||||
if not unique_chars:
|
||||
continue
|
||||
|
||||
all_unique_chars.update(unique_chars)
|
||||
session_data[session_id]['maps'][fid] = {
|
||||
'filepath': filepath,
|
||||
'filename': file.filename,
|
||||
'wafer_map': None,
|
||||
'reference': None
|
||||
}
|
||||
|
||||
if not session_data[session_id]['maps']:
|
||||
return jsonify({"error": "None of the files could be processed."}), 400
|
||||
|
||||
return jsonify({
|
||||
"unique_chars": sorted(list(all_unique_chars)),
|
||||
"maps": {fid: data['filename'] for fid, data in session_data[session_id]['maps'].items()}
|
||||
})
|
||||
|
||||
# Case 2: Single file from main editor page (input name="file")
|
||||
elif 'file' in request.files:
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({"error": "No selected file"}), 400
|
||||
|
||||
if file:
|
||||
session_id = get_session_id()
|
||||
filename = f"{session_id}_{file.filename}"
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
file.save(filepath)
|
||||
@@ -57,37 +106,176 @@ def upload_file():
|
||||
session_data[session_id] = {'filepath': filepath}
|
||||
return jsonify({"unique_chars": unique_chars})
|
||||
|
||||
return jsonify({"error": "File upload failed"}), 500
|
||||
# Case 3: No valid file part found
|
||||
return jsonify({"error": "No file part in the request."}), 400
|
||||
|
||||
|
||||
@app.route('/generate_map', methods=['POST'])
|
||||
def generate_map():
|
||||
"""Generates the initial wafer map data from the file and bin definitions."""
|
||||
"""
|
||||
Generates wafer map data. For comparison mode, it generates for all uploaded maps.
|
||||
For editor mode, it also initializes the undo/redo history.
|
||||
"""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data:
|
||||
return jsonify({"error": "Session not found. Please upload a file first."}), 400
|
||||
return jsonify({"error": "Session not found."}), 400
|
||||
|
||||
data = request.json
|
||||
char_map = data.get('char_map')
|
||||
|
||||
bin_type_to_value = {'good': 2, 'ng': 1, 'dummy': 0, 'ignore': -1}
|
||||
char_to_bin_mapping = {char: bin_type_to_value[bin_type] for char, bin_type in char_map.items()}
|
||||
|
||||
|
||||
# Comparison mode logic
|
||||
if 'maps' in session_data[session_id]:
|
||||
maps_data = {}
|
||||
for fid, map_info in session_data[session_id]['maps'].items():
|
||||
wafer_map = wp.read_wafer_map(map_info['filepath'], char_to_bin_mapping)
|
||||
if wafer_map.size == 0:
|
||||
continue
|
||||
session_data[session_id]['maps'][fid]['wafer_map'] = wafer_map.tolist()
|
||||
maps_data[fid] = {
|
||||
"wafer_map": wafer_map.tolist(),
|
||||
"rows": wafer_map.shape[0],
|
||||
"cols": wafer_map.shape[1]
|
||||
}
|
||||
session_data[session_id]['char_to_bin_mapping'] = char_to_bin_mapping
|
||||
return jsonify({"maps_data": maps_data})
|
||||
|
||||
# Editor mode logic
|
||||
filepath = session_data[session_id]['filepath']
|
||||
wafer_map = wp.read_wafer_map(filepath, char_to_bin_mapping)
|
||||
|
||||
if wafer_map.size == 0:
|
||||
return jsonify({"error": "Failed to read wafer map from file."}), 500
|
||||
return jsonify({"error": "Failed to read wafer map."}), 500
|
||||
|
||||
# Store data in session
|
||||
session_data[session_id]['wafer_map'] = wafer_map.tolist()
|
||||
wafer_map_list = wafer_map.tolist()
|
||||
session_data[session_id]['wafer_map'] = wafer_map_list
|
||||
session_data[session_id]['char_to_bin_mapping'] = char_to_bin_mapping
|
||||
|
||||
# Initialize history for undo/redo
|
||||
session_data[session_id]['history'] = [wafer_map_list]
|
||||
session_data[session_id]['history_index'] = 0
|
||||
|
||||
return jsonify({
|
||||
"wafer_map": wafer_map.tolist(),
|
||||
"wafer_map": wafer_map_list,
|
||||
"rows": wafer_map.shape[0],
|
||||
"cols": wafer_map.shape[1]
|
||||
})
|
||||
|
||||
def add_to_history(session_id, new_map_state):
|
||||
"""Adds a new state to the history stack for undo/redo."""
|
||||
history = session_data[session_id].get('history', [])
|
||||
index = session_data[session_id].get('history_index', -1)
|
||||
|
||||
# If we undo and then make a new change, truncate the future history
|
||||
if index < len(history) - 1:
|
||||
history = history[:index + 1]
|
||||
|
||||
history.append(new_map_state)
|
||||
session_data[session_id]['history'] = history
|
||||
session_data[session_id]['history_index'] = len(history) - 1
|
||||
|
||||
@app.route('/set_reference', methods=['POST'])
|
||||
def set_reference():
|
||||
"""Sets the reference point for a specific map in comparison mode."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or 'maps' not in session_data[session_id]:
|
||||
return jsonify({"error": "Not in comparison mode or session expired."}), 400
|
||||
|
||||
data = request.json
|
||||
fid = data.get('fid')
|
||||
reference = data.get('reference') # Expected: [row, col]
|
||||
|
||||
if fid not in session_data[session_id]['maps']:
|
||||
return jsonify({"error": "Invalid map ID."}), 400
|
||||
|
||||
session_data[session_id]['maps'][fid]['reference'] = reference
|
||||
return jsonify({"success": True, "fid": fid, "reference": reference})
|
||||
|
||||
@app.route('/run_comparison', methods=['POST'])
|
||||
def run_comparison():
|
||||
"""Runs the comparison algorithm on all maps with set references."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or 'maps' not in session_data[session_id]:
|
||||
return jsonify({"error": "Not in comparison mode or session expired."}), 400
|
||||
|
||||
maps_to_compare = []
|
||||
references = []
|
||||
for fid, map_info in session_data[session_id]['maps'].items():
|
||||
if map_info.get('wafer_map') is not None and map_info.get('reference') is not None:
|
||||
maps_to_compare.append(np.array(map_info['wafer_map']))
|
||||
references.append(map_info['reference'])
|
||||
else:
|
||||
return jsonify({"error": f"Map '{map_info['filename']}' is missing data or a reference point."}), 400
|
||||
|
||||
if len(maps_to_compare) < 2:
|
||||
return jsonify({"error": "At least two maps with reference points are required for comparison."}), 400
|
||||
|
||||
comparison_result, legend, detailed_grid = wp.compare_wafer_maps(maps_to_compare, references)
|
||||
|
||||
# Convert numpy types in detailed_grid to standard Python ints for JSON serialization
|
||||
serializable_grid = [[[int(b) for b in cell] for cell in row] for row in detailed_grid]
|
||||
|
||||
# Store the detailed grid in the session for potential later use if needed
|
||||
session_data[session_id]['comparison_details'] = {
|
||||
'grid': serializable_grid,
|
||||
'map_names': [info['filename'] for info in session_data[session_id]['maps'].values() if info.get('wafer_map') is not None and info.get('reference') is not None]
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"result_map": comparison_result.tolist(),
|
||||
"legend": legend,
|
||||
"detailed_grid": serializable_grid,
|
||||
"map_names": session_data[session_id]['comparison_details']['map_names'],
|
||||
"rows": comparison_result.shape[0],
|
||||
"cols": comparison_result.shape[1]
|
||||
})
|
||||
|
||||
@app.route('/compare/rotate', methods=['POST'])
|
||||
def compare_rotate():
|
||||
"""Rotates a single map within the comparison session."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or 'maps' not in session_data[session_id]:
|
||||
return jsonify({"error": "Not in comparison mode or session expired."}), 400
|
||||
|
||||
data = request.json
|
||||
fid = data.get('fid')
|
||||
|
||||
if fid not in session_data[session_id]['maps']:
|
||||
return jsonify({"error": "Invalid map ID."}), 400
|
||||
|
||||
map_info = session_data[session_id]['maps'][fid]
|
||||
wafer_map = np.array(map_info['wafer_map'])
|
||||
|
||||
rotated_map = np.rot90(wafer_map, k=-1)
|
||||
map_info['wafer_map'] = rotated_map.tolist()
|
||||
|
||||
# Clear reference if it was set, as rotation changes coordinates
|
||||
map_info['reference'] = None
|
||||
|
||||
return jsonify({
|
||||
"fid": fid,
|
||||
"wafer_map": rotated_map.tolist(),
|
||||
"rows": rotated_map.shape[0],
|
||||
"cols": rotated_map.shape[1]
|
||||
})
|
||||
|
||||
@app.route('/compare/clear_reference', methods=['POST'])
|
||||
def compare_clear_reference():
|
||||
"""Clears the reference point for a specific map."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or 'maps' not in session_data[session_id]:
|
||||
return jsonify({"error": "Not in comparison mode or session expired."}), 400
|
||||
|
||||
data = request.json
|
||||
fid = data.get('fid')
|
||||
|
||||
if fid not in session_data[session_id]['maps']:
|
||||
return jsonify({"error": "Invalid map ID."}), 400
|
||||
|
||||
session_data[session_id]['maps'][fid]['reference'] = None
|
||||
|
||||
return jsonify({"success": True, "fid": fid})
|
||||
|
||||
@app.route('/update_map', methods=['POST'])
|
||||
def update_map():
|
||||
"""Updates specified dies in the map and returns the full updated map."""
|
||||
@@ -96,7 +284,7 @@ def update_map():
|
||||
return jsonify({"error": "Session expired or invalid."}), 400
|
||||
|
||||
data = request.json
|
||||
dies_to_update = data.get('dies') # Expected format: [[row, col], [row, col], ...]
|
||||
dies_to_update = data.get('dies')
|
||||
new_bin_type = data.get('bin_type')
|
||||
|
||||
if not dies_to_update:
|
||||
@@ -107,14 +295,15 @@ def update_map():
|
||||
|
||||
wafer_map = np.array(session_data[session_id]['wafer_map'])
|
||||
|
||||
# Update the map based on the list of die coordinates
|
||||
for row, col in dies_to_update:
|
||||
if 0 <= row < wafer_map.shape[0] and 0 <= col < wafer_map.shape[1]:
|
||||
wafer_map[row, col] = new_bin_value
|
||||
|
||||
session_data[session_id]['wafer_map'] = wafer_map.tolist()
|
||||
wafer_map_list = wafer_map.tolist()
|
||||
session_data[session_id]['wafer_map'] = wafer_map_list
|
||||
add_to_history(session_id, wafer_map_list)
|
||||
|
||||
return jsonify({"wafer_map": wafer_map.tolist()})
|
||||
return jsonify({"wafer_map": wafer_map_list})
|
||||
|
||||
@app.route('/rotate_map', methods=['POST'])
|
||||
def rotate_map():
|
||||
@@ -124,23 +313,124 @@ def rotate_map():
|
||||
return jsonify({"error": "Session expired or invalid."}), 400
|
||||
|
||||
wafer_map = np.array(session_data[session_id]['wafer_map'])
|
||||
|
||||
# np.rot90 rotates counter-clockwise, so k=-1 rotates clockwise
|
||||
rotated_map = np.rot90(wafer_map, k=-1)
|
||||
rotated_map_list = rotated_map.tolist()
|
||||
|
||||
session_data[session_id]['wafer_map'] = rotated_map.tolist()
|
||||
session_data[session_id]['wafer_map'] = rotated_map_list
|
||||
add_to_history(session_id, rotated_map_list)
|
||||
|
||||
return jsonify({
|
||||
"wafer_map": rotated_map.tolist(),
|
||||
"wafer_map": rotated_map_list,
|
||||
"rows": rotated_map.shape[0],
|
||||
"cols": rotated_map.shape[1]
|
||||
})
|
||||
|
||||
@app.route('/inset_map', methods=['POST'])
|
||||
def inset_map():
|
||||
"""Applies an inset to the wafer map."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data:
|
||||
return jsonify({"error": "Session expired or invalid."}), 400
|
||||
|
||||
data = request.json
|
||||
layers = int(data.get('layers', 1))
|
||||
from_bin_name = data.get('from_bin')
|
||||
to_bin_name = data.get('to_bin')
|
||||
|
||||
bin_type_to_value = {'good': 2, 'ng': 1, 'dummy': 0, 'ignore': -1}
|
||||
from_bin_value = bin_type_to_value.get(from_bin_name)
|
||||
to_bin_value = bin_type_to_value.get(to_bin_name)
|
||||
|
||||
if from_bin_value is None or to_bin_value is None:
|
||||
return jsonify({"error": "Invalid 'from' or 'to' bin type specified."}), 400
|
||||
|
||||
wafer_map = np.array(session_data[session_id]['wafer_map'])
|
||||
inset_map_array = wp.inset_wafer_map(wafer_map, layers, from_bin_value, to_bin_value)
|
||||
inset_map_list = inset_map_array.tolist()
|
||||
|
||||
session_data[session_id]['wafer_map'] = inset_map_list
|
||||
add_to_history(session_id, inset_map_list)
|
||||
|
||||
return jsonify({
|
||||
"wafer_map": inset_map_list,
|
||||
"rows": inset_map_array.shape[0],
|
||||
"cols": inset_map_array.shape[1]
|
||||
})
|
||||
|
||||
@app.route('/undo', methods=['POST'])
|
||||
def undo():
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or not session_data[session_id].get('history'):
|
||||
return jsonify({"error": "No history to undo."}), 400
|
||||
|
||||
history = session_data[session_id]['history']
|
||||
index = session_data[session_id]['history_index']
|
||||
|
||||
if index > 0:
|
||||
index -= 1
|
||||
session_data[session_id]['history_index'] = index
|
||||
wafer_map = history[index]
|
||||
session_data[session_id]['wafer_map'] = wafer_map
|
||||
return jsonify({
|
||||
"wafer_map": wafer_map,
|
||||
"rows": len(wafer_map),
|
||||
"cols": len(wafer_map[0]) if wafer_map else 0,
|
||||
"can_undo": index > 0,
|
||||
"can_redo": True
|
||||
})
|
||||
return jsonify({"error": "Already at the beginning of history."}), 400
|
||||
|
||||
@app.route('/redo', methods=['POST'])
|
||||
def redo():
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or not session_data[session_id].get('history'):
|
||||
return jsonify({"error": "No history to redo."}), 400
|
||||
|
||||
history = session_data[session_id]['history']
|
||||
index = session_data[session_id]['history_index']
|
||||
|
||||
if index < len(history) - 1:
|
||||
index += 1
|
||||
session_data[session_id]['history_index'] = index
|
||||
wafer_map = history[index]
|
||||
session_data[session_id]['wafer_map'] = wafer_map
|
||||
return jsonify({
|
||||
"wafer_map": wafer_map,
|
||||
"rows": len(wafer_map),
|
||||
"cols": len(wafer_map[0]) if wafer_map else 0,
|
||||
"can_undo": True,
|
||||
"can_redo": index < len(history) - 1
|
||||
})
|
||||
return jsonify({"error": "Already at the end of history."}), 400
|
||||
|
||||
@app.route('/reset_map', methods=['POST'])
|
||||
def reset_map():
|
||||
"""Resets the map to its original state (the first state in history)."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data or not session_data[session_id].get('history'):
|
||||
return jsonify({"error": "No history available to reset from."}), 400
|
||||
|
||||
history = session_data[session_id]['history']
|
||||
original_map = history[0]
|
||||
|
||||
# Reset history to only the original state
|
||||
session_data[session_id]['history'] = [original_map]
|
||||
session_data[session_id]['history_index'] = 0
|
||||
session_data[session_id]['wafer_map'] = original_map
|
||||
|
||||
return jsonify({
|
||||
"wafer_map": original_map,
|
||||
"rows": len(original_map),
|
||||
"cols": len(original_map[0]) if original_map else 0,
|
||||
"can_undo": False,
|
||||
"can_redo": False
|
||||
})
|
||||
|
||||
@app.route('/download', methods=['GET'])
|
||||
def download_file():
|
||||
"""Saves the current wafer map to a text file and serves it for download."""
|
||||
session_id = get_session_id()
|
||||
if session_id not in session_data:
|
||||
if session_id not in session_data or 'wafer_map' not in session_data[session_id]:
|
||||
return jsonify({"error": "No data to download."}), 400
|
||||
|
||||
wafer_map = np.array(session_data[session_id]['wafer_map'])
|
||||
|
@@ -163,21 +163,109 @@ select {
|
||||
background-color: var(--secondary-color-hover);
|
||||
}
|
||||
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
gap: 10px; /* Gap between control sections */
|
||||
}
|
||||
|
||||
.control-section {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
.control-section:first-child {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.control-section-header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-dark);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px; /* row-gap column-gap */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.control-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.control-group select,
|
||||
.control-group input[type="number"],
|
||||
.control-group .button-secondary {
|
||||
flex-grow: 1; /* Allow items to grow */
|
||||
min-width: 80px; /* Prevent items from becoming too small */
|
||||
}
|
||||
|
||||
.control-group .btn-group {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.btn-group .button-secondary {
|
||||
border-radius: 0;
|
||||
}
|
||||
.btn-group .button-secondary:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
.btn-group .button-secondary:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
.btn-group .button-secondary.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.control-group.vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.inset-control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.inset-control-row label {
|
||||
width: 50px; /* Fixed width for alignment */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.inset-control-row select,
|
||||
.inset-control-row input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control-group-divider {
|
||||
height: 1px;
|
||||
background-color: var(--border-color);
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.control-group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.control-group-grid.four-cols {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.control-group-grid .button-primary,
|
||||
.control-group-grid .button-secondary {
|
||||
width: 100%;
|
||||
@@ -187,10 +275,8 @@ select {
|
||||
.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);
|
||||
text-align: left;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.hint-text strong {
|
||||
color: var(--text-color);
|
||||
@@ -214,7 +300,7 @@ select {
|
||||
}
|
||||
|
||||
/* --- Main Editor Area --- */
|
||||
#editor-section {
|
||||
#editor-section, #viewer-section, #comparison-result-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -244,6 +330,9 @@ select {
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.count-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.count-item span {
|
||||
@@ -255,8 +344,14 @@ select {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
#result-legend .count-item span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
#map-container {
|
||||
|
||||
#map-container, #result-map-container {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
@@ -265,31 +360,107 @@ select {
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--card-bg);
|
||||
}
|
||||
#map-container:active {
|
||||
#map-container:active, #result-map-container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
#wafer-map-canvas {
|
||||
#wafer-map-canvas, #result-map-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-viewer-controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.map-viewer-controls label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* --- Comparison Page Specifics --- */
|
||||
.reference-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--secondary-color);
|
||||
gap: 10px;
|
||||
}
|
||||
.reference-item .filename {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.reference-item .ref-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ref-status {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-dark);
|
||||
}
|
||||
.ref-status.set {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
.clear-ref-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color-dark);
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
.clear-ref-btn:hover {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
/* --- Tooltip --- */
|
||||
.map-tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
color: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
pointer-events: none; /* So it doesn't interfere with mouse events */
|
||||
white-space: pre; /* To respect newlines */
|
||||
z-index: 100;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
.reference-item.has-ref .clear-ref-btn {
|
||||
display: inline-block; /* Show only when ref is set */
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* --- Status & Spinner --- */
|
||||
.status {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
display: none; /* Use classes to show */
|
||||
}
|
||||
.status.success {
|
||||
background-color: var(--success-color);
|
||||
background-color: rgba(40, 167, 69, 0.8);
|
||||
color: white;
|
||||
display: block;
|
||||
}
|
||||
.status.error {
|
||||
background-color: var(--error-color);
|
||||
background-color: rgba(220, 53, 69, 0.8);
|
||||
color: white;
|
||||
display: block;
|
||||
}
|
||||
@@ -307,4 +478,4 @@ select {
|
||||
|
||||
@keyframes spinner-border {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
}
|
||||
|
430
static/js/compare.js
Normal file
430
static/js/compare.js
Normal file
@@ -0,0 +1,430 @@
|
||||
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);
|
||||
});
|
||||
|
@@ -20,6 +20,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const updateSelectionBtn = document.getElementById('update-selection-btn');
|
||||
const resetViewBtn = document.getElementById('reset-view-btn');
|
||||
const rotateBtn = document.getElementById('rotate-btn');
|
||||
const polygonSelectBtn = document.getElementById('polygon-select-btn');
|
||||
const boxSelectBtn = document.getElementById('box-select-btn');
|
||||
const insetMapBtn = document.getElementById('inset-map-btn');
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
const redoBtn = document.getElementById('redo-btn');
|
||||
const binCountsContainer = document.getElementById('bin-counts');
|
||||
|
||||
// --- State Management ---
|
||||
@@ -33,10 +38,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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 };
|
||||
|
||||
// --- Selection modes ---
|
||||
let selectionMode = 'box'; // 'box', 'polygon'
|
||||
let isBoxSelecting = false;
|
||||
let boxSelectionStart = { x: 0, y: 0 };
|
||||
let boxSelectionEnd = { x: 0, y: 0 };
|
||||
|
||||
let isDrawingPolygon = false;
|
||||
let polygonPoints = [];
|
||||
let currentMousePos = { x: 0, y: 0 };
|
||||
|
||||
// --- UI/UX Functions ---
|
||||
function setStatus(element, message, isError = false) {
|
||||
@@ -62,6 +74,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if(to) to.classList.add('active');
|
||||
}
|
||||
|
||||
function updateHistoryButtons(canUndo, canRedo) {
|
||||
undoBtn.disabled = !canUndo;
|
||||
redoBtn.disabled = !canRedo;
|
||||
}
|
||||
|
||||
function handleMapUpdate(result) {
|
||||
waferMap = result.wafer_map;
|
||||
mapRows = result.rows;
|
||||
mapCols = result.cols;
|
||||
|
||||
selection.clear();
|
||||
updateBinCounts();
|
||||
draw();
|
||||
}
|
||||
|
||||
// --- Canvas & Data Functions ---
|
||||
function worldToScreen(x, y) {
|
||||
return { x: x * transform.scale + transform.x, y: y * transform.scale + transform.y };
|
||||
@@ -121,17 +148,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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);
|
||||
@@ -145,24 +162,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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;
|
||||
@@ -174,19 +173,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
ctx.restore();
|
||||
|
||||
// Draw selection rectangle (in screen space)
|
||||
if (isSelecting) {
|
||||
if (isBoxSelecting) {
|
||||
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)
|
||||
x: Math.min(boxSelectionStart.x, boxSelectionEnd.x),
|
||||
y: Math.min(boxSelectionStart.y, boxSelectionEnd.y),
|
||||
w: Math.abs(boxSelectionStart.x - boxSelectionEnd.x),
|
||||
h: Math.abs(boxSelectionStart.y - boxSelectionEnd.y)
|
||||
};
|
||||
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
||||
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
||||
}
|
||||
|
||||
// Draw polygon (in screen space)
|
||||
if (isDrawingPolygon && polygonPoints.length > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
|
||||
for (let i = 1; i < polygonPoints.length; i++) {
|
||||
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
|
||||
}
|
||||
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Draw line to current mouse position
|
||||
ctx.lineTo(currentMousePos.x, currentMousePos.y);
|
||||
ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
|
||||
ctx.stroke();
|
||||
|
||||
// Draw vertices
|
||||
ctx.fillStyle = 'red';
|
||||
polygonPoints.forEach(p => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,16 +265,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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';
|
||||
|
||||
handleMapUpdate(result);
|
||||
resetTransform();
|
||||
updateBinCounts();
|
||||
draw();
|
||||
updateHistoryButtons(false, false); // History starts here
|
||||
|
||||
switchCard(binDefinitionSection, editorControlsSection);
|
||||
} catch (error) {
|
||||
@@ -260,6 +281,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
if (isDrawingPolygon) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouse = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||
const worldPos = screenToWorld(mouse.x, mouse.y);
|
||||
@@ -275,10 +297,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (e.shiftKey) {
|
||||
isSelecting = true;
|
||||
selectionStart = { x: e.offsetX, y: e.offsetY };
|
||||
selectionEnd = selectionStart;
|
||||
if (selectionMode === 'box' && e.shiftKey) {
|
||||
isBoxSelecting = true;
|
||||
boxSelectionStart = { x: e.offsetX, y: e.offsetY };
|
||||
boxSelectionEnd = boxSelectionStart;
|
||||
} else if (selectionMode === 'polygon') {
|
||||
// Polygon drawing is handled in 'click' to avoid conflict with panning
|
||||
} else {
|
||||
isPanning = true;
|
||||
panStart = { x: e.clientX, y: e.clientY };
|
||||
@@ -286,6 +310,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
currentMousePos = { x: e.offsetX, y: e.offsetY };
|
||||
if (isPanning) {
|
||||
const dx = e.clientX - panStart.x;
|
||||
const dy = e.clientY - panStart.y;
|
||||
@@ -293,55 +318,157 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
transform.y += dy;
|
||||
panStart = { x: e.clientX, y: e.clientY };
|
||||
draw();
|
||||
} else if (isSelecting) {
|
||||
selectionEnd = { x: e.offsetX, y: e.offsetY };
|
||||
} else if (isBoxSelecting) {
|
||||
boxSelectionEnd = { x: e.offsetX, y: e.offsetY };
|
||||
draw();
|
||||
} else if (isDrawingPolygon) {
|
||||
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.`);
|
||||
if (isBoxSelecting) {
|
||||
isBoxSelecting = false;
|
||||
selectDiesInBox();
|
||||
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;
|
||||
if (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();
|
||||
if (selectionMode === 'polygon') {
|
||||
handlePolygonClick(e);
|
||||
} else {
|
||||
if (e.shiftKey) return; // Box selection is handled in mousedown/up
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('dblclick', (e) => {
|
||||
if (selectionMode === 'polygon' && isDrawingPolygon) {
|
||||
e.preventDefault();
|
||||
closeAndSelectPolygon();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Selection Logic ---
|
||||
function selectDiesInBox() {
|
||||
const startWorld = screenToWorld(boxSelectionStart.x, boxSelectionStart.y);
|
||||
const endWorld = screenToWorld(boxSelectionEnd.x, boxSelectionEnd.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 (!window.event.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.`);
|
||||
}
|
||||
|
||||
function handlePolygonClick(e) {
|
||||
const point = { x: e.offsetX, y: e.offsetY };
|
||||
if (!isDrawingPolygon) {
|
||||
isDrawingPolygon = true;
|
||||
polygonPoints = [point];
|
||||
setStatus(editorStatus, 'Polygon drawing started. Click to add points, double-click to finish.');
|
||||
} else {
|
||||
// Check if closing polygon
|
||||
const firstPoint = polygonPoints[0];
|
||||
const dist = Math.sqrt(Math.pow(point.x - firstPoint.x, 2) + Math.pow(point.y - firstPoint.y, 2));
|
||||
if (dist < 10 && polygonPoints.length > 2) {
|
||||
closeAndSelectPolygon();
|
||||
} else {
|
||||
polygonPoints.push(point);
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function closeAndSelectPolygon() {
|
||||
if (!window.event.ctrlKey) selection.clear();
|
||||
selectDiesInPolygon();
|
||||
isDrawingPolygon = false;
|
||||
polygonPoints = [];
|
||||
draw();
|
||||
}
|
||||
|
||||
function selectDiesInPolygon() {
|
||||
const dieSize = 10;
|
||||
const polygonInWorld = polygonPoints.map(p => screenToWorld(p.x, p.y));
|
||||
|
||||
for (let r = 0; r < mapRows; r++) {
|
||||
for (let c = 0; c < mapCols; c++) {
|
||||
const dieCenter = {
|
||||
x: c * dieSize + dieSize / 2,
|
||||
y: r * dieSize + dieSize / 2
|
||||
};
|
||||
if (pointInPolygon(dieCenter, polygonInWorld)) {
|
||||
const key = `${r},${c}`;
|
||||
if (selection.has(key)) selection.delete(key);
|
||||
else selection.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
setStatus(editorStatus, `${selection.size} die(s) selected.`);
|
||||
}
|
||||
|
||||
function pointInPolygon(point, vs) {
|
||||
// ray-casting algorithm based on
|
||||
// https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
|
||||
const x = point.x, y = point.y;
|
||||
let inside = false;
|
||||
for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) {
|
||||
const xi = vs[i].x, yi = vs[i].y;
|
||||
const xj = vs[j].x, yj = vs[j].y;
|
||||
const intersect = ((yi > y) !== (yj > y))
|
||||
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// --- Action Buttons ---
|
||||
function setSelectionMode(mode) {
|
||||
selectionMode = mode;
|
||||
if (mode === 'polygon') {
|
||||
polygonSelectBtn.classList.add('active');
|
||||
boxSelectBtn.classList.remove('active');
|
||||
setStatus(editorStatus, 'Polygon select mode. Click to draw, dbl-click to finish.');
|
||||
isDrawingPolygon = false;
|
||||
polygonPoints = [];
|
||||
} else { // box mode
|
||||
boxSelectBtn.classList.add('active');
|
||||
polygonSelectBtn.classList.remove('active');
|
||||
setStatus(editorStatus, 'Box select mode. Hold Shift and drag to select.');
|
||||
isDrawingPolygon = false; // Cancel any ongoing polygon drawing
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
boxSelectBtn.addEventListener('click', () => setSelectionMode('box'));
|
||||
polygonSelectBtn.addEventListener('click', () => setSelectionMode('polygon'));
|
||||
|
||||
updateSelectionBtn.addEventListener('click', async () => {
|
||||
if (selection.size === 0) {
|
||||
setStatus(editorStatus, 'No dies selected to update.', true);
|
||||
@@ -361,19 +488,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error);
|
||||
|
||||
waferMap = result.wafer_map;
|
||||
selection.clear();
|
||||
updateBinCounts();
|
||||
handleMapUpdate(result);
|
||||
setStatus(editorStatus, 'Update successful.');
|
||||
draw();
|
||||
updateHistoryButtons(true, false);
|
||||
} catch (error) {
|
||||
setStatus(editorStatus, `Error: ${error.message}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
resetViewBtn.addEventListener('click', () => {
|
||||
resetTransform();
|
||||
draw();
|
||||
resetViewBtn.addEventListener('click', async () => {
|
||||
setStatus(editorStatus, 'Resetting map to original state...');
|
||||
try {
|
||||
const response = await fetch('/reset_map', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error);
|
||||
|
||||
handleMapUpdate(result);
|
||||
resetTransform(); // Also reset zoom and pan
|
||||
draw();
|
||||
setStatus(editorStatus, 'Map has been reset.');
|
||||
updateHistoryButtons(result.can_undo, result.can_redo);
|
||||
|
||||
} catch (error) {
|
||||
setStatus(editorStatus, `Error: ${error.message}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
rotateBtn.addEventListener('click', async () => {
|
||||
@@ -383,19 +521,69 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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();
|
||||
handleMapUpdate(result);
|
||||
setStatus(editorStatus, 'Map rotated successfully.');
|
||||
updateHistoryButtons(true, false);
|
||||
} catch (error) {
|
||||
setStatus(editorStatus, `Error: ${error.message}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
insetMapBtn.addEventListener('click', async () => {
|
||||
const layers = document.getElementById('inset-layers').value;
|
||||
const fromBin = document.getElementById('inset-from-bin').value;
|
||||
const toBin = document.getElementById('inset-to-bin').value;
|
||||
|
||||
if (parseInt(layers) < 1) {
|
||||
setStatus(editorStatus, 'Inset layers must be at least 1.', true);
|
||||
return;
|
||||
}
|
||||
if (fromBin === toBin) {
|
||||
setStatus(editorStatus, "'From' and 'To' bin types cannot be the same.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(editorStatus, `Insetting ${layers} layer(s)...`);
|
||||
|
||||
try {
|
||||
const response = await fetch('/inset_map', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
layers: layers,
|
||||
from_bin: fromBin,
|
||||
to_bin: toBin
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error);
|
||||
|
||||
handleMapUpdate(result);
|
||||
setStatus(editorStatus, 'Inset operation successful.');
|
||||
updateHistoryButtons(true, false);
|
||||
} catch (error) {
|
||||
setStatus(editorStatus, `Error: ${error.message}`, true);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleHistoryAction(action) {
|
||||
setStatus(editorStatus, `Performing ${action}...`);
|
||||
try {
|
||||
const response = await fetch(`/${action}`, { method: 'POST' });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error);
|
||||
|
||||
handleMapUpdate(result);
|
||||
setStatus(editorStatus, `${action} successful.`);
|
||||
updateHistoryButtons(result.can_undo, result.can_redo);
|
||||
|
||||
} catch (error) {
|
||||
setStatus(editorStatus, `Error: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
undoBtn.addEventListener('click', () => handleHistoryAction('undo'));
|
||||
redoBtn.addEventListener('click', () => handleHistoryAction('redo'));
|
||||
|
||||
new ResizeObserver(() => {
|
||||
if (mapRows > 0) { // Only resize if there is a map
|
||||
|
92
templates/compare.html
Normal file
92
templates/compare.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wafer Map Comparison</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Sidebar for Controls -->
|
||||
<aside class="sidebar">
|
||||
<header class="sidebar-header">
|
||||
<h1>Map Comparison</h1>
|
||||
<a href="/" class="button-secondary" style="text-align: center;">Back to Editor</a>
|
||||
</header>
|
||||
|
||||
<!-- Step 1: Upload Multiple Files -->
|
||||
<div id="upload-section" class="card active">
|
||||
<div class="card-header">
|
||||
<h2>Step 1: Upload Maps</h2>
|
||||
</div>
|
||||
<form id="upload-form">
|
||||
<input type="file" id="file-input" name="files" accept=".txt" multiple required>
|
||||
<button type="submit" class="button-primary">Upload & Analyze</button>
|
||||
</form>
|
||||
<div id="upload-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Define Bins -->
|
||||
<div id="bin-definition-section" class="card">
|
||||
<div class="card-header">
|
||||
<h2>Step 2: Define Bin Codes</h2>
|
||||
<p class="hint-text">These definitions will apply to all maps.</p>
|
||||
</div>
|
||||
<form id="bin-definition-form">
|
||||
<div id="bin-map-container"></div>
|
||||
<button type="submit" class="button-primary">Generate Maps</button>
|
||||
</form>
|
||||
<div id="bin-status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Set References & Compare -->
|
||||
<div id="comparison-controls-section" class="card">
|
||||
<div class="card-header">
|
||||
<h2>Step 3: Set References & Compare</h2>
|
||||
</div>
|
||||
<div id="reference-setter-container">
|
||||
<!-- Map reference setters will be dynamically inserted here -->
|
||||
</div>
|
||||
<div class="control-group-divider"></div>
|
||||
<button id="compare-btn" class="button-primary" disabled>Run Comparison</button>
|
||||
<div id="compare-status" class="status"></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area for Canvas -->
|
||||
<main class="main-content">
|
||||
<div id="viewer-section" style="display: none;">
|
||||
<div class="map-viewer-controls">
|
||||
<label for="map-selector">Select Map:</label>
|
||||
<select id="map-selector"></select>
|
||||
<button id="rotate-map-btn" class="button-secondary">Rotate 90°</button>
|
||||
</div>
|
||||
<div id="map-container">
|
||||
<canvas id="wafer-map-canvas"></canvas>
|
||||
<div id="reference-info" class="status">Click on a die to set its reference point.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="comparison-result-section" style="display: none;">
|
||||
<div class="map-viewer-controls">
|
||||
<button id="back-to-viewer-btn" class="button-secondary">Back to Viewer</button>
|
||||
</div>
|
||||
<div id="result-map-container">
|
||||
<canvas id="result-map-canvas"></canvas>
|
||||
<div id="map-tooltip" class="map-tooltip"></div>
|
||||
</div>
|
||||
<div id="result-legend" class="counts-container"></div>
|
||||
</div>
|
||||
<div id="welcome-message">
|
||||
<h2>Welcome to Map Comparison</h2>
|
||||
<p>Please upload two or more map files to begin.</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/compare.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
@@ -15,6 +15,7 @@
|
||||
<aside class="sidebar">
|
||||
<header class="sidebar-header">
|
||||
<h1>Wafer Map Pro</h1>
|
||||
<a href="/compare" class="button-secondary" style="text-align: center; margin-top: 10px;">Go to Comparison</a>
|
||||
</header>
|
||||
|
||||
<!-- Step 1: Upload -->
|
||||
@@ -50,23 +51,72 @@
|
||||
<h2>Step 3: Actions</h2>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<div class="control-group">
|
||||
<label for="bin-type-selector">Set Bin Type:</label>
|
||||
<select id="bin-type-selector">
|
||||
<option value="good">Good</option>
|
||||
<option value="ng">NG</option>
|
||||
<option value="dummy">Dummy</option>
|
||||
<option value="ignore">Ignore</option>
|
||||
</select>
|
||||
<button id="update-selection-btn" class="button-secondary">Apply</button>
|
||||
<!-- Edit Tools -->
|
||||
<div class="control-section">
|
||||
<h3 class="control-section-header">Edit Tools</h3>
|
||||
<div class="control-group">
|
||||
<label for="bin-type-selector">Set Bin Type:</label>
|
||||
<select id="bin-type-selector">
|
||||
<option value="good">Good</option>
|
||||
<option value="ng">NG</option>
|
||||
<option value="dummy">Dummy</option>
|
||||
<option value="ignore">Ignore</option>
|
||||
</select>
|
||||
<button id="update-selection-btn" class="button-secondary">Apply to Selection</button>
|
||||
</div>
|
||||
<div class="control-group-divider"></div>
|
||||
<div class="control-group">
|
||||
<label>Selection Mode:</label>
|
||||
<div class="btn-group">
|
||||
<button id="box-select-btn" class="button-secondary active">Box</button>
|
||||
<button id="polygon-select-btn" class="button-secondary">Polygon</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint-text">Pro-tip: Hold <strong>Shift</strong> for Box Select, or <strong>Ctrl</strong> for multi-select.</p>
|
||||
<div class="control-group-divider"></div>
|
||||
<div class="control-group vertical">
|
||||
<label>Inset from Edge:</label>
|
||||
<div class="inset-control-row">
|
||||
<label for="inset-layers">Layers:</label>
|
||||
<input type="number" id="inset-layers" value="1" min="1">
|
||||
</div>
|
||||
<div class="inset-control-row">
|
||||
<label for="inset-from-bin">From:</label>
|
||||
<select id="inset-from-bin">
|
||||
<option value="good">Good</option>
|
||||
<option value="ng">NG</option>
|
||||
<option value="dummy">Dummy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inset-control-row">
|
||||
<label for="inset-to-bin">To:</label>
|
||||
<select id="inset-to-bin">
|
||||
<option value="ng">NG</option>
|
||||
<option value="good">Good</option>
|
||||
<option value="dummy">Dummy</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="inset-map-btn" class="button-secondary">Run Inset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group-grid">
|
||||
<button id="rotate-btn" class="button-secondary">Rotate 90°</button>
|
||||
<button id="reset-view-btn" class="button-secondary">Reset View</button>
|
||||
|
||||
<!-- View Tools -->
|
||||
<div class="control-section">
|
||||
<h3 class="control-section-header">View Tools</h3>
|
||||
<div class="control-group-grid four-cols">
|
||||
<button id="undo-btn" class="button-secondary" disabled>Undo</button>
|
||||
<button id="redo-btn" class="button-secondary" disabled>Redo</button>
|
||||
<button id="rotate-btn" class="button-secondary">Rotate 90°</button>
|
||||
<button id="reset-view-btn" class="button-secondary">Reset Map</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Actions -->
|
||||
<div class="control-section">
|
||||
<h3 class="control-section-header">File</h3>
|
||||
<a id="download-btn" class="button-primary" href="/download" download>Download .txt</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint-text">Pro-tip: Hold <strong>Shift</strong> and drag to select a region.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
@@ -60,4 +60,161 @@ def save_wafer_map(wafer_map, bin_to_char_mapping, file_path):
|
||||
"""
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
for row in wafer_map:
|
||||
file.write(''.join([bin_to_char_mapping.get(int(bin_code), ' ') for bin_code in row]) + '\n')
|
||||
file.write(''.join([bin_to_char_mapping.get(int(bin_code), ' ') for bin_code in row]) + '\n')
|
||||
|
||||
def get_bin_counts(wafer_map):
|
||||
"""
|
||||
Calculates the count of each bin type in the wafer map.
|
||||
"""
|
||||
unique, counts = np.unique(wafer_map, return_counts=True)
|
||||
return dict(zip(unique, counts))
|
||||
|
||||
def inset_wafer_map(wafer_map, layers, from_bin_value, to_bin_value):
|
||||
"""
|
||||
Finds the outer N layers of the main die area, defined by proximity to
|
||||
'ignore' bins, and changes them from 'from_bin_value' to 'to_bin_value'.
|
||||
"""
|
||||
if layers <= 0:
|
||||
return wafer_map
|
||||
|
||||
rows, cols = wafer_map.shape
|
||||
|
||||
# Mask for all dies that are candidates for changing.
|
||||
target_mask = (wafer_map == from_bin_value)
|
||||
|
||||
# Mask for dies that are NOT part of the wafer (e.g., ignore bins)
|
||||
background_mask = (wafer_map == -1)
|
||||
|
||||
# This will hold all dies that have been identified for changing.
|
||||
final_change_mask = np.zeros_like(wafer_map, dtype=bool)
|
||||
|
||||
# This mask represents the "peeling front". Initially, it's the background.
|
||||
peel_front_mask = np.copy(background_mask)
|
||||
|
||||
for _ in range(layers):
|
||||
# Find dies in our target_mask that are adjacent to the current peel_front_mask
|
||||
newly_found_edge_mask = np.zeros_like(wafer_map, dtype=bool)
|
||||
|
||||
# Iterate only over the candidates to be more efficient
|
||||
for r, c in np.argwhere(target_mask):
|
||||
# Check neighbors
|
||||
for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
||||
nr, nc = r + dr, c + dc
|
||||
|
||||
# If a neighbor is part of the current "peel front"
|
||||
if (0 <= nr < rows and 0 <= nc < cols and peel_front_mask[nr, nc]):
|
||||
newly_found_edge_mask[r, c] = True
|
||||
break # Found it's an edge, no need to check other neighbors
|
||||
|
||||
# If we didn't find any new edge dies, we're done.
|
||||
if not np.any(newly_found_edge_mask):
|
||||
break
|
||||
|
||||
# Add the newly found layer to our final mask of dies to change.
|
||||
final_change_mask |= newly_found_edge_mask
|
||||
|
||||
# The layer we just found becomes the new "peel front" for the next iteration.
|
||||
peel_front_mask |= newly_found_edge_mask
|
||||
|
||||
# Remove the dies we just processed from the pool of candidates.
|
||||
target_mask &= ~newly_found_edge_mask
|
||||
|
||||
# Apply the change to a copy of the original map
|
||||
wafer_map_copy = np.copy(wafer_map)
|
||||
wafer_map_copy[final_change_mask] = to_bin_value
|
||||
|
||||
return wafer_map_copy
|
||||
|
||||
def compare_wafer_maps(maps, references):
|
||||
"""
|
||||
Compares multiple wafer maps by aligning them based on reference points.
|
||||
"""
|
||||
if not maps or len(maps) != len(references):
|
||||
return np.array([]), {}
|
||||
|
||||
# Determine the bounds of the combined map
|
||||
max_up = 0
|
||||
max_down = 0
|
||||
max_left = 0
|
||||
max_right = 0
|
||||
|
||||
for i, (map_arr, ref) in enumerate(zip(maps, references)):
|
||||
ref_r, ref_c = ref
|
||||
rows, cols = map_arr.shape
|
||||
|
||||
max_up = max(max_up, ref_r)
|
||||
max_down = max(max_down, rows - ref_r - 1)
|
||||
max_left = max(max_left, ref_c)
|
||||
max_right = max(max_right, cols - ref_c - 1)
|
||||
|
||||
# Total dimensions of the comparison grid
|
||||
total_rows = max_up + max_down + 1
|
||||
total_cols = max_left + max_right + 1
|
||||
|
||||
# A grid where each cell holds a list of bin codes from all maps at that position
|
||||
# Initialize with a special value for "no map data"
|
||||
NO_DATA = -10
|
||||
comparison_grid = [[[NO_DATA] * len(maps) for _ in range(total_cols)] for _ in range(total_rows)]
|
||||
|
||||
# Place each map onto the comparison grid
|
||||
for i, (map_arr, ref) in enumerate(zip(maps, references)):
|
||||
ref_r, ref_c = ref
|
||||
rows, cols = map_arr.shape
|
||||
|
||||
offset_r = max_up - ref_r
|
||||
offset_c = max_left - ref_c
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
# Only consider non-ignore bins
|
||||
if map_arr[r, c] != -1:
|
||||
comparison_grid[offset_r + r][offset_c + c][i] = map_arr[r, c]
|
||||
|
||||
# --- Process the grid to create a final result map ---
|
||||
# Result codes:
|
||||
# -1: No data from any map (Ignore)
|
||||
# 0: Match (Good)
|
||||
# 1: Match (NG)
|
||||
# 2: Match (Dummy)
|
||||
# 3: Mismatch (mix of Good, NG, Dummy)
|
||||
# 4: Partial data (some maps have data, others don't)
|
||||
|
||||
result_map = np.full((total_rows, total_cols), -1, dtype=int)
|
||||
|
||||
for r in range(total_rows):
|
||||
for c in range(total_cols):
|
||||
bins = [b for b in comparison_grid[r][c] if b != NO_DATA]
|
||||
|
||||
if not bins: # No map has data here
|
||||
result_map[r, c] = -1
|
||||
continue
|
||||
|
||||
if len(bins) < len(maps): # Some maps have data, others don't
|
||||
result_map[r, c] = 4
|
||||
continue
|
||||
|
||||
# All maps have data, check for match or mismatch
|
||||
first_bin = bins[0]
|
||||
is_match = all(b == first_bin for b in bins)
|
||||
|
||||
if is_match:
|
||||
if first_bin == 2: # Good
|
||||
result_map[r, c] = 0
|
||||
elif first_bin == 1: # NG
|
||||
result_map[r, c] = 1
|
||||
elif first_bin == 0: # Dummy
|
||||
result_map[r, c] = 2
|
||||
else: # Mismatch
|
||||
result_map[r, c] = 3
|
||||
|
||||
legend = {
|
||||
-1: {"label": "No Data", "color": "#3e3e42"},
|
||||
0: {"label": "Match (Good)", "color": "#28a745"},
|
||||
1: {"label": "Match (NG)", "color": "#4a90e2"}, # Different color for match NG
|
||||
2: {"label": "Match (Dummy)", "color": "#7f8c8d"},
|
||||
3: {"label": "Mismatch", "color": "#dc3545"},
|
||||
4: {"label": "Partial Data", "color": "#f39c12"}
|
||||
}
|
||||
|
||||
# Also return the detailed grid for tooltip purposes
|
||||
return result_map, legend, comparison_grid
|
||||
|
Reference in New Issue
Block a user