Welcome to Map Comparison
+Please upload two or more map files to begin.
+diff --git a/.gitignore b/.gitignore index 0297ba4..d1f578f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 649d3f1..d21e45f 100644 --- a/README.md +++ b/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 -``` +``` \ No newline at end of file diff --git a/USER_MANUAL.md b/USER_MANUAL.md new file mode 100644 index 0000000..db5c10c --- /dev/null +++ b/USER_MANUAL.md @@ -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. diff --git a/app.py b/app.py index a8495d0..e7b525d 100644 --- a/app.py +++ b/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']) diff --git a/static/css/style.css b/static/css/style.css index acf2cc5..83df5f8 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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); } -} \ No newline at end of file +} diff --git a/static/js/compare.js b/static/js/compare.js new file mode 100644 index 0000000..f568ec8 --- /dev/null +++ b/static/js/compare.js @@ -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 = ` + ${map.filename} +
Please upload two or more map files to begin.
+