GUI FIXES
This commit is contained in:
parent
51ff45bd5c
commit
2f8bbc3a7d
@ -2,16 +2,19 @@
|
||||
|
||||
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.
|
||||
* `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.
|
||||
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.
|
||||
|
||||
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`)
|
||||
|
||||
@ -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.
|
||||
* 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.
|
||||
@ -12,20 +12,20 @@ python -m gui.main_window
|
||||
|
||||
## 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):**
|
||||
* **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 Editor Tabs:** Edit the details of the selected preset.
|
||||
* **Processing Panel (Right):**
|
||||
* **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.
|
||||
* **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.
|
||||
* **Simple View (Preview Disabled):** Lists only top-level input asset paths.
|
||||
* **Progress Bar:** Shows overall processing progress.
|
||||
* **Blender Post-Processing:** Checkbox to enable Blender scripts. If enabled, shows fields and browse buttons for target `.blend` files (defaults from `config.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):**
|
||||
* `Overwrite Existing`: Checkbox to force reprocessing.
|
||||
* `Workers`: Spinbox for concurrent processes.
|
||||
@ -33,3 +33,11 @@ python -m gui.main_window
|
||||
* `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.
|
||||
* **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.)*
|
||||
@ -4,19 +4,12 @@ This document provides technical details about the configuration system and the
|
||||
|
||||
## 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.
|
||||
|
||||
## Core Definitions in `config.py`
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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`)
|
||||
|
||||
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`.
|
||||
* **Loading:**
|
||||
* It loads the core settings from `config.py` using `importlib.util`.
|
||||
* It 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.
|
||||
* It first loads the base application settings from `config/app_settings.json`.
|
||||
* It then loads the specified preset JSON file from the `Presets/` directory.
|
||||
* **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).
|
||||
* **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`)
|
||||
|
||||
|
||||
@ -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.
|
||||
* **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.
|
||||
* **`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`).
|
||||
* **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.
|
||||
* **`ComboBoxDelegate`:** Used for selecting from predefined lists (e.g., allowed asset types, allowed file types sourced from the configuration loaded by `configuration.py`).
|
||||
* **`LineEditDelegate`:** Used for free-form text editing (e.g., target asset name override).
|
||||
* **`SupplierSearchDelegate`:** A new delegate used for the "Supplier" column. It provides a `QLineEdit` with auto-completion suggestions loaded from `config/suppliers.json`. It also handles adding new, unique supplier names entered by the user to the list and saving the updated list back to the JSON file.
|
||||
|
||||
@ -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.
|
||||
|
||||
## 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.
|
||||
339
config/app_settings.json
Normal file
339
config/app_settings.json
Normal 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_"
|
||||
}
|
||||
@ -2,17 +2,17 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import re # Import the regex module
|
||||
import json # Import the json module
|
||||
|
||||
log = logging.getLogger(__name__) # Use logger defined in main.py
|
||||
|
||||
# --- 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
|
||||
CORE_CONFIG_PATH = BASE_DIR / "config.py"
|
||||
APP_SETTINGS_PATH = BASE_DIR / "config" / "app_settings.json"
|
||||
PRESETS_DIR = BASE_DIR / "presets"
|
||||
|
||||
# --- Custom Exception ---
|
||||
@ -198,48 +198,19 @@ class Configuration:
|
||||
|
||||
|
||||
def _load_core_config(self) -> dict:
|
||||
"""Loads settings from the core config.py file."""
|
||||
log.debug(f"Loading core config from: {CORE_CONFIG_PATH}")
|
||||
if not CORE_CONFIG_PATH.is_file():
|
||||
raise ConfigurationError(f"Core configuration file not found: {CORE_CONFIG_PATH}")
|
||||
"""Loads settings from the core app_settings.json file."""
|
||||
log.debug(f"Loading core config from: {APP_SETTINGS_PATH}")
|
||||
if not APP_SETTINGS_PATH.is_file():
|
||||
raise ConfigurationError(f"Core configuration file not found: {APP_SETTINGS_PATH}")
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("core_config", CORE_CONFIG_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
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)
|
||||
|
||||
with open(APP_SETTINGS_PATH, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
log.debug(f"Core config loaded successfully.")
|
||||
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:
|
||||
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:
|
||||
"""Loads the specified preset JSON file."""
|
||||
@ -416,3 +387,41 @@ class Configuration:
|
||||
def get_8bit_output_format(self) -> str:
|
||||
"""Gets the format name for 8-bit output."""
|
||||
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
623
gui/config_editor_dialog.py
Normal 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_())
|
||||
@ -2,7 +2,16 @@
|
||||
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
|
||||
from PySide6.QtCore import Qt, QModelIndex
|
||||
# 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):
|
||||
"""Delegate for editing string values using a QLineEdit."""
|
||||
@ -44,10 +53,16 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
||||
|
||||
# Populate based on column using keys from config dictionaries
|
||||
items_keys = None
|
||||
try:
|
||||
base_config = load_base_config() # Load base config
|
||||
if column == 2: # Asset-Type Override (AssetRule)
|
||||
items_keys = list(ASSET_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(FILE_TYPE_DEFINITIONS.keys())
|
||||
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:
|
||||
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):
|
||||
# Ensures the editor widget is placed correctly within the cell.
|
||||
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):
|
||||
"""
|
||||
|
||||
@ -39,22 +39,24 @@ if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
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 gui.processing_handler import ProcessingHandler # REMOVED Obsolete Handler
|
||||
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
|
||||
except ImportError as 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.")
|
||||
Configuration = None
|
||||
load_base_config = None # Set to None if import fails
|
||||
ConfigurationError = Exception
|
||||
AssetProcessor = None
|
||||
# ProcessingHandler = None # REMOVED Obsolete Handler
|
||||
PredictionHandler = None
|
||||
ConfigurationError = Exception
|
||||
AssetProcessingError = Exception
|
||||
|
||||
|
||||
# --- Constants ---
|
||||
PRESETS_DIR = project_root / "presets"
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def emit(self, record):
|
||||
@ -373,15 +375,22 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# --- Set Initial Output Path ---
|
||||
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
|
||||
default_output_dir = (project_root / output_base_dir_config).resolve()
|
||||
self.output_path_edit.setText(str(default_output_dir))
|
||||
log.info(f"Default output directory set to: {default_output_dir}")
|
||||
except Exception as e:
|
||||
log.error(f"Error setting default output directory: {e}")
|
||||
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:
|
||||
log.exception(f"Error setting default output directory: {e}")
|
||||
self.output_path_edit.setText("") # Clear on error
|
||||
self.statusBar().showMessage(f"Error setting default output path: {e}", 5000)
|
||||
|
||||
|
||||
# --- Drag and Drop Area ---
|
||||
self.drag_drop_area = QFrame()
|
||||
@ -466,13 +475,18 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Initialize paths from config
|
||||
try:
|
||||
default_ng_path = getattr(core_config, 'DEFAULT_NODEGROUP_BLEND_PATH', '')
|
||||
default_mat_path = getattr(core_config, 'DEFAULT_MATERIALS_BLEND_PATH', '')
|
||||
# Use load_base_config to get default Blender paths
|
||||
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.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:
|
||||
log.error(f"Error reading default Blender paths from config: {e}")
|
||||
|
||||
|
||||
# Disable Blender controls initially if checkbox is unchecked
|
||||
self.nodegroup_blend_path_input.setEnabled(False)
|
||||
self.browse_nodegroup_blend_button.setEnabled(False)
|
||||
@ -1001,14 +1015,22 @@ class MainWindow(QMainWindow):
|
||||
self._finalize_model_update()
|
||||
else:
|
||||
# Update status about remaining items
|
||||
remaining_count = len(self._pending_predictions)
|
||||
self.statusBar().showMessage(f"Prediction failed/finished for {Path(input_path).name}. Waiting for {remaining_count} more...", 5000)
|
||||
completed_count = len(self._accumulated_rules)
|
||||
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:
|
||||
log.debug(f"Prediction finished for '{input_path}', which was already processed.")
|
||||
|
||||
# Original status message might be misleading now, handled by accumulation logic.
|
||||
# self.statusBar().showMessage("Preview updated.", 3000) # Removed
|
||||
|
||||
|
||||
@Slot(str, str, str)
|
||||
def update_file_status(self, input_path_str, status, message):
|
||||
# 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_decal_keywords.clear()
|
||||
self.editor_list_decal_keywords.addItems(category_rules.get("decal_keywords", []))
|
||||
self.editor_table_archetype_rules.setRowCount(0)
|
||||
arch_rules = preset_data.get("archetype_rules", [])
|
||||
for i, rule in enumerate(arch_rules):
|
||||
if isinstance(rule, (list, tuple)) and len(rule) == 2:
|
||||
arch_name, conditions = rule
|
||||
match_any = ", ".join(conditions.get("match_any", []))
|
||||
match_all = ", ".join(conditions.get("match_all", []))
|
||||
self.editor_table_archetype_rules.insertRow(i)
|
||||
self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(arch_name))
|
||||
self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(match_any))
|
||||
self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(match_all))
|
||||
preset_data["asset_category_rules"] = category_rules
|
||||
arch_rules = []
|
||||
for r in range(self.editor_table_archetype_rules.rowCount()):
|
||||
name_item = self.editor_table_archetype_rules.item(r, 0)
|
||||
any_item = self.editor_table_archetype_rules.item(r, 1)
|
||||
all_item = self.editor_table_archetype_rules.item(r, 2)
|
||||
if name_item and any_item and all_item:
|
||||
match_any = [k.strip() for k in any_item.text().split(',') if k.strip()]
|
||||
match_all = [k.strip() for k in all_item.text().split(',') if k.strip()]
|
||||
arch_rules.append([name_item.text().strip(), {"match_any": match_any, "match_all": match_all}])
|
||||
preset_data["archetype_rules"] = arch_rules
|
||||
return preset_data
|
||||
finally:
|
||||
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}")
|
||||
self._clear_editor()
|
||||
self.setWindowTitle("Asset Processor Tool - New Preset*")
|
||||
self.editor_supplier_name.setText("MySupplier") # Set a default supplier name
|
||||
else:
|
||||
log.warning("Presets/_template.json not found. Creating empty preset.")
|
||||
self.setWindowTitle("Asset Processor Tool - New Preset*")
|
||||
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.editor_unsaved_changes = True
|
||||
self.editor_save_button.setEnabled(True)
|
||||
|
||||
|
||||
def _delete_selected_preset(self):
|
||||
"""Deletes the currently selected preset file from the editor list after confirmation."""
|
||||
current_item = self.editor_preset_list.currentItem()
|
||||
@ -1455,8 +1480,22 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# --- Menu Bar Setup ---
|
||||
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()
|
||||
|
||||
# --- 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")
|
||||
|
||||
# Log Console Action
|
||||
@ -1495,6 +1534,23 @@ class MainWindow(QMainWindow):
|
||||
log.info("UI Log Handler Initialized.") # Log that the handler is ready
|
||||
|
||||
# --- 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)
|
||||
def _toggle_log_console_visibility(self, checked):
|
||||
"""Shows or hides the log console widget based on menu action."""
|
||||
@ -1604,8 +1660,11 @@ class MainWindow(QMainWindow):
|
||||
# Update status bar with progress
|
||||
completed_count = len(self._accumulated_rules)
|
||||
pending_count = len(self._pending_predictions)
|
||||
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)..."
|
||||
# total_count = completed_count + pending_count # This might be slightly off if some failed without rules
|
||||
# We don't have the total count of *requested* predictions here easily,
|
||||
# but we can use the initial number of items added.
|
||||
total_requested = len(self.current_asset_paths) # Use the total number of items added
|
||||
status_msg = f"Preview finished for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_requested} requested)..."
|
||||
self.statusBar().showMessage(status_msg, 5000)
|
||||
log.debug(status_msg)
|
||||
|
||||
|
||||
@ -21,23 +21,24 @@ if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
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
|
||||
# from asset_processor import AssetProcessor, AssetProcessingError
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
|
||||
import config as app_config # Import project's config module
|
||||
# Import the new dictionaries directly for easier access
|
||||
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
|
||||
# Removed: import config as app_config # Import project's config module
|
||||
# Removed: Import the new dictionaries directly for easier access
|
||||
# Removed: from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
|
||||
BACKEND_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
|
||||
# Define placeholders if imports fail
|
||||
Configuration = None
|
||||
# AssetProcessor = None
|
||||
load_base_config = None # Placeholder
|
||||
ConfigurationError = Exception
|
||||
# AssetProcessingError = Exception
|
||||
SourceRule, AssetRule, FileRule, AssetType, ItemType = (None,)*5 # Placeholder for rule structures
|
||||
app_config = None # Placeholder for config
|
||||
SourceRule, AssetRule, FileRule = (None,)*3 # Placeholder for rule structures
|
||||
# Removed: AssetType, ItemType = (None,)*2 # Placeholder for types
|
||||
# Removed: app_config = None # Placeholder for config
|
||||
BACKEND_AVAILABLE = False
|
||||
|
||||
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:
|
||||
log.warning("Classification skipped: Missing file list or config.")
|
||||
return {}
|
||||
# Access compiled regex directly from the config object
|
||||
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
|
||||
if not hasattr(config, 'compiled_extra_regex'):
|
||||
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
|
||||
matched_rule_details = None
|
||||
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
|
||||
log.debug(f" Associated rule details: {matched_rule_details}")
|
||||
log.debug(f" 'is_gloss_source' flag from rule: {is_gloss_flag}")
|
||||
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
|
||||
# --- End DEBUG LOG ---
|
||||
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)
|
||||
|
||||
config: Configuration | None = None
|
||||
asset_type_definitions: Dict[str, Dict] = {}
|
||||
file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
|
||||
# Removed: asset_type_definitions: Dict[str, Dict] = {}
|
||||
# Removed: file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
|
||||
|
||||
try:
|
||||
config = Configuration(preset_name)
|
||||
# Load allowed types from the project's config module (now dictionaries)
|
||||
if app_config:
|
||||
asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
|
||||
file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
|
||||
log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
|
||||
log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
|
||||
else:
|
||||
log.warning("Project config module not loaded. Cannot get type definitions.")
|
||||
# Removed: Load allowed types from the project's config module (now dictionaries)
|
||||
# Removed: if app_config:
|
||||
# Removed: asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
|
||||
# Removed: file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
|
||||
# Removed: log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
|
||||
# Removed: log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
|
||||
# Removed: else:
|
||||
# Removed: log.warning("Project config module not loaded. Cannot get type definitions.")
|
||||
|
||||
except ConfigurationError as 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}")
|
||||
|
||||
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():
|
||||
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
|
||||
# Now predicted_asset_type is already a string
|
||||
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
|
||||
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:
|
||||
predicted_asset_type = default_type
|
||||
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}")
|
||||
|
||||
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:
|
||||
# Determine FileRule level overrides/defaults
|
||||
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
|
||||
|
||||
# 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)
|
||||
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"]:
|
||||
# Prefix map types that don't already have it
|
||||
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"]:
|
||||
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
|
||||
|
||||
# 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" Final Item Type (for model): {final_item_type}")
|
||||
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
|
||||
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}")
|
||||
# --- 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_path=file_info['file_path'], # This is static info based on input
|
||||
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,
|
||||
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
|
||||
standard_map_type=standard_map_type, # Assign the determined standard_map_type
|
||||
# --- Leave Static Fields as Default/None ---
|
||||
resolution_override=None,
|
||||
channel_merge_instructions={},
|
||||
|
||||
@ -26,15 +26,16 @@ try:
|
||||
# Import the worker function from main.py
|
||||
from main import process_single_asset_wrapper
|
||||
# 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
|
||||
import config as core_config # <<< ADDED IMPORT
|
||||
# Removed: import config as core_config # <<< ADDED IMPORT
|
||||
BACKEND_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
print(f"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}")
|
||||
# Define placeholders if imports fail, so the GUI doesn't crash immediately
|
||||
process_single_asset_wrapper = None
|
||||
ConfigurationError = Exception
|
||||
load_base_config = None # Placeholder
|
||||
AssetProcessingError = Exception
|
||||
BACKEND_AVAILABLE = False
|
||||
|
||||
@ -70,7 +71,11 @@ class ProcessingHandler(QObject):
|
||||
def is_running(self):
|
||||
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
|
||||
"""
|
||||
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:
|
||||
log.error("Backend modules or worker function not available. Cannot start processing.")
|
||||
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
|
||||
|
||||
self._is_running = True
|
||||
self._cancel_requested = False
|
||||
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
|
||||
skipped_count = 0
|
||||
failed_count = 0
|
||||
@ -105,9 +110,20 @@ class ProcessingHandler(QObject):
|
||||
self._executor = executor # Store for potential cancellation
|
||||
|
||||
# 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
|
||||
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
|
||||
self._futures[future] = input_path # Map future back to input path
|
||||
# Optionally emit "processing" status here
|
||||
@ -288,7 +304,14 @@ class ProcessingHandler(QObject):
|
||||
def _find_blender_executable(self) -> Optional[str]:
|
||||
"""Finds the Blender executable path from config or system PATH."""
|
||||
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:
|
||||
p = Path(blender_exe_config)
|
||||
if p.is_file():
|
||||
@ -306,6 +329,9 @@ class ProcessingHandler(QObject):
|
||||
else:
|
||||
log.warning("Could not find 'blender' in system PATH.")
|
||||
return None
|
||||
except ConfigurationError as e:
|
||||
log.error(f"Error reading base configuration for Blender executable path: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
log.error(f"Error checking Blender executable path: {e}")
|
||||
return None
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
# 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.QtGui import QColor # Added for background role
|
||||
from pathlib import Path # Added for file_name extraction
|
||||
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):
|
||||
# --- Color Constants for Row Backgrounds ---
|
||||
@ -181,7 +183,10 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# Determine effective asset type
|
||||
asset_type = item.asset_type_override if item.asset_type_override else item.asset_type
|
||||
if asset_type:
|
||||
type_info = ASSET_TYPE_DEFINITIONS.get(asset_type)
|
||||
try:
|
||||
base_config = load_base_config() # Load base config
|
||||
asset_type_definitions = base_config.get('ASSET_TYPE_DEFINITIONS', {}) # Get definitions
|
||||
type_info = asset_type_definitions.get(asset_type)
|
||||
if type_info:
|
||||
hex_color = type_info.get("color")
|
||||
if hex_color:
|
||||
@ -199,13 +204,18 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# Optional: Add logging for missing asset type definition
|
||||
# print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
|
||||
return None # Fallback if type not in config
|
||||
except Exception: # Catch errors during config loading
|
||||
return None # Fallback on error
|
||||
else:
|
||||
return None # Fallback if no asset_type determined
|
||||
elif isinstance(item, FileRule):
|
||||
# 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
|
||||
if effective_item_type:
|
||||
type_info = FILE_TYPE_DEFINITIONS.get(effective_item_type)
|
||||
try:
|
||||
base_config = load_base_config() # Load base config
|
||||
file_type_definitions = base_config.get('FILE_TYPE_DEFINITIONS', {}) # Get definitions
|
||||
type_info = file_type_definitions.get(effective_item_type)
|
||||
if type_info:
|
||||
hex_color = type_info.get("color")
|
||||
if hex_color:
|
||||
@ -222,6 +232,8 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
else:
|
||||
# File types often don't have specific colors, so no warning needed unless debugging
|
||||
return None # Fallback if type not in config
|
||||
except Exception: # Catch errors during config loading
|
||||
return None # Fallback on error
|
||||
else:
|
||||
return None # Fallback if no item_type determined
|
||||
else: # Other item types or if item is None
|
||||
@ -258,7 +270,14 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
if column == self.COL_TARGET_ASSET:
|
||||
return item.target_asset_name_override if item.target_asset_name_override is not None else ""
|
||||
if column == self.COL_ITEM_TYPE:
|
||||
return item.item_type_override 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_OUTPUT_PATH: return "" # Output Path (Not handled yet)
|
||||
elif role == Qt.EditRole:
|
||||
@ -436,9 +455,48 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
if new_value == "": new_value = None # Treat empty string as None
|
||||
# Update item_type_override
|
||||
if 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
|
||||
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:
|
||||
# Emit dataChanged for the specific index and affected roles
|
||||
|
||||
17
main.py
17
main.py
@ -25,7 +25,6 @@ try:
|
||||
# from asset_processor import AssetProcessor, AssetProcessingError # REMOVED OLD PROCESSOR
|
||||
from processing_engine import ProcessingEngine # <<< ADDED NEW ENGINE IMPORT
|
||||
from rule_structure import SourceRule # Import SourceRule for type hinting
|
||||
import config as core_config_module
|
||||
from gui.main_window import MainWindow # Import MainWindow
|
||||
except ImportError as e:
|
||||
# Provide a more helpful error message if imports fail
|
||||
@ -516,15 +515,19 @@ def run_cli(args): # Accept parsed args
|
||||
# --- Determine Output Directory ---
|
||||
output_dir_str = args.output_dir # Get value from args (might be None)
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
log.info(f"Using default output directory from config.py: {output_dir_str}")
|
||||
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)
|
||||
|
||||
# --- 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_rules = [FileRule(file_path=f) for f in file_list] if file_list else []
|
||||
# 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 ---
|
||||
|
||||
# --- TEMPORARY: Call engine process with just config and path ---
|
||||
|
||||
@ -965,9 +965,12 @@ class ProcessingEngine:
|
||||
# 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
|
||||
|
||||
# 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(
|
||||
image_data=img_resized,
|
||||
map_type=map_type,
|
||||
map_type=save_map_type, # Use the determined map type for saving
|
||||
resolution_key=res_key,
|
||||
asset_base_name=base_name,
|
||||
source_info=source_info,
|
||||
@ -1048,14 +1051,14 @@ class ProcessingEngine:
|
||||
found_rule_for_type = False
|
||||
# Search in the asset_rule's files
|
||||
for file_rule in asset_rule.files:
|
||||
# Check if the file_rule's map_type matches the required input type
|
||||
# Handle variants (e.g., ROUGH-1 should match ROUGH)
|
||||
if file_rule.item_type_override and file_rule.item_type_override.startswith(input_type): # Check override exists and matches
|
||||
# Check if the file_rule's standard_map_type matches the required input type
|
||||
# This uses the new alias system for exact matching
|
||||
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?)
|
||||
# For now, take the first match.
|
||||
required_input_file_rules[input_type] = file_rule
|
||||
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
|
||||
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}'.")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import dataclasses
|
||||
import json
|
||||
from typing import List, Dict, Any, Tuple
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
@dataclasses.dataclass
|
||||
class FileRule:
|
||||
file_path: str = None
|
||||
@ -11,6 +11,7 @@ class FileRule:
|
||||
channel_merge_instructions: Dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
output_format_override: str = None # Potentially others identified during integration
|
||||
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:
|
||||
return json.dumps(dataclasses.asdict(self), indent=4)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user