GUI FIXES

This commit is contained in:
Rusfort 2025-05-01 19:16:37 +02:00
parent 51ff45bd5c
commit 2f8bbc3a7d
16 changed files with 1397 additions and 198 deletions

View File

@ -2,16 +2,19 @@
This document explains how to configure the Asset Processor Tool and use presets. This document explains how to configure the Asset Processor Tool and use presets.
## Core Settings (`config.py`) ## Application Settings (`config/app_settings.json`)
The tool's behavior is controlled by core settings defined in `config.py`. While primarily for developers, some settings are important for users to be aware of: The tool's core settings are now stored in `config/app_settings.json`. This JSON file contains the base configuration for the application.
* `OUTPUT_BASE_DIR`: The default root directory where processed assets will be saved. 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.
* `IMAGE_RESOULTIONS`: Defines the target resolutions for processed texture maps (e.g., 4K, 2K).
* `BLENDER_EXECUTABLE_PATH`: The path to your Blender installation, required for optional Blender integration.
* Other settings control aspects like default asset category, filename patterns, map merge rules, and output formats.
These settings can often be overridden via the GUI or CLI arguments. ## 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.
*(Ideally, a screenshot of the GUI Configuration Editor would be included here.)*
## Preset Files (`presets/*.json`) ## Preset Files (`presets/*.json`)
@ -22,6 +25,6 @@ Preset files define supplier-specific rules for interpreting asset source files.
* Presets contain rules based on filename patterns and keywords to identify map types, models, and other files. * Presets contain rules based on filename patterns and keywords to identify map types, models, and other files.
* They also define how variants (like different resolutions or bit depths) are handled and how asset names and categories are determined from the source filename. * They also define how variants (like different resolutions or bit depths) are handled and how asset names and categories are determined from the source filename.
When processing assets, you must specify which preset to use. The tool then loads the core settings from `config.py` and merges them with the rules from the selected preset to determine how to process the input. When processing assets, you must specify which preset to use. The tool then loads the core settings from `config/app_settings.json` and merges them with the rules from the selected preset to determine how to process the input.
A template preset file (`presets/_template.json`) is provided as a base for creating new presets. A template preset file (`presets/_template.json`) is provided as a base for creating new presets.

View File

@ -12,24 +12,32 @@ python -m gui.main_window
## Interface Overview ## Interface Overview
* **Menu Bar:** The "View" menu allows you to toggle the visibility of the Log Console and the Detailed File Preview. * **Menu Bar:** The "Edit" menu contains the "Preferences..." option to open the GUI Configuration Editor. The "View" menu allows you to toggle the visibility of the Log Console and the Detailed File Preview.
* **Preset Editor Panel (Left):** * **Preset Editor Panel (Left):**
* **Optional Log Console:** Displays application logs (toggle via View menu). * **Optional Log Console:** Displays application logs (toggle via View menu).
* **Preset List:** Create, delete, load, edit, and save presets. On startup, the "-- Select a Preset --" item is explicitly selected. You must select a specific preset from this list to load it into the editor below, enable the detailed file preview, and enable the "Start Processing" button. * **Preset List:** Create, delete, load, edit, and save presets. On startup, the "-- Select a Preset --" item is explicitly selected. You must select a specific preset from this list to load it into the editor below, enable the detailed file preview, and enable the "Start Processing" button.
* **Preset Editor Tabs:** Edit the details of the selected preset. * **Preset Editor Tabs:** Edit the details of the selected preset.
* **Processing Panel (Right):** * **Processing Panel (Right):**
* **Preset Selector:** Choose the preset to use for *processing* the current queue. * **Preset Selector:** Choose the preset to use for *processing* the current queue.
* **Output Directory:** Set the output path (defaults to `config.py`, use "Browse...") * **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. * **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: * **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. * **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.
* **Simple View (Preview Disabled):** Lists only top-level input asset paths. * **Simple View (Preview Disabled):** Lists only top-level input asset paths.
* **Progress Bar:** Shows overall processing progress. * **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.py`). * **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`).
* **Options & Controls (Bottom):** * **Options & Controls (Bottom):**
* `Overwrite Existing`: Checkbox to force reprocessing. * `Overwrite Existing`: Checkbox to force reprocessing.
* `Workers`: Spinbox for concurrent processes. * `Workers`: Spinbox for concurrent processes.
* `Clear Queue`: Button to clear the queue and preview. * `Clear Queue`: Button to clear the queue and preview.
* `Start Processing`: Button to start processing the queue. This button is disabled until a valid preset is selected from the Preset List. * `Start Processing`: Button to start processing the queue. This button is disabled until a valid preset is selected from the Preset List.
* `Cancel`: Button to attempt stopping processing. * `Cancel`: Button to attempt stopping processing.
* **Status Bar:** Displays current status, errors, and completion messages. * **Status Bar:** Displays current status, errors, and completion messages.
## 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.
Any changes made in the GUI Configuration Editor require you to restart the application for them to take effect.
*(Ideally, a screenshot of the GUI Configuration Editor would be included here.)*

View File

@ -4,19 +4,12 @@ This document provides technical details about the configuration system and the
## Configuration Flow ## Configuration Flow
The tool utilizes a two-tiered configuration system: The tool utilizes a two-tiered configuration system managed by the `configuration.py` module:
1. **Core Settings (`config.py`):** This Python module defines global default settings, constants, and core 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 now 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.
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. 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.
## Core Definitions in `config.py` 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 `config.py` file now uses dictionary structures to define allowed asset and file types and their associated metadata:
* **`ASSET_TYPE_DEFINITIONS`:** A dictionary where keys are the standard asset type names (e.g., `"Surface"`, `"Model"`, `"Decal"`) and values are dictionaries containing metadata such as `description` and `color` (used for GUI coloring).
* **`FILE_TYPE_DEFINITIONS`:** A dictionary where keys are the standard file/item type names (e.g., `"MAP_COL"`, `"MAP_NRM"`, `"MODEL"`, `"EXTRA"`) and values are dictionaries containing metadata such as `description`, `color` (used for GUI coloring), and `examples` (example filename patterns).
These dictionaries serve as the central source of truth for valid types and their associated display information throughout the application, particularly in the GUI for dropdowns and coloring.
## Supplier Management (`config/suppliers.json`) ## Supplier Management (`config/suppliers.json`)
@ -27,17 +20,25 @@ A new file, `config/suppliers.json`, is used to store a persistent list of known
## `Configuration` Class (`configuration.py`) ## `Configuration` Class (`configuration.py`)
The `Configuration` class is responsible for loading, merging, and preparing the configuration settings for use by the `AssetProcessor`. The `Configuration` class is central to the new configuration system. It is responsible for loading, merging, and preparing the configuration settings for use by the `AssetProcessor`.
* **Initialization:** An instance is created with a specific `preset_name`. * **Initialization:** An instance is created with a specific `preset_name`.
* **Loading:** * **Loading:**
* It loads the core settings from `config.py` using `importlib.util`. * It first loads the base application settings from `config/app_settings.json`.
* It loads the specified preset JSON file from the `Presets/` directory. * It then loads the specified preset JSON file from the `Presets/` directory.
* **Merging:** The loaded core settings and preset rules are merged into a single configuration object accessible via instance attributes. Preset values generally override core settings where applicable. * **Merging:** The loaded settings from `app_settings.json` and the preset rules are merged into a single configuration object accessible via instance attributes. Preset values generally override the base settings from `app_settings.json` where applicable.
* **Validation (`_validate_configs`):** Performs basic structural validation on the loaded settings, checking for the presence of required keys and basic data types (e.g., ensuring `map_type_mapping` is a list of dictionaries). * **Validation (`_validate_configs`):** Performs basic structural validation on the loaded settings, checking for the presence of required keys and basic data types (e.g., ensuring `map_type_mapping` is a list of dictionaries).
* **Regex Compilation (`_compile_regex_patterns`):** A crucial step for performance. It iterates through the regex patterns defined in the preset (for extra files, models, bit depth variants, map keywords) and compiles them using `re.compile` (mostly case-insensitive). These compiled regex objects are stored as instance attributes (e.g., `self.compiled_map_keyword_regex`) for fast matching during file classification. It uses a helper (`_fnmatch_to_regex`) for basic wildcard (`*`, `?`) conversion in patterns. * **Regex Compilation (`_compile_regex_patterns`):** A crucial step for performance. It iterates through the regex patterns defined in the merged configuration (from both `app_settings.json` and the preset) and compiles them using `re.compile` (mostly case-insensitive). These compiled regex objects are stored as instance attributes (e.g., `self.compiled_map_keyword_regex`) for fast matching during file classification. It uses a helper (`_fnmatch_to_regex`) for basic wildcard (`*`, `?`) conversion in patterns.
An instance of `Configuration` is created within each worker process (`main.process_single_asset_wrapper`) to ensure that each concurrently processed asset uses the correct, isolated configuration based on the specified preset. An instance of `Configuration` is created within each worker process (`main.process_single_asset_wrapper`) to ensure that each concurrently processed asset uses the correct, isolated configuration based on the specified preset and the base application settings.
## GUI Configuration Editor (`gui/config_editor_dialog.py`)
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.
* **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`) ## Preset File Structure (`Presets/*.json`)

View File

@ -44,9 +44,9 @@ The core of the GUI's rule editing interface is the Unified Hierarchical View, i
* **`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. * **`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.
* **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. * **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 in `config.py`. `SourceRule` rows have a fixed color. * **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 `config.py`). * **`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). * **`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. * **`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.
@ -80,4 +80,12 @@ A custom `QtLogHandler` is used to redirect log messages from the standard Pytho
The GUI provides a "Cancel" button to stop ongoing processing. The `ProcessingHandler` implements logic to handle cancellation requests. This typically involves setting an internal flag and attempting to shut down the `ProcessPoolExecutor`. However, it's important to note that this does not immediately terminate worker processes that are already executing; it primarily prevents new tasks from starting and stops processing results from completed futures once the cancellation flag is checked. The GUI provides a "Cancel" button to stop ongoing processing. The `ProcessingHandler` implements logic to handle cancellation requests. This typically involves setting an internal flag and attempting to shut down the `ProcessPoolExecutor`. However, it's important to note that this does not immediately terminate worker processes that are already executing; it primarily prevents new tasks from starting and stops processing results from completed futures once the cancellation flag is checked.
## GUI Configuration Editor (`gui/config_editor_dialog.py`)
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.
* **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.
These key components work together to provide the tool's functionality, separating concerns and utilizing concurrency for performance and responsiveness. The Unified Hierarchical View centralizes rule management in the GUI, and the `SourceRule` object serves as a clear data contract passed to the processing engine. These key components work together to provide the tool's functionality, separating concerns and utilizing concurrency for performance and responsiveness. The Unified Hierarchical View centralizes rule management in the GUI, and the `SourceRule` object serves as a clear data contract passed to the processing engine.

339
config/app_settings.json Normal file
View File

@ -0,0 +1,339 @@
{
"ASSET_TYPE_DEFINITIONS": {
"Surface": {
"description": "Standard PBR material set for a surface.",
"color": "#87CEEB",
"examples": [
"WoodFloor01",
"MetalPlate05"
]
},
"Model": {
"description": "A 3D model file.",
"color": "#FFA500",
"examples": [
"Chair.fbx",
"Character.obj"
]
},
"Decal": {
"description": "A texture designed to be projected onto surfaces.",
"color": "#90EE90",
"examples": [
"Graffiti01",
"LeakStain03"
]
},
"Atlas": {
"description": "A texture sheet containing multiple smaller textures.",
"color": "#FFC0CB",
"examples": [
"FoliageAtlas",
"UITextureSheet"
]
},
"UtilityMap": {
"description": "A map used for specific technical purposes (e.g., flow map).",
"color": "#D3D3D3",
"examples": [
"FlowMap",
"CurvatureMap"
]
}
},
"FILE_TYPE_DEFINITIONS": {
"MAP_COL": {
"description": "Color/Albedo Map",
"color": "#FFFFE0",
"examples": [
"_col.",
"_basecolor."
]
},
"MAP_COL": {
"description": "Color/Albedo Map",
"color": "#FFFFE0",
"examples": [
"_col.",
"_basecolor."
],
"standard_type": "COL"
},
"MAP_NRM": {
"description": "Normal Map",
"color": "#E6E6FA",
"examples": [
"_nrm.",
"_normal."
]
},
"MAP_NRM": {
"description": "Normal Map",
"color": "#E6E6FA",
"examples": [
"_nrm.",
"_normal."
],
"standard_type": "NRM"
},
"MAP_METAL": {
"description": "Metalness Map",
"color": "#C0C0C0",
"examples": [
"_metal.",
"_met."
]
},
"MAP_METAL": {
"description": "Metalness Map",
"color": "#C0C0C0",
"examples": [
"_metal.",
"_met."
],
"standard_type": "METAL"
},
"MAP_ROUGH": {
"description": "Roughness Map",
"color": "#A0522D",
"examples": [
"_rough.",
"_rgh."
]
},
"MAP_ROUGH": {
"description": "Roughness Map",
"color": "#A0522D",
"examples": [
"_rough.",
"_rgh."
],
"standard_type": "ROUGH"
},
"MAP_AO": {
"description": "Ambient Occlusion Map",
"color": "#A9A9A9",
"examples": [
"_ao.",
"_ambientocclusion."
]
},
"MAP_AO": {
"description": "Ambient Occlusion Map",
"color": "#A9A9A9",
"examples": [
"_ao.",
"_ambientocclusion."
],
"standard_type": "AO"
},
"MAP_DISP": {
"description": "Displacement/Height Map",
"color": "#FFB6C1",
"examples": [
"_disp.",
"_height."
]
},
"MAP_DISP": {
"description": "Displacement/Height Map",
"color": "#FFB6C1",
"examples": [
"_disp.",
"_height."
],
"standard_type": "DISP"
},
"MAP_REFL": {
"description": "Reflection/Specular Map",
"color": "#E0FFFF",
"examples": [
"_refl.",
"_specular."
]
},
"MAP_REFL": {
"description": "Reflection/Specular Map",
"color": "#E0FFFF",
"examples": [
"_refl.",
"_specular."
],
"standard_type": "REFL"
},
"MAP_SSS": {
"description": "Subsurface Scattering Map",
"color": "#FFDAB9",
"examples": [
"_sss.",
"_subsurface."
]
},
"MAP_SSS": {
"description": "Subsurface Scattering Map",
"color": "#FFDAB9",
"examples": [
"_sss.",
"_subsurface."
],
"standard_type": "SSS"
},
"MAP_FUZZ": {
"description": "Fuzz/Sheen Map",
"color": "#FFA07A",
"examples": [
"_fuzz.",
"_sheen."
]
},
"MAP_FUZZ": {
"description": "Fuzz/Sheen Map",
"color": "#FFA07A",
"examples": [
"_fuzz.",
"_sheen."
],
"standard_type": "FUZZ"
},
"MAP_IDMAP": {
"description": "ID Map (for masking)",
"color": "#F08080",
"examples": [
"_id.",
"_matid."
]
},
"MAP_IDMAP": {
"description": "ID Map (for masking)",
"color": "#F08080",
"examples": [
"_id.",
"_matid."
],
"standard_type": "IDMAP"
},
"MAP_MASK": {
"description": "Generic Mask Map",
"color": "#FFFFFF",
"examples": [
"_mask."
]
},
"MAP_MASK": {
"description": "Generic Mask Map",
"color": "#FFFFFF",
"examples": [
"_mask."
],
"standard_type": "MASK"
},
"MAP_IMPERFECTION": {
"description": "Imperfection Map (scratches, dust)",
"color": "#F0E68C",
"examples": [
"_imp.",
"_imperfection."
]
},
"MAP_IMPERFECTION": {
"description": "Imperfection Map (scratches, dust)",
"color": "#F0E68C",
"examples": [
"_imp.",
"_imperfection."
],
"standard_type": "IMPERFECTION"
},
"MODEL": {
"description": "3D Model File",
"color": "#FFA500",
"examples": [
".fbx",
".obj"
]
},
"EXTRA": {
"description": "Non-standard/Unclassified File",
"color": "#778899",
"examples": [
".txt",
".zip"
]
},
"FILE_IGNORE": {
"description": "File to be ignored",
"color": "#2F4F4F",
"examples": [
"Thumbs.db",
".DS_Store"
]
}
},
"TARGET_FILENAME_PATTERN": "{base_name}_{map_type}_{resolution}.{ext}",
"STANDARD_MAP_TYPES": [
"COL",
"NRM",
"ROUGH",
"METAL",
"AO",
"DISP",
"REFL",
"SSS",
"FUZZ",
"IDMAP",
"MASK"
],
"RESPECT_VARIANT_MAP_TYPES": "COL",
"EXTRA_FILES_SUBDIR": "Extra",
"OUTPUT_BASE_DIR": "../Asset_Processor_Output",
"METADATA_FILENAME": "metadata.json",
"DEFAULT_NODEGROUP_BLEND_PATH": "G:/02 Content/10-19 Content/19 Catalogs/19.01 Blender Asset Catalogue/_CustomLibraries/Nodes-Linked/PBRSET-Nodes-Testing.blend",
"DEFAULT_MATERIALS_BLEND_PATH": "G:/02 Content/10-19 Content/19 Catalogs/19.01 Blender Asset Catalogue/_CustomLibraries/Materials-Append/PBR Materials-Testing.blend",
"BLENDER_EXECUTABLE_PATH": "C:/Program Files/Blender Foundation/Blender 4.4/blender.exe",
"PNG_COMPRESSION_LEVEL": 6,
"JPG_QUALITY": 98,
"RESOLUTION_THRESHOLD_FOR_JPG": 4096,
"IMAGE_RESOLUTIONS": {
"8K": 8192,
"4K": 4096,
"2K": 2048,
"1K": 1024
},
"ASPECT_RATIO_DECIMALS": 2,
"MAP_BIT_DEPTH_RULES": {
"COL": "force_8bit",
"NRM": "respect",
"ROUGH": "force_8bit",
"METAL": "force_8bit",
"AO": "force_8bit",
"DISP": "respect",
"REFL": "force_8bit",
"SSS": "respect",
"FUZZ": "force_8bit",
"IDMAP": "force_8bit",
"MASK": "force_8bit",
"DEFAULT": "respect"
},
"OUTPUT_FORMAT_16BIT_PRIMARY": "png",
"OUTPUT_FORMAT_16BIT_FALLBACK": "png",
"OUTPUT_FORMAT_8BIT": "png",
"MAP_MERGE_RULES": [
{
"output_map_type": "NRMRGH",
"inputs": {
"R": "NRM",
"G": "NRM",
"B": "ROUGH"
},
"defaults": {
"R": 0.5,
"G": 0.5,
"B": 0.5
},
"output_bit_depth": "respect_inputs"
}
],
"CALCULATE_STATS_RESOLUTION": "1K",
"DEFAULT_ASSET_CATEGORY": "Surface",
"TEMP_DIR_PREFIX": "_PROCESS_ASSET_"
}

View File

@ -2,17 +2,17 @@
import json import json
import os import os
import importlib.util
from pathlib import Path from pathlib import Path
import logging import logging
import re # Import the regex module import re # Import the regex module
import json # Import the json module
log = logging.getLogger(__name__) # Use logger defined in main.py log = logging.getLogger(__name__) # Use logger defined in main.py
# --- Constants --- # --- Constants ---
# Assumes config.py and presets/ are relative to this file's location # Assumes config/ and presets/ are relative to this file's location
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
CORE_CONFIG_PATH = BASE_DIR / "config.py" APP_SETTINGS_PATH = BASE_DIR / "config" / "app_settings.json"
PRESETS_DIR = BASE_DIR / "presets" PRESETS_DIR = BASE_DIR / "presets"
# --- Custom Exception --- # --- Custom Exception ---
@ -198,48 +198,19 @@ class Configuration:
def _load_core_config(self) -> dict: def _load_core_config(self) -> dict:
"""Loads settings from the core config.py file.""" """Loads settings from the core app_settings.json file."""
log.debug(f"Loading core config from: {CORE_CONFIG_PATH}") log.debug(f"Loading core config from: {APP_SETTINGS_PATH}")
if not CORE_CONFIG_PATH.is_file(): if not APP_SETTINGS_PATH.is_file():
raise ConfigurationError(f"Core configuration file not found: {CORE_CONFIG_PATH}") raise ConfigurationError(f"Core configuration file not found: {APP_SETTINGS_PATH}")
try: try:
spec = importlib.util.spec_from_file_location("core_config", CORE_CONFIG_PATH) with open(APP_SETTINGS_PATH, 'r', encoding='utf-8') as f:
if spec is None or spec.loader is None: settings = json.load(f)
raise ConfigurationError(f"Could not create module spec for {CORE_CONFIG_PATH}")
core_config_module = importlib.util.module_from_spec(spec)
# Define default values for core settings in case they are missing in config.py
default_core_settings = {
'TARGET_FILENAME_PATTERN': "{base_name}_{map_type}_{resolution}.{ext}",
'STANDARD_MAP_TYPES': [],
'EXTRA_FILES_SUBDIR': "Extra",
'METADATA_FILENAME': "metadata.json",
'IMAGE_RESOLUTIONS': {},
'ASPECT_RATIO_DECIMALS': 2,
'MAP_BIT_DEPTH_RULES': {"DEFAULT": "respect"},
'OUTPUT_FORMAT_16BIT_PRIMARY': "png",
'OUTPUT_FORMAT_16BIT_FALLBACK': "png",
'OUTPUT_FORMAT_8BIT': "png",
'MAP_MERGE_RULES': [],
'CALCULATE_STATS_RESOLUTION': "1K",
'DEFAULT_ASSET_CATEGORY': "Texture",
'TEMP_DIR_PREFIX': "_PROCESS_ASSET_",
# --- Additions ---
'JPG_QUALITY': 95, # Default JPG quality
'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold
'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix
'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless
}
# Load attributes from module, using defaults if missing
settings = default_core_settings.copy()
spec.loader.exec_module(core_config_module)
for name in default_core_settings:
if hasattr(core_config_module, name):
settings[name] = getattr(core_config_module, name)
log.debug(f"Core config loaded successfully.") log.debug(f"Core config loaded successfully.")
return settings return settings
except json.JSONDecodeError as e:
raise ConfigurationError(f"Failed to parse core configuration file {APP_SETTINGS_PATH}: Invalid JSON - {e}")
except Exception as e: except Exception as e:
raise ConfigurationError(f"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}") raise ConfigurationError(f"Failed to read core configuration file {APP_SETTINGS_PATH}: {e}")
def _load_preset(self, preset_name: str) -> dict: def _load_preset(self, preset_name: str) -> dict:
"""Loads the specified preset JSON file.""" """Loads the specified preset JSON file."""
@ -415,4 +386,42 @@ class Configuration:
def get_8bit_output_format(self) -> str: def get_8bit_output_format(self) -> str:
"""Gets the format name for 8-bit output.""" """Gets the format name for 8-bit output."""
return self._core_settings.get('OUTPUT_FORMAT_8BIT', 'png').lower() return self._core_settings.get('OUTPUT_FORMAT_8BIT', 'png').lower()
# --- Standalone Base Config Functions ---
def load_base_config() -> dict:
"""
Loads only the base configuration from app_settings.json.
Does not load presets or perform merging/validation.
"""
#log.debug(f"Loading base config from: {APP_SETTINGS_PATH}")
if not APP_SETTINGS_PATH.is_file():
log.error(f"Base configuration file not found: {APP_SETTINGS_PATH}")
# Return empty dict or raise a specific error if preferred
# For now, return empty dict to allow GUI to potentially start with defaults
return {}
try:
with open(APP_SETTINGS_PATH, 'r', encoding='utf-8') as f:
settings = json.load(f)
#log.debug(f"Base config loaded successfully.")
return settings
except json.JSONDecodeError as e:
log.error(f"Failed to parse base configuration file {APP_SETTINGS_PATH}: Invalid JSON - {e}")
return {} # Return empty dict on error
except Exception as e:
log.error(f"Failed to read base configuration file {APP_SETTINGS_PATH}: {e}")
return {} # Return empty dict on error
def save_base_config(settings_dict: dict):
"""
Saves the provided settings dictionary to app_settings.json.
"""
log.debug(f"Saving base config to: {APP_SETTINGS_PATH}")
try:
with open(APP_SETTINGS_PATH, 'w', encoding='utf-8') as f:
json.dump(settings_dict, f, indent=4)
log.debug(f"Base config saved successfully.")
except Exception as e:
log.error(f"Failed to save base configuration file {APP_SETTINGS_PATH}: {e}")
raise ConfigurationError(f"Failed to save configuration: {e}")

623
gui/config_editor_dialog.py Normal file
View File

@ -0,0 +1,623 @@
# gui/config_editor_dialog.py
import json
from PySide6.QtWidgets import ( # Changed from PyQt5
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox,
QPushButton, QFileDialog, QLabel, QTableWidget, # Removed QColorDialog
QTableWidgetItem, QDialogButtonBox, QMessageBox, QListWidget,
QListWidgetItem, QFormLayout, QGroupBox
)
from PySide6.QtGui import QColor # Changed from PyQt5
from PySide6.QtCore import Qt # Changed from PyQt5
from PySide6.QtWidgets import QColorDialog # Import QColorDialog separately for PySide6
# Assuming configuration.py is in the parent directory or accessible
# Adjust import path if necessary
try:
from configuration import load_base_config, save_base_config
except ImportError:
# Fallback import for testing or different project structure
from ..configuration import load_base_config, save_base_config
class ConfigEditorDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Configuration Editor")
self.setGeometry(100, 100, 800, 600)
self.settings = {}
self.widgets = {} # Dictionary to hold references to created widgets
self.main_layout = QVBoxLayout(self)
self.tab_widget = QTabWidget()
self.main_layout.addWidget(self.tab_widget)
self.button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.save_settings)
self.button_box.rejected.connect(self.reject)
self.main_layout.addWidget(self.button_box)
self.load_settings()
self.create_tabs()
self.populate_widgets()
def load_settings(self):
"""Loads settings from the configuration file."""
try:
self.settings = load_base_config()
print("Configuration loaded successfully.") # Debug print
except Exception as e:
QMessageBox.critical(self, "Loading Error", f"Failed to load configuration: {e}")
self.settings = {} # Use empty settings on failure
def create_tabs(self):
"""Creates tabs based on logical groupings of settings."""
if not self.settings:
return
# --- Create Tabs ---
self.tabs = {
"definitions": QWidget(),
"paths_output": QWidget(),
"image_settings": QWidget(),
"blender": QWidget(),
"misc": QWidget() # For settings that don't fit elsewhere
}
self.tab_widget.addTab(self.tabs["definitions"], "Definitions")
self.tab_widget.addTab(self.tabs["paths_output"], "Paths & Output")
self.tab_widget.addTab(self.tabs["image_settings"], "Image Settings")
self.tab_widget.addTab(self.tabs["blender"], "Blender")
self.tab_widget.addTab(self.tabs["misc"], "Miscellaneous")
# --- Setup Layouts for Tabs ---
self.tab_layouts = {name: QVBoxLayout(tab) for name, tab in self.tabs.items()}
# --- Populate Tabs ---
self.populate_definitions_tab(self.tab_layouts["definitions"])
self.populate_paths_output_tab(self.tab_layouts["paths_output"])
self.populate_image_settings_tab(self.tab_layouts["image_settings"])
self.populate_blender_tab(self.tab_layouts["blender"])
self.populate_misc_tab(self.tab_layouts["misc"])
def create_widget_for_setting(self, parent_layout, key, value, setting_key_prefix=""):
"""Creates an appropriate widget for a single setting key-value pair."""
full_key = f"{setting_key_prefix}{key}" if setting_key_prefix else key
label_text = key.replace('_', ' ').title()
label = QLabel(label_text + ":")
widget = None
layout_to_add = None # Use this for widgets needing extra controls (like browse button)
if isinstance(value, str):
if 'PATH' in key.upper() or 'DIR' in key.upper() or key == "BLENDER_EXECUTABLE_PATH":
widget = QLineEdit(value)
button = QPushButton("Browse...")
# Determine if it's a file or directory browse
is_dir = 'DIR' in key.upper()
button.clicked.connect(lambda checked, w=widget, k=full_key, is_dir=is_dir: self.browse_path(w, k, is_dir))
h_layout = QHBoxLayout()
h_layout.addWidget(widget)
h_layout.addWidget(button)
layout_to_add = h_layout
elif 'COLOR' in key.upper() or 'COLOUR' in key.upper():
widget = QLineEdit(value)
button = QPushButton("Pick Color...")
button.clicked.connect(lambda checked, w=widget: self.pick_color(w))
h_layout = QHBoxLayout()
h_layout.addWidget(widget)
h_layout.addWidget(button)
layout_to_add = h_layout
else:
widget = QLineEdit(value)
elif isinstance(value, int):
widget = QSpinBox()
widget.setRange(-2147483648, 2147483647)
widget.setValue(value)
elif isinstance(value, float):
widget = QDoubleSpinBox()
widget.setRange(-1.7976931348623157e+308, 1.7976931348623157e+308)
widget.setValue(value)
elif isinstance(value, bool):
widget = QCheckBox()
widget.setChecked(value)
elif isinstance(value, list) and key != "MAP_MERGE_RULES": # Handle simple lists (excluding complex ones)
# Assuming list of strings or simple types
widget = QLineEdit(", ".join(map(str, value)))
# Complex dicts/lists like ASSET_TYPE_DEFINITIONS, MAP_MERGE_RULES etc. are handled in dedicated methods
if widget or layout_to_add:
if layout_to_add:
parent_layout.addRow(label, layout_to_add)
else:
parent_layout.addRow(label, widget)
# Store reference using the full key only if a widget was created
if widget:
self.widgets[full_key] = widget
else:
# Optionally handle unsupported types or log a warning
# print(f"Skipping widget creation for key '{full_key}' with unsupported type: {type(value)}")
pass
def populate_definitions_tab(self, layout):
"""Populates the Definitions tab."""
if "ASSET_TYPE_DEFINITIONS" in self.settings:
group = QGroupBox("Asset Type Definitions")
group_layout = QVBoxLayout(group)
self.create_asset_definitions_widget(group_layout, self.settings["ASSET_TYPE_DEFINITIONS"])
layout.addWidget(group)
if "FILE_TYPE_DEFINITIONS" in self.settings:
group = QGroupBox("File Type Definitions")
group_layout = QVBoxLayout(group)
self.create_file_type_definitions_widget(group_layout, self.settings["FILE_TYPE_DEFINITIONS"])
layout.addWidget(group)
# Add STANDARD_MAP_TYPES and RESPECT_VARIANT_MAP_TYPES here
form_layout = QFormLayout()
if "STANDARD_MAP_TYPES" in self.settings:
self.create_widget_for_setting(form_layout, "STANDARD_MAP_TYPES", self.settings["STANDARD_MAP_TYPES"])
if "RESPECT_VARIANT_MAP_TYPES" in self.settings:
self.create_widget_for_setting(form_layout, "RESPECT_VARIANT_MAP_TYPES", self.settings["RESPECT_VARIANT_MAP_TYPES"])
if "DEFAULT_ASSET_CATEGORY" in self.settings:
self.create_widget_for_setting(form_layout, "DEFAULT_ASSET_CATEGORY", self.settings["DEFAULT_ASSET_CATEGORY"])
layout.addLayout(form_layout)
layout.addStretch()
def populate_paths_output_tab(self, layout):
"""Populates the Paths & Output tab."""
form_layout = QFormLayout()
keys_to_include = [
"OUTPUT_BASE_DIR", "EXTRA_FILES_SUBDIR", "METADATA_FILENAME",
"TARGET_FILENAME_PATTERN", "TEMP_DIR_PREFIX"
]
for key in keys_to_include:
if key in self.settings:
self.create_widget_for_setting(form_layout, key, self.settings[key])
layout.addLayout(form_layout)
layout.addStretch()
def populate_image_settings_tab(self, layout):
"""Populates the Image Settings tab."""
form_layout = QFormLayout()
simple_keys = [
"PNG_COMPRESSION_LEVEL", "JPG_QUALITY", "RESOLUTION_THRESHOLD_FOR_JPG",
"ASPECT_RATIO_DECIMALS", "CALCULATE_STATS_RESOLUTION",
"OUTPUT_FORMAT_16BIT_PRIMARY", "OUTPUT_FORMAT_16BIT_FALLBACK",
"OUTPUT_FORMAT_8BIT"
]
for key in simple_keys:
if key in self.settings:
self.create_widget_for_setting(form_layout, key, self.settings[key])
layout.addLayout(form_layout)
# Add complex widgets
if "IMAGE_RESOLUTIONS" in self.settings:
group = QGroupBox("Image Resolutions")
group_layout = QVBoxLayout(group)
self.create_image_resolutions_widget(group_layout, self.settings["IMAGE_RESOLUTIONS"])
layout.addWidget(group)
if "MAP_BIT_DEPTH_RULES" in self.settings:
group = QGroupBox("Map Bit Depth Rules")
group_layout = QVBoxLayout(group)
self.create_map_bit_depth_rules_widget(group_layout, self.settings["MAP_BIT_DEPTH_RULES"])
layout.addWidget(group)
if "MAP_MERGE_RULES" in self.settings:
group = QGroupBox("Map Merge Rules")
group_layout = QVBoxLayout(group)
self.create_map_merge_rules_widget(group_layout, self.settings["MAP_MERGE_RULES"])
layout.addWidget(group)
layout.addStretch()
def populate_blender_tab(self, layout):
"""Populates the Blender tab."""
form_layout = QFormLayout()
keys_to_include = [
"DEFAULT_NODEGROUP_BLEND_PATH", "DEFAULT_MATERIALS_BLEND_PATH",
"BLENDER_EXECUTABLE_PATH"
]
for key in keys_to_include:
if key in self.settings:
self.create_widget_for_setting(form_layout, key, self.settings[key])
layout.addLayout(form_layout)
layout.addStretch()
def populate_misc_tab(self, layout):
"""Populates the Miscellaneous tab with any remaining settings."""
form_layout = QFormLayout()
handled_keys = set()
# Collect keys handled by other tabs
handled_keys.update([
"ASSET_TYPE_DEFINITIONS", "FILE_TYPE_DEFINITIONS", "STANDARD_MAP_TYPES",
"RESPECT_VARIANT_MAP_TYPES", "DEFAULT_ASSET_CATEGORY", "OUTPUT_BASE_DIR",
"EXTRA_FILES_SUBDIR", "METADATA_FILENAME", "TARGET_FILENAME_PATTERN",
"TEMP_DIR_PREFIX", "PNG_COMPRESSION_LEVEL", "JPG_QUALITY",
"RESOLUTION_THRESHOLD_FOR_JPG", "ASPECT_RATIO_DECIMALS",
"CALCULATE_STATS_RESOLUTION", "OUTPUT_FORMAT_16BIT_PRIMARY",
"OUTPUT_FORMAT_16BIT_FALLBACK", "OUTPUT_FORMAT_8BIT",
"IMAGE_RESOLUTIONS", "MAP_BIT_DEPTH_RULES", "MAP_MERGE_RULES",
"DEFAULT_NODEGROUP_BLEND_PATH", "DEFAULT_MATERIALS_BLEND_PATH",
"BLENDER_EXECUTABLE_PATH"
])
for key, value in self.settings.items():
if key not in handled_keys:
# Only create widgets for simple types here
if isinstance(value, (str, int, float, bool, list)):
# Check if list is simple
is_simple_list = isinstance(value, list) and (not value or not isinstance(value[0], (dict, list)))
if not isinstance(value, list) or is_simple_list:
self.create_widget_for_setting(form_layout, key, value)
handled_keys.add(key) # Mark as handled
if form_layout.rowCount() == 0:
layout.addWidget(QLabel("No miscellaneous settings found."))
layout.addLayout(form_layout)
layout.addStretch()
# Remove the old create_widgets_for_section method as it's replaced
# def create_widgets_for_section(self, layout, section_data, section_key):
# ... (old implementation removed) ...
def create_asset_definitions_widget(self, layout, definitions_data):
"""Creates a widget for editing asset type definitions."""
table = QTableWidget()
table.setColumnCount(3) # Asset Type, Description, Color
table.setHorizontalHeaderLabels(["Asset Type", "Description", "Color"])
table.setRowCount(len(definitions_data))
row = 0
for asset_type, details in definitions_data.items():
table.setItem(row, 0, QTableWidgetItem(asset_type))
table.setItem(row, 1, QTableWidgetItem(details.get("description", "")))
color_widget = QLineEdit(details.get("color", ""))
color_button = QPushButton("Pick Color...")
color_button.clicked.connect(lambda checked, w=color_widget: self.pick_color(w))
h_layout = QHBoxLayout()
h_layout.addWidget(color_widget)
h_layout.addWidget(color_button)
cell_widget = QWidget()
cell_widget.setLayout(h_layout)
table.setCellWidget(row, 2, cell_widget)
row += 1
table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(table)
self.widgets["DEFINITION_SETTINGS.ASSET_TYPE_DEFINITIONS"] = table # Store table reference
def create_file_type_definitions_widget(self, layout, definitions_data):
"""Creates a widget for editing file type definitions."""
table = QTableWidget()
table.setColumnCount(3) # File Type, Description, Color
table.setHorizontalHeaderLabels(["File Type", "Description", "Color"])
table.setRowCount(len(definitions_data))
row = 0
for file_type, details in definitions_data.items():
table.setItem(row, 0, QTableWidgetItem(file_type))
table.setItem(row, 1, QTableWidgetItem(details.get("description", "")))
color_widget = QLineEdit(details.get("color", ""))
color_button = QPushButton("Pick Color...")
color_button.clicked.connect(lambda checked, w=color_widget: self.pick_color(w))
h_layout = QHBoxLayout()
h_layout.addWidget(color_widget)
h_layout.addWidget(color_button)
cell_widget = QWidget()
cell_widget.setLayout(h_layout)
table.setCellWidget(row, 2, cell_widget)
row += 1
table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(table)
self.widgets["DEFINITION_SETTINGS.FILE_TYPE_DEFINITIONS"] = table # Store table reference
def create_image_resolutions_widget(self, layout, resolutions_data):
"""Creates a widget for editing image resolutions."""
table = QTableWidget()
table.setColumnCount(2) # Width, Height
table.setHorizontalHeaderLabels(["Width", "Height"])
table.setRowCount(len(resolutions_data))
for row, resolution in enumerate(resolutions_data):
table.setItem(row, 0, QTableWidgetItem(str(resolution[0])))
table.setItem(row, 1, QTableWidgetItem(str(resolution[1])))
table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(table)
self.widgets["IMAGE_PROCESSING_SETTINGS.IMAGE_RESOLUTIONS"] = table # Store table reference
def create_map_bit_depth_rules_widget(self, layout, rules_data: dict):
"""Creates a widget for editing map bit depth rules (Map Type -> Rule)."""
table = QTableWidget()
table.setColumnCount(2) # Map Type, Rule
table.setHorizontalHeaderLabels(["Map Type", "Rule (respect/force_8bit)"])
table.setRowCount(len(rules_data))
# Iterate through dictionary items (key-value pairs)
for row, (map_type, rule_string) in enumerate(rules_data.items()):
table.setItem(row, 0, QTableWidgetItem(map_type))
# Optionally use a ComboBox for the rule selection later
table.setItem(row, 1, QTableWidgetItem(str(rule_string)))
table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(table)
# Store reference using a more specific key if needed, or handle in save_settings
self.widgets["MAP_BIT_DEPTH_RULES_TABLE"] = table # Use a distinct key for the table widget
def create_map_merge_rules_widget(self, layout, rules_data):
"""Creates a widget for editing map merge rules."""
# This is a more complex structure (list of dicts)
# Using a ListWidget to select rules and a separate form to edit details
h_layout = QHBoxLayout()
layout.addLayout(h_layout)
self.merge_rules_list = QListWidget()
self.merge_rules_list.currentItemChanged.connect(self.display_merge_rule_details)
h_layout.addWidget(self.merge_rules_list, 1) # Give list more space
self.merge_rule_details_group = QGroupBox("Rule Details")
self.merge_rule_details_layout = QFormLayout(self.merge_rule_details_group)
h_layout.addWidget(self.merge_rule_details_group, 2) # Give details form more space
self.merge_rule_widgets = {} # Widgets for the currently displayed rule
self.populate_merge_rules_list(rules_data)
self.widgets["IMAGE_PROCESSING_SETTINGS.MAP_MERGE_RULES"] = rules_data # Store original data reference
def populate_merge_rules_list(self, rules_data):
"""Populates the list widget with map merge rules."""
self.merge_rules_list.clear()
for rule in rules_data:
item = QListWidgetItem(rule.get("output_name", "Unnamed Rule"))
item.setData(Qt.UserRole, rule) # Store the rule dictionary in the item
self.merge_rules_list.addItem(item)
def display_merge_rule_details(self, current, previous):
"""Displays details of the selected merge rule."""
# Clear previous widgets
for i in reversed(range(self.merge_rule_details_layout.count())):
widget_item = self.merge_rule_details_layout.itemAt(i)
if widget_item:
widget = widget_item.widget()
if widget:
widget.deleteLater()
layout = widget_item.layout()
if layout:
# Recursively delete widgets in layout
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
elif item.layout():
# Handle nested layouts if necessary
pass # For simplicity, assuming no deeply nested layouts here
self.merge_rule_widgets.clear()
if current:
rule_data = current.data(Qt.UserRole)
if rule_data:
for key, value in rule_data.items():
label = QLabel(key.replace('_', ' ').title() + ":")
if isinstance(value, str):
widget = QLineEdit(value)
elif isinstance(value, (int, float)):
if isinstance(value, int):
widget = QSpinBox()
widget.setRange(-2147483648, 2147483647)
widget.setValue(value)
else:
widget = QDoubleSpinBox()
widget.setRange(-1.7976931348623157e+308, 1.7976931348623157e+308)
widget.setValue(value)
elif isinstance(value, bool):
widget = QCheckBox()
widget.setChecked(value)
elif isinstance(value, list):
# Assuming list of strings or simple types for now
widget = QLineEdit(", ".join(map(str, value)))
elif isinstance(value, dict):
# Assuming simple key-value dicts for now
widget = QLineEdit(json.dumps(value)) # Display as JSON string
else:
widget = QLabel(f"Unsupported type: {type(value)}")
self.merge_rule_details_layout.addRow(label, widget)
self.merge_rule_widgets[key] = widget # Store widget reference
def populate_widgets(self):
"""Populates the created widgets with loaded settings (for simple types)."""
# This method is less critical with the recursive create_widgets_for_section
# but could be used for specific post-creation population if needed.
pass
def browse_path(self, widget, key):
"""Opens a file or directory dialog based on the setting key."""
if 'DIR' in key.upper():
path = QFileDialog.getExistingDirectory(self, "Select Directory", widget.text())
else:
path, _ = QFileDialog.getOpenFileName(self, "Select File", widget.text())
if path:
widget.setText(path)
def pick_color(self, widget):
"""Opens a color dialog and sets the selected color in the widget."""
color = QColorDialog.getColor(QColor(widget.text()))
if color.isValid():
widget.setText(color.name()) # Get color as hex string
def save_settings(self):
"""Reads values from widgets and saves them to the configuration file."""
new_settings = {}
# Reconstruct the settings dictionary from widgets
# This requires iterating through the widgets and mapping them back
# to the original structure. This is a simplified approach and might
# need refinement for complex nested structures or dynamic lists/tables.
# Start with a deep copy of the original settings structure to preserve
# sections/keys that might not have dedicated widgets (though ideally all should)
import copy
new_settings = copy.deepcopy(self.settings)
# Iterate through the stored widgets and update the new_settings dictionary
for key, widget in self.widgets.items():
# Handle simple widgets
if isinstance(widget, (QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox)):
# Split the key to navigate the dictionary structure
keys = key.split('.')
current_dict = new_settings
for i, k in enumerate(keys):
if i == len(keys) - 1:
# This is the final key, update the value
if isinstance(widget, QLineEdit):
current_dict[k] = widget.text()
elif isinstance(widget, QSpinBox):
current_dict[k] = widget.value()
elif isinstance(widget, QDoubleSpinBox):
current_dict[k] = widget.value()
elif isinstance(widget, QCheckBox):
current_dict[k] = widget.isChecked()
else:
# Navigate to the next level
if k not in current_dict or not isinstance(current_dict[k], dict):
# This should not happen if create_tabs is correct, but handle defensively
print(f"Warning: Structure mismatch for key part '{k}' in '{key}'")
break # Stop processing this key
current_dict = current_dict[k]
# Handle TableWidgets (for definitions, resolutions, bit depth rules)
elif isinstance(widget, QTableWidget):
keys = key.split('.')
if len(keys) >= 2:
section_key = keys[0]
list_key = keys[1]
if section_key in new_settings and list_key in new_settings[section_key]:
if list_key == "ASSET_TYPE_DEFINITIONS":
new_definitions = {}
for row in range(widget.rowCount()):
asset_type_item = widget.item(row, 0)
description_item = widget.item(row, 1)
color_widget_container = widget.cellWidget(row, 2)
if asset_type_item and color_widget_container:
asset_type = asset_type_item.text()
description = description_item.text() if description_item else ""
color_widget = color_widget_container.findChild(QLineEdit)
if color_widget:
color = color_widget.text()
new_definitions[asset_type] = {"description": description, "color": color}
new_settings[section_key][list_key] = new_definitions
elif list_key == "FILE_TYPE_DEFINITIONS":
new_definitions = {}
for row in range(widget.rowCount()):
file_type_item = widget.item(row, 0)
description_item = widget.item(row, 1)
color_widget_container = widget.cellWidget(row, 2)
if file_type_item and color_widget_container:
file_type = file_type_item.text()
description = description_item.text() if description_item else ""
color_widget = color_widget_container.findChild(QLineEdit)
if color_widget:
color = color_widget.text()
new_definitions[file_type] = {"description": description, "color": color}
new_settings[section_key][list_key] = new_definitions
elif list_key == "IMAGE_RESOLUTIONS":
new_resolutions = []
for row in range(widget.rowCount()):
width_item = widget.item(row, 0)
height_item = widget.item(row, 1)
if width_item and height_item:
try:
width = int(width_item.text())
height = int(height_item.text())
new_resolutions.append([width, height])
except ValueError:
print(f"Warning: Invalid resolution value at row {row}")
new_settings[section_key][list_key] = new_resolutions
elif list_key == "MAP_BIT_DEPTH_RULES":
new_rules = []
for row in range(widget.rowCount()):
pattern_item = widget.item(row, 0)
bit_depth_item = widget.item(row, 1)
if pattern_item and bit_depth_item:
try:
bit_depth = int(bit_depth_item.text())
new_rules.append({"pattern": pattern_item.text(), "bit_depth": bit_depth})
except ValueError:
print(f"Warning: Invalid bit depth value at row {row}")
new_settings[section_key][list_key] = new_rules
# Handle Map Merge Rules (more complex)
# This requires reading from the details form for the currently selected item
# and updating the corresponding dictionary in the original list stored in self.widgets
elif key == "IMAGE_PROCESSING_SETTINGS.MAP_MERGE_RULES":
# The original list is stored in self.widgets["IMAGE_PROCESSING_SETTINGS.MAP_MERGE_RULES"]
# We need to iterate through the list widget items and update the corresponding
# dictionary in the list based on the details form if that item was selected and edited.
# A simpler approach for now is to just read the currently displayed rule details
# and update the corresponding item in the list widget's data, then reconstruct the list.
# Reconstruct the list from the list widget items' data
new_merge_rules = []
for i in range(self.merge_rules_list.count()):
item = self.merge_rules_list.item(i)
rule_data = item.data(Qt.UserRole)
if rule_data:
# If this item is the currently selected one, update its data from the details widgets
if item == self.merge_rules_list.currentItem():
updated_rule_data = {}
for detail_key, detail_widget in self.merge_rule_widgets.items():
if isinstance(detail_widget, QLineEdit):
updated_rule_data[detail_key] = detail_widget.text()
elif isinstance(detail_widget, QSpinBox):
updated_rule_data[detail_key] = detail_widget.value()
elif isinstance(detail_widget, QDoubleSpinBox):
updated_rule_data[detail_key] = detail_widget.value()
elif isinstance(detail_widget, QCheckBox):
updated_rule_data[detail_key] = detail_widget.isChecked()
# Add handling for other widget types in details form if needed
# Merge updated data with original data (in case some fields weren't in details form)
rule_data.update(updated_rule_data)
new_merge_rules.append(rule_data)
# Update the new_settings dictionary with the reconstructed list
keys = key.split('.')
if len(keys) == 2:
section_key = keys[0]
list_key = keys[1]
if section_key in new_settings and list_key in new_settings[section_key]:
new_settings[section_key][list_key] = new_merge_rules
# Save the new settings
try:
save_base_config(new_settings)
QMessageBox.information(self, "Settings Saved", "Configuration saved successfully.\nRestart the application to apply changes.")
self.accept() # Close the dialog
except Exception as e:
QMessageBox.critical(self, "Saving Error", f"Failed to save configuration: {e}")
# Example usage (for testing the dialog independently)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
dialog = ConfigEditorDialog()
dialog.exec_()
sys.exit(app.exec_())

View File

@ -2,7 +2,16 @@
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
from PySide6.QtCore import Qt, QModelIndex from PySide6.QtCore import Qt, QModelIndex
# Import the new config dictionaries # Import the new config dictionaries
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS from configuration import load_base_config # Import load_base_config
import json
import logging
import os # Added for path manipulation if needed, though json.dump handles creation
from PySide6.QtWidgets import QCompleter # Added QCompleter
# Configure logging
log = logging.getLogger(__name__)
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
class LineEditDelegate(QStyledItemDelegate): class LineEditDelegate(QStyledItemDelegate):
"""Delegate for editing string values using a QLineEdit.""" """Delegate for editing string values using a QLineEdit."""
@ -44,10 +53,16 @@ class ComboBoxDelegate(QStyledItemDelegate):
# Populate based on column using keys from config dictionaries # Populate based on column using keys from config dictionaries
items_keys = None items_keys = None
if column == 2: # Asset-Type Override (AssetRule) try:
items_keys = list(ASSET_TYPE_DEFINITIONS.keys()) base_config = load_base_config() # Load base config
elif column == 4: # Item-Type Override (FileRule) if column == 2: # Asset-Type Override (AssetRule)
items_keys = list(FILE_TYPE_DEFINITIONS.keys()) items_keys = list(base_config.get('ASSET_TYPE_DEFINITIONS', {}).keys()) # Access from base_config
elif column == 4: # Item-Type Override (FileRule)
items_keys = list(base_config.get('FILE_TYPE_DEFINITIONS', {}).keys()) # Access from base_config
except Exception as e:
log.error(f"Error loading base config for ComboBoxDelegate: {e}")
items_keys = [] # Fallback to empty list on error
if items_keys: if items_keys:
for item_key in sorted(items_keys): # Sort keys alphabetically for consistency for item_key in sorted(items_keys): # Sort keys alphabetically for consistency
@ -88,16 +103,6 @@ class ComboBoxDelegate(QStyledItemDelegate):
def updateEditorGeometry(self, editor, option, index): def updateEditorGeometry(self, editor, option, index):
# Ensures the editor widget is placed correctly within the cell. # Ensures the editor widget is placed correctly within the cell.
editor.setGeometry(option.rect) editor.setGeometry(option.rect)
# gui/delegates.py - New content to insert
import json
import logging
import os # Added for path manipulation if needed, though json.dump handles creation
from PySide6.QtWidgets import QCompleter # Added QCompleter
# Configure logging
log = logging.getLogger(__name__)
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
class SupplierSearchDelegate(QStyledItemDelegate): class SupplierSearchDelegate(QStyledItemDelegate):
""" """

View File

@ -39,22 +39,24 @@ if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
try: try:
from configuration import Configuration, ConfigurationError from configuration import Configuration, ConfigurationError, load_base_config # Import Configuration and load_base_config
from asset_processor import AssetProcessor, AssetProcessingError from asset_processor import AssetProcessor, AssetProcessingError
# from gui.processing_handler import ProcessingHandler # REMOVED Obsolete Handler # from gui.processing_handler import ProcessingHandler # REMOVED Obsolete Handler
from gui.prediction_handler import PredictionHandler from gui.prediction_handler import PredictionHandler
import config as core_config # Import the config module # Removed: import config as core_config # Import the config module
# PresetEditorDialog is no longer needed # PresetEditorDialog is no longer needed
except ImportError as e: except ImportError as e:
print(f"ERROR: Failed to import backend modules: {e}") print(f"ERROR: Failed to import backend modules: {e}")
print(f"Ensure GUI is run from project root or backend modules are in PYTHONPATH.") print(f"Ensure GUI is run from project root or backend modules are in PYTHONPATH.")
Configuration = None Configuration = None
load_base_config = None # Set to None if import fails
ConfigurationError = Exception
AssetProcessor = None AssetProcessor = None
# ProcessingHandler = None # REMOVED Obsolete Handler # ProcessingHandler = None # REMOVED Obsolete Handler
PredictionHandler = None PredictionHandler = None
ConfigurationError = Exception
AssetProcessingError = Exception AssetProcessingError = Exception
# --- Constants --- # --- Constants ---
PRESETS_DIR = project_root / "presets" PRESETS_DIR = project_root / "presets"
TEMPLATE_PATH = PRESETS_DIR / "_template.json" TEMPLATE_PATH = PRESETS_DIR / "_template.json"
@ -75,7 +77,7 @@ class QtLogHandler(logging.Handler, QObject):
log_record_received = Signal(str) # Signal emitting the formatted log string log_record_received = Signal(str) # Signal emitting the formatted log string
def __init__(self, parent=None): def __init__(self, parent=None):
logging.Handler.__init__(self) logging.Handler.__init__(self) # Call parent Handler init (level is optional)
QObject.__init__(self, parent) # Initialize QObject part QObject.__init__(self, parent) # Initialize QObject part
def emit(self, record): def emit(self, record):
@ -373,16 +375,23 @@ class MainWindow(QMainWindow):
# --- Set Initial Output Path --- # --- Set Initial Output Path ---
try: try:
output_base_dir_config = getattr(core_config, 'OUTPUT_BASE_DIR', '../Asset_Processor_Output') # Default if not found # Use load_base_config to get the default output directory
base_config = load_base_config()
output_base_dir_config = base_config.get('OUTPUT_BASE_DIR', '../Asset_Processor_Output') # Default if not found
# Resolve the path relative to the project root # Resolve the path relative to the project root
default_output_dir = (project_root / output_base_dir_config).resolve() default_output_dir = (project_root / output_base_dir_config).resolve()
self.output_path_edit.setText(str(default_output_dir)) self.output_path_edit.setText(str(default_output_dir))
log.info(f"Default output directory set to: {default_output_dir}") log.info(f"Default output directory set to: {default_output_dir}")
except ConfigurationError as e:
log.error(f"Error reading base configuration for default output directory: {e}")
self.output_path_edit.setText("") # Clear on error
self.statusBar().showMessage(f"Error setting default output path: {e}", 5000)
except Exception as e: except Exception as e:
log.error(f"Error setting default output directory: {e}") log.exception(f"Error setting default output directory: {e}")
self.output_path_edit.setText("") # Clear on error self.output_path_edit.setText("") # Clear on error
self.statusBar().showMessage(f"Error setting default output path: {e}", 5000) self.statusBar().showMessage(f"Error setting default output path: {e}", 5000)
# --- Drag and Drop Area --- # --- Drag and Drop Area ---
self.drag_drop_area = QFrame() self.drag_drop_area = QFrame()
self.drag_drop_area.setFrameShape(QFrame.Shape.StyledPanel) self.drag_drop_area.setFrameShape(QFrame.Shape.StyledPanel)
@ -466,13 +475,18 @@ class MainWindow(QMainWindow):
# Initialize paths from config # Initialize paths from config
try: try:
default_ng_path = getattr(core_config, 'DEFAULT_NODEGROUP_BLEND_PATH', '') # Use load_base_config to get default Blender paths
default_mat_path = getattr(core_config, 'DEFAULT_MATERIALS_BLEND_PATH', '') base_config = load_base_config()
default_ng_path = base_config.get('DEFAULT_NODEGROUP_BLEND_PATH', '')
default_mat_path = base_config.get('DEFAULT_MATERIALS_BLEND_PATH', '')
self.nodegroup_blend_path_input.setText(default_ng_path if default_ng_path else "") self.nodegroup_blend_path_input.setText(default_ng_path if default_ng_path else "")
self.materials_blend_path_input.setText(default_mat_path if default_mat_path else "") self.materials_blend_path_input.setText(default_mat_path if default_mat_path else "")
except ConfigurationError as e:
log.error(f"Error reading base configuration for default Blender paths: {e}")
except Exception as e: except Exception as e:
log.error(f"Error reading default Blender paths from config: {e}") log.error(f"Error reading default Blender paths from config: {e}")
# Disable Blender controls initially if checkbox is unchecked # Disable Blender controls initially if checkbox is unchecked
self.nodegroup_blend_path_input.setEnabled(False) self.nodegroup_blend_path_input.setEnabled(False)
self.browse_nodegroup_blend_button.setEnabled(False) self.browse_nodegroup_blend_button.setEnabled(False)
@ -1001,14 +1015,22 @@ class MainWindow(QMainWindow):
self._finalize_model_update() self._finalize_model_update()
else: else:
# Update status about remaining items # Update status about remaining items
remaining_count = len(self._pending_predictions) completed_count = len(self._accumulated_rules)
self.statusBar().showMessage(f"Prediction failed/finished for {Path(input_path).name}. Waiting for {remaining_count} more...", 5000) 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"Prediction finished for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_requested} requested)..."
self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg)
else: else:
log.debug(f"Prediction finished for '{input_path}', which was already processed.") log.debug(f"Prediction finished for '{input_path}', which was already processed.")
# Original status message might be misleading now, handled by accumulation logic. # Original status message might be misleading now, handled by accumulation logic.
# self.statusBar().showMessage("Preview updated.", 3000) # Removed # self.statusBar().showMessage("Preview updated.", 3000) # Removed
@Slot(str, str, str) @Slot(str, str, str)
def update_file_status(self, input_path_str, status, message): def update_file_status(self, input_path_str, status, message):
# TODO: Update status bar or potentially find rows in table later # TODO: Update status bar or potentially find rows in table later
@ -1220,17 +1242,18 @@ class MainWindow(QMainWindow):
self.editor_list_model_patterns.addItems(category_rules.get("model_patterns", [])) self.editor_list_model_patterns.addItems(category_rules.get("model_patterns", []))
self.editor_list_decal_keywords.clear() self.editor_list_decal_keywords.clear()
self.editor_list_decal_keywords.addItems(category_rules.get("decal_keywords", [])) self.editor_list_decal_keywords.addItems(category_rules.get("decal_keywords", []))
self.editor_table_archetype_rules.setRowCount(0) preset_data["asset_category_rules"] = category_rules
arch_rules = preset_data.get("archetype_rules", []) arch_rules = []
for i, rule in enumerate(arch_rules): for r in range(self.editor_table_archetype_rules.rowCount()):
if isinstance(rule, (list, tuple)) and len(rule) == 2: name_item = self.editor_table_archetype_rules.item(r, 0)
arch_name, conditions = rule any_item = self.editor_table_archetype_rules.item(r, 1)
match_any = ", ".join(conditions.get("match_any", [])) all_item = self.editor_table_archetype_rules.item(r, 2)
match_all = ", ".join(conditions.get("match_all", [])) if name_item and any_item and all_item:
self.editor_table_archetype_rules.insertRow(i) match_any = [k.strip() for k in any_item.text().split(',') if k.strip()]
self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(arch_name)) match_all = [k.strip() for k in all_item.text().split(',') if k.strip()]
self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(match_any)) arch_rules.append([name_item.text().strip(), {"match_any": match_any, "match_all": match_all}])
self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(match_all)) preset_data["archetype_rules"] = arch_rules
return preset_data
finally: finally:
self._is_loading_editor = False self._is_loading_editor = False
@ -1424,15 +1447,17 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self, "Error", f"Could not load template preset file:\n{TEMPLATE_PATH}\n\nError: {e}") QMessageBox.critical(self, "Error", f"Could not load template preset file:\n{TEMPLATE_PATH}\n\nError: {e}")
self._clear_editor() self._clear_editor()
self.setWindowTitle("Asset Processor Tool - New Preset*") self.setWindowTitle("Asset Processor Tool - New Preset*")
self.editor_supplier_name.setText("MySupplier") # Set a default supplier name
else: else:
log.warning("Presets/_template.json not found. Creating empty preset.") log.warning("Presets/_template.json not found. Creating empty preset.")
self.setWindowTitle("Asset Processor Tool - New Preset*") self.setWindowTitle("Asset Processor Tool - New Preset*")
self.editor_preset_name.setText("NewPreset") self.editor_preset_name.setText("NewPreset")
self.editor_supplier_name.setText("MySupplier") self.editor_supplier_name.setText("MySupplier") # Set a default supplier name
self._set_editor_enabled(True) self._set_editor_enabled(True)
self.editor_unsaved_changes = True self.editor_unsaved_changes = True
self.editor_save_button.setEnabled(True) self.editor_save_button.setEnabled(True)
def _delete_selected_preset(self): def _delete_selected_preset(self):
"""Deletes the currently selected preset file from the editor list after confirmation.""" """Deletes the currently selected preset file from the editor list after confirmation."""
current_item = self.editor_preset_list.currentItem() current_item = self.editor_preset_list.currentItem()
@ -1455,8 +1480,22 @@ class MainWindow(QMainWindow):
# --- Menu Bar Setup --- # --- Menu Bar Setup ---
def setup_menu_bar(self): def setup_menu_bar(self):
"""Creates the main menu bar and View menu.""" """Creates the main menu bar and adds menus/actions."""
self.menu_bar = self.menuBar() self.menu_bar = self.menuBar()
# --- File Menu (Optional, add if needed later) ---
# file_menu = self.menu_bar.addMenu("&File")
# Add actions like New, Open, Save, Exit
# --- Edit Menu ---
edit_menu = self.menu_bar.addMenu("&Edit")
# Preferences/Settings Action
self.preferences_action = QAction("&Preferences...", self)
self.preferences_action.triggered.connect(self._open_config_editor)
edit_menu.addAction(self.preferences_action)
# --- View Menu ---
view_menu = self.menu_bar.addMenu("&View") view_menu = self.menu_bar.addMenu("&View")
# Log Console Action # Log Console Action
@ -1495,6 +1534,23 @@ class MainWindow(QMainWindow):
log.info("UI Log Handler Initialized.") # Log that the handler is ready log.info("UI Log Handler Initialized.") # Log that the handler is ready
# --- Slots for Menu Actions and Logging --- # --- Slots for Menu Actions and Logging ---
@Slot()
def _open_config_editor(self):
"""Opens the configuration editor dialog."""
log.debug("Opening configuration editor dialog.")
try:
from .config_editor_dialog import ConfigEditorDialog # Import locally to avoid circular dependency if needed
dialog = ConfigEditorDialog(self)
dialog.exec_() # Use exec_() to run as a modal dialog
log.debug("Configuration editor dialog closed.")
except ImportError:
log.error("Failed to import ConfigEditorDialog. Ensure gui/config_editor_dialog.py exists and is accessible.")
QMessageBox.critical(self, "Error", "Could not open configuration editor.\nRequired file not found or has errors.")
except Exception as e:
log.exception(f"Error opening configuration editor dialog: {e}")
QMessageBox.critical(self, "Error", f"An error occurred while opening the configuration editor:\n{e}")
@Slot(bool) @Slot(bool)
def _toggle_log_console_visibility(self, checked): def _toggle_log_console_visibility(self, checked):
"""Shows or hides the log console widget based on menu action.""" """Shows or hides the log console widget based on menu action."""
@ -1604,8 +1660,11 @@ class MainWindow(QMainWindow):
# Update status bar with progress # Update status bar with progress
completed_count = len(self._accumulated_rules) completed_count = len(self._accumulated_rules)
pending_count = len(self._pending_predictions) pending_count = len(self._pending_predictions)
total_count = completed_count + pending_count # This might be slightly off if some failed without rules # total_count = completed_count + pending_count # This might be slightly off if some failed without rules
status_msg = f"Preview updated for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_count} requested)..." # 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)..."
self.statusBar().showMessage(status_msg, 5000) self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg) log.debug(status_msg)

View File

@ -21,23 +21,24 @@ if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
try: try:
from configuration import Configuration, ConfigurationError from configuration import Configuration, ConfigurationError, load_base_config # Import Configuration, ConfigurationError, and load_base_config
# AssetProcessor might not be needed directly anymore if logic is moved here # AssetProcessor might not be needed directly anymore if logic is moved here
# from asset_processor import AssetProcessor, AssetProcessingError # from asset_processor import AssetProcessor, AssetProcessingError
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
import config as app_config # Import project's config module # Removed: import config as app_config # Import project's config module
# Import the new dictionaries directly for easier access # Removed: Import the new dictionaries directly for easier access
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS # Removed: from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
BACKEND_AVAILABLE = True BACKEND_AVAILABLE = True
except ImportError as e: except ImportError as e:
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}") print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
# Define placeholders if imports fail # Define placeholders if imports fail
Configuration = None Configuration = None
# AssetProcessor = None load_base_config = None # Placeholder
ConfigurationError = Exception ConfigurationError = Exception
# AssetProcessingError = Exception # AssetProcessingError = Exception
SourceRule, AssetRule, FileRule, AssetType, ItemType = (None,)*5 # Placeholder for rule structures SourceRule, AssetRule, FileRule = (None,)*3 # Placeholder for rule structures
app_config = None # Placeholder for config # Removed: AssetType, ItemType = (None,)*2 # Placeholder for types
# Removed: app_config = None # Placeholder for config
BACKEND_AVAILABLE = False BACKEND_AVAILABLE = False
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -76,8 +77,9 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
if not file_list or not config: if not file_list or not config:
log.warning("Classification skipped: Missing file list or config.") log.warning("Classification skipped: Missing file list or config.")
return {} return {}
# Access compiled regex directly from the config object
if not hasattr(config, 'compiled_map_keyword_regex') or not config.compiled_map_keyword_regex: if not hasattr(config, 'compiled_map_keyword_regex') or not config.compiled_map_keyword_regex:
log.warning("Classification skipped: Missing compiled map keyword regex.") log.warning("Classification skipped: Missing compiled map keyword regex in config.")
# Don't return yet, might still find extras # Don't return yet, might still find extras
if not hasattr(config, 'compiled_extra_regex'): if not hasattr(config, 'compiled_extra_regex'):
log.warning("Configuration object missing 'compiled_extra_regex'. Cannot classify extra files.") log.warning("Configuration object missing 'compiled_extra_regex'. Cannot classify extra files.")
@ -119,12 +121,14 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
# Access the full rule details directly from the config's map_type_mapping list using the index # Access the full rule details directly from the config's map_type_mapping list using the index
matched_rule_details = None matched_rule_details = None
try: try:
matched_rule_details = config.map_type_mapping[rule_index] # Access rule by index # Access map_type_mapping using the property
map_type_mapping_list = config.map_type_mapping # Use the property
matched_rule_details = map_type_mapping_list[rule_index] # Access rule by index
is_gloss_flag = matched_rule_details.get('is_gloss_source', False) # Get flag or default False is_gloss_flag = matched_rule_details.get('is_gloss_source', False) # Get flag or default False
log.debug(f" Associated rule details: {matched_rule_details}") log.debug(f" Associated rule details: {matched_rule_details}")
log.debug(f" 'is_gloss_source' flag from rule: {is_gloss_flag}") log.debug(f" 'is_gloss_source' flag from rule: {is_gloss_flag}")
except IndexError: except IndexError:
log.warning(f" Could not access map_type_mapping rule at index {rule_index}. Cannot determine 'is_gloss_source' flag.") log.warning(f" Could not access map_type_mapping rule at index {rule_index} in config.settings. Cannot determine 'is_gloss_source' flag.")
is_gloss_flag = False # Default if rule cannot be accessed is_gloss_flag = False # Default if rule cannot be accessed
# --- End DEBUG LOG --- # --- End DEBUG LOG ---
matched_item_type = target_type # The standard type (e.g., MAP_COL) matched_item_type = target_type # The standard type (e.g., MAP_COL)
@ -269,19 +273,19 @@ class PredictionHandler(QObject):
self.status_message.emit(f"Analyzing '{source_path.name}'...", 0) self.status_message.emit(f"Analyzing '{source_path.name}'...", 0)
config: Configuration | None = None config: Configuration | None = None
asset_type_definitions: Dict[str, Dict] = {} # Removed: asset_type_definitions: Dict[str, Dict] = {}
file_type_definitions: Dict[str, Dict] = {} # These are ItemType names # Removed: file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
try: try:
config = Configuration(preset_name) config = Configuration(preset_name)
# Load allowed types from the project's config module (now dictionaries) # Removed: Load allowed types from the project's config module (now dictionaries)
if app_config: # Removed: if app_config:
asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {}) # Removed: asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {}) # Removed: file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}") # Removed: log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}") # Removed: log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
else: # Removed: else:
log.warning("Project config module not loaded. Cannot get type definitions.") # Removed: log.warning("Project config module not loaded. Cannot get type definitions.")
except ConfigurationError as e: except ConfigurationError as e:
log.error(f"Failed to load configuration for preset '{preset_name}': {e}") log.error(f"Failed to load configuration for preset '{preset_name}': {e}")
@ -331,6 +335,10 @@ class PredictionHandler(QObject):
log.debug(f"Created SourceRule for identifier: {input_source_identifier} with supplier: {supplier_identifier}") log.debug(f"Created SourceRule for identifier: {input_source_identifier} with supplier: {supplier_identifier}")
asset_rules = [] asset_rules = []
# Get allowed asset types from config's internal core settings
asset_type_definitions = config._core_settings.get('ASSET_TYPE_DEFINITIONS', {})
log.debug(f"Loaded AssetType Definitions from config: {list(asset_type_definitions.keys())}")
for asset_name, files_info in classified_assets.items(): for asset_name, files_info in classified_assets.items():
if not files_info: continue # Skip empty asset groups if not files_info: continue # Skip empty asset groups
@ -349,9 +357,10 @@ class PredictionHandler(QObject):
# Ensure the predicted type is allowed, fallback if necessary # Ensure the predicted type is allowed, fallback if necessary
# Now predicted_asset_type is already a string # Now predicted_asset_type is already a string
if asset_type_definitions and predicted_asset_type not in asset_type_definitions: if asset_type_definitions and predicted_asset_type not in asset_type_definitions:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. Falling back.") log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS from config. Falling back.")
# Fallback logic: use the default from config if allowed, else first allowed type # Fallback logic: use the default from config if allowed, else first allowed type
default_type = getattr(app_config, 'DEFAULT_ASSET_CATEGORY', 'Surface') # Access DEFAULT_ASSET_CATEGORY using the property
default_type = config.default_asset_category # Use the property
if default_type in asset_type_definitions: if default_type in asset_type_definitions:
predicted_asset_type = default_type predicted_asset_type = default_type
elif asset_type_definitions: elif asset_type_definitions:
@ -368,20 +377,38 @@ class PredictionHandler(QObject):
log.debug(f"Created AssetRule for asset: {asset_name} with type: {predicted_asset_type}") log.debug(f"Created AssetRule for asset: {asset_name} with type: {predicted_asset_type}")
file_rules = [] file_rules = []
# Get allowed file types from config's internal core settings
file_type_definitions = config._core_settings.get('FILE_TYPE_DEFINITIONS', {})
log.debug(f"Loaded FileType Definitions (ItemTypes) from config: {list(file_type_definitions.keys())}")
for file_info in files_info: for file_info in files_info:
# Determine FileRule level overrides/defaults # Determine FileRule level overrides/defaults
base_item_type = file_info['item_type'] # Type from classification (e.g., COL, NRM, EXTRA) base_item_type = file_info['item_type'] # Type from classification (e.g., COL, NRM, EXTRA)
target_asset_name_override = file_info['asset_name'] # From classification target_asset_name_override = file_info['asset_name'] # From classification
# Retrieve the standard_type from the config if available
standard_map_type = None
file_type_details = file_type_definitions.get(base_item_type)
if file_type_details:
standard_map_type = file_type_details.get('standard_type') # Try to get explicit standard_type
# If standard_type wasn't found in the definition, use the base_item_type itself
# (which is the alias in presets like Poliigon.json)
if standard_map_type is None and base_item_type in file_type_definitions: # Check base_item_type is a valid key
log.debug(f" No explicit 'standard_type' found for item type '{base_item_type}'. Using base_item_type itself as standard_map_type.")
standard_map_type = base_item_type # Fallback to using the base type (alias)
elif standard_map_type is None:
log.debug(f" No 'standard_type' found and base_item_type '{base_item_type}' not in definitions. Setting standard_map_type to None.")
# Determine the final item_type string (prefix maps, check if allowed) # Determine the final item_type string (prefix maps, check if allowed)
final_item_type = base_item_type # Start with the base type final_item_type = base_item_type # Start with the base type
if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]: if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]:
# Prefix map types that don't already have it # Prefix map types that don't already have it
final_item_type = f"MAP_{base_item_type}" final_item_type = f"MAP_{base_item_type}"
# Check if the final type is allowed (exists as a key in config) # Check if the final type is allowed (exists as a key in config settings)
if file_type_definitions and final_item_type not in file_type_definitions and base_item_type not in ["FILE_IGNORE", "EXTRA"]: if file_type_definitions and final_item_type not in file_type_definitions and base_item_type not in ["FILE_IGNORE", "EXTRA"]:
log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS. Setting base type to FILE_IGNORE.") log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS from config. Setting base type to FILE_IGNORE.")
final_item_type = "FILE_IGNORE" # Fallback base type to FILE_IGNORE string final_item_type = "FILE_IGNORE" # Fallback base type to FILE_IGNORE string
# Output format is determined by the engine, not predicted here. Leave as None. # Output format is determined by the engine, not predicted here. Leave as None.
@ -394,12 +421,37 @@ class PredictionHandler(QObject):
log.debug(f" Base Item Type (from classification): {base_item_type}") log.debug(f" Base Item Type (from classification): {base_item_type}")
log.debug(f" Final Item Type (for model): {final_item_type}") log.debug(f" Final Item Type (for model): {final_item_type}")
log.debug(f" Target Asset Name Override: {target_asset_name_override}") log.debug(f" Target Asset Name Override: {target_asset_name_override}")
# --- DETAILED DEBUG LOG: Inspect standard_map_type assignment ---
log.debug(f" DEBUG: Processing file: {file_info['file_path']}")
log.debug(f" DEBUG: base_item_type = {base_item_type}")
log.debug(f" DEBUG: file_type_definitions keys = {list(file_type_definitions.keys())}")
# --- Fix: Use final_item_type (prefixed) for lookup, fallback to base_item_type (alias) ---
standard_map_type = None
# Use final_item_type (e.g., "MAP_AO") for the lookup
file_type_details = file_type_definitions.get(final_item_type)
log.debug(f" DEBUG: file_type_definitions.get({final_item_type}) = {file_type_details}") # Log lookup result
if file_type_details:
# Try to get explicit standard_type (might still be missing in some presets)
standard_map_type = file_type_details.get('standard_type')
log.debug(f" DEBUG: Explicit standard_type from details = {standard_map_type}")
# If standard_type wasn't found in the definition, use the base_item_type (alias)
# This handles presets like Poliigon.json where the alias is the target_type
if standard_map_type is None and final_item_type in file_type_definitions: # Check if the prefixed type was valid
log.debug(f" No explicit 'standard_type' found for item type '{final_item_type}'. Using base_item_type ('{base_item_type}') as standard_map_type.")
standard_map_type = base_item_type # Fallback to using the base type (alias)
elif standard_map_type is None:
log.debug(f" Could not determine standard_map_type for base '{base_item_type}' / final '{final_item_type}'. Setting to None.")
# --- End Fix ---
log.debug(f" DEBUG: Final standard_map_type variable value = {standard_map_type}") # Log final value
# --- END DETAILED DEBUG LOG ---
# Explicitly check and log the flag value from file_info # Explicitly check and log the flag value from file_info
is_gloss_source_value = file_info.get('is_gloss_source', 'MISSING') # Get value or 'MISSING' is_gloss_source_value = file_info.get('is_gloss_source', 'MISSING') # Get value or 'MISSING'
log.debug(f" Value for 'is_gloss_source' from file_info: {is_gloss_source_value}") log.debug(f" Value for 'is_gloss_source' from file_info: {is_gloss_source_value}")
# --- End DEBUG LOG --- # --- End DEBUG LOG ---
# Pass the retrieved flag value to the constructor
# Pass the retrieved flag value and standard_map_type to the constructor
file_rule = FileRule( file_rule = FileRule(
file_path=file_info['file_path'], # This is static info based on input file_path=file_info['file_path'], # This is static info based on input
item_type=final_item_type, # Set the new base item_type field item_type=final_item_type, # Set the new base item_type field
@ -409,6 +461,7 @@ class PredictionHandler(QObject):
target_asset_name_override=target_asset_name_override, target_asset_name_override=target_asset_name_override,
output_format_override=output_format_override, output_format_override=output_format_override,
is_gloss_source=is_gloss_source_value if isinstance(is_gloss_source_value, bool) else False, # Pass the flag, ensure boolean is_gloss_source=is_gloss_source_value if isinstance(is_gloss_source_value, bool) else False, # Pass the flag, ensure boolean
standard_map_type=standard_map_type, # Assign the determined standard_map_type
# --- Leave Static Fields as Default/None --- # --- Leave Static Fields as Default/None ---
resolution_override=None, resolution_override=None,
channel_merge_instructions={}, channel_merge_instructions={},

View File

@ -26,15 +26,16 @@ try:
# Import the worker function from main.py # Import the worker function from main.py
from main import process_single_asset_wrapper from main import process_single_asset_wrapper
# Import exceptions if needed for type hinting or specific handling # Import exceptions if needed for type hinting or specific handling
from configuration import ConfigurationError from configuration import ConfigurationError, load_base_config # Import ConfigurationError and load_base_config
from asset_processor import AssetProcessingError from asset_processor import AssetProcessingError
import config as core_config # <<< ADDED IMPORT # Removed: import config as core_config # <<< ADDED IMPORT
BACKEND_AVAILABLE = True BACKEND_AVAILABLE = True
except ImportError as e: except ImportError as e:
print(f"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}") print(f"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}")
# Define placeholders if imports fail, so the GUI doesn't crash immediately # Define placeholders if imports fail, so the GUI doesn't crash immediately
process_single_asset_wrapper = None process_single_asset_wrapper = None
ConfigurationError = Exception ConfigurationError = Exception
load_base_config = None # Placeholder
AssetProcessingError = Exception AssetProcessingError = Exception
BACKEND_AVAILABLE = False BACKEND_AVAILABLE = False
@ -70,7 +71,11 @@ class ProcessingHandler(QObject):
def is_running(self): def is_running(self):
return self._is_running return self._is_running
def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int, # Removed _predict_single_asset method
@Slot(str, list, str, str, bool, int,
bool, str, str, bool, SourceRule) # Explicitly define types for the slot
def run_processing(self, input_source_identifier: str, original_input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,
run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str, verbose: bool, rules: SourceRule): # <<< ADDED verbose PARAM run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str, verbose: bool, rules: SourceRule): # <<< ADDED verbose PARAM
""" """
Starts the asset processing task and optionally runs Blender scripts afterwards. Starts the asset processing task and optionally runs Blender scripts afterwards.
@ -84,13 +89,13 @@ class ProcessingHandler(QObject):
if not BACKEND_AVAILABLE or not process_single_asset_wrapper: if not BACKEND_AVAILABLE or not process_single_asset_wrapper:
log.error("Backend modules or worker function not available. Cannot start processing.") log.error("Backend modules or worker function not available. Cannot start processing.")
self.status_message.emit("Error: Backend components missing. Cannot process.", 5000) self.status_message.emit("Error: Backend components missing. Cannot process.", 5000)
self.processing_finished.emit(0, 0, len(input_paths)) # Emit finished with all failed self.processing_finished.emit(0, 0, len(original_input_paths)) # Emit finished with all failed
return return
self._is_running = True self._is_running = True
self._cancel_requested = False self._cancel_requested = False
self._futures = {} # Reset futures self._futures = {} # Reset futures
total_files = len(input_paths) total_files = len(original_input_paths) # Use original_input_paths for total count
processed_count = 0 processed_count = 0
skipped_count = 0 skipped_count = 0
failed_count = 0 failed_count = 0
@ -105,9 +110,20 @@ class ProcessingHandler(QObject):
self._executor = executor # Store for potential cancellation self._executor = executor # Store for potential cancellation
# Submit tasks # Submit tasks
for input_path in input_paths: for input_path in original_input_paths: # Iterate through the list of input paths
if self._cancel_requested: break # Check before submitting more if self._cancel_requested: break # Check before submitting more
log.debug(f"Submitting task for: {input_path}") log.debug(f"Submitting task for: {input_path}")
# Pass the single SourceRule object to the worker
# --- DEBUG LOG: Inspect FileRule overrides before sending to worker ---
log.debug(f"ProcessingHandler: Inspecting rules for input '{input_path}' before submitting to worker:")
if rules: # Check if rules object exists
for asset_rule in rules.assets:
log.debug(f" Asset: {asset_rule.asset_name}")
for file_rule in asset_rule.files:
log.debug(f" File: {Path(file_rule.file_path).name}, ItemType: {file_rule.item_type}, Override: {file_rule.item_type_override}, StandardMap: {getattr(file_rule, 'standard_map_type', 'N/A')}")
else:
log.debug(" Rules object is None.")
# --- END DEBUG LOG ---
future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=verbose, rules=rules) # Pass verbose flag from GUI and rules future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=verbose, rules=rules) # Pass verbose flag from GUI and rules
self._futures[future] = input_path # Map future back to input path self._futures[future] = input_path # Map future back to input path
# Optionally emit "processing" status here # Optionally emit "processing" status here
@ -288,7 +304,14 @@ class ProcessingHandler(QObject):
def _find_blender_executable(self) -> Optional[str]: def _find_blender_executable(self) -> Optional[str]:
"""Finds the Blender executable path from config or system PATH.""" """Finds the Blender executable path from config or system PATH."""
try: try:
blender_exe_config = getattr(core_config, 'BLENDER_EXECUTABLE_PATH', None) # Use load_base_config to get the Blender executable path
if load_base_config:
base_config = load_base_config()
blender_exe_config = base_config.get('BLENDER_EXECUTABLE_PATH', None)
else:
blender_exe_config = None
log.warning("load_base_config not available. Cannot read BLENDER_EXECUTABLE_PATH from config.")
if blender_exe_config: if blender_exe_config:
p = Path(blender_exe_config) p = Path(blender_exe_config)
if p.is_file(): if p.is_file():
@ -306,6 +329,9 @@ class ProcessingHandler(QObject):
else: else:
log.warning("Could not find 'blender' in system PATH.") log.warning("Could not find 'blender' in system PATH.")
return None return None
except ConfigurationError as e:
log.error(f"Error reading base configuration for Blender executable path: {e}")
return None
except Exception as e: except Exception as e:
log.error(f"Error checking Blender executable path: {e}") log.error(f"Error checking Blender executable path: {e}")
return None return None

View File

@ -1,9 +1,11 @@
# gui/unified_view_model.py # gui/unified_view_model.py
import logging # Added for debugging
log = logging.getLogger(__name__) # Added for debugging
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal # Added Signal from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal # Added Signal
from PySide6.QtGui import QColor # Added for background role from PySide6.QtGui import QColor # Added for background role
from pathlib import Path # Added for file_name extraction from pathlib import Path # Added for file_name extraction
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS # Added for coloring from configuration import load_base_config # Import load_base_config
class UnifiedViewModel(QAbstractItemModel): class UnifiedViewModel(QAbstractItemModel):
# --- Color Constants for Row Backgrounds --- # --- Color Constants for Row Backgrounds ---
@ -133,10 +135,10 @@ class UnifiedViewModel(QAbstractItemModel):
if not parent.isValid(): if not parent.isValid():
# Parent is invisible root. Children are SourceRules. # Parent is invisible root. Children are SourceRules.
if row < len(self._source_rules): if row < len(self._source_rules):
child_item = self._source_rules[row] child_item = self._source_rules[row]
return self.createIndex(row, column, child_item) return self.createIndex(row, column, child_item)
else: else:
return QModelIndex() # Row out of bounds for top-level items return QModelIndex() # Row out of bounds for top-level items
else: else:
# Parent is a valid index, get its item # Parent is a valid index, get its item
parent_item = parent.internalPointer() parent_item = parent.internalPointer()
@ -181,47 +183,57 @@ class UnifiedViewModel(QAbstractItemModel):
# Determine effective asset type # Determine effective asset type
asset_type = item.asset_type_override if item.asset_type_override else item.asset_type asset_type = item.asset_type_override if item.asset_type_override else item.asset_type
if asset_type: if asset_type:
type_info = ASSET_TYPE_DEFINITIONS.get(asset_type) try:
if type_info: base_config = load_base_config() # Load base config
hex_color = type_info.get("color") asset_type_definitions = base_config.get('ASSET_TYPE_DEFINITIONS', {}) # Get definitions
if hex_color: type_info = asset_type_definitions.get(asset_type)
try: if type_info:
return QColor(hex_color) hex_color = type_info.get("color")
except ValueError: if hex_color:
# Optional: Add logging for invalid hex color try:
# print(f"Warning: Invalid hex color '{hex_color}' for asset type '{asset_type}' in config.") return QColor(hex_color)
return None # Fallback for invalid hex except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for asset type '{asset_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for asset type '{asset_type}' in config.")
return None # Fallback if color key missing
else: else:
# Optional: Add logging for missing color key # Optional: Add logging for missing asset type definition
# print(f"Warning: No color defined for asset type '{asset_type}' in config.") # print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
return None # Fallback if color key missing return None # Fallback if type not in config
else: except Exception: # Catch errors during config loading
# Optional: Add logging for missing asset type definition return None # Fallback on error
# print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
return None # Fallback if type not in config
else: else:
return None # Fallback if no asset_type determined return None # Fallback if no asset_type determined
elif isinstance(item, FileRule): elif isinstance(item, FileRule):
# Determine effective item type: Prioritize override, then use base type # Determine effective item type: Prioritize override, then use base type
effective_item_type = item.item_type_override if item.item_type_override is not None else item.item_type effective_item_type = item.item_type_override if item.item_type_override is not None else item.item_type
if effective_item_type: if effective_item_type:
type_info = FILE_TYPE_DEFINITIONS.get(effective_item_type) try:
if type_info: base_config = load_base_config() # Load base config
hex_color = type_info.get("color") file_type_definitions = base_config.get('FILE_TYPE_DEFINITIONS', {}) # Get definitions
if hex_color: type_info = file_type_definitions.get(effective_item_type)
try: if type_info:
return QColor(hex_color) hex_color = type_info.get("color")
except ValueError: if hex_color:
# Optional: Add logging for invalid hex color try:
# print(f"Warning: Invalid hex color '{hex_color}' for file type '{item_type}' in config.") return QColor(hex_color)
return None # Fallback for invalid hex except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for file type '{item_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for file type '{item_type}' in config.")
return None # Fallback if color key missing
else: else:
# Optional: Add logging for missing color key # File types often don't have specific colors, so no warning needed unless debugging
# print(f"Warning: No color defined for file type '{item_type}' in config.") return None # Fallback if type not in config
return None # Fallback if color key missing except Exception: # Catch errors during config loading
else: return None # Fallback on error
# File types often don't have specific colors, so no warning needed unless debugging
return None # Fallback if type not in config
else: else:
return None # Fallback if no item_type determined return None # Fallback if no item_type determined
else: # Other item types or if item is None else: # Other item types or if item is None
@ -258,7 +270,14 @@ class UnifiedViewModel(QAbstractItemModel):
if column == self.COL_TARGET_ASSET: if column == self.COL_TARGET_ASSET:
return item.target_asset_name_override if item.target_asset_name_override is not None else "" return item.target_asset_name_override if item.target_asset_name_override is not None else ""
if column == self.COL_ITEM_TYPE: if column == self.COL_ITEM_TYPE:
return item.item_type_override if item.item_type_override else "" # Reverted Logic: Display override if set, otherwise base type. Shows prefixed keys.
override = item.item_type_override
initial_type = item.item_type
if override is not None:
return override
else:
return initial_type if initial_type else ""
if column == self.COL_STATUS: return "" # Status (Not handled yet) if column == self.COL_STATUS: return "" # Status (Not handled yet)
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet) if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
elif role == Qt.EditRole: elif role == Qt.EditRole:
@ -436,8 +455,47 @@ class UnifiedViewModel(QAbstractItemModel):
if new_value == "": new_value = None # Treat empty string as None if new_value == "": new_value = None # Treat empty string as None
# Update item_type_override # Update item_type_override
if item.item_type_override != new_value: if item.item_type_override != new_value:
item.item_type_override = new_value log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Original Override='{item.item_type_override}', Original Standard='{getattr(item, 'standard_map_type', 'N/A')}', New Value='{new_value}'") # DEBUG LOG - Added getattr for safety
changed = True old_override = item.item_type_override # Store old value for logging
item.item_type_override = new_value
changed = True
# --- BEGIN FIX: Update standard_map_type ---
try:
base_config = load_base_config()
file_type_definitions = base_config.get('FILE_TYPE_DEFINITIONS', {})
# Determine the type to look up (override first, then original)
type_to_lookup = new_value if new_value is not None else item.item_type
new_standard_type = None
if type_to_lookup:
type_info = file_type_definitions.get(type_to_lookup)
if type_info:
new_standard_type = type_info.get("standard_type")
# If standard_type itself is missing in the definition, treat as None or keep old? Let's default to None.
if new_standard_type is None:
log.warning(f"setData: No 'standard_type' defined for item type '{type_to_lookup}' in FILE_TYPE_DEFINITIONS.")
else:
log.warning(f"setData: Item type '{type_to_lookup}' not found in FILE_TYPE_DEFINITIONS.")
# Fallback: Keep the existing standard_map_type if lookup fails completely
new_standard_type = getattr(item, 'standard_map_type', None)
else:
# If both override and original type are None, standard type should be None
new_standard_type = None
# Update the standard_map_type if it changed or needs setting
current_standard_type = getattr(item, 'standard_map_type', None)
if current_standard_type != new_standard_type:
item.standard_map_type = new_standard_type
log.debug(f"setData: Updated standard_map_type from '{current_standard_type}' to '{new_standard_type}' for file '{Path(item.file_path).name}' based on type '{type_to_lookup}'")
# No need to set 'changed = True' again, already set above
except Exception as e:
log.exception(f"setData: Error updating standard_map_type for file '{Path(item.file_path).name}': {e}")
# --- END FIX ---
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Final Override='{item.item_type_override}', Final Standard='{getattr(item, 'standard_map_type', 'N/A')}'") # DEBUG LOG - Updated
if changed: if changed:

17
main.py
View File

@ -25,7 +25,6 @@ try:
# from asset_processor import AssetProcessor, AssetProcessingError # REMOVED OLD PROCESSOR # from asset_processor import AssetProcessor, AssetProcessingError # REMOVED OLD PROCESSOR
from processing_engine import ProcessingEngine # <<< ADDED NEW ENGINE IMPORT from processing_engine import ProcessingEngine # <<< ADDED NEW ENGINE IMPORT
from rule_structure import SourceRule # Import SourceRule for type hinting from rule_structure import SourceRule # Import SourceRule for type hinting
import config as core_config_module
from gui.main_window import MainWindow # Import MainWindow from gui.main_window import MainWindow # Import MainWindow
except ImportError as e: except ImportError as e:
# Provide a more helpful error message if imports fail # Provide a more helpful error message if imports fail
@ -516,15 +515,19 @@ def run_cli(args): # Accept parsed args
# --- Determine Output Directory --- # --- Determine Output Directory ---
output_dir_str = args.output_dir # Get value from args (might be None) output_dir_str = args.output_dir # Get value from args (might be None)
if not output_dir_str: if not output_dir_str:
log.debug("Output directory not specified via -o, reading default from config.py.") log.debug("Output directory not specified via -o, reading default from app_settings.json via load_base_config().")
try: try:
output_dir_str = getattr(core_config_module, 'OUTPUT_BASE_DIR', None) base_config = load_base_config()
output_dir_str = base_config.get('OUTPUT_BASE_DIR')
if not output_dir_str: if not output_dir_str:
log.error("Output directory not specified with -o and OUTPUT_BASE_DIR not found or empty in config.py. Exiting.") log.error("Output directory not specified with -o and 'OUTPUT_BASE_DIR' not found or empty in app_settings.json. Exiting.")
sys.exit(1) sys.exit(1)
log.info(f"Using default output directory from config.py: {output_dir_str}") log.info(f"Using default output directory from app_settings.json: {output_dir_str}")
except ConfigurationError as e:
log.error(f"Error reading base configuration for OUTPUT_BASE_DIR: {e}")
sys.exit(1)
except Exception as e: except Exception as e:
log.error(f"Could not read OUTPUT_BASE_DIR from config.py: {e}") log.exception(f"Unexpected error reading base configuration for OUTPUT_BASE_DIR: {e}")
sys.exit(1) sys.exit(1)
# --- Resolve Output Path --- # --- Resolve Output Path ---
@ -585,7 +588,7 @@ def run_cli(args): # Accept parsed args
# file_list = _extract_file_list(input_path_str) # Need to define/import this helper # file_list = _extract_file_list(input_path_str) # Need to define/import this helper
# file_rules = [FileRule(file_path=f) for f in file_list] if file_list else [] # file_rules = [FileRule(file_path=f) for f in file_list] if file_list else []
# asset_rule = AssetRule(asset_name=asset_name, files=file_rules) # asset_rule = AssetRule(asset_name=asset_name, files=file_rules)
# rule = SourceRule(input_path=input_path_str, assets=[asset_rule], supplier_identifier=config.supplier_name) # rule = SourceRule(input_path=input_path_str, assets=[asset_rule], supplier_identifier=config.settings.get('supplier_identifier')) # Access from config object
# --- End Placeholder --- # --- End Placeholder ---
# --- TEMPORARY: Call engine process with just config and path --- # --- TEMPORARY: Call engine process with just config and path ---

View File

@ -965,9 +965,12 @@ class ProcessingEngine:
# Get bit depth rule solely from the static configuration using the correct method signature # Get bit depth rule solely from the static configuration using the correct method signature
bit_depth_rule = self.config_obj.get_bit_depth_rule(map_type) # Pass only map_type bit_depth_rule = self.config_obj.get_bit_depth_rule(map_type) # Pass only map_type
# Determine the map_type to use for saving (prioritize standard_map_type)
save_map_type = file_rule.standard_map_type if file_rule.standard_map_type else map_type
save_result = self._save_image( save_result = self._save_image(
image_data=img_resized, image_data=img_resized,
map_type=map_type, map_type=save_map_type, # Use the determined map type for saving
resolution_key=res_key, resolution_key=res_key,
asset_base_name=base_name, asset_base_name=base_name,
source_info=source_info, source_info=source_info,
@ -1048,14 +1051,14 @@ class ProcessingEngine:
found_rule_for_type = False found_rule_for_type = False
# Search in the asset_rule's files # Search in the asset_rule's files
for file_rule in asset_rule.files: for file_rule in asset_rule.files:
# Check if the file_rule's map_type matches the required input type # Check if the file_rule's standard_map_type matches the required input type
# Handle variants (e.g., ROUGH-1 should match ROUGH) # This uses the new alias system for exact matching
if file_rule.item_type_override and file_rule.item_type_override.startswith(input_type): # Check override exists and matches if hasattr(file_rule, 'standard_map_type') and file_rule.standard_map_type == input_type:
# TODO: Add prioritization logic if multiple files match (e.g., prefer non-gloss rough if gloss exists but isn't needed?) # TODO: Add prioritization logic if multiple files match (e.g., prefer non-gloss rough if gloss exists but isn't needed?)
# For now, take the first match. # For now, take the first match.
required_input_file_rules[input_type] = file_rule required_input_file_rules[input_type] = file_rule
found_rule_for_type = True found_rule_for_type = True
log.debug(f"Found source FileRule for merge input '{input_type}': {file_rule.file_path} (ItemTypeOverride: {file_rule.item_type_override})") # Gloss status checked during load log.debug(f"Found source FileRule for merge input '{input_type}': {file_rule.file_path} (StandardMapType: {file_rule.standard_map_type})") # Use standard_map_type in log
break # Found the first matching source for this input type break # Found the first matching source for this input type
if not found_rule_for_type: if not found_rule_for_type:
log.warning(f"Asset '{asset_name}': Required source FileRule for input map type '{input_type}' not found in AssetRule. Cannot perform merge for '{output_map_type}'.") log.warning(f"Asset '{asset_name}': Required source FileRule for input map type '{input_type}' not found in AssetRule. Cannot perform merge for '{output_map_type}'.")

View File

@ -1,6 +1,6 @@
import dataclasses import dataclasses
import json import json
from typing import List, Dict, Any, Tuple from typing import List, Dict, Any, Tuple, Optional
@dataclasses.dataclass @dataclasses.dataclass
class FileRule: class FileRule:
file_path: str = None file_path: str = None
@ -11,6 +11,7 @@ class FileRule:
channel_merge_instructions: Dict[str, Any] = dataclasses.field(default_factory=dict) channel_merge_instructions: Dict[str, Any] = dataclasses.field(default_factory=dict)
output_format_override: str = None # Potentially others identified during integration output_format_override: str = None # Potentially others identified during integration
is_gloss_source: bool = False # Added flag to indicate if source is glossiness is_gloss_source: bool = False # Added flag to indicate if source is glossiness
standard_map_type: Optional[str] = None # Added for map type unification
def to_json(self) -> str: def to_json(self) -> str:
return json.dumps(dataclasses.asdict(self), indent=4) return json.dumps(dataclasses.asdict(self), indent=4)