Prototype > PreAlpha #67

Merged
Rusfort merged 54 commits from Dev into Stable 2025-05-15 09:10:54 +02:00
13 changed files with 1236 additions and 498 deletions
Showing only changes of commit 1ac23eb252 - Show all commits

View File

@ -6,13 +6,18 @@ This document explains how to configure the Asset Processor Tool and use presets
The tool's core settings are now stored in `config/app_settings.json`. This JSON file contains the base configuration for the application.
The `configuration.py` module is responsible for loading the settings from `app_settings.json` and merging them with the rules from the selected preset file.
The `configuration.py` module is responsible for loading the settings from `app_settings.json` (including loading and saving the JSON content), merging them with the rules from the selected preset file, and providing the base configuration via the `load_base_config()` function. Note that the old `config.py` file has been deleted.
The `app_settings.json` file is structured into several key sections, including:
* `FILE_TYPE_DEFINITIONS`: Defines known file types (like different texture maps, models, etc.) and their properties. Each definition now includes a `"standard_type"` key for aliasing to a common type and a `"bit_depth_rule"` key specifying how to handle bit depth for this file type. The separate `MAP_BIT_DEPTH_RULES` section has been removed.
* `ASSET_TYPE_DEFINITIONS`: Defines known asset types (like Surface, Model, Decal) and their properties.
* `MAP_MERGE_RULES`: Defines how multiple input maps can be merged into a single output map (e.g., combining Normal and Roughness into one).
## GUI Configuration Editor
You can modify the `app_settings.json` file using the built-in GUI editor. Access it via the **Edit** -> **Preferences...** menu.
This editor allows you to view and change the core application settings. Note that any changes made through the GUI editor require an application restart to take effect.
This editor provides a tabbed interface (e.g., "General", "Output & Naming") to view and change the core application settings defined in `app_settings.json`. Settings in the editor directly correspond to the structure and values within the JSON file. Note that any changes made through the GUI editor require an application restart to take effect.
*(Ideally, a screenshot of the GUI Configuration Editor would be included here.)*

View File

@ -21,8 +21,8 @@ python -m gui.main_window
* **Preset Selector:** Choose the preset to use for *processing* the current queue.
* **Output Directory:** Set the output path (defaults to `config/app_settings.json`, use "Browse...")
* **Drag and Drop Area:** Add asset `.zip`, `.rar`, `.7z` files, or folders by dragging and dropping them here.
* **Preview Table:** Shows queued assets. Initially, this area displays a message prompting you to select a preset. Once a preset is selected from the Preset List, the detailed file preview will load here. The mode of the preview depends on the "View" menu:
* **Detailed Preview (Default):** Lists all files, predicted status (`Mapped`, `Model`, `Extra`, `Unrecognised`, `Ignored`, `Error`), output name, etc., based on the selected *processing* preset. Text colors are applied to cells based on the status of the individual file they represent. Rows use alternating background colors per asset group for visual separation.
* **Preview Table:** Shows queued assets in a hierarchical view (Source -> Asset -> File). Initially, this area displays a message prompting you to select a preset. Once a preset is selected from the Preset List, the detailed file preview will load here. The mode of the preview depends on the "View" menu:
* **Detailed Preview (Default):** Lists all files, predicted status (`Mapped`, `Model`, `Extra`, `Unrecognised`, `Ignored`, `Error`), output name, etc., based on the selected *processing* preset. The columns displayed are: Name, Target Asset, Supplier, Asset Type, Item Type. The "Target Asset" column stretches to fill available space, while others resize to content. The previous "Status" and "Output Path" columns have been removed. Text colors are applied to cells based on the status of the individual file they represent. Rows use alternating background colors per asset group for visual separation.
* **Simple View (Preview Disabled):** Lists only top-level input asset paths.
* **Progress Bar:** Shows overall processing progress.
* **Blender Post-Processing:** Checkbox to enable Blender scripts. If enabled, shows fields and browse buttons for target `.blend` files (defaults from `config/app_settings.json`).
@ -36,7 +36,7 @@ python -m gui.main_window
## GUI Configuration Editor
Access the GUI Configuration Editor via the **Edit** -> **Preferences...** menu. This dialog allows you to directly edit the `config/app_settings.json` file, which contains the core application settings.
Access the GUI Configuration Editor via the **Edit** -> **Preferences...** menu. This dialog allows you to directly edit the `config/app_settings.json` file, which contains the core application settings. The editor uses a tabbed layout (e.g., "General", "Output & Naming") to organize settings.
Any changes made in the GUI Configuration Editor require you to restart the application for them to take effect.

View File

@ -27,16 +27,16 @@ This hierarchy allows for fine-grained control over processing parameters. The G
## Core Components
* `config.py`: Defines core, global settings, constants, and centralized definitions for allowed asset and file types (`ASSET_TYPE_DEFINITIONS`, `FILE_TYPE_DEFINITIONS`), including metadata like colors and descriptions.
* `config/app_settings.json`: Defines core, global settings, constants, and centralized definitions for allowed asset and file types (`ASSET_TYPE_DEFINITIONS`, `FILE_TYPE_DEFINITIONS`), including metadata like colors and descriptions. This replaces the old `config.py` file.
* `config/suppliers.json`: A persistent JSON file storing known supplier names for GUI auto-completion.
* `Presets/*.json`: Supplier-specific JSON files defining rules for file interpretation and initial prediction.
* `configuration.py` (`Configuration` class): Loads `config.py` settings and merges them with a selected preset, pre-compiling regex patterns for efficiency. This static configuration is used by the processing engine.
* `configuration.py` (`Configuration` class): Loads `config/app_settings.json` settings and merges them with a selected preset, pre-compiling regex patterns for efficiency. This static configuration is used by the processing engine.
* `rule_structure.py`: Defines the `SourceRule`, `AssetRule`, and `FileRule` dataclasses used to represent the hierarchical processing rules.
* `gui/`: Directory containing modules for the Graphical User Interface (GUI), built with PySide6. The GUI is responsible for generating and managing the `SourceRule` hierarchy via the Unified View, accumulating prediction results, and interacting with background handlers (`ProcessingHandler`, `PredictionHandler`).
* `unified_view_model.py`: Implements the `QAbstractItemModel` for the Unified Hierarchical View, holding the `SourceRule` data, handling inline editing (including direct model restructuring for `target_asset_name_override`), and managing row coloring based on config definitions.
* `delegates.py`: Contains custom `QStyledItemDelegate` implementations for inline editing in the Unified View, including the new `SupplierSearchDelegate` for supplier name auto-completion and management.
* `prediction_handler.py`: Generates the initial `SourceRule` hierarchy with predicted values for a single input source based on its files and the selected preset.
* `processing_engine.py` (`ProcessingEngine` class): The new core component that executes the processing pipeline for a single `SourceRule` object using the static `Configuration`. It contains no internal prediction or fallback logic.
* `prediction_handler.py`: Generates the initial `SourceRule` hierarchy with predicted values for a single input source based on its files and the selected preset. It uses the `"standard_type"` from the configuration's `FILE_TYPE_DEFINITIONS` to populate `FileRule.standard_map_type` and implements a two-pass classification logic to handle and prioritize bit-depth variants (e.g., `_DISP16_` vs `_DISP_`).
* `processing_engine.py` (`ProcessingEngine` class): The new core component that executes the processing pipeline for a single `SourceRule` object using the static `Configuration`. A new instance is created per task for state isolation. It contains no internal prediction or fallback logic. Supplier overrides from the GUI are correctly preserved and used by the engine for output path generation and metadata.
* `asset_processor.py` (`AssetProcessor` class): The older processing engine, kept for reference but not used in the main processing flow.
* `main.py`: The entry point for the Command-Line Interface (CLI). It handles argument parsing, logging, parallel processing orchestration, and triggering Blender scripts. It now orchestrates processing by passing `SourceRule` objects to the `ProcessingEngine`.
* `monitor.py`: Implements the directory monitoring feature using `watchdog`.

View File

@ -6,10 +6,10 @@ This document provides technical details about the configuration system and the
The tool utilizes a two-tiered configuration system managed by the `configuration.py` module:
1. **Application Settings (`config/app_settings.json`):** This JSON file defines the core global default settings, constants, and rules that apply generally across different asset sources. Examples include default output paths, standard image resolutions, map merge rules, output format rules, Blender executable paths, and default map types. It also centrally defines metadata for allowed asset and file types.
1. **Application Settings (`config/app_settings.json`):** This JSON file defines the core global default settings, constants, and rules that apply generally across different asset sources. Examples include default output paths, standard image resolutions, map merge rules, output format rules, Blender executable paths, and default map types. It also centrally defines metadata for allowed asset and file types. Key sections include `FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`, and `MAP_MERGE_RULES`.
2. **Preset Files (`Presets/*.json`):** These JSON files define supplier-specific rules and overrides. They contain patterns (often regular expressions) to interpret filenames, classify map types, handle variants, define naming conventions, and specify other source-specific behaviors.
The `configuration.py` module is responsible for loading the base settings from `config/app_settings.json` and then merging them with the rules from the selected preset file. Preset values generally override core settings where applicable.
The `configuration.py` module is responsible for loading the base settings from `config/app_settings.json` (including loading and saving the JSON content), merging them with the rules from the selected preset file, and providing the base configuration via the `load_base_config()` function. Preset values generally override core settings where applicable. Note that the old `config.py` file has been deleted.
## Supplier Management (`config/suppliers.json`)
@ -37,7 +37,8 @@ An instance of `Configuration` is created within each worker process (`main.proc
The GUI includes a dedicated editor for modifying the `config/app_settings.json` file. This is implemented in `gui/config_editor_dialog.py`.
* **Purpose:** Provides a user-friendly interface for viewing and editing the core application settings defined in `app_settings.json`.
* **Implementation:** The dialog loads the JSON content of `app_settings.json`, presents it in an editable format (likely using standard GUI widgets mapped to the JSON structure), and saves the changes back to the file.
* **Implementation:** The dialog loads the JSON content of `app_settings.json`, presents it in a tabbed layout ("General", "Output & Naming", etc.) using standard GUI widgets mapped to the JSON structure, and saves the changes back to the file. It supports editing basic fields, tables for definitions (`FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`), and a list/detail view for merge rules (`MAP_MERGE_RULES`). The definitions tables include dynamic color editing features.
* **Limitations:** Currently, editing complex fields like `IMAGE_RESOLUTIONS` or the full details of `MAP_MERGE_RULES` via the UI is not fully supported.
* **Note:** Changes made through the GUI editor are written directly to `config/app_settings.json` but require an application restart to be loaded and applied by the `Configuration` class.
## Preset File Structure (`Presets/*.json`)

View File

@ -1,6 +1,6 @@
# Developer Guide: Processing Pipeline
This document details the step-by-step technical process executed by the `ProcessingEngine` class (`processing_engine.py`) when processing a single asset.
This document details the step-by-step technical process executed by the `ProcessingEngine` class (`processing_engine.py`) when processing a single asset. A new instance of `ProcessingEngine` is created for each processing task to ensure state isolation.
The `ProcessingEngine.process()` method orchestrates the following pipeline based *solely* on the provided `SourceRule` object and the static `Configuration` object passed during engine initialization. It contains no internal prediction, classification, or fallback logic. All necessary overrides and static configuration values are accessed directly from these inputs.
@ -21,7 +21,7 @@ The pipeline steps are:
* Sorts potential map variants based on the order provided in the `SourceRule` or static configuration.
4. **Base Metadata Determination (`_determine_base_metadata`, `_determine_single_asset_metadata`)**:
* Determines the base asset name, category, and archetype using the explicit values provided in the input `SourceRule` object and the static configuration from the `Configuration` object. Overrides (like `supplier_identifier`, `asset_type`, and `asset_name_override`) are taken directly from the `SourceRule`.
* Determines the base asset name, category, and archetype using the explicit values provided in the input `SourceRule` object and the static configuration from the `Configuration` object. Overrides (like `supplier_identifier`, `asset_type`, and `asset_name_override`), including supplier overrides from the GUI, are taken directly from the `SourceRule`.
5. **Skip Check**:
* If the `overwrite` flag (passed during initialization) is `False`, the tool checks if the final output directory for the determined asset name already exists and contains a `metadata.json` file.
@ -57,7 +57,7 @@ The pipeline steps are:
* Writes this collected data into the `metadata.json` file within the temporary workspace using `json.dump`.
9. **Output Organization (`_organize_output_files`)**:
* Creates the final structured output directory: `<output_base_dir>/<supplier_name>/<asset_name>/`.
* Creates the final structured output directory: `<output_base_dir>/<supplier_name>/<asset_name>/`. The `supplier_name` used here is derived from the `SourceRule`, ensuring that supplier overrides from the GUI are respected in the output path.
* Creates subdirectories `Extra/`, `Unrecognised/`, and `Ignored/` within the asset directory.
* Moves the processed maps, merged maps, model files, `metadata.json`, and files classified as Extra, Unrecognised, or Ignored from the temporary workspace into their respective locations in the final output directory structure.

View File

@ -40,12 +40,15 @@ The GUI includes an integrated preset editor panel. This allows users to interac
## Unified Hierarchical View (`gui/unified_view_model.py`, `gui/delegates.py`)
The core of the GUI's rule editing interface is the Unified Hierarchical View, implemented using a `QTreeView` with a custom model and delegates.
## Unified Hierarchical View (`gui/unified_view_model.py`, `gui/delegates.py`, `gui/main_window.py`)
The core of the GUI's rule editing interface is the Unified Hierarchical View, implemented using a `QTreeView` with a custom model and delegates. This view is managed within the `MainWindow`.
* **`Unified View Model` (`gui/unified_view_model.py`):** This class implements a `QAbstractItemModel` to expose the structure of a list of `SourceRule` objects (Source -> Asset -> File) to the `QTreeView`. It holds the `SourceRule` data that is the single source of truth for the GUI's processing rules. It provides data and flags for display in multiple columns and supports inline editing of specific rule attributes (e.g., asset type, item type override, target asset name override) by interacting with delegates.
* **Column Order and Resizing:** The view currently displays the following columns in order: Name, Target Asset, Supplier, Asset Type, Item Type. The "Target Asset" column is set to stretch to fill available space, while other columns resize to their contents. The previous "Status" and "Output Path" columns have been removed.
* **Direct Model Restructuring:** The `setData` method now includes logic to directly restructure the underlying `SourceRule` hierarchy when the `target_asset_name_override` field of a `FileRule` is edited. This involves moving the `FileRule` to a different `AssetRule` (creating a new one if necessary) and removing the old `AssetRule` if it becomes empty. This replaces the previous mechanism of re-running prediction after an edit.
* **Row Coloring:** Row background colors are dynamically determined based on the `asset_type` (for `AssetRule`s) and `item_type` or `item_type_override` (for `FileRule`s), using the color metadata defined in the `ASSET_TYPE_DEFINITIONS` and `FILE_TYPE_DEFINITIONS` dictionaries sourced from the configuration loaded by `configuration.py` (which includes data from `config/app_settings.json`). `SourceRule` rows have a fixed color.
* **`Delegates` (`gui/delegates.py`):** This module contains custom `QStyledItemDelegate` implementations used by the `QTreeView` to provide inline editors for specific data types or rule attributes.
* **`Delegates` (`gui/delegates.py`):** This module contains custom `QStyledItemDelegate` implementations used by the `QTreeView` to provide inline editors for specific data types or rule attributes.
* **`ComboBoxDelegate`:** Used for selecting from predefined lists (e.g., allowed asset types, allowed file types sourced from the configuration loaded by `configuration.py`).
* **`LineEditDelegate`:** Used for free-form text editing (e.g., target asset name override).
* **`SupplierSearchDelegate`:** A new delegate used for the "Supplier" column. It provides a `QLineEdit` with auto-completion suggestions loaded from `config/suppliers.json`. It also handles adding new, unique supplier names entered by the user to the list and saving the updated list back to the JSON file.
@ -84,7 +87,8 @@ The GUI provides a "Cancel" button to stop ongoing processing. The `ProcessingHa
A dedicated dialog, implemented in `gui/config_editor_dialog.py`, provides a graphical interface for editing the core application settings stored in `config/app_settings.json`.
* **Functionality:** This dialog loads the current content of `config/app_settings.json`, presents it in an editable format (likely using standard Qt widgets), and allows the user to save modifications back to the file.
* **Functionality:** This dialog loads the current content of `config/app_settings.json` and presents it in a tabbed layout (e.g., "General", "Output & Naming") using standard GUI widgets mapped to the JSON structure. It supports editing basic fields, tables for definitions (`FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`), and a list/detail view for merge rules (`MAP_MERGE_RULES`). The definitions tables include dynamic color editing features.
* **Limitations:** Currently, editing complex fields like `IMAGE_RESOLUTIONS` or the full details of `MAP_MERGE_RULES` via the UI is not fully supported.
* **Integration:** The `MainWindow` is responsible for creating and displaying an instance of this dialog when the user selects the "Edit" -> "Preferences..." menu option.
* **Persistence:** Changes saved via this editor are written directly to the `config/app_settings.json` file, ensuring they persist across application sessions. However, the `Configuration` class loads settings at application startup, so a restart is required for changes made in the editor to take effect in the application's processing logic.

View File

@ -0,0 +1,52 @@
3. Tab Breakdown and Widget Specifications:
Tab 1: General
OUTPUT_BASE_DIR: QLineEdit + QPushButton (opens QFileDialog.getExistingDirectory). Label: "Output Base Directory".
EXTRA_FILES_SUBDIR: QLineEdit. Label: "Subdirectory for Extra Files".
METADATA_FILENAME: QLineEdit. Label: "Metadata Filename".
Tab 2: Output & Naming
TARGET_FILENAME_PATTERN: QLineEdit. Label: "Output Filename Pattern". (Tooltip explaining placeholders recommended).
STANDARD_MAP_TYPES: QListWidget + "Add"/"Remove" QPushButtons. Label: "Standard Map Types".
RESPECT_VARIANT_MAP_TYPES: QLineEdit. Label: "Map Types Respecting Variants (comma-separated)".
ASPECT_RATIO_DECIMALS: QSpinBox (Min: 0, Max: ~6). Label: "Aspect Ratio Precision (Decimals)".
Tab 3: Image Processing
IMAGE_RESOLUTIONS: QTableWidget (Columns: "Name", "Resolution (px)") + "Add Row"/"Remove Row" QPushButtons. Label: "Defined Image Resolutions".
CALCULATE_STATS_RESOLUTION: QComboBox (populated from IMAGE_RESOLUTIONS keys). Label: "Resolution for Stats Calculation".
PNG_COMPRESSION_LEVEL: QSpinBox (Range: 0-9). Label: "PNG Compression Level".
JPG_QUALITY: QSpinBox (Range: 1-100). Label: "JPG Quality".
RESOLUTION_THRESHOLD_FOR_JPG: QComboBox (populated from IMAGE_RESOLUTIONS keys + "Never"/"Always"). Label: "Use JPG Above Resolution".
OUTPUT_FORMAT_8BIT: QComboBox (Options: "png", "jpg"). Label: "Output Format (8-bit)".
OUTPUT_FORMAT_16BIT_PRIMARY: QComboBox (Options: "png", "exr", "tif"). Label: "Primary Output Format (16-bit+)".
OUTPUT_FORMAT_16BIT_FALLBACK: QComboBox (Options: "png", "exr", "tif"). Label: "Fallback Output Format (16-bit+)".
Tab 4: Definitions (Overall QVBoxLayout)
Top Widget: DEFAULT_ASSET_CATEGORY: QComboBox (populated dynamically from Asset Types table below). Label: "Default Asset Category".
Bottom Widget: Inner QTabWidget:
Inner Tab 1: Asset Types
ASSET_TYPE_DEFINITIONS: QTableWidget (Columns: "Type Name", "Description", "Color", "Examples (comma-sep.)") + "Add Row"/"Remove Row" QPushButtons.
"Color" cell: QPushButton opening QColorDialog, button background shows color. Use QStyledItemDelegate.
"Examples" cell: Editable QLineEdit.
Inner Tab 2: File Types
FILE_TYPE_DEFINITIONS: QTableWidget (Columns: "Type ID", "Description", "Color", "Examples (comma-sep.)", "Standard Type", "Bit Depth Rule") + "Add Row"/"Remove Row" QPushButtons.
"Color" cell: QPushButton opening QColorDialog. Use QStyledItemDelegate.
"Examples" cell: Editable QLineEdit.
"Standard Type" cell: QComboBox (populated from STANDARD_MAP_TYPES + empty option). Use QStyledItemDelegate.
"Bit Depth Rule" cell: QComboBox (Options: "respect", "force_8bit"). Use QStyledItemDelegate.
Tab 5: Map Merging
Layout: QHBoxLayout.
Left Side: QListWidget displaying output_map_type for each rule. "Add Rule"/"Remove Rule" QPushButtons below. Label: "Merge Rules".
Right Side: QStackedWidget or dynamically populated QWidget showing details for the selected rule.
Rule Detail Form:
output_map_type: QLineEdit. Label: "Output Map Type Name".
inputs: QTableWidget (Fixed Rows: R, G, B, A. Columns: "Channel", "Input Map Type"). Label: "Channel Inputs". "Input Map Type" cell: QComboBox (populated from STANDARD_MAP_TYPES).
defaults: QTableWidget (Fixed Rows: R, G, B, A. Columns: "Channel", "Default Value"). Label: "Channel Defaults (if input missing)". "Default Value" cell: QDoubleSpinBox (Range: 0.0 - 1.0).
output_bit_depth: QComboBox (Options: "respect_inputs", "force_8bit", "force_16bit"). Label: "Output Bit Depth".
Tab 6: Postprocess Scripts
DEFAULT_NODEGROUP_BLEND_PATH: QLineEdit + QPushButton (opens QFileDialog.getOpenFileName, filter: "*.blend"). Label: "Default Node Group Library (.blend)".
DEFAULT_MATERIALS_BLEND_PATH: QLineEdit + QPushButton (opens QFileDialog.getOpenFileName, filter: "*.blend"). Label: "Default Materials Library (.blend)".
BLENDER_EXECUTABLE_PATH: QLineEdit + QPushButton (opens QFileDialog.getOpenFileName). Label: "Blender Executable Path".

View File

@ -2,7 +2,7 @@
"ASSET_TYPE_DEFINITIONS": {
"Surface": {
"description": "Standard PBR material set for a surface.",
"color": "#87CEEB",
"color": "#1f3e5d",
"examples": [
"WoodFloor01",
"MetalPlate05"
@ -44,7 +44,7 @@
"FILE_TYPE_DEFINITIONS": {
"MAP_COL": {
"description": "Color/Albedo Map",
"color": "#FFFFE0",
"color": "#3d3021",
"examples": [
"_col.",
"_basecolor."
@ -54,7 +54,7 @@
},
"MAP_NRM": {
"description": "Normal Map",
"color": "#E6E6FA",
"color": "#23263d",
"examples": [
"_nrm.",
"_normal."
@ -64,7 +64,7 @@
},
"MAP_METAL": {
"description": "Metalness Map",
"color": "#C0C0C0",
"color": "#1f1f1f",
"examples": [
"_metal.",
"_met."
@ -74,7 +74,7 @@
},
"MAP_ROUGH": {
"description": "Roughness Map",
"color": "#A0522D",
"color": "#3d1f11",
"examples": [
"_rough.",
"_rgh."
@ -84,7 +84,7 @@
},
"MAP_AO": {
"description": "Ambient Occlusion Map",
"color": "#A9A9A9",
"color": "#3d3d3d",
"examples": [
"_ao.",
"_ambientocclusion."
@ -94,7 +94,7 @@
},
"MAP_DISP": {
"description": "Displacement/Height Map",
"color": "#FFB6C1",
"color": "#35343d",
"examples": [
"_disp.",
"_height."
@ -104,7 +104,7 @@
},
"MAP_REFL": {
"description": "Reflection/Specular Map",
"color": "#E0FFFF",
"color": "#363d3d",
"examples": [
"_refl.",
"_specular."
@ -114,7 +114,7 @@
},
"MAP_SSS": {
"description": "Subsurface Scattering Map",
"color": "#FFDAB9",
"color": "#3d342c",
"examples": [
"_sss.",
"_subsurface."
@ -124,7 +124,7 @@
},
"MAP_FUZZ": {
"description": "Fuzz/Sheen Map",
"color": "#FFA07A",
"color": "#3d261d",
"examples": [
"_fuzz.",
"_sheen."
@ -134,7 +134,7 @@
},
"MAP_IDMAP": {
"description": "ID Map (for masking)",
"color": "#F08080",
"color": "#3d2121",
"examples": [
"_id.",
"_matid."
@ -144,7 +144,7 @@
},
"MAP_MASK": {
"description": "Generic Mask Map",
"color": "#FFFFFF",
"color": "#3d3d3d",
"examples": [
"_mask."
],
@ -153,7 +153,7 @@
},
"MAP_IMPERFECTION": {
"description": "Imperfection Map (scratches, dust)",
"color": "#F0E68C",
"color": "#3d3a24",
"examples": [
"_imp.",
"_imperfection."
@ -163,30 +163,33 @@
},
"MODEL": {
"description": "3D Model File",
"color": "#FFA500",
"color": "#3d2700",
"examples": [
".fbx",
".obj"
],
"bit_depth_rule": "respect"
"standard_type": "",
"bit_depth_rule": ""
},
"EXTRA": {
"description": "Non-standard/Unclassified File",
"color": "#778899",
"color": "#2f363d",
"examples": [
".txt",
".zip"
],
"bit_depth_rule": "respect"
"standard_type": "",
"bit_depth_rule": ""
},
"FILE_IGNORE": {
"description": "File to be ignored",
"color": "#2F4F4F",
"color": "#243d3d",
"examples": [
"Thumbs.db",
".DS_Store"
],
"bit_depth_rule": "respect"
"standard_type": "",
"bit_depth_rule": ""
}
},
"TARGET_FILENAME_PATTERN": "{base_name}_{map_type}_{resolution}.{ext}",
@ -203,7 +206,9 @@
"IDMAP",
"MASK"
],
"RESPECT_VARIANT_MAP_TYPES": "COL",
"RESPECT_VARIANT_MAP_TYPES": [
"COL"
],
"EXTRA_FILES_SUBDIR": "Extra",
"OUTPUT_BASE_DIR": "../Asset_Processor_Output",
"METADATA_FILENAME": "metadata.json",
@ -220,7 +225,7 @@
"1K": 1024
},
"ASPECT_RATIO_DECIMALS": 2,
"OUTPUT_FORMAT_16BIT_PRIMARY": "png",
"OUTPUT_FORMAT_16BIT_PRIMARY": "exr",
"OUTPUT_FORMAT_16BIT_FALLBACK": "png",
"OUTPUT_FORMAT_8BIT": "png",
"MAP_MERGE_RULES": [

File diff suppressed because it is too large Load Diff

View File

@ -425,13 +425,19 @@ class MainWindow(QMainWindow):
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, comboBoxDelegate)
# Configure View Appearance (optional, customize as needed)
self.unified_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # Expand horizontally and vertically
self.unified_view.setAlternatingRowColors(True)
self.unified_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.unified_view.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)
self.unified_view.header().setStretchLastSection(False) # Adjust as needed
# Set the "Name" column (index 0) to resize to contents
self.unified_view.header().setSectionResizeMode(UnifiedViewModel.COL_NAME, QHeaderView.ResizeMode.ResizeToContents)
# self.unified_view.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Example: Resize others to contents
# Configure Header Resize Modes based on new column order
header = self.unified_view.header()
header.setStretchLastSection(False) # Don't stretch the last section by default
header.setSectionResizeMode(UnifiedViewModel.COL_NAME, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(UnifiedViewModel.COL_TARGET_ASSET, QHeaderView.ResizeMode.Stretch) # Stretch Target Asset
header.setSectionResizeMode(UnifiedViewModel.COL_SUPPLIER, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(UnifiedViewModel.COL_ASSET_TYPE, QHeaderView.ResizeMode.ResizeToContents)
header.setSectionResizeMode(UnifiedViewModel.COL_ITEM_TYPE, QHeaderView.ResizeMode.ResizeToContents)
# Add the Unified View to the main layout
main_layout.addWidget(self.unified_view, 1) # Give it stretch factor 1
@ -1613,80 +1619,129 @@ class MainWindow(QMainWindow):
@Slot(list)
def _on_rule_hierarchy_ready(self, source_rules_list: list):
"""Receives prediction results (a list containing one SourceRule) for a single input path,
accumulates them, and updates the model when all are ready."""
finds the corresponding existing rule in the model, updates it while preserving overrides,
and emits dataChanged/layoutChanged signals."""
# --- Extract input_path from the received rule ---
# --- Extract input_path and the new rule from the received list ---
input_path = None
source_rule = None
new_source_rule = None
if source_rules_list and isinstance(source_rules_list[0], SourceRule):
source_rule = source_rules_list[0]
input_path = source_rule.input_path
log.debug(f"--> Entered _on_rule_hierarchy_ready for '{input_path}' with {len(source_rules_list)} SourceRule(s)")
new_source_rule = source_rules_list[0]
input_path = new_source_rule.input_path
log.debug(f"--> Entered _on_rule_hierarchy_ready for '{input_path}' with 1 SourceRule")
elif source_rules_list:
log.error(f"Received non-SourceRule object in list: {type(source_rules_list[0])}. Cannot process.")
# Attempt to find which pending prediction this might correspond to? Difficult.
# For now, we can't reliably remove from pending without the path.
return
else:
# This case might happen if prediction failed critically before creating a rule.
# The prediction_finished signal (which now includes input_path) should handle removing from pending.
log.warning("Received empty source_rules_list in _on_rule_hierarchy_ready. Prediction likely failed.")
return # Nothing to accumulate
# Try to deduce input_path if possible (e.g., if only one is pending)
if len(self._pending_predictions) == 1:
input_path = list(self._pending_predictions)[0]
log.warning(f"Assuming failed prediction corresponds to pending path: {input_path}")
else:
log.error("Cannot determine input_path for empty/failed prediction result when multiple predictions are pending.")
return
if input_path is None:
log.error("Could not determine input_path from received source_rules_list. Aborting accumulation.")
log.error("Could not determine input_path from received source_rules_list or pending state.")
return
# Log received rule details (even if it's None due to failure)
log.debug(f"_on_rule_hierarchy_ready: Processing result for '{input_path}'. Received Supplier ID='{getattr(new_source_rule, 'supplier_identifier', 'N/A')}', Received Override='{getattr(new_source_rule, 'supplier_override', 'N/A')}'")
if input_path not in self._pending_predictions:
log.warning(f"Received rule hierarchy for '{input_path}', but it was not in the pending set. Ignoring stale result? Pending: {self._pending_predictions}")
return # Ignore if not expected
# --- Accumulate Result ---
if source_rule: # Check if we successfully got the rule object
self._accumulated_rules[input_path] = source_rule
log.debug(f"Accumulated rule for '{input_path}'. Total accumulated: {len(self._accumulated_rules)}")
else:
# This path is already handled by the initial checks, but log just in case.
log.warning(f"No valid SourceRule found for '{input_path}' to accumulate.")
# --- Find existing rule in the model's internal list ---
# Access the model directly
existing_rule = None
existing_rule_index = -1
model_rules = self.unified_model.get_all_source_rules() # Get current rules from model
for i, rule in enumerate(model_rules):
if rule.input_path == input_path:
existing_rule = rule
existing_rule_index = i
break
# --- Mark as Completed ---
if existing_rule:
log.debug(f"Found existing rule for '{input_path}' in model at index {existing_rule_index}. Updating it.")
if new_source_rule: # Only update if prediction was successful
# Preserve existing user overrides from the rule currently in the model
preserved_supplier_override = existing_rule.supplier_override
# Preserve other potential user overrides if they exist
preserved_asset_overrides = {asset.asset_name: asset.asset_type_override for asset in existing_rule.assets}
preserved_file_overrides = {(file.file_path, 'target'): file.target_asset_name_override for asset in existing_rule.assets for file in asset.files}
preserved_file_overrides.update({(file.file_path, 'item'): file.item_type_override for asset in existing_rule.assets for file in asset.files})
# --- Update existing rule with new prediction data ---
existing_rule.supplier_identifier = new_source_rule.supplier_identifier
existing_rule.preset_name = new_source_rule.preset_name
existing_rule.assets = new_source_rule.assets # Replace assets list
# Re-apply preserved overrides
existing_rule.supplier_override = preserved_supplier_override
for asset in existing_rule.assets:
asset.asset_type_override = preserved_asset_overrides.get(asset.asset_name)
asset.parent_source = existing_rule # Set parent reference
for file in asset.files:
file.target_asset_name_override = preserved_file_overrides.get((file.file_path, 'target'))
file.item_type_override = preserved_file_overrides.get((file.file_path, 'item'))
file.parent_asset = asset # Set parent reference
# --- End Update ---
# Emit dataChanged and layoutChanged for the updated existing rule via the model
start_index = self.unified_model.createIndex(existing_rule_index, 0, existing_rule)
end_index = self.unified_model.createIndex(existing_rule_index, self.unified_model.columnCount() - 1, existing_rule)
log.debug(f"Emitting dataChanged and layoutChanged for updated existing rule at index {existing_rule_index}")
self.unified_model.dataChanged.emit(start_index, end_index)
self.unified_model.layoutChanged.emit() # Signal layout change
else:
log.warning(f"Prediction failed for '{input_path}'. Not updating existing rule data in model.")
else:
# If no existing rule was found (e.g., first time result arrives)
if new_source_rule: # Only add if prediction was successful
log.debug(f"No existing rule found for '{input_path}'. Adding new rule to model.")
# Ensure parent references are set
for asset_rule in new_source_rule.assets:
asset_rule.parent_source = new_source_rule
for file_rule in asset_rule.files:
file_rule.parent_asset = asset_rule
# Add to model's internal list and emit signal via model methods
self.unified_model.beginInsertRows(QModelIndex(), len(model_rules), len(model_rules))
model_rules.append(new_source_rule) # Append to the list obtained from the model
self.unified_model.endInsertRows()
else:
log.warning(f"Prediction failed for '{input_path}' and no existing rule found. Nothing to add to model.")
# --- Remove from pending ---
self._pending_predictions.discard(input_path)
log.debug(f"Removed '{input_path}' from pending predictions. Remaining: {self._pending_predictions}")
log.debug(f"Removed '{input_path}' from pending predictions. Remaining: {len(self._pending_predictions)} -> {self._pending_predictions}")
# --- Check for Completion ---
if not self._pending_predictions:
log.info("All pending predictions received. Finalizing model update.")
self._finalize_model_update()
log.info("All pending predictions processed. Model should be up-to-date.")
self.statusBar().showMessage(f"Preview complete.", 5000) # Update status
# Optional: Resize columns after all updates are done
for col in range(self.unified_model.columnCount()):
self.unified_view.resizeColumnToContents(col)
self.unified_view.expandToDepth(1) # Expand Source -> Asset level
else:
# Update status bar with progress
completed_count = len(self._accumulated_rules)
completed_count = len(self.unified_model.get_all_source_rules()) # Count rules in model
pending_count = len(self._pending_predictions)
# total_count = completed_count + pending_count # This might be slightly off if some failed without rules
# We don't have the total count of *requested* predictions here easily,
# but we can use the initial number of items added.
total_requested = len(self.current_asset_paths) # Use the total number of items added
status_msg = f"Preview finished for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_requested} requested)..."
total_requested = completed_count + pending_count # Estimate total
status_msg = f"Preview updated for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_requested} processed)..."
self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg)
def _finalize_model_update(self):
"""Combines accumulated rules and updates the UI model and view."""
log.debug("Entering _finalize_model_update")
final_rules = list(self._accumulated_rules.values())
log.info(f"Finalizing model with {len(final_rules)} accumulated SourceRule(s).")
# Load the FINAL LIST of data into the UnifiedViewModel
self.unified_model.load_data(final_rules)
log.debug("Unified view model updated with final list of SourceRules.")
# Resize columns to fit content after loading data
for col in range(self.unified_model.columnCount()):
self.unified_view.resizeColumnToContents(col)
log.debug("Unified view columns resized to contents.")
self.unified_view.expandToDepth(1) # Expand Source -> Asset level
self.statusBar().showMessage(f"Preview complete for {len(final_rules)} asset(s).", 5000)
# REMOVED _finalize_model_update method as it's no longer needed
# def _finalize_model_update(self):
# """Combines accumulated rules and updates the UI model and view."""
# ... (old code removed) ...
# --- Main Execution ---

View File

@ -18,14 +18,14 @@ class UnifiedViewModel(QAbstractItemModel):
of SourceRule -> AssetRule -> FileRule.
"""
Columns = [
"Name", "Supplier", "Asset Type",
"Target Asset", "Item Type"
"Name", "Target Asset", "Supplier",
"Asset Type", "Item Type"
]
COL_NAME = 0
COL_SUPPLIER = 1
COL_ASSET_TYPE = 2
COL_TARGET_ASSET = 3
COL_TARGET_ASSET = 1
COL_SUPPLIER = 2
COL_ASSET_TYPE = 3
COL_ITEM_TYPE = 4
# COL_STATUS = 5 # Removed
# COL_OUTPUT_PATH = 6 # Removed
@ -242,7 +242,7 @@ class UnifiedViewModel(QAbstractItemModel):
if isinstance(item, SourceRule):
if role == Qt.DisplayRole or role == Qt.EditRole: # Combine Display and Edit logic
if column == self.COL_NAME:
return Path(item.input_path).name # Display only basename for SourceRule
return Path(item.input_path).name
elif column == self.COL_SUPPLIER:
# Return override if set, otherwise the original identifier, else empty string
display_value = item.supplier_override if item.supplier_override is not None else item.supplier_identifier
@ -253,21 +253,21 @@ class UnifiedViewModel(QAbstractItemModel):
elif isinstance(item, AssetRule):
if role == Qt.DisplayRole:
if column == self.COL_NAME: return item.asset_name
if column == self.COL_ASSET_TYPE:
elif column == self.COL_ASSET_TYPE:
display_value = item.asset_type_override if item.asset_type_override is not None else item.asset_type
return display_value if display_value else ""
# Removed Status and Output Path columns
elif role == Qt.EditRole:
if column == self.COL_ASSET_TYPE:
return item.asset_type_override # Return string or None
return item.asset_type_override
return None # Default for AssetRule
elif isinstance(item, FileRule):
if role == Qt.DisplayRole:
if column == self.COL_NAME: return Path(item.file_path).name # Display only filename
if column == self.COL_TARGET_ASSET:
elif column == self.COL_TARGET_ASSET:
return item.target_asset_name_override if item.target_asset_name_override is not None else ""
if column == self.COL_ITEM_TYPE:
elif column == self.COL_ITEM_TYPE:
# Reverted Logic: Display override if set, otherwise base type. Shows prefixed keys.
override = item.item_type_override
initial_type = item.item_type
@ -278,8 +278,8 @@ class UnifiedViewModel(QAbstractItemModel):
return initial_type if initial_type else ""
# Removed Status and Output Path columns
elif role == Qt.EditRole:
if column == self.COL_TARGET_ASSET: return item.target_asset_name_override if item.target_asset_name_override is not None else ""
if column == self.COL_ITEM_TYPE: return item.item_type_override # Return string or None
if column == self.COL_TARGET_ASSET: return item.target_asset_name_override if item.target_asset_name_override is not None else "" # Return string or ""
elif column == self.COL_ITEM_TYPE: return item.item_type_override # Return string or None
return None # Default for FileRule
return None # Default return if role/item combination not handled
@ -299,6 +299,7 @@ class UnifiedViewModel(QAbstractItemModel):
if isinstance(item, SourceRule): # If SourceRule is editable
if column == self.COL_SUPPLIER:
# Get the new value, strip whitespace, treat empty as None
log.debug(f"setData COL_SUPPLIER: Index=({index.row()},{column}), Value='{value}', Type={type(value)}") # <-- ADDED LOGGING (Corrected Indentation)
new_value = str(value).strip() if value is not None and str(value).strip() else None
# Get the original identifier (assuming it exists on SourceRule)
@ -516,12 +517,12 @@ class UnifiedViewModel(QAbstractItemModel):
can_edit = False
# Determine editability based on item type and column
if isinstance(item, SourceRule): # If SourceRule is displayed/editable
if column == 1: can_edit = True
if column == self.COL_SUPPLIER: can_edit = True # Supplier is editable
elif isinstance(item, AssetRule):
if column == 2: can_edit = True
if column == self.COL_ASSET_TYPE: can_edit = True # Asset Type is editable
elif isinstance(item, FileRule):
if column == 3: can_edit = True
if column == 4: can_edit = True
if column == self.COL_TARGET_ASSET: can_edit = True # Target Asset is editable
if column == self.COL_ITEM_TYPE: can_edit = True # Item Type is editable
if can_edit:
return default_flags | Qt.ItemIsEditable
@ -545,4 +546,5 @@ class UnifiedViewModel(QAbstractItemModel):
item = index.internalPointer()
if item: # Ensure internal pointer is not None
return item
return None # Return None for invalid index or None pointer
return None # Return None for invalid index or None pointer

42
main.py
View File

@ -410,10 +410,48 @@ class App(QObject):
log.debug("DEBUG: Entering task queuing loop.") # <-- Keep this log
for i, rule in enumerate(source_rules): # Added enumerate for index logging
if isinstance(rule, SourceRule):
log.info(f"DEBUG Task {i+1}: Rule Input='{rule.input_path}', Supplier ID='{getattr(rule, 'supplier_identifier', 'Not Set')}', Preset='{getattr(rule, 'preset_name', 'Not Set')}'") # <-- ADDED LOGGING (Corrected Indentation)
log.debug(f"DEBUG: Preparing to queue task {i+1}/{len(source_rules)} for rule: {rule.input_path}") # <-- Keep this log
# Pass the required paths to the ProcessingTask constructor
# --- Create a new Configuration and Engine instance for this specific task ---
task_engine = None
try:
# Get preset name from the rule, fallback to app's default if missing
preset_name_for_task = getattr(rule, 'preset_name', None)
if not preset_name_for_task:
log.warning(f"Task {i+1} (Rule: {rule.input_path}): SourceRule missing preset_name. Falling back to default preset '{self.config_obj.preset_name}'.")
preset_name_for_task = self.config_obj.preset_name # Use the initially loaded default
# Load the specific configuration for this task's preset
task_config = Configuration(preset_name=preset_name_for_task)
task_engine = ProcessingEngine(task_config)
log.debug(f"Task {i+1}: Created new ProcessingEngine instance with preset '{preset_name_for_task}'.")
except ConfigurationError as config_err:
log.error(f"Task {i+1} (Rule: {rule.input_path}): Failed to load configuration for preset '{preset_name_for_task}': {config_err}. Skipping task.")
self._active_tasks_count -= 1 # Decrement count as this task won't run
self._task_results["failed"] += 1
# Optionally update GUI status for this specific rule
self.main_window.update_file_status(str(rule.input_path), "failed", f"Config Error: {config_err}")
continue # Skip to the next rule
except Exception as engine_err:
log.exception(f"Task {i+1} (Rule: {rule.input_path}): Failed to initialize ProcessingEngine for preset '{preset_name_for_task}': {engine_err}. Skipping task.")
self._active_tasks_count -= 1 # Decrement count
self._task_results["failed"] += 1
self.main_window.update_file_status(str(rule.input_path), "failed", f"Engine Init Error: {engine_err}")
continue # Skip to the next rule
if task_engine is None: # Should not happen if exceptions are caught, but safety check
log.error(f"Task {i+1} (Rule: {rule.input_path}): Engine is None after initialization attempt. Skipping task.")
self._active_tasks_count -= 1 # Decrement count
self._task_results["failed"] += 1
self.main_window.update_file_status(str(rule.input_path), "failed", "Engine initialization failed (unknown reason).")
continue # Skip to the next rule
# --- End Engine Instantiation ---
# Pass the required paths and the NEW engine instance to the ProcessingTask constructor
task = ProcessingTask(
engine=self.processing_engine,
engine=task_engine, # Pass the newly created engine
rule=rule,
workspace_path=workspace_path,
output_base_path=output_base_path

View File

@ -319,21 +319,32 @@ class ProcessingEngine:
temp_metadata_path_asset = None # Track metadata file for this asset
try:
# --- Skip Check ---
# Use static config for supplier name and metadata filename
supplier_sanitized = _sanitize_filename(self.config_obj.supplier_name)
# --- Determine Effective Supplier (Override > Identifier > Fallback) ---
effective_supplier = source_rule.supplier_override # Prioritize override
if effective_supplier is None:
effective_supplier = source_rule.supplier_identifier # Fallback to original identifier
if not effective_supplier: # Check if still None or empty
log.warning(f"Asset '{asset_name}': Supplier identifier missing from rule and override. Using fallback 'UnknownSupplier'.")
effective_supplier = "UnknownSupplier" # Final fallback
log.debug(f"Asset '{asset_name}': Effective supplier determined as '{effective_supplier}' (Override: '{source_rule.supplier_override}', Original: '{source_rule.supplier_identifier}')")
# --- Skip Check (using effective supplier) ---
supplier_sanitized = _sanitize_filename(effective_supplier)
asset_name_sanitized = _sanitize_filename(asset_name)
final_dir = output_base_path / supplier_sanitized / asset_name_sanitized
metadata_file_path = final_dir / self.config_obj.metadata_filename
metadata_file_path = final_dir / self.config_obj.metadata_filename # Metadata filename still comes from config
log.debug(f"Checking for existing output/overwrite at: {final_dir} (using effective supplier: '{effective_supplier}')")
if not overwrite and final_dir.exists():
log.info(f"Output directory and metadata found for asset '{asset_name_sanitized}' and overwrite is False. Skipping.")
log.info(f"Output directory found for asset '{asset_name_sanitized}' (Supplier: '{effective_supplier}') and overwrite is False. Skipping.")
overall_status["skipped"].append(asset_name)
asset_skipped = True
continue # Skip to the next asset
elif overwrite and final_dir.exists():
log.warning(f"Output directory exists for '{asset_name_sanitized}' and overwrite is True. Removing existing directory: {final_dir}")
log.warning(f"Output directory exists for '{asset_name_sanitized}' (Supplier: '{effective_supplier}') and overwrite is True. Removing existing directory: {final_dir}")
try:
shutil.rmtree(final_dir)
except Exception as rm_err:
@ -343,6 +354,8 @@ class ProcessingEngine:
# Start with common metadata from the rule, add asset name
current_asset_metadata = asset_rule.common_metadata.copy()
current_asset_metadata["asset_name"] = asset_name
# Use the EFFECTIVE supplier here
current_asset_metadata["supplier_name"] = effective_supplier
# Add other fields that will be populated
current_asset_metadata["maps_present"] = []
current_asset_metadata["merged_maps"] = []
@ -371,8 +384,9 @@ class ProcessingEngine:
)
# --- Generate Metadata ---
# Pass effective_supplier instead of the whole source_rule
temp_metadata_path_asset = self._generate_metadata_file(
source_rule=source_rule, # Pass the parent SourceRule
effective_supplier=effective_supplier, # Pass the determined supplier
asset_rule=asset_rule,
current_asset_metadata=current_asset_metadata, # Pass the populated dict
processed_maps_details_asset=processed_maps_details_asset,
@ -380,17 +394,18 @@ class ProcessingEngine:
)
# --- Organize Output ---
# Pass effective_supplier instead of source_rule.supplier_identifier
self._organize_output_files(
asset_rule=asset_rule,
workspace_path=workspace_path, # Pass the original workspace path
supplier_identifier=source_rule.supplier_identifier, # Pass supplier from SourceRule
supplier_identifier=effective_supplier, # Pass the determined supplier
output_base_path=output_base_path, # Pass output path
processed_maps_details_asset=processed_maps_details_asset,
merged_maps_details_asset=merged_maps_details_asset,
temp_metadata_path=temp_metadata_path_asset
)
log.info(f"--- Asset '{asset_name}' processed successfully. ---")
log.info(f"--- Asset '{asset_name}' processed successfully (Supplier: {effective_supplier}). ---")
overall_status["processed"].append(asset_name)
asset_processed = True
@ -1257,12 +1272,13 @@ class ProcessingEngine:
return merged_maps_details_asset
def _generate_metadata_file(self, source_rule: SourceRule, asset_rule: AssetRule, current_asset_metadata: Dict, processed_maps_details_asset: Dict[str, Dict[str, Dict]], merged_maps_details_asset: Dict[str, Dict[str, Dict]]) -> Path:
def _generate_metadata_file(self, effective_supplier: str, asset_rule: AssetRule, current_asset_metadata: Dict, processed_maps_details_asset: Dict[str, Dict[str, Dict]], merged_maps_details_asset: Dict[str, Dict[str, Dict]]) -> Path:
"""
Gathers metadata for a specific asset based on the AssetRule and processing results,
and writes it to a temporary JSON file in the engine's temp_dir.
Args:
effective_supplier: The supplier name to use (override or original).
asset_rule: The AssetRule object for this asset.
current_asset_metadata: Base metadata dictionary (already contains name, category, archetype, stats, aspect ratio, map_details).
processed_maps_details_asset: Details of processed maps for this asset.
@ -1277,13 +1293,13 @@ class ProcessingEngine:
log.warning("Asset name missing during metadata generation, file may be incomplete or incorrectly named.")
asset_name = "UnknownAsset_Metadata" # Fallback for filename
log.info(f"Generating metadata file for asset '{asset_name}'...")
log.info(f"Generating metadata file for asset '{asset_name}' (Supplier: {effective_supplier})...")
# Start with the base metadata passed in (already contains name, category, archetype, stats, aspect, map_details)
final_metadata = current_asset_metadata.copy()
# Use the supplier identifier determined by rules/selection (from SourceRule)
final_metadata["supplier_name"] = source_rule.supplier_identifier or self.config_obj.supplier_name # Fallback to config if empty
# Use the effective supplier passed as argument
final_metadata["supplier_name"] = effective_supplier # Already determined in process()
# Populate map resolution details from processing results
final_metadata["processed_map_resolutions"] = {}
@ -1326,11 +1342,11 @@ class ProcessingEngine:
source_files_in_extra_set.add(str(file_rule.file_path))
final_metadata["source_files_in_extra"] = sorted(list(source_files_in_extra_set))
# Add processing info (using static config for preset name)
# Add processing info
final_metadata["_processing_info"] = {
"preset_used": getattr(source_rule, 'preset_name', self.config_obj.preset_name), # Use preset name from SourceRule (fallback to static config)
"preset_used": self.config_obj.preset_name, # Preset name comes from the engine's config
"timestamp_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"input_source": source_rule.supplier_identifier or "Unknown", # Use identifier from parent SourceRule
"input_source": effective_supplier, # Use the effective supplier
}
# Sort lists just before writing
@ -1381,7 +1397,7 @@ class ProcessingEngine:
log.warning(f"Asset '{asset_name}': Supplier identifier missing in SourceRule. Using fallback 'UnknownSupplier'.")
supplier_identifier = "UnknownSupplier"
supplier_sanitized = _sanitize_filename(supplier_identifier) # <<< FIX: Use passed identifier
supplier_sanitized = _sanitize_filename(supplier_identifier) # Use the effective supplier passed in
asset_name_sanitized = _sanitize_filename(asset_name)
final_dir = output_base_path / supplier_sanitized / asset_name_sanitized
log.info(f"Organizing output files for asset '{asset_name_sanitized}' (Supplier: '{supplier_identifier}') into: {final_dir}")