Compare commits

42 Commits

Author SHA1 Message Date
b43b2522d7 Implemented Item type priority handling ( DISP16 ) 2025-05-15 20:52:58 +02:00
ca92c72070 CONPORT implementation - Autotest fix 2025-05-14 23:19:35 +02:00
85e94a3d0d Debugsession N2 - New fallback for LOWRES images 2025-05-14 18:07:28 +02:00
ce1d8c770c Debugsession N1 2025-05-14 16:46:09 +02:00
dfe6500141 Merge branch 'Dev' into GUI-and-Configs 2025-05-14 14:56:13 +02:00
58eb10b7dc AutoTest Implementation 2025-05-14 14:55:30 +02:00
87673507d8 New Definitions editor 2025-05-13 13:08:52 +02:00
344ae078a8 UI Updates - Error with Definitions 2025-05-13 11:54:22 +02:00
dec5d7d27f Config Updates - User settings - Saving Methods 2025-05-13 10:32:19 +02:00
383e904e1a Merge pull request 'Processing-Refactor' (#63) from Processing-Refactor into Dev
Reviewed-on: #63
2025-05-13 09:25:06 +02:00
6e7daf260a Metadata reformat done 2025-05-13 09:21:38 +02:00
1cd81cb87a Metadata reformatting 2025-05-13 09:15:43 +02:00
f800bb25a9 channelpacking now works 2025-05-13 04:01:38 +02:00
35a7221f57 Cleanup of inconsistencies 2025-05-13 03:07:00 +02:00
0de4db1826 Fixed inconcistencies - only processes MAP_ files now 2025-05-13 02:52:07 +02:00
b441174076 Processing Documentation Update 2025-05-13 02:28:42 +02:00
c2ad299ce2 Various Attempted fixes 2025-05-12 23:32:35 +02:00
528d9be47f Closer to feature parity - missing merge still 2025-05-12 23:03:26 +02:00
81d8404576 yet another processing refactor :3 Mostly works 2025-05-12 22:46:49 +02:00
ab4db1b8bd BugFixes 2025-05-12 16:49:57 +02:00
06552216d5 Logic Update - Perform MapType transforms before merging 2025-05-12 14:22:01 +02:00
4ffb2ff78c Pipeline simplification - Needs testing! 2025-05-12 13:31:58 +02:00
5bf53f036c More Refactor Fixes, Issuetracker updated 2025-05-09 21:48:45 +02:00
beb8640085 Futher changes to bring refactor up to feature parity + Updated Docs 2025-05-09 20:47:44 +02:00
deeb1595fd No crashes anymore :3 2025-05-09 13:57:22 +02:00
12cf557dd7 Uncompleted Processing Refactor 2025-05-09 11:32:16 +02:00
d473ddd7f4 Bug Fixes 2025-05-07 19:05:43 +02:00
4e1cea56ae Bugfixes #46 #36 2025-05-07 18:03:08 +02:00
93d4f21ca7 Updates to LLM settings and examples 2025-05-07 17:04:42 +02:00
50f9716b83 Updated Readme 2025-05-07 11:41:16 +02:00
946fd44251 Readme Update 2025-05-07 11:37:18 +02:00
f9b4ca154e Added readme 2025-05-07 10:48:42 +02:00
fcbaa04a0a Removed PythonCheatsheat Files 2025-05-07 10:34:18 +02:00
d394efe13d Is gloss source logic removal - metadata handling update 2025-05-06 22:59:08 +02:00
932b39fd01 Major Comment and codebase cleanup 2025-05-06 22:47:26 +02:00
ddb5a43a21 Processing-Engine - Gloss > Rough conversion 2025-05-06 20:58:01 +02:00
9a27d23a4c GUI - File Type Keybinds And F2 Renaming 2025-05-06 20:31:53 +02:00
ff548e902e Major Terminogy unification and refactor [Needs thorough testing] 2025-05-06 18:26:26 +02:00
0a3100d448 Token-based output support - needs testing 2025-05-04 15:54:59 +02:00
6b704c561a LLM GUI updates and tests 2025-05-04 14:33:18 +02:00
336d698f9b Dedicated LLM settings file - UNTESTED! 2025-05-04 13:24:10 +02:00
01c8f68ea0 LLM Restructure - UNTESTED! 2025-05-04 12:56:16 +02:00
214 changed files with 17459 additions and 17787 deletions

4
.gitignore vendored
View File

@@ -30,6 +30,6 @@ Thumbs.db
gui/__pycache__
__pycache__
Testfiles
Testfiles/
Testfiles/TestOutputs
Testfiles_

46
.roo/mcp.json Normal file
View File

@@ -0,0 +1,46 @@
{
"mcpServers": {
"conport": {
"command": "C:\\Users\\theis\\context-portal\\.venv\\Scripts\\python.exe",
"args": [
"C:\\Users\\theis\\context-portal\\src\\context_portal_mcp\\main.py",
"--mode",
"stdio",
"--workspace_id",
"${workspaceFolder}"
],
"alwaysAllow": [
"get_product_context",
"update_product_context",
"get_active_context",
"update_active_context",
"log_decision",
"get_decisions",
"search_decisions_fts",
"log_progress",
"get_progress",
"update_progress",
"delete_progress_by_id",
"log_system_pattern",
"get_system_patterns",
"log_custom_data",
"get_custom_data",
"delete_custom_data",
"search_project_glossary_fts",
"export_conport_to_markdown",
"import_markdown_to_conport",
"link_conport_items",
"search_custom_data_value_fts",
"get_linked_items",
"batch_log_items",
"get_item_history",
"delete_decision_by_id",
"delete_system_pattern_by_id",
"get_conport_schema",
"get_recent_activity_summary",
"semantic_search_conport",
"search_system_patterns_fts"
]
}
}
}

View File

15
.roomodes Normal file
View File

@@ -0,0 +1,15 @@
{
"customModes": [
{
"slug": "Task-Initiator",
"name": "Task Initiator",
"roleDefinition": "You are Task Initiator. Your exclusive function is comprehensive initial context gathering, focusing solely on ConPort data. Do NOT perform other tasks or use direct file system tools for context gathering.",
"customInstructions": "1. First, execute standard initial context setup procedures (as per global ConPort strategy).\n2. Next, if a specific user request is pending, YOU, as Task Initiator, should analyze it and proactively gather relevant information, strictly by querying ConPort. Your process for this is:\n a. Identify the key subject(s) of the request.\n b. Loosely search relevant ConPort data for information or summaries related to these identified subject(s).\n3. After completing both standard setup AND any ConPort-based task-specific gathering, briefly report the overall context status. This report must cover ConPort initialization and summarize any specific information found (or explicitly not found) within ConPort relevant to the user's request.\n4. Then, output `[TASK_INITIATOR_COMPLETE]`.\n5. Finally, to address the user's main request with the context you've gathered (or confirmed is missing from ConPort), use the `switch_mode` tool to transition to the determined most appropriate mode by analysing the initial request. you should ALWAYS finish context-gathering before switching modes.",
"groups": [
"mcp",
"read"
],
"source": "project"
}
]
}

View File

@@ -8,9 +8,6 @@
".vscode": true,
".vs": true,
".lh": true,
"__pycache__": true,
"Deprecated-POC": true,
"BlenderDocumentation": true,
"PythonCheatsheats": true
"__pycache__": true
}
}

112
AUTOTEST_GUI_PLAN.md Normal file
View File

@@ -0,0 +1,112 @@
# Plan for Autotest GUI Mode Implementation
**I. Objective:**
Create an `autotest.py` script that can launch the Asset Processor GUI headlessly, load a predefined asset (`.zip`), select a predefined preset, verify the predicted rule structure against an expected JSON, trigger processing to a predefined output directory, check the output, and analyze logs for errors or specific messages. This serves as a sanity check for core GUI-driven workflows.
**II. `TestFiles` Directory:**
A new directory named `TestFiles` will be created in the project root (`c:/Users/Theis/Assetprocessor/Asset-Frameworker/TestFiles/`). This directory will house:
* Sample asset `.zip` files for testing (e.g., `TestFiles/SampleAsset1.zip`).
* Expected rule structure JSON files (e.g., `TestFiles/SampleAsset1_PresetX_expected_rules.json`).
* A subdirectory for test outputs (e.g., `TestFiles/TestOutputs/`).
**III. `autotest.py` Script:**
1. **Location:** `c:/Users/Theis/Assetprocessor/Asset-Frameworker/autotest.py` (or `scripts/autotest.py`).
2. **Command-Line Arguments (with defaults pointing to `TestFiles/`):**
* `--zipfile`: Path to the test asset. Default: `TestFiles/default_test_asset.zip`.
* `--preset`: Name of the preset. Default: `DefaultTestPreset`.
* `--expectedrules`: Path to expected rules JSON. Default: `TestFiles/default_test_asset_rules.json`.
* `--outputdir`: Path for processing output. Default: `TestFiles/TestOutputs/DefaultTestOutput`.
* `--search` (optional): Log search term. Default: `None`.
* `--additional-lines` (optional): Context lines for log search. Default: `0`.
3. **Core Structure:**
* Imports necessary modules from the main application and PySide6.
* Adds project root to `sys.path` for imports.
* `AutoTester` class:
* **`__init__(self, app_instance: App)`:**
* Stores `app_instance` and `main_window`.
* Initializes `QEventLoop`.
* Connects `app_instance.all_tasks_finished` to `self._on_all_tasks_finished`.
* Loads expected rules from the `--expectedrules` file.
* **`run_test(self)`:** Orchestrates the test steps sequentially:
1. Load ZIP (`main_window.add_input_paths()`).
2. Select Preset (`main_window.preset_editor_widget.editor_preset_list.setCurrentItem()`).
3. Await Prediction (using `QTimer` to poll `main_window._pending_predictions`, manage with `QEventLoop`).
4. Retrieve & Compare Rulelist:
* Get actual rules: `main_window.unified_model.get_all_source_rules()`.
* Convert actual rules to comparable dict (`_convert_rules_to_comparable()`).
* Compare with loaded expected rules (`_compare_rules()`). If mismatch, log and fail.
5. Start Processing (emit `main_window.start_backend_processing` with rules and output settings).
6. Await Processing (use `QEventLoop` waiting for `_on_all_tasks_finished`).
7. Check Output Path (verify existence of output dir, list contents, basic sanity checks like non-emptiness or presence of key asset folders).
8. Retrieve & Analyze Logs (`main_window.log_console.log_console_output.toPlainText()`, filter by `--search`, check for tracebacks).
9. Report result and call `cleanup_and_exit()`.
* **`_check_prediction_status(self)`:** Slot for prediction polling timer.
* **`_on_all_tasks_finished(self, processed_count, skipped_count, failed_count)`:** Slot for `App.all_tasks_finished` signal.
* **`_convert_rules_to_comparable(self, source_rules_list: List[SourceRule]) -> dict`:** Converts `SourceRule` objects to the JSON structure defined below.
* **`_compare_rules(self, actual_rules_data: dict, expected_rules_data: dict) -> bool`:** Implements Option 1 comparison logic:
* Errors if an expected field is missing or its value mismatches.
* Logs (but doesn't error on) fields present in actual but not in expected.
* **`_process_and_display_logs(self, logs_text: str)`:** Handles log filtering/display.
* **`cleanup_and_exit(self, success=True)`:** Quits `QCoreApplication` and `sys.exit()`.
* `main()` function:
* Parses CLI arguments.
* Initializes `QApplication`.
* Instantiates `main.App()` (does *not* show the GUI).
* Instantiates `AutoTester(app_instance)`.
* Uses `QTimer.singleShot(0, tester.run_test)` to start the test.
* Runs `q_app.exec()`.
**IV. `expected_rules.json` Structure (Revised):**
Located in `TestFiles/`. Example: `TestFiles/SampleAsset1_PresetX_expected_rules.json`.
```json
{
"source_rules": [
{
"input_path": "SampleAsset1.zip",
"supplier_identifier": "ExpectedSupplier",
"preset_name": "PresetX",
"assets": [
{
"asset_name": "AssetNameFromPrediction",
"asset_type": "Prop",
"files": [
{
"file_path": "relative/path/to/file1.png",
"item_type": "MAP_COL",
"target_asset_name_override": null
}
]
}
]
}
]
}
```
**V. Mermaid Diagram of Autotest Flow:**
```mermaid
graph TD
A[Start autotest.py with CLI Args (defaults to TestFiles/)] --> B{Setup Args & Logging};
B --> C[Init QApplication & main.App (GUI Headless)];
C --> D[Instantiate AutoTester(app_instance)];
D --> E[QTimer.singleShot -> AutoTester.run_test()];
subgraph AutoTester.run_test()
E --> F[Load Expected Rules from --expectedrules JSON];
F --> G[Load ZIP (--zipfile) via main_window.add_input_paths()];
G --> H[Select Preset (--preset) via main_window.preset_editor_widget];
H --> I[Await Prediction (Poll main_window._pending_predictions via QTimer & QEventLoop)];
I -- Prediction Done --> J[Get Actual Rules from main_window.unified_model];
J --> K[Convert Actual Rules to Comparable JSON Structure];
K --> L{Compare Actual vs Expected Rules (Option 1 Logic)};
L -- Match --> M[Start Processing (Emit main_window.start_backend_processing with --outputdir)];
L -- Mismatch --> ZFAIL[Log Mismatch & Call cleanup_and_exit(False)];
M --> N[Await Processing (QEventLoop for App.all_tasks_finished signal)];
N -- Processing Done --> O[Check Output Dir (--outputdir): Exists? Not Empty? Key Asset Folders?];
O --> P[Retrieve & Analyze Logs (Search, Tracebacks)];
P --> Q[Log Test Success & Call cleanup_and_exit(True)];
end
ZFAIL --> ZEND[AutoTester.cleanup_and_exit() -> QCoreApplication.quit() & sys.exit()];
Q --> ZEND;

View File

@@ -1,3 +0,0 @@
def invoke(self, context, event):
# Example: Open a dialog to select materials if not already selected
return context.window_manager.invoke_props_dialog(self)

View File

@@ -1,6 +0,0 @@
# --- REMOVED Slots for Old Hierarchy and Rule Editor ---
# @Slot(QModelIndex)
# def _on_hierarchy_item_clicked(self, index: QModelIndex): ...
#
# @Slot(object)
# def _on_rule_updated(self, rule_object): ...

View File

@@ -1,6 +0,0 @@
# Slot for prediction results (Updated for new format and coloring) - REMOVED
# @Slot(list)
# def on_prediction_results_ready(self, results: list):
# """Populates the preview table model with detailed prediction results."""
# # This is no longer needed as _on_rule_hierarchy_ready handles data loading for the new model.
# pass

View File

@@ -1 +0,0 @@
# REMOVED Placeholder SourceRule creation

View File

@@ -1,6 +0,0 @@
# --- REMOVED connections causing thread/handler cleanup ---
# self.prediction_handler.prediction_finished.connect(self.prediction_thread.quit)
# self.prediction_handler.prediction_finished.connect(self.prediction_handler.deleteLater)
# self.prediction_thread.finished.connect(self.prediction_thread.deleteLater)
# self.prediction_thread.finished.connect(self._reset_prediction_thread_references)
# --- END REMOVED ---

View File

@@ -1,7 +0,0 @@
self.disable_preview_checkbox = QCheckBox("Disable Detailed Preview") # REMOVED - Moved to View Menu
self.disable_preview_checkbox.setToolTip("If checked, shows only the list of input assets instead of detailed file predictions.")
self.disable_preview_checkbox.setChecked(False) # Default is detailed preview enabled
self.disable_preview_checkbox.toggled.connect(self.update_preview) # Update preview when toggled
bottom_controls_layout.addWidget(self.disable_preview_checkbox)
bottom_controls_layout.addSpacing(20) # Add some space # REMOVED - No longer needed after checkbox removal

View File

@@ -1,3 +0,0 @@
# REMOVED Old Preview Model Mode Setting and Table Configuration ---
# The Unified View does not have a simple/detailed mode toggle.
# The Prediction Handler is triggered regardless of view settings.

View File

@@ -1,20 +0,0 @@
# --- REMOVED Old Processing Thread Setup ---
# if ProcessingHandler and self.processing_thread is None:
# self.processing_thread = QThread(self)
# self.processing_handler = ProcessingHandler()
# self.processing_handler.moveToThread(self.processing_thread)
# self.processing_handler.progress_updated.connect(self.update_progress_bar)
# self.processing_handler.file_status_updated.connect(self.update_file_status)
# self.processing_handler.processing_finished.connect(self.on_processing_finished)
# self.processing_handler.status_message.connect(self.show_status_message)
# self.processing_handler.processing_finished.connect(self.processing_thread.quit)
# self.processing_handler.processing_finished.connect(self.processing_handler.deleteLater)
# self.processing_thread.finished.connect(self.processing_thread.deleteLater)
# self.processing_thread.finished.connect(self._reset_processing_thread_references)
# log.debug("Processing thread and handler set up.")
# elif not ProcessingHandler:
# log.error("ProcessingHandler not available. Cannot set up processing thread.")
# if hasattr(self, 'start_button'):
# self.start_button.setEnabled(False)
# self.start_button.setToolTip("Error: Backend processing components failed to load.")
# --- END REMOVED ---

View File

@@ -1,8 +0,0 @@
# --- REMOVED Old Processing Thread Reset ---
# @Slot()
# def _reset_processing_thread_references(self):
# # This might still be needed if processing is meant to be single-shot
# log.debug("Resetting processing thread and handler references.")
# self.processing_thread = None
# self.processing_handler = None
# --- END REMOVED ---

View File

@@ -1,70 +0,0 @@
import logging
import subprocess
from pathlib import Path
log = logging.getLogger(__name__) # Assume logger is configured elsewhere
def run_blender_script(blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str):
"""
Executes a Python script within Blender in the background.
Args:
blender_exe_path: Path to the Blender executable.
blend_file_path: Path to the .blend file to open.
python_script_path: Path to the Python script to execute within Blender.
asset_root_dir: Path to the processed asset library root directory (passed to the script).
Returns:
True if the script executed successfully (return code 0), False otherwise.
"""
log.info(f"Attempting to run Blender script: {Path(python_script_path).name} on {Path(blend_file_path).name}")
# Ensure paths are absolute strings for subprocess
blender_exe_path = str(Path(blender_exe_path).resolve())
blend_file_path = str(Path(blend_file_path).resolve())
python_script_path = str(Path(python_script_path).resolve())
asset_root_dir = str(Path(asset_root_dir).resolve())
# Construct the command arguments
# -b: Run in background (no UI)
# -S: Save the file after running the script
# --python: Execute the specified Python script
# --: Separator, arguments after this are passed to the Python script's sys.argv
command = [
blender_exe_path,
"-b", # Run in background
blend_file_path,
"--python", python_script_path,
"--", # Pass subsequent arguments to the script
asset_root_dir,
"-S" # Save the blend file after script execution
]
log.debug(f"Executing Blender command: {' '.join(command)}") # Log the command for debugging
try:
# Execute the command
# capture_output=True captures stdout and stderr
# text=True decodes stdout/stderr as text
# check=False prevents raising CalledProcessError on non-zero exit codes
result = subprocess.run(command, capture_output=True, text=True, check=False)
# Log results
log.info(f"Blender script '{Path(python_script_path).name}' finished with exit code: {result.returncode}")
if result.stdout:
log.debug(f"Blender stdout:\n{result.stdout.strip()}")
if result.stderr:
# Log stderr as warning or error depending on return code
if result.returncode != 0:
log.error(f"Blender stderr:\n{result.stderr.strip()}")
else:
log.warning(f"Blender stderr (Return Code 0):\n{result.stderr.strip()}") # Log stderr even on success as scripts might print warnings
return result.returncode == 0
except FileNotFoundError:
log.error(f"Blender executable not found at: {blender_exe_path}")
return False
except Exception as e:
log.exception(f"An unexpected error occurred while running Blender script '{Path(python_script_path).name}': {e}")
return False

View File

@@ -1,346 +0,0 @@
# Deprecated/Old-Code/main_py_cli_main_entry_line_329.py
import argparse
import sys
import time
import os
import logging
from pathlib import Path
import shutil
# Assuming these imports are needed based on the original context
try:
import config as core_config_module
# Import functions from previously created files
# Note: These imports assume the files are in the same directory or Python path
from main_py_cli_run_processing_line_258 import run_processing
from main_py_cli_blender_script_runner_line_365 import run_blender_script
# setup_arg_parser and setup_logging are defined in the main script,
# so they might not be directly importable here without refactoring.
# This function is modified to accept them as arguments.
except ImportError as e:
print(f"Warning: Could not import necessary modules/functions: {e}")
core_config_module = None
run_processing = None
run_blender_script = None
log = logging.getLogger(__name__) # Assume logger is configured elsewhere
# Note: setup_arg_parser and setup_logging were originally defined in main.py
# This function now accepts them as arguments.
def main(setup_arg_parser_func, setup_logging_func):
"""Parses arguments, sets up logging, runs processing, and reports summary."""
parser = setup_arg_parser_func()
args = parser.parse_args()
# Setup logging based on verbosity argument *before* logging status messages
setup_logging_func(args.verbose)
start_time = time.time()
log.info("Asset Processor Script Started (CLI Mode)")
# --- Validate Input Paths ---
valid_inputs = []
for p_str in args.input_paths:
p = Path(p_str)
if p.exists():
suffix = p.suffix.lower()
# Original check included .rar, .7z - keeping for historical accuracy
if p.is_dir() or (p.is_file() and suffix in ['.zip', '.rar', '.7z']):
valid_inputs.append(p_str) # Store the original string path
else:
log.warning(f"Input is not a directory or a supported archive type (.zip, .rar, .7z), skipping: {p_str}")
else:
log.warning(f"Input path not found, skipping: {p_str}")
if not valid_inputs:
log.error("No valid input paths found. Exiting.")
sys.exit(1) # Exit with error code
# --- 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.")
try:
if core_config_module is None:
raise RuntimeError("core_config_module not imported.")
output_dir_str = getattr(core_config_module, 'OUTPUT_BASE_DIR', None)
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.")
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}")
sys.exit(1)
# --- Resolve Output Path (Handles Relative Paths Explicitly) ---
output_path_obj: Path
if os.path.isabs(output_dir_str):
output_path_obj = Path(output_dir_str)
log.info(f"Using absolute output directory: {output_path_obj}")
else:
# Path() interprets relative paths against CWD by default
output_path_obj = Path(output_dir_str)
log.info(f"Using relative output directory '{output_dir_str}'. Resolved against CWD to: {output_path_obj.resolve()}")
# --- Validate and Setup Output Directory ---
try:
# Resolve to ensure we have an absolute path for consistency and creation
resolved_output_dir = output_path_obj.resolve()
log.info(f"Ensuring output directory exists: {resolved_output_dir}")
resolved_output_dir.mkdir(parents=True, exist_ok=True)
# Use the resolved absolute path string for the processor
output_dir_for_processor = str(resolved_output_dir)
except Exception as e:
log.error(f"Cannot create or access output directory '{resolved_output_dir}': {e}", exc_info=True)
sys.exit(1)
# --- Check Preset Existence (Basic Check) ---
# Assuming __file__ might not be reliable here, using relative path logic
try:
# Try relative to CWD first
preset_dir = Path("Presets")
if not preset_dir.is_dir():
# Try relative to script location if possible? Less reliable.
# Go up two levels from Deprecated/Old-Code
preset_dir = Path(__file__).parent.parent / "Presets"
preset_file = preset_dir / f"{args.preset}.json"
if not preset_file.is_file():
log.error(f"Preset file not found: {preset_file}")
log.error(f"Ensure a file named '{args.preset}.json' exists in the directory: {preset_dir.resolve()}")
sys.exit(1)
except NameError: # __file__ might not be defined
log.error("Could not determine preset directory path.")
sys.exit(1)
# --- Execute Processing via the new function ---
if run_processing is None:
log.error("run_processing function not available. Cannot execute processing.")
sys.exit(1)
processing_results = run_processing(
valid_inputs=valid_inputs,
preset_name=args.preset,
output_dir_for_processor=output_dir_for_processor,
overwrite=args.overwrite,
num_workers=args.workers,
verbose=args.verbose # Pass the verbose flag
)
# --- Report Summary ---
duration = time.time() - start_time
successful_processed_count = processing_results["processed"]
skipped_count = processing_results["skipped"]
failed_count = processing_results["failed"]
results_list = processing_results["results_list"]
log.info("=" * 40)
log.info("Processing Summary")
log.info(f" Duration: {duration:.2f} seconds")
log.info(f" Assets Attempted: {len(valid_inputs)}")
log.info(f" Successfully Processed: {successful_processed_count}")
log.info(f" Skipped (Already Existed): {skipped_count}")
log.info(f" Failed: {failed_count}")
if processing_results.get("pool_error"):
log.error(f" Process Pool Error: {processing_results['pool_error']}")
# Ensure failed count reflects pool error if it happened
if failed_count == 0 and successful_processed_count == 0 and skipped_count == 0:
failed_count = len(valid_inputs) # Assume all failed if pool died early
exit_code = 0
if failed_count > 0:
log.warning("Failures occurred:")
# Iterate through results to show specific errors for failed items
for input_path, status, err_msg in results_list:
if status == "failed":
log.warning(f" - {Path(input_path).name}: {err_msg}")
exit_code = 1 # Exit with error code if failures occurred
else:
# Consider skipped assets as a form of success for the overall run exit code
if successful_processed_count > 0 or skipped_count > 0:
log.info("All assets processed or skipped successfully.")
exit_code = 0 # Exit code 0 indicates success (including skips)
else:
# This case might happen if all inputs were invalid initially
log.warning("No assets were processed, skipped, or failed (check input validation logs).")
exit_code = 0 # Still exit 0 as the script itself didn't crash
# --- Blender Script Execution (Optional) ---
run_nodegroups = False # Flags were defined but never set to True in original code
run_materials = False
nodegroup_blend_path = None
materials_blend_path = None
blender_exe = None
# 1. Find Blender Executable
try:
if core_config_module is None:
raise RuntimeError("core_config_module not imported.")
blender_exe_config = getattr(core_config_module, 'BLENDER_EXECUTABLE_PATH', None)
if blender_exe_config:
# Check if the path in config exists
if Path(blender_exe_config).is_file():
blender_exe = str(Path(blender_exe_config).resolve())
log.info(f"Using Blender executable from config: {blender_exe}")
else:
# Try finding it in PATH if config path is invalid
log.warning(f"Blender path in config not found: '{blender_exe_config}'. Trying to find 'blender' in PATH.")
blender_exe = shutil.which("blender")
if blender_exe:
log.info(f"Found Blender executable in PATH: {blender_exe}")
else:
log.warning("Could not find 'blender' in system PATH.")
else:
# Try finding it in PATH if not set in config
log.info("BLENDER_EXECUTABLE_PATH not set in config. Trying to find 'blender' in PATH.")
blender_exe = shutil.which("blender")
if blender_exe:
log.info(f"Found Blender executable in PATH: {blender_exe}")
else:
log.warning("Could not find 'blender' in system PATH.")
if not blender_exe:
log.warning("Blender executable not found or configured. Skipping Blender script execution.")
except Exception as e:
log.error(f"Error checking Blender executable path: {e}")
blender_exe = None # Ensure it's None on error
# 2. Determine Blend File Paths if Blender Exe is available
if blender_exe:
# Nodegroup Blend Path
nodegroup_blend_arg = args.nodegroup_blend
if nodegroup_blend_arg:
p = Path(nodegroup_blend_arg)
if p.is_file() and p.suffix.lower() == '.blend':
nodegroup_blend_path = str(p.resolve())
log.info(f"Using nodegroup blend file from argument: {nodegroup_blend_path}")
else:
log.warning(f"Invalid nodegroup blend file path from argument: '{nodegroup_blend_arg}'. Ignoring.")
else:
if core_config_module is None:
log.warning("core_config_module not available to check default nodegroup path.")
else:
default_ng_path_str = getattr(core_config_module, 'DEFAULT_NODEGROUP_BLEND_PATH', None)
if default_ng_path_str:
p = Path(default_ng_path_str)
if p.is_file() and p.suffix.lower() == '.blend':
nodegroup_blend_path = str(p.resolve())
log.info(f"Using default nodegroup blend file from config: {nodegroup_blend_path}")
else:
log.warning(f"Invalid default nodegroup blend file path in config: '{default_ng_path_str}'. Ignoring.")
# Materials Blend Path
materials_blend_arg = args.materials_blend
if materials_blend_arg:
p = Path(materials_blend_arg)
if p.is_file() and p.suffix.lower() == '.blend':
materials_blend_path = str(p.resolve())
log.info(f"Using materials blend file from argument: {materials_blend_path}")
else:
log.warning(f"Invalid materials blend file path from argument: '{materials_blend_arg}'. Ignoring.")
else:
if core_config_module is None:
log.warning("core_config_module not available to check default materials path.")
else:
default_mat_path_str = getattr(core_config_module, 'DEFAULT_MATERIALS_BLEND_PATH', None)
if default_mat_path_str:
p = Path(default_mat_path_str)
if p.is_file() and p.suffix.lower() == '.blend':
materials_blend_path = str(p.resolve())
log.info(f"Using default materials blend file from config: {materials_blend_path}")
else:
log.warning(f"Invalid default materials blend file path in config: '{default_mat_path_str}'. Ignoring.")
# 3. Execute Scripts if Paths are Valid
if blender_exe:
# Determine script directory relative to this file's assumed location
try:
# Go up two levels from Deprecated/Old-Code
script_dir = Path(__file__).parent.parent / "blenderscripts"
except NameError:
script_dir = Path("blenderscripts") # Fallback if __file__ is not defined
nodegroup_script_path = script_dir / "create_nodegroups.py"
materials_script_path = script_dir / "create_materials.py"
asset_output_root = output_dir_for_processor # Use the resolved output dir
if run_blender_script is None:
log.error("run_blender_script function not available. Cannot execute Blender scripts.")
else:
# Check if nodegroup execution should run (based on original commented code, it wasn't explicitly triggered)
# if run_nodegroups: # This flag was never set to True
if nodegroup_blend_path: # Check if path exists instead
if nodegroup_script_path.is_file():
log.info("-" * 40)
log.info("Starting Blender Node Group Script Execution...")
success_ng = run_blender_script(
blender_exe_path=blender_exe,
blend_file_path=nodegroup_blend_path,
python_script_path=str(nodegroup_script_path),
asset_root_dir=asset_output_root
)
if not success_ng:
log.error("Blender node group script execution failed.")
# Optionally change exit code if Blender script fails?
# exit_code = 1
log.info("Finished Blender Node Group Script Execution.")
log.info("-" * 40)
else:
log.error(f"Node group script not found: {nodegroup_script_path}")
# Check if material execution should run (based on original commented code, it wasn't explicitly triggered)
# if run_materials: # This flag was never set to True
if materials_blend_path: # Check if path exists instead
if materials_script_path.is_file():
log.info("-" * 40)
log.info("Starting Blender Material Script Execution...")
success_mat = run_blender_script(
blender_exe_path=blender_exe,
blend_file_path=materials_blend_path,
python_script_path=str(materials_script_path),
asset_root_dir=asset_output_root
)
if not success_mat:
log.error("Blender material script execution failed.")
# Optionally change exit code if Blender script fails?
# exit_code = 1
log.info("Finished Blender Material Script Execution.")
log.info("-" * 40)
else:
log.error(f"Material script not found: {materials_script_path}")
# --- Final Exit ---
log.info("Asset Processor Script Finished.")
sys.exit(exit_code)
# Example of how this might be called if run standalone (requires providing the setup functions)
# if __name__ == "__main__":
# # Define dummy or actual setup functions here if needed for testing
# def dummy_setup_arg_parser():
# # Minimal parser for testing
# parser = argparse.ArgumentParser()
# parser.add_argument("input_paths", nargs='*', default=[])
# parser.add_argument("-p", "--preset")
# parser.add_argument("-o", "--output-dir")
# parser.add_argument("-w", "--workers", type=int, default=1)
# parser.add_argument("-v", "--verbose", action="store_true")
# parser.add_argument("--overwrite", action="store_true")
# parser.add_argument("--nodegroup-blend")
# parser.add_argument("--materials-blend")
# return parser
#
# def dummy_setup_logging(verbose):
# logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO,
# format='%(asctime)s [%(levelname)-8s] %(name)s: %(message)s')
#
# # Configure basic logging for the example run
# logging.basicConfig(level=logging.INFO)
#
# # Need to get setup_arg_parser and setup_logging from the original main.py somehow
# # This example won't run directly without them.
# # from main import setup_arg_parser, setup_logging # This would cause circular import if run directly
#
# # main(setup_arg_parser, setup_logging) # Call with the actual functions if available

View File

@@ -1,123 +0,0 @@
import logging
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed
from typing import List, Dict, Tuple, Optional
# Assuming this import is needed based on the original context
try:
# Import the wrapper function from the file created in the previous step
# Note: This assumes the file is in the same directory or Python path
from main_py_cli_worker_wrapper_line_254 import process_single_asset_wrapper
except ImportError:
print("Warning: Could not import process_single_asset_wrapper. Ensure main_py_cli_worker_wrapper_line_254.py exists.")
# Define a dummy function if import fails
def process_single_asset_wrapper(*args, **kwargs) -> Tuple[str, str, Optional[str]]:
input_path = args[0] if args else "unknown_path"
return (input_path, "failed", "Dummy function: process_single_asset_wrapper not imported")
log = logging.getLogger(__name__) # Assume logger is configured elsewhere
def run_processing(
valid_inputs: List[str],
preset_name: str,
output_dir_for_processor: str,
overwrite: bool,
num_workers: int,
verbose: bool # Add verbose parameter here
) -> Dict:
"""
Executes the core asset processing logic using a process pool.
Args:
valid_inputs: List of validated input file/directory paths (strings).
preset_name: Name of the preset to use.
output_dir_for_processor: Absolute path string for the output base directory.
overwrite: Boolean flag to force reprocessing.
num_workers: Maximum number of worker processes.
verbose: Boolean flag for verbose logging.
Returns:
A dictionary containing processing results:
{
"processed": int,
"skipped": int,
"failed": int,
"results_list": List[Tuple[str, str, Optional[str]]] # (input_path, status, error_msg)
}
"""
log.info(f"Processing {len(valid_inputs)} asset(s) using preset '{preset_name}' with up to {num_workers} worker(s)...")
results_list = []
successful_processed_count = 0
skipped_count = 0
failed_count = 0
# Ensure at least one worker
num_workers = max(1, num_workers)
# Using ProcessPoolExecutor is generally good if AssetProcessor tasks are CPU-bound.
# If tasks are mostly I/O bound, ThreadPoolExecutor might be sufficient.
# Important: Ensure Configuration and AssetProcessor are "pickleable".
try:
with ProcessPoolExecutor(max_workers=num_workers) as executor:
# Create futures
futures = {}
log.debug(f"Submitting {len(valid_inputs)} tasks...")
# Removed the 1-second delay for potentially faster submission in non-CLI use
for i, input_path in enumerate(valid_inputs):
log.debug(f"Submitting task {i+1}/{len(valid_inputs)} for: {Path(input_path).name}")
future = executor.submit(
process_single_asset_wrapper, # Use the imported wrapper
input_path,
preset_name,
output_dir_for_processor,
overwrite,
verbose # Pass the verbose flag
)
futures[future] = input_path # Store future -> input_path mapping
# Process completed futures
for i, future in enumerate(as_completed(futures), 1):
input_path = futures[future]
asset_name = Path(input_path).name
log.info(f"--- [{i}/{len(valid_inputs)}] Worker finished attempt for: {asset_name} ---")
try:
# Get result tuple: (input_path_str, status_string, error_message_or_None)
result_tuple = future.result()
results_list.append(result_tuple)
input_path_res, status, err_msg = result_tuple
# Increment counters based on status
if status == "processed":
successful_processed_count += 1
elif status == "skipped":
skipped_count += 1
elif status == "failed":
failed_count += 1
else: # Should not happen, but log as warning/failure
log.warning(f"Unknown status '{status}' received for {asset_name}. Counting as failed.")
failed_count += 1
except Exception as e:
# Catch errors if the future itself fails (e.g., worker process crashed hard)
log.exception(f"Critical worker failure for {asset_name}: {e}")
results_list.append((input_path, "failed", f"Worker process crashed: {e}"))
failed_count += 1 # Count crashes as failures
except Exception as pool_exc:
log.exception(f"An error occurred with the process pool: {pool_exc}")
# Re-raise or handle as appropriate for the calling context (monitor.py)
# For now, log and return current counts
return {
"processed": successful_processed_count,
"skipped": skipped_count,
"failed": failed_count + (len(valid_inputs) - len(results_list)), # Count unprocessed as failed
"results_list": results_list,
"pool_error": str(pool_exc) # Add pool error info
}
return {
"processed": successful_processed_count,
"skipped": skipped_count,
"failed": failed_count,
"results_list": results_list
}

View File

@@ -1,100 +0,0 @@
import os
import logging
from pathlib import Path
from typing import Tuple, Optional, List, Dict # Added List, Dict
# Assuming these imports are needed based on the original context
try:
from configuration import Configuration, ConfigurationError
from asset_processor import AssetProcessor, AssetProcessingError # Assuming this was the old processor
from rule_structure import SourceRule # Assuming this might be needed
except ImportError:
# Handle missing imports if this file is run standalone
print("Warning: Could not import necessary classes (Configuration, AssetProcessor, etc.).")
Configuration = None
AssetProcessor = None
ConfigurationError = Exception
AssetProcessingError = Exception
SourceRule = None # Define as None if not found
def process_single_asset_wrapper(input_path_str: str, preset_name: str, output_dir_str: str, overwrite: bool, verbose: bool, rules) -> Tuple[str, str, Optional[str]]:
"""
Wrapper function for processing a single input path (which might contain multiple assets)
in a separate process. Handles instantiation of Configuration and AssetProcessor,
passes the overwrite flag, catches errors, and interprets the multi-asset status dictionary.
Ensures logging is configured for the worker process.
Returns:
Tuple[str, str, Optional[str]]:
- input_path_str: The original input path processed.
- overall_status_string: A single status string summarizing the outcome
("processed", "skipped", "failed", "partial_success").
- error_message_or_None: An error message if failures occurred, potentially
listing failed assets.
"""
# Explicitly configure logging for this worker process
worker_log = logging.getLogger(f"Worker_{os.getpid()}") # Log with worker PID
if not logging.root.handlers:
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)-8s] %(name)s: %(message)s')
worker_log.setLevel(logging.DEBUG if verbose else logging.INFO)
if verbose:
logging.root.setLevel(logging.DEBUG)
input_path_obj = Path(input_path_str)
input_name = input_path_obj.name
try:
worker_log.info(f"Starting processing attempt for input: {input_name}")
# Ensure Configuration is available before using
if Configuration is None:
raise RuntimeError("Configuration class not imported.")
config = Configuration(preset_name)
output_base_path = Path(output_dir_str)
# Ensure AssetProcessor is available before using
if AssetProcessor is None:
raise RuntimeError("AssetProcessor class not imported.")
processor = AssetProcessor(input_path_obj, config, output_base_path, overwrite=overwrite)
# processor.process() now returns a Dict[str, List[str]]
status_dict = processor.process(rules=rules)
# --- Interpret the status dictionary ---
processed_assets = status_dict.get("processed", [])
skipped_assets = status_dict.get("skipped", [])
failed_assets = status_dict.get("failed", [])
overall_status_string = "failed" # Default
error_message = None
if failed_assets:
overall_status_string = "failed"
error_message = f"Failed assets within {input_name}: {', '.join(failed_assets)}"
worker_log.error(error_message) # Log the failure details
elif processed_assets:
overall_status_string = "processed"
# Check for partial success (mix of processed/skipped and failed should be caught above)
if skipped_assets:
worker_log.info(f"Input '{input_name}' processed with some assets skipped. Processed: {processed_assets}, Skipped: {skipped_assets}")
else:
worker_log.info(f"Input '{input_name}' processed successfully. Assets: {processed_assets}")
elif skipped_assets:
overall_status_string = "skipped"
worker_log.info(f"Input '{input_name}' skipped (all contained assets already exist). Assets: {skipped_assets}")
else:
# Should not happen if input contained files, but handle as failure.
worker_log.warning(f"Input '{input_name}' resulted in no processed, skipped, or failed assets. Reporting as failed.")
overall_status_string = "failed"
error_message = f"No assets processed, skipped, or failed within {input_name}."
return (input_path_str, overall_status_string, error_message)
except (ConfigurationError, AssetProcessingError) as e:
# Catch errors during processor setup or the process() call itself if it raises before returning dict
worker_log.error(f"Processing failed for input '{input_name}': {type(e).__name__}: {e}")
return (input_path_str, "failed", f"{type(e).__name__}: {e}")
except Exception as e:
# Catch any other unexpected errors
worker_log.exception(f"Unexpected worker failure processing input '{input_name}': {e}")
return (input_path_str, "failed", f"Unexpected Worker Error: {e}")

View File

@@ -1,291 +0,0 @@
import bpy
from pathlib import Path
import time
import os
import math
# Try importing NumPy
try:
import numpy as np
numpy_available = True
# print("NumPy module found.") # Less verbose
except ImportError:
print("Warning: NumPy module not found. Median calc disabled, mean uses loop.")
numpy_available = False
# --- Configuration ---
ASSET_LIBRARY_NAME = "Nodes-Linked" # <<< Name of Asset Library in Prefs
TEMPLATE_MATERIAL_NAME = "Template_PBRMaterial" # <<< Name of template Material in current file
PLACEHOLDER_NODE_LABEL = "PBRSET_PLACEHOLDER" # <<< Label of placeholder node in template mat
ASSET_NAME_PREFIX = "PBRSET_" # <<< Prefix of Node Group assets to process
MATERIAL_NAME_PREFIX = "Mat_" # <<< Prefix for created Materials
THUMBNAIL_PROPERTY_NAME = "thumbnail_filepath" # <<< Custom property name on Node Groups
VALID_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
DERIVED_MAP_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.tif', '.tiff']
VIEWPORT_GAMMA = 0.4
SCALED_SIZE = (32, 32) # Downscale target size for calculations
# --- >>> SET MATERIAL CREATION LIMIT HERE <<< ---
# Max number of *new* materials created per run (0 = no limit)
MATERIAL_CREATION_LIMIT = 900
# ------------------------------------------------
# --- Helper Functions ---
def find_node_by_label(node_tree, label, node_type=None):
# Finds first node by label and optional type (using node.type)
if not node_tree: return None
for node in node_tree.nodes:
if node.label and node.label == label:
if node_type is None or node.type == node_type: return node
return None
def calculate_value_from_image(image, target_size=(64, 64), mode='color', method='median'):
# Calculates median/mean from downscaled image copy, cleans up temp image
temp_img = None; #... (Full implementation from previous step) ...
if not image: return None
try:
if not image.has_data:
try: _ = len(image.pixels); image.update()
except Exception: pass
if not image.has_data: return None # Cannot proceed
temp_img = image.copy()
if not temp_img: return None
temp_img.scale(target_size[0], target_size[1])
try: _ = len(temp_img.pixels); temp_img.update()
except Exception: pass # Ignore access error, check has_data
if not temp_img.has_data: return None
width=temp_img.size[0]; height=temp_img.size[1]; channels=temp_img.channels
if width == 0 or height == 0 or channels == 0: return None
pixels = temp_img.pixels[:]; result_value = None;
if numpy_available: # Use NumPy
np_pixels = np.array(pixels); num_elements = len(np_pixels); num_pixels_actual = num_elements // channels;
if num_pixels_actual == 0: return None
np_pixels = np_pixels[:num_pixels_actual * channels]; pixels_reshaped = np_pixels.reshape((num_pixels_actual, channels))
if mode == 'color': # Color Median/Mean (NumPy)
if channels < 3: return None
calc_linear = np.median(pixels_reshaped[:, :3], axis=0) if method == 'median' else np.mean(pixels_reshaped[:, :3], axis=0)
inv_gamma = 1.0 / VIEWPORT_GAMMA; calc_linear_clamped = np.clip(calc_linear, 0.0, None)
calc_srgb_np = np.power(calc_linear_clamped, inv_gamma); calc_srgb_clamped = np.clip(calc_srgb_np, 0.0, 1.0)
result_value = (calc_srgb_clamped[0], calc_srgb_clamped[1], calc_srgb_clamped[2], 1.0)
elif mode == 'grayscale': # Grayscale Median/Mean (NumPy)
calc_val = np.median(pixels_reshaped[:, 0]) if method == 'median' else np.mean(pixels_reshaped[:, 0])
result_value = min(max(0.0, calc_val), 1.0)
elif method == 'mean': # Fallback Mean Loop
# print(" Calculating mean using standard loop...") # Verbose
actual_len = len(pixels); #... (Mean loop logic) ...
if actual_len == 0: return None; num_pixels_in_buffer=actual_len//channels; max_elements=num_pixels_in_buffer*channels
if num_pixels_in_buffer == 0: return None
if mode == 'color':
sum_r,sum_g,sum_b = 0.0,0.0,0.0; step=channels
for i in range(0, max_elements, step):
if i+2 >= actual_len: break; sum_r+=pixels[i]; sum_g+=pixels[i+1]; sum_b+=pixels[i+2]
avg_r_lin,avg_g_lin,avg_b_lin = sum_r/num_pixels_in_buffer, sum_g/num_pixels_in_buffer, sum_b/num_pixels_in_buffer
inv_gamma = 1.0/VIEWPORT_GAMMA
avg_r_srgb,avg_g_srgb,avg_b_srgb = min(max(0.0,pow(max(0.0,avg_r_lin),inv_gamma)),1.0), min(max(0.0,pow(max(0.0,avg_g_lin),inv_gamma)),1.0), min(max(0.0,pow(max(0.0,avg_b_lin),inv_gamma)),1.0)
result_value = (avg_r_srgb, avg_g_srgb, avg_b_srgb, 1.0)
elif mode == 'grayscale':
sum_val=0.0; step=channels
for i in range(0, max_elements, step): sum_val+=pixels[i]
result_value = min(max(0.0, sum_val/num_pixels_in_buffer), 1.0)
else: print(" Error: NumPy required for median calculation."); return None
return result_value
except Exception as e: print(f" Error during value calculation for '{image.name}': {e}"); return None
finally: # Cleanup
if temp_img:
try: bpy.data.images.remove(temp_img, do_unlink=True)
except Exception: pass # Ignore cleanup errors
# --- Main Function ---
def create_materials_for_library_assets(library_name):
start_time = time.time(); print(f"--- Starting Material Creation for Library '{library_name}' ---")
print(f"Material Creation Limit per run: {'Unlimited' if MATERIAL_CREATION_LIMIT <= 0 else MATERIAL_CREATION_LIMIT}")
# (Prerequisite checks...)
template_mat=bpy.data.materials.get(TEMPLATE_MATERIAL_NAME); #... etc ...
if not template_mat or not template_mat.use_nodes or not find_node_by_label(template_mat.node_tree, PLACEHOLDER_NODE_LABEL, 'GROUP'): print("Template Prereq Failed."); return
library=bpy.context.preferences.filepaths.asset_libraries.get(library_name); #... etc ...
if not library or not Path(bpy.path.abspath(library.path)).exists(): print("Library Prereq Failed."); return
print(f"Found template material and library path...")
# (File scanning...)
materials_created=0; materials_skipped=0; nodegroups_processed=0; link_errors=0; files_to_process=[]; library_path_obj=Path(bpy.path.abspath(library.path))
#... (populate files_to_process) ...
if library_path_obj.is_dir():
for item in library_path_obj.iterdir():
if item.is_file() and item.suffix.lower() == '.blend': files_to_process.append(str(item))
if not files_to_process: print(f"Warning: No .blend files found in dir: {library_path_obj}")
elif library_path_obj.is_file() and library_path_obj.suffix.lower() == '.blend':
files_to_process.append(str(library_path_obj))
else: print(f"Error: Library path not dir or .blend: {library_path_obj}"); return
print(f"Found {len(files_to_process)} .blend file(s) to inspect.")
# Initialize counters and flag for limit
created_in_this_run = 0
limit_reached_flag = False
for blend_file_path in files_to_process: # ... (inspect loop) ...
print(f"\nInspecting library file: {os.path.basename(blend_file_path)}...")
potential_nodegroups = []; # ... (inspection logic) ...
try:
with bpy.data.libraries.load(blend_file_path, link=False) as (data_from, data_to): potential_nodegroups = list(data_from.node_groups)
except Exception as e_load_inspect: print(f" Error inspecting file '{blend_file_path}': {e_load_inspect}"); continue
print(f" Found {len(potential_nodegroups)} NGs. Checking for '{ASSET_NAME_PREFIX}'...")
for asset_nodegroup_name in potential_nodegroups: # ... (NG loop) ...
if not asset_nodegroup_name.startswith(ASSET_NAME_PREFIX): continue
nodegroups_processed += 1
base_name = asset_nodegroup_name.removeprefix(ASSET_NAME_PREFIX)
material_name = f"{MATERIAL_NAME_PREFIX}{base_name}"
if bpy.data.materials.get(material_name): materials_skipped += 1; continue
linked_nodegroup = None; preview_path = None
try: # --- Start Main Processing Block for NG ---
# (Linking logic...)
existing_group = bpy.data.node_groups.get(asset_nodegroup_name); #... etc linking ...
is_correctly_linked = (existing_group and existing_group.library and bpy.path.abspath(existing_group.library.filepath) == blend_file_path)
if is_correctly_linked: linked_nodegroup = existing_group
else: # Link it
with bpy.data.libraries.load(blend_file_path, link=True, relative=False) as (data_from, data_to):
if asset_nodegroup_name in data_from.node_groups: data_to.node_groups = [asset_nodegroup_name]
else: print(f" Error: NG '{asset_nodegroup_name}' not found during link."); continue # Skip NG
linked_nodegroup = bpy.data.node_groups.get(asset_nodegroup_name)
if not linked_nodegroup or not linked_nodegroup.library: print(f" Error: NG '{asset_nodegroup_name}' link failed."); linked_nodegroup = None; link_errors += 1
if not linked_nodegroup: print(f" Failed link NG '{asset_nodegroup_name}'. Skip."); continue # Skip NG
preview_path = linked_nodegroup.get(THUMBNAIL_PROPERTY_NAME) # Path to COL-1 1K
# (Duplicate, Rename, Replace Placeholder...)
new_material = template_mat.copy(); #... checks ...
if not new_material: print(f" Error: Failed copy template mat. Skip."); continue
new_material.name = material_name
if not new_material.use_nodes or not new_material.node_tree: print(f" Error: New mat '{material_name}' no nodes."); continue
placeholder_node = find_node_by_label(new_material.node_tree, PLACEHOLDER_NODE_LABEL, 'GROUP'); #... checks ...
if not placeholder_node: print(f" Error: Placeholder '{PLACEHOLDER_NODE_LABEL}' not found."); continue
placeholder_node.node_tree = linked_nodegroup
print(f" Created material '{material_name}' and linked NG '{linked_nodegroup.name}'.")
# --- Load base COL-1 image once ---
thumbnail_image = None
if preview_path and Path(preview_path).is_file():
try: thumbnail_image = bpy.data.images.load(preview_path, check_existing=True)
except Exception as e_load_base: print(f" Error loading base thumbnail '{preview_path}': {e_load_base}")
# --- Set Viewport Color (Median) ---
median_color = None
if thumbnail_image: median_color = calculate_value_from_image(thumbnail_image, target_size=SCALED_SIZE, mode='color', method='median')
if median_color: new_material.diffuse_color = median_color; print(f" Set viewport color: {median_color[:3]}")
else: print(f" Warn: Could not set viewport color.")
# --- Determine Paths and Metal Map Existence ---
roughness_path = None; metallic_path = None; metal_map_found = False; #... etc ...
if preview_path and "_COL-1" in preview_path:
try: # ... path derivation logic ...
base_path_obj=Path(preview_path); directory=base_path_obj.parent; base_stem=base_path_obj.stem
if "_COL-1" in base_stem:
rough_stem=base_stem.replace("_COL-1", "_ROUGH")
for ext in DERIVED_MAP_EXTENSIONS:
potential_path=directory/f"{rough_stem}{ext}";
if potential_path.is_file(): roughness_path=str(potential_path); break
metal_stem=base_stem.replace("_COL-1", "_METAL")
for ext in DERIVED_MAP_EXTENSIONS:
potential_path=directory/f"{metal_stem}{ext}";
if potential_path.is_file(): metallic_path=str(potential_path); metal_map_found=True; break
except Exception as e_derive: print(f" Error deriving paths: {e_derive}")
if not metal_map_found: print(f" Info: No METAL map found. Assuming Spec/Gloss.")
# --- Set Viewport Roughness (Median, Conditional Inversion) ---
median_roughness = None; # ... etc ...
if roughness_path:
try: rough_img = bpy.data.images.load(roughness_path, check_existing=True)
except Exception as e_load_rough: print(f" Error loading rough image: {e_load_rough}")
if rough_img: median_roughness = calculate_value_from_image(rough_img, target_size=SCALED_SIZE, mode='grayscale', method='median')
else: print(f" Error: load None for rough path.")
if median_roughness is not None:
final_roughness_value = median_roughness
if not metal_map_found: final_roughness_value = 1.0 - median_roughness; print(f" Inverting ROUGH->Gloss: {median_roughness:.3f} -> {final_roughness_value:.3f}")
new_material.roughness = min(max(0.0, final_roughness_value), 1.0); print(f" Set viewport roughness: {new_material.roughness:.3f}")
else: print(f" Warn: Could not set viewport roughness.")
# --- Set Viewport Metallic (Median) ---
median_metallic = None; # ... etc ...
if metal_map_found:
try: metal_img = bpy.data.images.load(metallic_path, check_existing=True)
except Exception as e_load_metal: print(f" Error loading metal image: {e_load_metal}")
if metal_img: median_metallic = calculate_value_from_image(metal_img, target_size=SCALED_SIZE, mode='grayscale', method='median')
else: print(f" Error: load None for metal path.")
if median_metallic is not None: new_material.metallic = median_metallic; print(f" Set viewport metallic: {median_metallic:.3f}")
else: new_material.metallic = 0.0; # Default
if metal_map_found: print(f" Warn: Could not calc viewport metallic. Set 0.0.")
else: print(f" Set viewport metallic to default: 0.0")
# --- Mark Material as Asset ---
mat_asset_data = None; # ... (logic remains same) ...
try: # ... asset marking ...
if not new_material.asset_data: new_material.asset_mark(); print(f" Marked material as asset.")
mat_asset_data = new_material.asset_data
except Exception as e_asset: print(f" Error marking mat asset: {e_asset}")
# --- Copy Asset Tags ---
if mat_asset_data and linked_nodegroup.asset_data: # ... (logic remains same) ...
try: # ... tag copying ...
source_tags=linked_nodegroup.asset_data.tags; target_tags=mat_asset_data.tags
tags_copied_count=0; existing_target_tag_names={t.name for t in target_tags}
for src_tag in source_tags:
if src_tag.name not in existing_target_tag_names: target_tags.new(name=src_tag.name); tags_copied_count += 1
if tags_copied_count > 0: print(f" Copied {tags_copied_count} asset tags.")
except Exception as e_tags: print(f" Error copying tags: {e_tags}")
# --- Set Custom Preview for Material ---
if preview_path and Path(preview_path).is_file(): # ... (logic remains same) ...
try: # ... preview setting ...
with bpy.context.temp_override(id=new_material): bpy.ops.ed.lib_id_load_custom_preview(filepath=preview_path)
except RuntimeError as e_op: print(f" Error running preview op for mat '{new_material.name}': {e_op}")
except Exception as e_prev: print(f" Unexpected preview error for mat: {e_prev}")
elif preview_path: print(f" Warn: Thumb path not found for preview step: '{preview_path}'")
# --- Increment Counters & Check Limit ---
materials_created += 1 # Overall counter for summary
created_in_this_run += 1 # Counter for this run's limit
# Check limit AFTER successful creation
if MATERIAL_CREATION_LIMIT > 0 and created_in_this_run >= MATERIAL_CREATION_LIMIT:
print(f"\n--- Material Creation Limit ({MATERIAL_CREATION_LIMIT}) Reached ---")
limit_reached_flag = True
break # Exit inner loop
except Exception as e: # Catch errors for the whole NG processing block
print(f" An unexpected error occurred processing NG '{asset_nodegroup_name}': {e}")
# --- End Main Processing Block for NG ---
# Check flag to stop outer loop
if limit_reached_flag:
print("Stopping library file iteration due to limit.")
break # Exit outer loop
# (Completion summary...)
end_time = time.time(); duration = end_time - start_time; print("\n--- Material Creation Finished ---"); # ... etc ...
print(f"Duration: {duration:.2f} seconds")
print(f"Summary: Processed {nodegroups_processed} NGs. Created {materials_created} Mats this run. Skipped {materials_skipped}. Link Errors {link_errors}.")
if limit_reached_flag: print(f"NOTE: Script stopped early due to creation limit ({MATERIAL_CREATION_LIMIT}). Run again to process more.")
# --- How to Run ---
# 1. Rerun Script 1 to add "thumbnail_filepath" property.
# 2. Setup Asset Library in Prefs. Set ASSET_LIBRARY_NAME below.
# 3. In current file, create "Template_PBRMaterial" with "PBRSET_PLACEHOLDER" node.
# 4. Set MATERIAL_CREATION_LIMIT in Config section above (0 for unlimited).
# 5. Paste script & Run (Alt+P).
if __name__ == "__main__":
# Only need ASSET_LIBRARY_NAME configuration here now
if ASSET_LIBRARY_NAME == "My Asset Library": # Default check
print("\nERROR: Please update the 'ASSET_LIBRARY_NAME' variable in the script's Configuration section.")
print(" Set it to the name of your asset library in Blender Preferences before running.\n")
elif not bpy.data.materials.get(TEMPLATE_MATERIAL_NAME):
print(f"\nERROR: Template material '{TEMPLATE_MATERIAL_NAME}' not found in current file.\n")
else:
create_materials_for_library_assets(ASSET_LIBRARY_NAME)

View File

@@ -1,988 +0,0 @@
# Full script - PBR Texture Importer V4 (Manifest, Auto-Save/Reload, Aspect Ratio, Asset Tags)
import bpy
import os # For auto-save rename/remove
from pathlib import Path
import time
import base64
import numpy as np # For stats calculation
import json # For manifest handling
import re # For parsing scaling string
# --- USER CONFIGURATION ---
# File Paths & Templates
texture_root_directory = r"G:\02 Content\10-19 Content\13 Textures Power of Two\13.00" # <<< CHANGE THIS PATH!
PARENT_TEMPLATE_NAME = "Template_PBRSET" # Name of the parent node group template
CHILD_TEMPLATE_NAME = "Template_PBRTYPE" # Name of the child node group template
# Processing Limits & Intervals
MAX_NEW_GROUPS_PER_RUN = 1000 # Max NEW parent groups created per run before stopping
SAVE_INTERVAL = 25 # Auto-save interval during NEW group creation (every N groups)
# Features & Behavior
AUTO_SAVE_ENABLED = True # Enable periodic auto-saving (main file + manifest) during processing?
AUTO_RELOAD_ON_FINISH = True # Save and reload the blend file upon successful script completion?
# Naming & Structure Conventions
VALID_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff"} # Allowed texture file types
RESOLUTION_LABELS = ["1k", "2k", "4k", "8k"] # Expected resolution labels (LOWEST FIRST for aspect/tag calc)
SG_VALUE_NODE_LABEL = "SpecularGlossy" # Label for the Specular/Glossy value node in parent template
HISTOGRAM_NODE_PREFIX = "Histogram-" # Prefix for Combine XYZ nodes storing stats (e.g., "Histogram-ROUGH")
ASPECT_RATIO_NODE_LABEL = "AspectRatioCorrection" # Label for the Value node storing the aspect ratio correction factor
# Texture Map Properties
PBR_COLOR_SPACE_MAP = { # Map PBR type (from filename) to Blender color space
"AO": "sRGB", "COL-1": "sRGB", "COL-2": "sRGB", "COL-3": "sRGB",
"DISP": "Non-Color", "NRM": "Non-Color", "REFL": "Non-Color", "ROUGH": "Non-Color",
"METAL": "Non-Color", "FUZZ": "Non-Color", "MASK": "Non-Color", "SSS": "sRGB",
}
DEFAULT_COLOR_SPACE = "sRGB" # Fallback color space if PBR type not in map
# --- END USER CONFIGURATION ---
# --- Helper Functions ---
def parse_texture_filename(filename_stem):
"""Parses texture filename stem based on expected convention."""
parts = filename_stem.split('_');
# Expecting Tag_Groupname_Resolution_Scaling_PBRType
if len(parts) == 5:
return {"Tag": parts[0], "Groupname": parts[1], "Resolution": parts[2], "Scaling": parts[3], "PBRType": parts[4]}
else:
print(f" Warn: Skip '{filename_stem}' - Expected 5 parts, found {len(parts)}.");
return None
def find_nodes_by_label(node_tree, label, node_type=None):
"""Finds ALL nodes in a node tree matching the label and optionally type."""
if not node_tree:
return []
matching_nodes = []
for node in node_tree.nodes:
if node.label and node.label == label:
if node_type is None or node.type == node_type:
matching_nodes.append(node)
return matching_nodes
def encode_name_b64(name_str):
"""Encodes a string using URL-safe Base64 for node group names."""
try:
return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')
except Exception as e:
print(f" Error base64 encoding '{name_str}': {e}");
return name_str # Fallback to original name on error
def calculate_image_stats(image):
"""Calculates Min, Max, Median of the first channel of a Blender image."""
if not image: return None
pixels_arr, value_channel_arr, result = None, None, None
try:
width = image.size[0]; height = image.size[1]; channels = image.channels
if width == 0 or height == 0 or channels == 0:
print(f" Warn: Invalid dims for '{image.name}'. Skip stats."); return None
actual_len = len(image.pixels); expected_len = width * height * channels
if expected_len != actual_len:
print(f" Warn: Pixel buffer mismatch for '{image.name}'. Skip stats."); return None
if actual_len == 0: return None
pixels_arr = np.fromiter(image.pixels, dtype=np.float32, count=actual_len)
if channels == 1: value_channel_arr = pixels_arr
elif channels >= 2: value_channel_arr = pixels_arr[0::channels]
else: return None
if value_channel_arr is None or value_channel_arr.size == 0:
print(f" Warn: No value channel for '{image.name}'. Skip stats."); return None
min_val = float(np.min(value_channel_arr))
max_val = float(np.max(value_channel_arr))
median_val = float(np.median(value_channel_arr))
result = (min_val, max_val, median_val)
except MemoryError:
print(f" Error: Not enough memory for stats calc on '{image.name}'.")
except Exception as e:
print(f" Error during stats calc for '{image.name}': {e}");
import traceback; traceback.print_exc()
finally:
# Explicitly delete potentially large numpy arrays
if 'value_channel_arr' in locals() and value_channel_arr is not None:
try:
del value_channel_arr
except NameError:
pass # Ignore if already gone
if 'pixels_arr' in locals() and pixels_arr is not None:
try:
del pixels_arr
except NameError:
pass # Ignore if already gone
return result
def calculate_aspect_ratio_factor(image_width, image_height, scaling_string):
"""Calculates the X-axis UV scaling factor based on image dims and scaling string."""
if image_height <= 0:
print(" Warn: Image height is zero, cannot calculate aspect ratio. Returning 1.0.")
return 1.0 # Return 1.0 if height is invalid
# Calculate the actual aspect ratio of the image file
current_aspect_ratio = image_width / image_height
# Check the scaling string
if scaling_string.upper() == "EVEN":
# 'EVEN' means uniform scaling was applied (or none needed).
# The correction factor is the image's own aspect ratio.
return current_aspect_ratio
else:
# Handle non-uniform scaling cases ("Xnnn", "Ynnn")
match = re.match(r"([XY])(\d+)", scaling_string, re.IGNORECASE)
if not match:
print(f" Warn: Invalid Scaling string format '{scaling_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.")
# Fallback to the image's own ratio if scaling string is invalid
return current_aspect_ratio
axis = match.group(1).upper()
try:
amount = int(match.group(2))
if amount <= 0:
print(f" Warn: Zero or negative Amount in Scaling string '{scaling_string}'. Returning current ratio {current_aspect_ratio:.4f}.")
return current_aspect_ratio
except ValueError:
print(f" Warn: Invalid Amount in Scaling string '{scaling_string}'. Returning current ratio {current_aspect_ratio:.4f}.")
return current_aspect_ratio
# Apply the non-uniform correction formula
factor = current_aspect_ratio # Default to current ratio in case of issues below
scaling_factor_percent = amount / 100.0
try:
if axis == 'X':
if scaling_factor_percent == 0: raise ZeroDivisionError
factor = current_aspect_ratio / scaling_factor_percent
elif axis == 'Y':
factor = current_aspect_ratio * scaling_factor_percent
# No 'else' needed due to regex structure
except ZeroDivisionError:
print(f" Warn: Division by zero during factor calculation. Returning current ratio {current_aspect_ratio:.4f}.")
return current_aspect_ratio
return factor
# --- Manifest Helper Functions ---
def get_manifest_path(context_filepath):
"""Gets the expected path for the manifest JSON file based on blend filepath."""
if not context_filepath:
return None
blend_path = Path(context_filepath)
manifest_filename = f"{blend_path.stem}_manifest.json"
return blend_path.parent / manifest_filename
def load_manifest(manifest_path):
"""Loads the manifest data from the JSON file."""
if not manifest_path or not manifest_path.exists():
return {}
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f" Loaded manifest from: {manifest_path.name}")
return data
except json.JSONDecodeError:
print(f"!!! ERROR: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!")
return {}
except Exception as e:
print(f"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!")
return {}
def save_manifest(manifest_path, data):
"""Saves the manifest data to the JSON file."""
if not manifest_path:
return False
try:
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
return True
except Exception as e:
print(f"!!!!!!!!!!!!!!!!!!!\n!!! Manifest save FAILED: {e} !!!\n!!!!!!!!!!!!!!!!!!!")
return False
# --- Auto-Save Helper Function ---
def perform_safe_autosave(manifest_path, manifest_data):
"""Performs a safe auto-save of the main blend file and manifest."""
blend_filepath = bpy.data.filepath
if not blend_filepath or not manifest_path:
print(" Skipping auto-save: Blend file is not saved.")
return False
print(f"\n--- Attempting Auto-Save ({time.strftime('%H:%M:%S')}) ---")
blend_path = Path(blend_filepath)
manifest_path_obj = Path(manifest_path) # Ensure it's a Path object
blend_bak_path = blend_path.with_suffix('.blend.bak')
manifest_bak_path = manifest_path_obj.with_suffix('.json.bak')
# 1. Delete old backups if they exist
try:
if blend_bak_path.exists():
blend_bak_path.unlink()
if manifest_bak_path.exists():
manifest_bak_path.unlink()
except OSError as e:
print(f" Warn: Could not delete old backup file: {e}")
# Continue anyway, renaming might still work
# 2. Rename current files to backup
renamed_blend = False
renamed_manifest = False
try:
if blend_path.exists():
os.rename(blend_path, blend_bak_path)
renamed_blend = True
# print(f" Renamed '{blend_path.name}' to '{blend_bak_path.name}'") # Optional verbose log
if manifest_path_obj.exists():
os.rename(manifest_path_obj, manifest_bak_path)
renamed_manifest = True
# print(f" Renamed '{manifest_path_obj.name}' to '{manifest_bak_path.name}'") # Optional verbose log
except OSError as e:
print(f"!!! ERROR: Failed to rename files for backup: {e} !!!")
# Attempt to roll back renames if only one succeeded
if renamed_blend and not renamed_manifest and blend_bak_path.exists():
print(f" Attempting rollback: Renaming {blend_bak_path.name} back...")
try:
os.rename(blend_bak_path, blend_path)
except OSError as rb_e:
print(f" Rollback rename of blend file FAILED: {rb_e}")
if renamed_manifest and not renamed_blend and manifest_bak_path.exists():
print(f" Attempting rollback: Renaming {manifest_bak_path.name} back...")
try:
os.rename(manifest_bak_path, manifest_path_obj)
except OSError as rb_e:
print(f" Rollback rename of manifest file FAILED: {rb_e}")
print("--- Auto-Save ABORTED ---")
return False
# 3. Save new main blend file
save_blend_success = False
try:
bpy.ops.wm.save_mainfile()
print(f" Saved main blend file: {blend_path.name}")
save_blend_success = True
except Exception as e:
print(f"!!!!!!!!!!!!!!!!!!!!!!!!!\n!!! Auto-Save FAILED (Blend File Save): {e} !!!\n!!!!!!!!!!!!!!!!!!!!!!!!!")
# Attempt to restore from backup
print(" Attempting to restore from backup...")
try:
if blend_bak_path.exists():
os.rename(blend_bak_path, blend_path)
if manifest_bak_path.exists():
os.rename(manifest_bak_path, manifest_path_obj)
print(" Restored from backup.")
except OSError as re:
print(f"!!! CRITICAL: Failed to restore from backup after save failure: {re} !!!")
print(f"!!! Please check for '.bak' files manually in: {blend_path.parent} !!!")
print("--- Auto-Save ABORTED ---")
return False
# 4. Save new manifest file (only if blend save succeeded)
if save_blend_success:
if save_manifest(manifest_path, manifest_data):
print(f" Saved manifest file: {manifest_path_obj.name}")
print("--- Auto-Save Successful ---")
return True
else:
# Manifest save failed, but blend file is okay. Warn user.
print("!!! WARNING: Auto-save completed for blend file, but manifest save FAILED. Manifest may be out of sync. !!!")
return True # Still counts as 'completed' in terms of blend file safety
return False # Should not be reached
# --- Asset Tagging Helper Functions ---
def add_tag_if_new(asset_data, tag_name):
"""Adds a tag to the asset data if it's not None/empty and doesn't already exist."""
if not asset_data or not tag_name or not isinstance(tag_name, str) or tag_name.strip() == "":
return False # Invalid input
cleaned_tag_name = tag_name.strip() # Remove leading/trailing whitespace
if not cleaned_tag_name:
return False # Don't add empty tags
# Check if tag already exists
if cleaned_tag_name not in [t.name for t in asset_data.tags]:
try:
asset_data.tags.new(cleaned_tag_name)
print(f" + Added Asset Tag: '{cleaned_tag_name}'")
return True
except Exception as e:
print(f" Error adding tag '{cleaned_tag_name}': {e}")
return False
else:
# print(f" Tag '{cleaned_tag_name}' already exists.") # Optional info
return False # Not added because it existed
def get_supplier_tag_from_path(file_path_str, groupname):
"""
Determines supplier tag based on directory structure.
Assumes structure is .../Supplier/Groupname/file.ext or .../Supplier/file.ext
"""
try:
file_path = Path(file_path_str).resolve()
groupname_lower = groupname.lower()
if not file_path.is_file():
print(f" Warn (get_supplier_tag): Input path is not a file: {file_path_str}")
return None
current_dir = file_path.parent # Directory containing the file
if not current_dir:
print(f" Warn (get_supplier_tag): Cannot get parent directory for {file_path_str}")
return None # Cannot determine without parent
parent_dir = current_dir.parent # Directory potentially containing the 'supplier' name
# Check if we are at the root or have no parent
if not parent_dir or parent_dir == current_dir:
# If the file is in the root scan directory or similar shallow path,
# maybe the directory it's in IS the supplier tag? Or return None?
# Returning current_dir.name might be unexpected, let's return None for safety.
print(f" Warn (get_supplier_tag): File path too shallow to determine supplier reliably: {file_path_str}")
return None
# Compare the file's directory name with the groupname
if current_dir.name.lower() == groupname_lower:
# Structure is likely .../Supplier/Groupname/file.ext
# Return the name of the directory ABOVE the groupname directory
return parent_dir.name
else:
# Structure is likely .../Supplier/file.ext
# Return the name of the directory CONTAINING the file
return current_dir.name
except Exception as e:
print(f" Error getting supplier tag for {groupname} from path {file_path_str}: {e}")
return None
def apply_asset_tags(parent_group, groupname, group_info):
"""Applies various asset tags to the parent node group."""
if not parent_group:
return
# 1. Ensure group is marked as an asset
try:
if not parent_group.asset_data:
print(f" Marking '{parent_group.name}' as asset for tagging.")
parent_group.asset_mark()
# Ensure asset_data is available after marking
if not parent_group.asset_data:
print(f" Error: Could not access asset_data for '{parent_group.name}' after marking.")
return
asset_data = parent_group.asset_data
except Exception as e_mark:
print(f" Error marking group '{parent_group.name}' as asset: {e_mark}")
return # Cannot proceed without asset_data
# 2. Apply Supplier Tag (Current Requirement)
try:
# Find lowest resolution path (reuse logic from aspect ratio)
lowest_res_path = None; found_res = False
pbr_types_dict = group_info.get("pbr_types", {})
# Check RESOLUTION_LABELS in order (assuming lowest is first)
for res_label in RESOLUTION_LABELS:
for res_data in pbr_types_dict.values(): # Check all PBR types for this res
if res_label in res_data:
lowest_res_path = res_data[res_label]
found_res = True
break # Found path for this resolution label
if found_res:
break # Found lowest available resolution path
if lowest_res_path:
supplier_tag = get_supplier_tag_from_path(lowest_res_path, groupname)
if supplier_tag:
add_tag_if_new(asset_data, supplier_tag) # Use helper to add if new
else:
print(f" Warn (apply_asset_tags): No image path found for group '{groupname}' to determine supplier tag.")
except Exception as e_supp:
print(f" Error during supplier tag processing for '{groupname}': {e_supp}")
# 3. --- Future Tagging Logic Placeholder ---
# Example: Tag based on PBR Types present
# try:
# present_pbr_types = list(group_info.get("pbr_types", {}).keys())
# for pbr_tag in present_pbr_types:
# # Maybe add prefix or modify tag name
# add_tag_if_new(asset_data, f"PBR_{pbr_tag}")
# except Exception as e_pbr:
# print(f" Error during PBR type tagging for '{groupname}': {e_pbr}")
# Example: Tag based on filename Tag (if not default like 'T-MR')
# filename_tag = group_info.get("tag") # Need to store 'Tag' in group_info during scan
# if filename_tag and filename_tag not in ["T-MR", "T-SG"]:
# add_tag_if_new(asset_data, f"Tag_{filename_tag}")
# --- End Future Tagging Logic ---
# --- Main Processing Function ---
def process_textures_to_groups(root_directory):
"""Scans textures, creates/updates node groups based on templates and manifest."""
start_time = time.time()
print(f"--- Starting Texture Processing ---")
print(f"Scanning directory: {root_directory}")
root_path = Path(root_directory)
if not root_path.is_dir():
print(f"Error: Directory not found: {root_directory}")
return False # Indicate failure
# --- Manifest Setup ---
current_blend_filepath = bpy.data.filepath
manifest_path = get_manifest_path(current_blend_filepath)
manifest_data = {}
manifest_enabled = False
if manifest_path:
manifest_data = load_manifest(manifest_path)
manifest_enabled = True
# Flag will be True if any change requires saving the manifest
manifest_needs_saving = False
# --- End Manifest Setup ---
# --- Load Templates ---
template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)
template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)
if not template_parent:
print(f"Error: Parent template '{PARENT_TEMPLATE_NAME}' not found.")
return False
if not template_child:
print(f"Error: Child template '{CHILD_TEMPLATE_NAME}' not found.")
return False
print(f"Found templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'")
# --- End Load Templates ---
# --- Initialize Data Structures ---
# Stores {"GroupName": {"pbr_types": {...}, "scaling": "...", "sg": False, "thumb": "..."}}
texture_data = {}
file_count, processed_files = 0, 0
groups_created, groups_updated, child_groups_created, child_groups_updated = 0, 0, 0, 0
nodes_updated, links_created = 0, 0
# Cache for image datablocks loaded in THIS RUN only
loaded_images_this_run = {}
# --- End Initialize Data Structures ---
print("Scanning files...")
# --- File Scanning ---
for dirpath, _, filenames in os.walk(root_directory):
for filename in filenames:
file_path = Path(dirpath) / filename
# Check extension
if file_path.suffix.lower() not in VALID_EXTENSIONS:
continue
file_count += 1
filename_stem = file_path.stem
parsed = parse_texture_filename(filename_stem)
if not parsed:
continue # Skip if filename doesn't match format
# Extract parts
groupname = parsed["Groupname"]
pbr_type = parsed["PBRType"]
resolution_label = parsed["Resolution"].lower()
scaling_str = parsed["Scaling"]
tag_str = parsed["Tag"].upper()
file_path_str = str(file_path)
# Validate resolution label
if resolution_label not in RESOLUTION_LABELS:
print(f"Warn: Skip '{filename}' - Invalid Res '{resolution_label}'. Expected one of {RESOLUTION_LABELS}")
continue
# Ensure base structure for group exists in texture_data
group_entry = texture_data.setdefault(groupname, {
"pbr_types": {}, "scaling": None, "sg": False, "thumb": None
})
# Store texture path under the specific PBR type and resolution
group_entry["pbr_types"].setdefault(pbr_type, {})[resolution_label] = file_path_str
# Store scaling string ONCE per groupname (first encountered wins)
if group_entry["scaling"] is None:
group_entry["scaling"] = scaling_str
elif group_entry["scaling"] != scaling_str:
# Warn only once per group if inconsistency found
if not group_entry.get("scaling_warning_printed", False):
print(f" Warn: Inconsistent 'Scaling' string found for group '{groupname}'. "
f"Using first encountered: '{group_entry['scaling']}'.")
group_entry["scaling_warning_printed"] = True
# Track SG status and thumbnail path
if tag_str == "T-SG":
group_entry["sg"] = True
# Use 1k COL-1 as the potential thumbnail source
if resolution_label == "1k" and pbr_type == "COL-1":
group_entry["thumb"] = file_path_str
processed_files += 1
# --- End File Scanning ---
print(f"\nFile Scan Complete. Found {file_count} files, parsed {processed_files} valid textures.")
total_groups_found = len(texture_data)
print(f"Total unique Groupnames found: {total_groups_found}")
if not texture_data:
print("No valid textures found. Exiting.")
return True # No work needed is considered success
print("\n--- Processing Node Groups ---")
all_groupnames = sorted(list(texture_data.keys()))
processing_stopped_early = False
# --- Main Processing Loop ---
for groupname in all_groupnames:
group_info = texture_data[groupname] # Get pre-scanned info
pbr_types_data = group_info.get("pbr_types", {})
scaling_string_for_group = group_info.get("scaling")
sg_status_for_group = group_info.get("sg", False)
thumbnail_path_for_group = group_info.get("thumb")
target_parent_name = f"PBRSET_{groupname}"
print(f"\nProcessing Group: '{target_parent_name}'")
parent_group = bpy.data.node_groups.get(target_parent_name)
is_new_parent = False
# --- Find or Create Parent Group ---
if parent_group is None:
# Check batch limit BEFORE creating
if groups_created >= MAX_NEW_GROUPS_PER_RUN:
print(f"\n--- Reached NEW parent group limit ({MAX_NEW_GROUPS_PER_RUN}). Stopping. ---")
processing_stopped_early = True
break # Exit the main groupname loop
print(f" Creating new parent group: '{target_parent_name}'")
parent_group = template_parent.copy()
if not parent_group:
print(f" Error: Failed copy parent template. Skip group '{groupname}'.")
continue # Skip to next groupname
parent_group.name = target_parent_name
groups_created += 1
is_new_parent = True
# --- Auto-Save Trigger ---
# Trigger AFTER creating the group and incrementing counter
if AUTO_SAVE_ENABLED and groups_created > 0 and groups_created % SAVE_INTERVAL == 0:
if perform_safe_autosave(manifest_path, manifest_data):
# If auto-save succeeded, manifest is up-to-date on disk
manifest_needs_saving = False
else:
# Auto-save failed, continue but warn
print("!!! WARNING: Auto-save failed. Continuing processing... !!!")
# --- End Auto-Save Trigger ---
else: # Update Existing Parent Group
print(f" Updating existing parent group: '{target_parent_name}'")
groups_updated += 1
# --- End Find or Create Parent Group ---
# --- Process Parent Group Internals ---
# This block processes both newly created and existing parent groups
try:
# --- Calculate and Store Aspect Ratio Correction (Once per group) ---
# Find the designated Value node in the parent template
aspect_node_list = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'VALUE')
if aspect_node_list:
aspect_node = aspect_node_list[0] # Assume first found is correct
if scaling_string_for_group:
# Find the path to the lowest resolution image available
lowest_res_path = None; found_res = False
# Check resolution labels in configured order (e.g., "1k", "2k"...)
for res_label in RESOLUTION_LABELS:
# Check all PBR types for this resolution
for res_data in pbr_types_data.values():
if res_label in res_data:
lowest_res_path = res_data[res_label]
found_res = True
break # Found path for this resolution label
if found_res:
break # Found lowest available resolution path
if lowest_res_path:
# Load the image (use cache if possible)
img = None; img_load_error = False
if lowest_res_path in loaded_images_this_run:
img = loaded_images_this_run[lowest_res_path]
img_load_error = (img is None) # Check if cached result was failure
else:
# Attempt to load if not cached
try:
img_path_obj = Path(lowest_res_path)
if img_path_obj.is_file():
img = bpy.data.images.load(lowest_res_path, check_existing=True)
else:
img_load_error = True
print(f" Error: Aspect source image not found: {lowest_res_path}")
if img is None and not img_load_error: # Check if load function returned None
img_load_error = True
print(f" Error: Failed loading aspect source image: {lowest_res_path}")
except Exception as e_load_aspect:
print(f" Error loading aspect source image: {e_load_aspect}")
img_load_error = True
# Cache the result (image object or None)
loaded_images_this_run[lowest_res_path] = img if not img_load_error else None
if not img_load_error and img:
# Get dimensions and calculate factor
img_width, img_height = img.size[0], img.size[1]
factor = calculate_aspect_ratio_factor(img_width, img_height, scaling_string_for_group)
print(f" Calculated Aspect Ratio Factor: {factor:.4f} (from {img_width}x{img_height}, Scaling='{scaling_string_for_group}')")
# Store factor in node if value changed significantly
if abs(aspect_node.outputs[0].default_value - factor) > 0.0001:
aspect_node.outputs[0].default_value = factor
print(f" Set '{ASPECT_RATIO_NODE_LABEL}' node value to {factor:.4f}")
else:
print(f" Warn: Could not load image '{lowest_res_path}' for aspect ratio calc.")
else:
print(f" Warn: No suitable image found (e.g., 1k) to calculate aspect ratio for '{groupname}'.")
else:
print(f" Warn: No Scaling string found for group '{groupname}'. Cannot calculate aspect ratio.")
# else: # Optional Warning if node is missing from template
# print(f" Warn: Value node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group '{parent_group.name}'. Cannot store aspect ratio.")
# --- End Aspect Ratio Correction ---
# Set SG Value
sg_nodes = find_nodes_by_label(parent_group, SG_VALUE_NODE_LABEL, 'VALUE')
if sg_nodes:
sg_node = sg_nodes[0]
target_val = 1.0 if sg_status_for_group else 0.0
if abs(sg_node.outputs[0].default_value - target_val) > 0.001:
sg_node.outputs[0].default_value = target_val
print(f" Set '{SG_VALUE_NODE_LABEL}' to: {target_val}")
# Set Asset Info (Thumbnail Path Prop, Initial Preview & Tagging)
# This block runs for both new and existing groups
try:
# 1. Set/Update Thumbnail Path Property & Mark Asset
if not parent_group.asset_data:
parent_group.asset_mark()
print(f" Marked '{parent_group.name}' as asset.")
# Update thumbnail property logic
if thumbnail_path_for_group:
thumb_path_obj = Path(thumbnail_path_for_group)
if thumb_path_obj.is_file():
if parent_group.get("thumbnail_filepath") != thumbnail_path_for_group:
parent_group["thumbnail_filepath"] = thumbnail_path_for_group
if not is_new_parent: print(f" Updated thumbnail path property.") # Log update only if not new
elif "thumbnail_filepath" in parent_group:
del parent_group["thumbnail_filepath"]
if not is_new_parent: print(f" Removed thumbnail path property (file not found).")
elif "thumbnail_filepath" in parent_group:
del parent_group["thumbnail_filepath"]
if not is_new_parent: print(f" Removed old thumbnail path property.")
# 2. Set Initial Preview (Only if NEW parent)
if is_new_parent and thumbnail_path_for_group and Path(thumbnail_path_for_group).is_file():
print(f" Attempting initial preview from '{Path(thumbnail_path_for_group).name}'...")
try:
with bpy.context.temp_override(id=parent_group):
bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbnail_path_for_group)
print(f" Set initial custom preview.")
except Exception as e_prev:
print(f" Preview Error: {e_prev}")
# 3. Apply Asset Tags (Supplier, etc.)
apply_asset_tags(parent_group, groupname, group_info)
except Exception as e_asset_info:
print(f" Error setting asset info/tags: {e_asset_info}")
# --- End Asset Info ---
# --- Process Child Groups (PBR Types) ---
for pbr_type, resolutions_data in pbr_types_data.items():
# print(f" Processing PBR Type: {pbr_type}") # Can be verbose
# Find placeholder node in parent
holder_nodes = find_nodes_by_label(parent_group, pbr_type, 'GROUP')
if not holder_nodes:
print(f" Warn: No placeholder node labeled '{pbr_type}' in parent group '{parent_group.name}'. Skipping PBR Type.")
continue
holder_node = holder_nodes[0] # Assume first is correct
# Determine child group name (Base64 encoded)
logical_child_name = f"{groupname}_{pbr_type}"
target_child_name_b64 = encode_name_b64(logical_child_name)
# Find or Create Child Group
child_group = bpy.data.node_groups.get(target_child_name_b64)
if child_group is None:
# print(f" Creating new child group for '{pbr_type}'") # Verbose
child_group = template_child.copy()
if not child_group:
print(f" Error: Failed copy child template. Skip PBR Type.")
continue
child_group.name = target_child_name_b64
child_groups_created += 1
else:
# print(f" Updating existing child group for '{pbr_type}'") # Verbose
child_groups_updated += 1
# Assign child group to placeholder if needed
if holder_node.node_tree != child_group:
holder_node.node_tree = child_group
print(f" Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.")
# Connect placeholder output to parent output socket if needed
try:
source_socket = holder_node.outputs[0] if holder_node.outputs else None
group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)
target_socket = None
if group_output_node:
target_socket = group_output_node.inputs.get(pbr_type) # Get socket by name/label
if source_socket and target_socket:
# Check if link already exists
link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)
if not link_exists:
parent_group.links.new(source_socket, target_socket)
links_created += 1
print(f" Connected '{holder_node.label}' output to parent output socket '{pbr_type}'.")
# else: # Optional warning if sockets aren't found
# if not source_socket: print(f" Warn: No output socket found on placeholder '{holder_node.label}'.")
# if not target_socket: print(f" Warn: No input socket '{pbr_type}' found on parent output node.")
except Exception as e_link:
print(f" Error linking sockets for '{pbr_type}': {e_link}")
# Ensure parent output socket type is Color
try:
item = parent_group.interface.items_tree.get(pbr_type)
if item and item.in_out == 'OUTPUT' and item.socket_type != 'NodeSocketColor':
item.socket_type = 'NodeSocketColor'
# print(f" Set parent output socket '{pbr_type}' type to Color.") # Optional info
except Exception as e_sock:
print(f" Error updating socket type for '{pbr_type}': {e_sock}")
# --- Process Resolutions within Child Group ---
for resolution_label, image_path_str in resolutions_data.items():
# Find image texture nodes within the CHILD group
image_nodes = find_nodes_by_label(child_group, resolution_label, 'TEX_IMAGE')
if not image_nodes:
# print(f" Warn: No node labeled '{resolution_label}' found in child group for '{pbr_type}'.") # Optional
continue
# --- >>> Manifest Check <<< ---
is_processed = False
if manifest_enabled: # Only check if manifest is enabled
# Check if this specific group/pbr/res combo is done
processed_resolutions = manifest_data.get(groupname, {}).get(pbr_type, [])
if resolution_label in processed_resolutions:
is_processed = True
# print(f" Skipping {groupname}/{pbr_type}/{resolution_label} (Manifest)") # Verbose skip log
if is_processed:
continue # Skip to the next resolution
# --- >>> End Manifest Check <<< ---
# --- Load Image & Assign (if not skipped) ---
# print(f" Processing Resolution: {resolution_label} for {pbr_type}") # Verbose
img = None
image_load_failed = False
# Check intra-run cache first
if image_path_str in loaded_images_this_run:
img = loaded_images_this_run[image_path_str]
image_load_failed = (img is None) # Respect cached failure
else:
# Not cached in this run, attempt to load
try:
image_path = Path(image_path_str)
if not image_path.is_file():
print(f" Error: Image file not found: {image_path_str}")
image_load_failed = True
else:
# Use check_existing=True to potentially reuse existing datablocks
img = bpy.data.images.load(str(image_path), check_existing=True)
if not img:
print(f" Error: Failed loading image via bpy.data.images.load: {image_path_str}")
image_load_failed = True
# else: # Success block is handled below
# pass
except RuntimeError as e_runtime_load:
print(f" Runtime Error loading image '{image_path_str}': {e_runtime_load}")
image_load_failed = True
except Exception as e_gen_load:
print(f" Unexpected error loading image '{image_path_str}': {e_gen_load}")
image_load_failed = True
# Cache result (image object or None for failure)
loaded_images_this_run[image_path_str] = img if not image_load_failed else None
# --- Process image if loaded/cached successfully ---
if not image_load_failed and img:
try:
# Set Color Space
correct_color_space = PBR_COLOR_SPACE_MAP.get(pbr_type, DEFAULT_COLOR_SPACE)
if img.colorspace_settings.name != correct_color_space:
print(f" Setting '{Path(img.filepath).name}' color space -> {correct_color_space}")
img.colorspace_settings.name = correct_color_space
# Histogram Stats Calculation
if resolution_label == "1k" and pbr_type in ["ROUGH", "DISP"]:
target_node_label = f"{HISTOGRAM_NODE_PREFIX}{pbr_type}"
target_nodes = find_nodes_by_label(parent_group, target_node_label, 'COMBXYZ')
if target_nodes:
target_node = target_nodes[0]
try:
socket_x = target_node.inputs.get("X")
socket_y = target_node.inputs.get("Y")
socket_z = target_node.inputs.get("Z")
if socket_x and socket_y and socket_z:
print(f" Calculating histogram stats for {pbr_type} 1K...")
stats = calculate_image_stats(img)
if stats:
min_val, max_val, median_val = stats
print(f" Stats: Min={min_val:.4f}, Max={max_val:.4f}, Median={median_val:.4f}")
# Store stats in the Combine XYZ node
socket_x.default_value = min_val
socket_y.default_value = max_val
socket_z.default_value = median_val
print(f" Stored stats in '{target_node_label}'.")
else:
print(f" Warn: Failed calc stats for '{Path(img.filepath).name}'.")
# else: print(f" Warn: Node '{target_node_label}' missing X/Y/Z sockets.")
except Exception as e_combxyz_store:
print(f" Error processing stats in '{target_node_label}': {e_combxyz_store}")
# else: print(f" Warn: No stats node '{target_node_label}' found.")
# Assign Image to nodes in child group
nodes_updated_this_res = 0
for image_node in image_nodes:
if image_node.image != img:
image_node.image = img
nodes_updated_this_res += 1
nodes_updated += nodes_updated_this_res
if nodes_updated_this_res > 0:
print(f" Assigned image '{Path(img.filepath).name}' to {nodes_updated_this_res} node(s).")
# --- >>> Update Manifest <<< ---
if manifest_enabled:
# Ensure nested structure exists
manifest_data.setdefault(groupname, {}).setdefault(pbr_type, [])
# Add resolution if not already present
if resolution_label not in manifest_data[groupname][pbr_type]:
manifest_data[groupname][pbr_type].append(resolution_label)
# Keep the list sorted for consistency in the JSON file
manifest_data[groupname][pbr_type].sort()
manifest_needs_saving = True # Mark that we need to save later
# print(f" Marked {groupname}/{pbr_type}/{resolution_label} processed in manifest.") # Verbose
# --- >>> End Update Manifest <<< ---
except Exception as e_proc_img:
print(f" Error during post-load processing for image '{image_path_str}': {e_proc_img}")
# Continue to next resolution even if post-load fails
# --- End Process image ---
# --- End Resolution Loop ---
# --- End PBR Type Loop ---
except Exception as e_group:
print(f" !!! ERROR processing group '{groupname}': {e_group} !!!")
import traceback; traceback.print_exc()
continue # Continue to next groupname
# --- End Main Processing Loop ---
# --- Final Manifest Save ---
# Save if manifest is enabled AND changes were made since the last save/start.
# This happens even if the script stopped early due to MAX_NEW_GROUPS_PER_RUN.
if manifest_enabled and manifest_needs_saving:
print("\n--- Attempting Final Manifest Save (End of Run) ---")
if save_manifest(manifest_path, manifest_data):
print(" Manifest saved successfully.")
# Error message handled within save_manifest
# --- End Final Manifest Save ---
# --- Final Summary ---
end_time = time.time(); duration = end_time - start_time
print("\n--- Script Run Finished ---")
if processing_stopped_early:
print(f"--- NOTE: Reached NEW parent group processing limit ({MAX_NEW_GROUPS_PER_RUN}). ---")
print(f"--- You may need to SAVE manually, REVERT/RELOAD file, and RUN SCRIPT AGAIN. ---")
print(f"Duration: {duration:.2f} seconds this run.")
print(f"Summary: New Parents={groups_created}, Updated Parents={groups_updated}, New Children={child_groups_created}, Updated Children={child_groups_updated}.")
print(f" Images assigned={nodes_updated} times. Links created={links_created}.")
# Add other stats if needed, e.g., number of tags added
# --- End Final Summary ---
return True # Indicate successful completion (or reaching limit)
# --- How to Run ---
# 1. Ensure 'numpy' is available in Blender's Python environment.
# 2. Create Node Group "Template_PBRSET": Configure placeholders, Value nodes (SG, Aspect Ratio), Stats nodes, outputs.
# 3. Create Node Group "Template_PBRTYPE": Configure Image Texture nodes labeled by resolution.
# 4. !! SAVE YOUR BLEND FILE AT LEAST ONCE !! for manifest, auto-saving, and auto-reloading to work.
# 5. Adjust variables in the '--- USER CONFIGURATION ---' section at the top as needed.
# 6. Paste into Blender's Text Editor and run (Alt+P or Run Script button). Check Window -> Toggle System Console.
# 7. If script stops due to limit: SAVE manually, REVERT/REOPEN file, RUN SCRIPT AGAIN. Manifest prevents reprocessing.
if __name__ == "__main__":
print(f"Script execution started at: {time.strftime('%Y-%m-%d %H:%M:%S')}")
# Pre-run Checks using variables from CONFIG section
valid_run_setup = True
try:
tex_dir_path = Path(texture_root_directory)
# Basic check if path looks like a placeholder or doesn't exist
if texture_root_directory == r"C:\path\to\your\texture\library" or not tex_dir_path.is_dir() :
print(f"\nERROR: 'texture_root_directory' is invalid or a placeholder.")
print(f" Current value: '{texture_root_directory}'")
valid_run_setup = False
except Exception as e_path:
print(f"\nERROR checking texture_root_directory: {e_path}")
valid_run_setup = False
# Check templates
if not bpy.data.node_groups.get(PARENT_TEMPLATE_NAME):
print(f"\nERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found.")
valid_run_setup = False
if not bpy.data.node_groups.get(CHILD_TEMPLATE_NAME):
print(f"\nERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found.")
valid_run_setup = False
# Check numpy (needed for stats)
try:
import numpy
except ImportError:
print("\nCRITICAL ERROR: Python library 'numpy' not found (required for image stats).")
print(" Please install numpy into Blender's Python environment.")
valid_run_setup = False
# Execute main function if setup checks pass
script_completed_successfully = False
if valid_run_setup:
# Check if file is saved before running features that depend on it
if not bpy.data.filepath:
print("\nWARNING: Blend file not saved. Manifest, Auto-Save, and Auto-Reload features disabled.")
script_completed_successfully = process_textures_to_groups(texture_root_directory)
else:
print("\nScript aborted due to configuration errors.")
# --- Final Save & Reload ---
# Use config variables directly as they are in module scope
if script_completed_successfully and AUTO_RELOAD_ON_FINISH:
if bpy.data.filepath: # Only if file is saved
print("\n--- Auto-saving and reloading blend file ---")
try:
bpy.ops.wm.save_mainfile()
print(" Blend file saved.")
print(" Reloading...")
# Ensure script execution stops cleanly before reload starts
bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath)
# Script execution effectively stops here upon reload
except Exception as e:
print(f"!!! ERROR during final save/reload: {e} !!!")
else:
print("\nSkipping final save & reload because the blend file is not saved.")
# --- End Final Save & Reload ---
# This print might not be reached if reload occurs
print(f"Script execution finished processing at: {time.strftime('%Y-%m-%d %H:%M:%S')}")

File diff suppressed because it is too large Load Diff

View File

@@ -1,144 +0,0 @@
# config.py
# Core settings defining the pipeline standards and output format.
# --- Core Definitions ---
# Old definitions (commented out)
# ALLOWED_ASSET_TYPES = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"]
# ALLOWED_FILE_TYPES = [
# "MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP",
# "MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK",
# "MAP_IMPERFECTION", # Added for imperfection maps
# "MODEL", "EXTRA", "FILE_IGNORE"
# ]
# New definitions using dictionaries
ASSET_TYPE_DEFINITIONS = {
"Surface": {
"description": "Standard PBR material set for a surface.",
"color": "#87CEEB", # Light Blue
"examples": ["WoodFloor01", "MetalPlate05"]
},
"Model": {
"description": "A 3D model file.",
"color": "#FFA500", # Orange
"examples": ["Chair.fbx", "Character.obj"]
},
"Decal": {
"description": "A texture designed to be projected onto surfaces.",
"color": "#90EE90", # Light Green
"examples": ["Graffiti01", "LeakStain03"]
},
"Atlas": {
"description": "A texture sheet containing multiple smaller textures.",
"color": "#FFC0CB", # Pink
"examples": ["FoliageAtlas", "UITextureSheet"]
},
"UtilityMap": {
"description": "A map used for specific technical purposes (e.g., flow map).",
"color": "#D3D3D3", # Light Grey
"examples": ["FlowMap", "CurvatureMap"]
}
}
FILE_TYPE_DEFINITIONS = {
"MAP_COL": {"description": "Color/Albedo Map", "color": "#FFFFE0", "examples": ["_col.", "_basecolor."]},
"MAP_NRM": {"description": "Normal Map", "color": "#E6E6FA", "examples": ["_nrm.", "_normal."]},
"MAP_METAL": {"description": "Metalness Map", "color": "#C0C0C0", "examples": ["_metal.", "_met."]},
"MAP_ROUGH": {"description": "Roughness Map", "color": "#A0522D", "examples": ["_rough.", "_rgh."]},
"MAP_AO": {"description": "Ambient Occlusion Map", "color": "#A9A9A9", "examples": ["_ao.", "_ambientocclusion."]},
"MAP_DISP": {"description": "Displacement/Height Map", "color": "#FFB6C1", "examples": ["_disp.", "_height."]},
"MAP_REFL": {"description": "Reflection/Specular Map", "color": "#E0FFFF", "examples": ["_refl.", "_specular."]},
"MAP_SSS": {"description": "Subsurface Scattering Map", "color": "#FFDAB9", "examples": ["_sss.", "_subsurface."]},
"MAP_FUZZ": {"description": "Fuzz/Sheen Map", "color": "#FFA07A", "examples": ["_fuzz.", "_sheen."]},
"MAP_IDMAP": {"description": "ID Map (for masking)", "color": "#F08080", "examples": ["_id.", "_matid."]},
"MAP_MASK": {"description": "Generic Mask Map", "color": "#FFFFFF", "examples": ["_mask."]},
"MAP_IMPERFECTION": {"description": "Imperfection Map (scratches, dust)", "color": "#F0E68C", "examples": ["_imp.", "_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 Output Standards ---
TARGET_FILENAME_PATTERN = "{base_name}_{map_type}_{resolution}.{ext}"
STANDARD_MAP_TYPES = [
"COL", "NRM", "ROUGH", "METAL", "AO", "DISP", "REFL",
"SSS", "FUZZ", "IDMAP", "MASK"
]
# Map types that should always receive a numeric suffix (e.g., COL-1, COL-2)
# based on preset keyword order, even if only one variant is found.
RESPECT_VARIANT_MAP_TYPES = ["COL"]
# Subdirectory within the final set folder for non-essential/unknown files
EXTRA_FILES_SUBDIR = "Extra"
OUTPUT_BASE_DIR = "../Asset_Processor_Output" #accepts both relative and absolute paths
METADATA_FILENAME = "metadata.json"
# --- Blender Integration Settings ---
# Default paths to Blender files for node group and material creation.
# Set these to absolute or relative paths if you want defaults.
# Command-line arguments (--nodegroup-blend, --materials-blend) will override these.
DEFAULT_NODEGROUP_BLEND_PATH = r"G:/02 Content/10-19 Content/19 Catalogs/19.01 Blender Asset Catalogue/_CustomLibraries/Nodes-Linked/PBRSET-Nodes-Testing.blend" # e.g., r"G:\Blender\Libraries\NodeGroups.blend"
DEFAULT_MATERIALS_BLEND_PATH = r"G:/02 Content/10-19 Content/19 Catalogs/19.01 Blender Asset Catalogue/_CustomLibraries/Materials-Append/PBR Materials-Testing.blend" # e.g., r"G:\Blender\Libraries\Materials.blend"
# Path to the Blender executable. Required for running Blender scripts.
# Example: r"C:\Program Files\Blender Foundation\Blender 3.6\blender.exe"
BLENDER_EXECUTABLE_PATH = r"C:/Program Files/Blender Foundation/Blender 4.4/blender.exe" # <<< SET THIS PATH!
# --- Image Processing Settings ---
# Target resolutions (Largest dimension in pixels)
PNG_COMPRESSION_LEVEL = 6 # 0 (none) to 9 (max)
# Quality for JPG output (0-100)
JPG_QUALITY = 98
# Resolution dimension threshold (pixels) above which 8-bit images are forced to JPG, overriding input format logic.
RESOLUTION_THRESHOLD_FOR_JPG = 4096
IMAGE_RESOLUTIONS = {"8K": 8192,"4K": 4096, "2K": 2048, "1K": 1024}
# Aspect ratio decimals (used for metadata, could potentially be removed later)
ASPECT_RATIO_DECIMALS = 2
# Bit depth rules per standard map type ('respect' or 'force_8bit')
MAP_BIT_DEPTH_RULES = {
"COL": "force_8bit", "NRM": "respect", "ROUGH": "force_8bit", "METAL": "force_8bit",
"AO": "force_8bit", "DISP": "respect", "REFL": "force_bit", "SSS": "respect",
"FUZZ": "force_bit", "IDMAP": "force_8bit", "MASK": "force_8bit",
"DEFAULT": "respect" # Fallback for map types not listed
}
# Output format preferences for 16-bit data
OUTPUT_FORMAT_16BIT_PRIMARY = "png" # Options: exr_dwaa, exr_dwab, exr_zip, png, tif
OUTPUT_FORMAT_16BIT_FALLBACK = "png"
# Output format for 8-bit data
OUTPUT_FORMAT_8BIT = "png" # Could allow 'jpg' later with quality settings
# Map types that should always be saved in a lossless format (e.g., PNG, EXR)
# regardless of resolution threshold or input format.
#FORCE_LOSSLESS_MAP_TYPES = ["NRM", "NRMRGN"]
# --- Map Merging Rules ---
# List of dictionaries, each defining a merge operation.
MAP_MERGE_RULES = [
{
"output_map_type": "NRMRGH", # Suffix or standard name for the merged map
"inputs": { # Map target RGB channels to standard input map type names
"R": "NRM", # Use Red channel from NRM
"G": "NRM", # Use Green channel from NRM
"B": "ROUGH" # Use Red channel from ROUGH (assuming it's grayscale)
},
"defaults": { # Default values (0.0 - 1.0) if an input map is missing
"R": 0.5, "G": 0.5, "B": 0.5
},
# 'respect_inputs' (use 16bit if any input is), 'force_8bit', 'force_16bit'
"output_bit_depth": "respect_inputs"
},
# Example: Merge Metalness(R), Roughness(G), AO(B) -> "MRA" map often used in engines
# {
# "output_map_type": "MRA",
# "inputs": {"R": "METAL", "G": "ROUGH", "B": "AO"},
# "defaullllts": {"R": 0.0, "G": 1.0, "B": 1.0}, # Default Metal=0, Rough=1, AO=1
# "output_bit_depth": "force_8bit" # Usually fine as 8bit
# },
]
# --- Metadata Settings ---
CALCULATE_STATS_RESOLUTION = "1K" # Resolution suffix used for calculating stats
DEFAULT_ASSET_CATEGORY = "Surface" # If rules don't identify Asset or Decal
# --- Internal Settings ---
# Temporary directory prefix for processing folders
TEMP_DIR_PREFIX = "_PROCESS_ASSET_"

View File

@@ -1,90 +0,0 @@
# Final Plan for Updating documentation.txt with Debugging Details
This document outlines the final plan for enhancing `documentation.txt` with detailed internal information crucial for debugging the Asset Processor Tool. This plan incorporates analysis of `asset_processor.py`, `configuration.py`, `main.py`, and `gui/processing_handler.py`.
## Task Objective
Analyze relevant source code (`asset_processor.py`, `configuration.py`, `main.py`, `gui/processing_handler.py`) to gather specific details about internal logic, state management, error handling, data structures, concurrency, resource management, and limitations. Integrate this information into the existing `documentation.txt` to aid developers in debugging.
## Analysis Steps Completed
1. Read and analyzed `readme.md`.
2. Listed code definitions in the root directory (`.`).
3. Listed code definitions in the `gui/` directory.
4. Read and analyzed `asset_processor.py`.
5. Read and analyzed `configuration.py`.
6. Read and analyzed `main.py`.
7. Read and analyzed `gui/processing_handler.py`.
## Final Integration Plan (Merged Structure)
1. **Add New Top-Level Section:**
* Append the following section header to the end of `documentation.txt`:
```
================================
Internal Details for Debugging
================================
```
2. **Create Subsections:**
* Under the new "Internal Details" section, create the following subsections (renumbering from 7 onwards):
* `7. Internal Logic & Algorithms`
* `8. State Management`
* `9. Error Handling & Propagation`
* `10. Key Data Structures`
* `11. Concurrency Models (CLI & GUI)`
* `12. Resource Management`
* `13. Known Limitations & Edge Cases`
3. **Populate Subsections with Specific Details:**
* **7. Internal Logic & Algorithms:**
* **Configuration Preparation (`Configuration` class):** Detail the `__init__` process: loading `config.py` and preset JSON, validating structure (`_validate_configs`), compiling regex (`_compile_regex_patterns` using `_fnmatch_to_regex`). Mention compiled patterns storage.
* **CLI Argument Parsing (`main.py:setup_arg_parser`):** Briefly describe `argparse` usage and key flags influencing execution.
* **Output Directory Resolution (`main.py:main`):** Explain how the final output path is determined and resolved.
* **Asset Processing (`AssetProcessor` class):**
* *Classification (`_inventory_and_classify_files`):* Describe multi-pass approach using compiled regex from `Configuration`. Detail variant sorting criteria.
* *Map Processing (`_process_maps`):* Detail image loading, Gloss->Rough inversion, resizing, format determination (using `Configuration` rules), bit depth conversion, stats calculation, aspect ratio logic, save fallback.
* *Merging (`_merge_maps`):* Detail common resolution finding, input loading, channel merging (using `Configuration` rules), output determination, save fallback.
* *Metadata (`_determine_base_metadata`, etc.):* Summarize base name extraction, category/archetype determination (using `Configuration` rules), `metadata.json` population.
* **Blender Integration (`main.py:run_blender_script`, `gui/processing_handler.py:_run_blender_script_subprocess`):** Explain subprocess execution, command construction (`-b`, `--python`, `--`), argument passing (`asset_root_dir`).
* **8. State Management:**
* `Configuration` object: Holds loaded config state and compiled regex. Instantiated per worker.
* `AssetProcessor`: Primarily stateless between `process()` calls. Internal state within `process()` managed by local variables (e.g., `current_asset_metadata`). `self.classified_files` populated once and filtered per asset.
* `main.py`: Tracks overall CLI run counts (processed, skipped, failed).
* `gui/processing_handler.py`: Manages GUI processing run state via flags (`_is_running`, `_cancel_requested`) and stores `Future` objects (`_futures`).
* **9. Error Handling & Propagation:**
* Custom Exceptions: `AssetProcessingError`, `ConfigurationError`.
* `Configuration`: Raises `ConfigurationError` on load/validation failure. Logs regex compilation warnings.
* `AssetProcessor`: Catches exceptions within per-asset loop, logs error, marks asset "failed", continues loop. Handles specific save fallbacks (EXR->PNG). Raises `AssetProcessingError` for critical setup failures.
* Worker Wrapper (`main.py:process_single_asset_wrapper`): Catches exceptions from `Configuration`/`AssetProcessor`, logs, returns "failed" status tuple.
* Process Pool (`main.py`, `gui/processing_handler.py`): `try...except` around executor block catches pool-level errors.
* GUI Communication (`ProcessingHandler`): Catches errors during future result retrieval, emits failure status via signals.
* Blender Scripts: Checks subprocess return code, logs stderr. Catches `FileNotFoundError`.
* **10. Key Data Structures:**
* `Configuration` attributes: `compiled_map_keyword_regex`, `compiled_extra_regex`, etc. (compiled `re.Pattern` objects).
* `AssetProcessor` structures: `self.classified_files` (dict[str, list[dict]]), `processed_maps_details_asset` (dict[str, dict[str, dict]]), `file_to_base_name_map` (dict[Path, Optional[str]]).
* Return values: Status dictionary from `AssetProcessor.process()`, status tuple from `process_single_asset_wrapper`.
* `ProcessingHandler._futures`: dict[Future, str].
* **11. Concurrency Models (CLI & GUI):**
* **Common Core:** Both use `concurrent.futures.ProcessPoolExecutor` running `main.process_single_asset_wrapper`. `Configuration` and `AssetProcessor` instantiated within each worker process for isolation.
* **CLI Orchestration (`main.py:run_processing`):** Direct executor usage, `as_completed` gathers results synchronously.
* **GUI Orchestration (`gui/processing_handler.py`):** `ProcessingHandler` (QObject) runs executor logic in a `QThread`. Results processed in handler thread, communicated asynchronously to UI thread via Qt signals (`progress_updated`, `file_status_updated`, `processing_finished`).
* **Cancellation (`gui/processing_handler.py:request_cancel`):** Sets flag, attempts `executor.shutdown(wait=False)`, tries cancelling pending futures. Limitation: Does not stop already running workers.
* **12. Resource Management:**
* `Configuration`: Uses `with open` for preset files.
* `AssetProcessor`: Manages temporary workspace (`tempfile.mkdtemp`, `shutil.rmtree` in `finally`). Uses `with open` for metadata JSON.
* `ProcessPoolExecutor`: Lifecycle managed via `with` statement in `main.py` and `gui/processing_handler.py`.
* **13. Known Limitations & Edge Cases:**
* Configuration: Basic structural validation only; regex compilation errors are warnings; `_fnmatch_to_regex` helper is basic.
* AssetProcessor: Relies heavily on filename patterns; high memory potential for large images; limited intra-asset error recovery; simplified prediction logic.
* CLI: Basic preset file existence check only before starting workers; Blender executable finding logic order (config > PATH).
* GUI Concurrency: Cancellation doesn't stop currently executing worker processes.
4. **Switch Mode:** Request switching to Code mode to apply these changes by appending to `documentation.txt`.

View File

@@ -1,269 +0,0 @@
================================
Asset Processor Tool - Developer Documentation
================================
This document provides a concise overview of the Asset Processor Tool's codebase for developers joining the project. It focuses on the architecture, key components, and development workflow.
**NOTE:** This documentation strictly excludes details on environment setup, dependency installation, building the project, or deployment procedures. It assumes familiarity with Python and the relevant libraries (OpenCV, NumPy, PySide6).
--------------------------------
1. Project Overview
--------------------------------
* **Purpose:** To process 3D asset source files (texture sets, models, etc., typically from ZIP archives or folders) into a standardized library format.
* **Core Functionality:** Uses configurable JSON presets to interpret different asset sources, automating tasks like file classification, image resizing, channel merging, and metadata generation.
* **High-Level Architecture:** Consists of a core processing engine (`AssetProcessor`), a configuration system handling presets (`Configuration`), multiple interfaces (GUI, CLI, Directory Monitor), and optional integration with Blender for automated material/nodegroup creation.
--------------------------------
2. Codebase Structure
--------------------------------
Key files and directories:
* `asset_processor.py`: Contains the `AssetProcessor` class, the core logic for processing a single asset through the pipeline. Includes methods for classification, map processing, merging, metadata generation, and output organization. Also provides methods for predicting output structure used by the GUI.
* `configuration.py`: Defines the `Configuration` class. Responsible for loading core settings from `config.py` and merging them with a specified preset JSON file (`Presets/*.json`). Pre-compiles regex patterns from presets for efficiency.
* `config.py`: Stores global default settings, constants, and core rules (e.g., standard map types, default resolutions, merge rules, output format rules, Blender paths).
* `main.py`: Entry point for the Command-Line Interface (CLI). Handles argument parsing, logging setup, parallel processing orchestration (using `concurrent.futures.ProcessPoolExecutor`), calls `AssetProcessor` via a wrapper function, and optionally triggers Blender scripts.
* `monitor.py`: Implements the automated directory monitoring feature using the `watchdog` library. Contains the `ZipHandler` class to detect new ZIP files and trigger processing via `main.run_processing`.
* `gui/`: Directory containing all code related to the Graphical User Interface (GUI), built with PySide6.
* `main_window.py`: Defines the `MainWindow` class, the main application window structure, UI layout (preset editor, processing panel, drag-and-drop, preview table, controls), event handling (button clicks, drag/drop), and menu setup. Manages GUI-specific logging (`QtLogHandler`).
* `processing_handler.py`: Defines the `ProcessingHandler` class (runs on a `QThread`). Manages the execution of the main asset processing pipeline (using `ProcessPoolExecutor`) and Blender script execution in the background to keep the GUI responsive. Communicates progress and results back to the `MainWindow` via signals.
* `prediction_handler.py`: Defines the `PredictionHandler` class (runs on a `QThread`). Manages background file analysis/preview generation by calling `AssetProcessor.get_detailed_file_predictions()`. Sends results back to the `MainWindow` via signals to update the preview table.
* `preview_table_model.py`: Defines `PreviewTableModel` (inherits `QAbstractTableModel`) and `PreviewSortFilterProxyModel` for managing and displaying data in the GUI's preview table, including custom sorting logic.
* `blenderscripts/`: Contains Python scripts (`create_nodegroups.py`, `create_materials.py`) designed to be executed *within* Blender, typically triggered by the main tool after processing to automate PBR nodegroup and material setup in `.blend` files.
* `Presets/`: Contains supplier-specific configuration files in JSON format (e.g., `Poliigon.json`). These define rules for interpreting asset filenames, classifying maps, handling variants, etc. `_template.json` serves as a base for new presets.
* `Testfiles/`: Contains example input assets for testing purposes.
* `Tickets/`: Directory for issue and feature tracking using Markdown files.
--------------------------------
3. Key Components/Modules
--------------------------------
* **`AssetProcessor` (`asset_processor.py`):** The heart of the tool. Orchestrates the entire processing pipeline for a single input asset (ZIP or folder). Responsibilities include workspace management, file classification, metadata extraction, map processing (resizing, format conversion), channel merging, `metadata.json` generation, and organizing final output files.
* **`Configuration` (`configuration.py`):** Manages the loading and merging of configuration settings. Takes a preset name, loads defaults from `config.py`, loads the specified `Presets/*.json`, merges them, validates settings, and pre-compiles regex patterns defined in the preset for efficient use by `AssetProcessor`.
* **`MainWindow` (`gui/main_window.py`):** The main class for the GUI application. Sets up the UI layout, connects user actions (button clicks, drag/drop) to slots, manages the preset editor, interacts with background handlers (`ProcessingHandler`, `PredictionHandler`) via signals/slots, and displays feedback (logs, progress, status).
* **`ProcessingHandler` (`gui/processing_handler.py`):** Handles the execution of the core asset processing logic and Blender scripts in a background thread for the GUI. Manages the `ProcessPoolExecutor` for parallel asset processing and communicates progress/results back to the `MainWindow`.
* **`PredictionHandler` (`gui/prediction_handler.py`):** Handles the generation of file classification previews in a background thread for the GUI. Calls `AssetProcessor`'s prediction methods and sends results back to the `MainWindow` to populate the preview table without blocking the UI.
* **`ZipHandler` (`monitor.py`):** A `watchdog` event handler used by `monitor.py`. Detects newly created ZIP files in the monitored input directory, validates the filename format (for preset extraction), and triggers the main processing logic via `main.run_processing`.
--------------------------------
4. Core Concepts & Data Flow
--------------------------------
* **Preset-Driven Configuration:**
* Global defaults are set in `config.py`.
* Supplier-specific rules (filename patterns, map keywords, variant handling, etc.) are defined using regex in `Presets/*.json` files.
* The `Configuration` class loads `config.py` and merges it with the selected preset JSON, providing a unified configuration object to the `AssetProcessor`. Regex patterns are pre-compiled during `Configuration` initialization for performance.
* **Asset Processing Pipeline (Simplified Flow):**
1. **Workspace Setup:** Create a temporary directory.
2. **Extract/Copy:** Extract ZIP or copy folder contents to the workspace.
3. **Classify Files:** Scan workspace, use compiled regex from `Configuration` to classify files (Map, Model, Extra, Ignored, Unrecognized). Handle 16-bit variants and assign suffixes based on rules.
4. **Determine Metadata:** Extract asset name, category, archetype based on preset rules.
5. **Skip Check:** If overwrite is false, check if output already exists; if so, skip this asset.
6. **Process Maps:** Load images, resize (no upscale), convert format/bit depth based on complex rules (`config.py` and preset), handle Gloss->Roughness inversion, calculate stats, determine aspect ratio change. Save processed maps.
7. **Merge Maps:** Combine channels from different processed maps based on `MAP_MERGE_RULES` in `config.py`. Save merged maps.
8. **Generate `metadata.json`:** Collect all relevant information (map details, stats, aspect ratio, category, etc.) and write to `metadata.json` in the workspace.
9. **Organize Output:** Create the final output directory structure (`<output_base>/<supplier>/<asset_name>/`) and move processed maps, merged maps, models, `metadata.json`, Extra files, and Ignored files into it.
10. **Cleanup Workspace:** Delete the temporary directory.
11. **(Optional) Blender Scripts:** If triggered via CLI/GUI, execute `blenderscripts/*.py` using the configured Blender executable via a subprocess.
* **Parallel Processing:**
* Multiple input assets are processed concurrently using `concurrent.futures.ProcessPoolExecutor`.
* This pool is managed by `main.py` (CLI) or `gui/processing_handler.py` (GUI).
* Each asset runs in an isolated worker process, ensuring separate `Configuration` and `AssetProcessor` instances.
* **GUI Interaction & Threading:**
* The GUI (`PySide6`) uses `QThread` to run `ProcessingHandler` (asset processing) and `PredictionHandler` (file preview generation) in the background, preventing the UI from freezing.
* Communication between the main UI thread (`MainWindow`) and background threads relies on Qt's signals and slots mechanism for thread safety (e.g., updating progress, status messages, preview table data).
* `PredictionHandler` calls `AssetProcessor` methods to get file classification details, which are then sent back to `MainWindow` to populate the `PreviewTableModel`.
* **Output (`metadata.json`):**
* A key output file generated for each processed asset.
* Contains structured data about the asset: map filenames, resolutions, formats, bit depths, merged map details, calculated image statistics, aspect ratio change info, asset category/archetype, source preset used, list of ignored source files, etc. This file is intended for use by downstream tools or scripts (like the Blender integration scripts).
--------------------------------
5. Development Workflow
--------------------------------
* **Modifying Core Processing Logic:** Changes to how assets are classified, maps are processed/resized/converted, channels are merged, or metadata is generated typically involve editing the `AssetProcessor` class in `asset_processor.py`.
* **Changing Global Settings/Rules:** Adjustments to default output paths, standard resolutions, default format rules, map merge definitions, or Blender paths should be made in `config.py`.
* **Adding/Modifying Supplier Rules:** To add support for a new asset source or change how an existing one is interpreted, create or edit the corresponding JSON file in the `Presets/` directory. Refer to `_template.json` and existing presets for structure. Focus on defining accurate regex patterns in `map_type_mapping`, `bit_depth_variants`, `model_patterns`, `source_naming_convention`, etc.
* **Adjusting CLI Behavior:** Changes to command-line arguments, argument parsing, or the overall CLI workflow are handled in `main.py`.
* **Modifying the GUI:** UI layout changes, adding new controls, altering event handling, or modifying background task management for the GUI involves working within the `gui/` directory, primarily `main_window.py`, `processing_handler.py`, and `prediction_handler.py`. UI elements are built using PySide6 widgets.
* **Enhancing Blender Integration:** Improvements or changes to how nodegroups or materials are created in Blender require editing the Python scripts within the `blenderscripts/` directory. Consider how these scripts are invoked and what data they expect (primarily from `metadata.json` and command-line arguments passed via subprocess calls in `main.py` or `gui/processing_handler.py`).
--------------------------------
6. Coding Conventions
--------------------------------
* **Object-Oriented:** The codebase heavily utilizes classes (e.g., `AssetProcessor`, `Configuration`, `MainWindow`, various Handlers).
* **Type Hinting:** Python type hints are used throughout the code for clarity and static analysis.
* **Logging:** Standard Python `logging` module is used for logging messages at different levels (DEBUG, INFO, WARNING, ERROR). The GUI uses a custom `QtLogHandler` to display logs in the UI console.
* **Error Handling:** Uses standard `try...except` blocks and defines some custom exceptions (e.g., `ConfigurationError`, `AssetProcessingError`).
* **Parallelism:** Uses `concurrent.futures.ProcessPoolExecutor` for CPU-bound tasks (asset processing).
* **GUI:** Uses `PySide6` (Qt for Python) with signals and slots for communication between UI elements and background threads (`QThread`).
* **Configuration:** Relies on Python modules (`config.py`) for core settings and JSON files (`Presets/`) for specific rule sets.
* **File Paths:** Uses `pathlib.Path` for handling file system paths.
================================
Internal Details for Debugging
================================
This section provides deeper technical details about the internal workings, intended to aid in debugging unexpected behavior.
--------------------------------
7. Internal Logic & Algorithms
--------------------------------
* **Configuration Preparation (`Configuration` class in `configuration.py`):**
* Instantiated per preset (`__init__`).
* Loads core settings from `config.py` using `importlib.util`.
* Loads specified preset from `presets/{preset_name}.json`.
* Validates basic structure of loaded settings (`_validate_configs`), checking for required keys and basic types (e.g., `map_type_mapping` is a list of dicts).
* Compiles regex patterns (`_compile_regex_patterns`) from preset rules (extra, model, bit depth, map keywords) using `re.compile` (mostly case-insensitive) and stores them on the instance (e.g., `self.compiled_map_keyword_regex`). Uses `_fnmatch_to_regex` helper for basic wildcard conversion.
* **CLI Argument Parsing (`main.py:setup_arg_parser`):**
* Uses `argparse` to define and parse command-line arguments.
* Key arguments influencing flow: `--preset` (required), `--output-dir` (optional override), `--workers` (concurrency), `--overwrite` (force reprocessing), `--verbose` (logging level), `--nodegroup-blend`, `--materials-blend`.
* Calculates a default worker count based on `os.cpu_count()`.
* **Output Directory Resolution (`main.py:main`):**
* Determines the base output directory by checking `--output-dir` argument first, then falling back to `OUTPUT_BASE_DIR` from `config.py`.
* Resolves the path to an absolute path and ensures the directory exists (`Path.resolve()`, `Path.mkdir(parents=True, exist_ok=True)`).
* **Asset Processing (`AssetProcessor` class in `asset_processor.py`):**
* **Classification (`_inventory_and_classify_files`):**
* Multi-pass approach: Explicit Extra (regex) -> Models (regex) -> Potential Maps (keyword regex) -> Standalone 16-bit check (regex) -> Prioritize 16-bit variants -> Final Maps -> Remaining as Unrecognised (Extra).
* Uses compiled regex patterns provided by the `Configuration` object passed during initialization.
* Sorts potential map variants based on: 1. Preset rule index, 2. Keyword index within rule, 3. Alphabetical path. Suffixes (`-1`, `-2`) are assigned later per-asset based on this sort order and `RESPECT_VARIANT_MAP_TYPES`.
* **Map Processing (`_process_maps`):**
* Loads images using `cv2.imread` (flags: `IMREAD_UNCHANGED` or `IMREAD_GRAYSCALE`). Converts loaded 3-channel images from BGR to RGB for internal consistency (stats, merging).
* **Saving Channel Order:** Before saving with `cv2.imwrite`, 3-channel images are conditionally converted back from RGB to BGR *only* if the target output format is *not* EXR (e.g., for PNG, JPG, TIF). This ensures correct channel order for standard formats while preserving RGB for EXR. (Fix for ISSUE-010).
* Handles Gloss->Roughness inversion: Loads gloss, inverts using float math (`1.0 - img/norm`), stores as float32 with original dtype. Prioritizes gloss source if both gloss and native rough exist.
* Resizes using `cv2.resize` (interpolation: `INTER_LANCZOS4` for downscale, `INTER_CUBIC` for potential same-size/upscale - though upscaling is generally avoided by checks).
* Determines output format based on hierarchy: `FORCE_LOSSLESS_MAP_TYPES` > `RESOLUTION_THRESHOLD_FOR_JPG` > Input format priority (TIF/EXR often lead to lossless) > Configured defaults (`OUTPUT_FORMAT_16BIT_PRIMARY`, `OUTPUT_FORMAT_8BIT`).
* Determines output bit depth based on `MAP_BIT_DEPTH_RULES` ('respect' vs 'force_8bit').
* Converts dtype before saving (e.g., float to uint8/uint16 using scaling factors 255.0/65535.0).
* Calculates stats (`_calculate_image_stats`) on normalized float64 data (in RGB space) for a specific resolution (`CALCULATE_STATS_RESOLUTION`).
* Calculates aspect ratio string (`_normalize_aspect_ratio_change`) based on relative dimension changes.
* Handles save fallback: If primary 16-bit format (e.g., EXR) fails, attempts fallback (e.g., PNG).
* **Merging (`_merge_maps_from_source`):**
* Identifies the required *source* files for merge inputs based on classified files.
* Determines common resolutions based on available processed maps (as a proxy for size compatibility).
* Loads required source maps for each common resolution using the `_load_and_transform_source` helper (utilizing the cache).
* Converts loaded inputs to float32 (normalized 0-1).
* Injects default values (from rule `defaults`) for missing channels.
* Merges channels using `cv2.merge`.
* Determines output bit depth based on rule (`force_16bit`, `respect_inputs`).
* Determines output format based on complex rules (`config.py` and preset), considering the highest format among *source* inputs if not forced lossless or over JPG threshold. Handles JPG 16-bit conflict by forcing 8-bit.
* Saves the merged image using the `_save_image` helper, including final data type/color space conversions and fallback logic (e.g., EXR->PNG).
* **Metadata (`_determine_base_metadata`, `_determine_single_asset_metadata`, `_generate_metadata_file`):**
* Base name determined using `source_naming` separator/index from `Configuration`, with fallback to common prefix or input name. Handles multiple assets within one input.
* Category determined by model presence or `decal_keywords` from `Configuration`.
* Archetype determined by matching keywords in `archetype_rules` (from `Configuration`) against file stems/base name.
* Final `metadata.json` populated by accumulating results (map details, stats, features, etc.) during the per-asset processing loop.
* **Blender Integration (`main.py:run_blender_script`, `gui/processing_handler.py:_run_blender_script_subprocess`):**
* Uses `subprocess.run` to execute Blender.
* Command includes `-b` (background), the target `.blend` file, `--python` followed by the script path (`blenderscripts/*.py`), and `--` separator.
* Arguments after `--` (currently just the `asset_root_dir`, and optionally the nodegroup blend path for the materials script) are passed to the Python script via `sys.argv`.
* Uses `--factory-startup` in GUI handler. Checks return code and logs stdout/stderr.
--------------------------------
8. State Management
--------------------------------
* **`Configuration` Object:** Holds the loaded and merged configuration state (core + preset) and compiled regex patterns. Designed to be immutable after initialization. Instantiated once per worker process.
* **`AssetProcessor` Instance:** Primarily stateless between calls to `process()`. State *within* a `process()` call is managed through local variables scoped to the overall call or the per-asset loop (e.g., `current_asset_metadata`, `processed_maps_details_asset`). `self.classified_files` is populated once by `_inventory_and_classify_files` early in `process()` and then used read-only (filtered copies) within the per-asset loop.
* **`main.py` (CLI):** Tracks overall run progress (processed, skipped, failed counts) based on results returned from worker processes.
* **`gui/processing_handler.py`:** Manages the state of a GUI processing run using internal flags (`_is_running`, `_cancel_requested`) and stores `Future` objects in `self._futures` dictionary while the pool is active.
--------------------------------
9. Error Handling & Propagation
--------------------------------
* **Custom Exceptions:** `ConfigurationError` (raised by `Configuration` on load/validation failure), `AssetProcessingError` (raised by `AssetProcessor` for various processing failures).
* **Configuration:** `ConfigurationError` halts initialization. Regex compilation errors are logged as warnings but do not stop initialization.
* **AssetProcessor:** Uses `try...except Exception` within key pipeline steps (`_process_maps`, `_merge_maps`, etc.) and within the per-asset loop in `process()`. Errors specific to one asset are logged (`log.error(exc_info=True)`), the asset is marked "failed" in the returned status dictionary, and the loop continues to the next asset. Critical setup errors (e.g., workspace creation) raise `AssetProcessingError`, halting the entire `process()` call. Includes specific save fallback logic (EXR->PNG) on `cv2.imwrite` failure for 16-bit formats.
* **Worker Wrapper (`main.py:process_single_asset_wrapper`):** Catches `ConfigurationError`, `AssetProcessingError`, and general `Exception` during worker execution. Logs the error and returns a ("failed", error_message) status tuple to the main process.
* **Process Pool (`main.py`, `gui/processing_handler.py`):** The `with ProcessPoolExecutor(...)` block handles pool setup/teardown. A `try...except` around `as_completed` or `future.result()` catches critical worker failures (e.g., process crash).
* **GUI Communication (`ProcessingHandler`):** Catches exceptions during `future.result()` retrieval. Emits `file_status_updated` signal with "failed" status and error message. Emits `processing_finished` with final counts.
* **Blender Scripts:** Checks `subprocess.run` return code. Logs stderr as ERROR if return code is non-zero, otherwise as WARNING. Catches `FileNotFoundError` if the Blender executable path is invalid.
--------------------------------
10. Key Data Structures
--------------------------------
* **`Configuration` Instance Attributes:**
* `compiled_map_keyword_regex`: `dict[str, list[tuple[re.Pattern, str, int]]]` (Base type -> list of compiled regex tuples)
* `compiled_extra_regex`, `compiled_model_regex`: `list[re.Pattern]`
* `compiled_bit_depth_regex_map`: `dict[str, re.Pattern]` (Base type -> compiled regex)
* **`AssetProcessor` Internal Structures (within `process()`):**
* `self.classified_files`: `dict[str, list[dict]]` (Category -> list of file info dicts like `{'source_path': Path, 'map_type': str, ...}`)
* `processed_maps_details_asset`, `merged_maps_details_asset`: `dict[str, dict[str, dict]]` (Map Type -> Resolution Key -> Details Dict `{'path': Path, 'width': int, ...}`)
* `file_to_base_name_map`: `dict[Path, Optional[str]]` (Source relative path -> Determined asset base name or None)
* `current_asset_metadata`: `dict` (Accumulates name, category, archetype, stats, map details per asset)
* **Return Values:**
* `AssetProcessor.process()`: `Dict[str, List[str]]` (e.g., `{"processed": [...], "skipped": [...], "failed": [...]}`)
* `main.process_single_asset_wrapper()`: `Tuple[str, str, Optional[str]]` (input_path, status_string, error_message)
* **`ProcessingHandler._futures`:** `dict[Future, str]` (Maps `concurrent.futures.Future` object to the input path string)
* **Image Data:** `numpy.ndarray` (Handled by OpenCV).
--------------------------------
11. Concurrency Models (CLI & GUI)
--------------------------------
* **Common Core:** Both CLI and GUI utilize `concurrent.futures.ProcessPoolExecutor` for parallel processing. The target function executed by workers is `main.process_single_asset_wrapper`.
* **Isolation:** Crucially, `Configuration` and `AssetProcessor` objects are instantiated *within* the `process_single_asset_wrapper` function, meaning each worker process gets its own independent configuration and processor instance based on the arguments passed. This prevents state conflicts between concurrent asset processing tasks. Data is passed between the main process and workers via pickling of arguments and return values.
* **CLI Orchestration (`main.py:run_processing`):**
* Creates the `ProcessPoolExecutor`.
* Submits all `process_single_asset_wrapper` tasks.
* Uses `concurrent.futures.as_completed` to iterate over finished futures as they complete, blocking until the next one is done.
* Gathers results synchronously within the main script's execution flow.
* **GUI Orchestration (`gui/processing_handler.py`):**
* The `ProcessingHandler` object (a `QObject`) contains the `run_processing` method.
* This method is intended to be run in a separate `QThread` (managed by `MainWindow`) to avoid blocking the main UI thread.
* Inside `run_processing`, it creates and manages the `ProcessPoolExecutor`.
* It uses `as_completed` similarly to the CLI to iterate over finished futures.
* **Communication:** Instead of blocking the thread gathering results, it emits Qt signals (`progress_updated`, `file_status_updated`, `processing_finished`) from within the `as_completed` loop. These signals are connected to slots in `MainWindow` (running on the main UI thread), allowing for thread-safe updates to the GUI (progress bar, table status, status bar messages).
* **Cancellation (GUI - `gui/processing_handler.py:request_cancel`):**
* Sets an internal `_cancel_requested` flag.
* Attempts `executor.shutdown(wait=False)` which prevents new tasks from starting and may cancel pending ones (depending on Python version).
* Manually iterates through stored `_futures` and calls `future.cancel()` on those not yet running or done.
* **Limitation:** This does *not* forcefully terminate worker processes that are already executing the `process_single_asset_wrapper` function. Cancellation primarily affects pending tasks and the processing of results from already running tasks (they will be marked as failed/cancelled when their future completes).
--------------------------------
12. Resource Management
--------------------------------
* **Configuration:** Preset JSON files are opened and closed using `with open(...)`.
* **AssetProcessor:**
* Temporary workspace directory created using `tempfile.mkdtemp()`.
* Cleanup (`_cleanup_workspace`) uses `shutil.rmtree()` and is called within a `finally` block in the main `process()` method, ensuring cleanup attempt even if errors occur.
* Metadata JSON file written using `with open(...)`.
* Image data is loaded into memory using OpenCV/NumPy; memory usage depends on image size and number of concurrent workers.
* **Process Pool:** The `ProcessPoolExecutor` manages the lifecycle of worker processes. Using it within a `with` statement (as done in `main.py` and `gui/processing_handler.py`) ensures proper shutdown and resource release for the pool itself.
--------------------------------
13. Known Limitations & Edge Cases
--------------------------------
* **Configuration:**
* Validation (`_validate_configs`) is primarily structural (key presence, basic types), not deeply logical (e.g., doesn't check if regex patterns are *sensible*).
* Regex compilation errors in `_compile_regex_patterns` are logged as warnings but don't prevent `Configuration` initialization, potentially leading to unexpected classification later.
* `_fnmatch_to_regex` helper only handles basic `*` and `?` wildcards. Complex fnmatch patterns might not translate correctly.
* **AssetProcessor:**
* Heavily reliant on correct filename patterns and rules defined in presets. Ambiguous or incorrect patterns lead to misclassification.
* Potential for high memory usage when processing very large images, especially with many workers.
* Error handling within `process()` is per-asset; a failure during map processing for one asset marks the whole asset as failed, without attempting other maps for that asset. No partial recovery within an asset.
* Gloss->Roughness inversion assumes gloss map is single channel or convertible to grayscale.
* `predict_output_structure` and `get_detailed_file_predictions` use simplified logic (e.g., assuming PNG output, highest resolution only) and may not perfectly match final output names/formats in all cases.
* Filename sanitization (`_sanitize_filename`) is basic and might not cover all edge cases for all filesystems.
* **CLI (`main.py`):**
* Preset existence check (`{preset}.json`) happens only in the main process before workers start.
* Blender executable finding logic relies on `config.py` path being valid or `blender` being in the system PATH.
* **GUI Concurrency (`gui/processing_handler.py`):**
* Cancellation (`request_cancel`) is not immediate for tasks already running in worker processes. It prevents new tasks and stops processing results from completed futures once the flag is checked.
* **General:**
* Limited input format support (ZIP archives, folders). Internal file formats limited by OpenCV (`cv2.imread`, `cv2.imwrite`). Optional `OpenEXR` package recommended for full EXR support.
* Error messages propagated from workers might lack full context in some edge cases.

View File

@@ -1,70 +0,0 @@
# Asset Processor Tool Documentation Plan
This document outlines the proposed structure for the documentation of the Asset Processor Tool, based on the content from `readme.md` and `documentation.txt`. The goal is to create a clear, modular, and comprehensive documentation set within a new `Documentation` directory.
## Proposed Directory Structure
```
Documentation/
├── 00_Overview.md
├── 01_User_Guide/
│ ├── 01_Introduction.md
│ ├── 02_Features.md
│ ├── 03_Installation.md
│ ├── 04_Configuration_and_Presets.md
│ ├── 05_Usage_GUI.md
│ ├── 06_Usage_CLI.md
│ ├── 07_Usage_Monitor.md
│ ├── 08_Usage_Blender.md
│ ├── 09_Output_Structure.md
│ └── 10_Docker.md
└── 02_Developer_Guide/
├── 01_Architecture.md
├── 02_Codebase_Structure.md
├── 03_Key_Components.md
├── 04_Configuration_System_and_Presets.md
├── 05_Processing_Pipeline.md
├── 06_GUI_Internals.md
├── 07_Monitor_Internals.md
├── 08_Blender_Integration_Internals.md
├── 09_Development_Workflow.md
├── 10_Coding_Conventions.md
└── 11_Debugging_Notes.md
```
## File Content Breakdown
### `Documentation/00_Overview.md`
* Project purpose, scope, and intended audience.
* High-level summary of the tool's functionality.
* Table of Contents for the entire documentation set.
### `Documentation/01_User_Guide/`
* **`01_Introduction.md`**: Brief welcome and purpose for users.
* **`02_Features.md`**: Detailed list of user-facing features.
* **`03_Installation.md`**: Requirements and step-by-step installation instructions.
* **`04_Configuration_and_Presets.md`**: Explains user-level configuration options (`config.py` settings relevant to users) and how to select and understand presets.
* **`05_Usage_GUI.md`**: Guide on using the Graphical User Interface, including descriptions of panels, controls, and workflow.
* **`06_Usage_CLI.md`**: Guide on using the Command-Line Interface, including arguments and examples.
* **`07_Usage_Monitor.md`**: Guide on setting up and using the Directory Monitor for automated processing.
* **`08_Usage_Blender.md`**: Explains the user-facing aspects of the Blender integration.
* **`09_Output_Structure.md`**: Describes the structure and contents of the generated asset library.
* **`10_Docker.md`**: Instructions for building and running the tool using Docker.
### `Documentation/02_Developer_Guide/`
* **`01_Architecture.md`**: High-level technical architecture, core components, and their relationships.
* **`02_Codebase_Structure.md`**: Detailed breakdown of key files and directories within the project.
* **`03_Key_Components.md`**: In-depth explanation of major classes and modules (`AssetProcessor`, `Configuration`, GUI Handlers, etc.).
* **`04_Configuration_System_and_Presets.md`**: Technical details of the configuration loading and merging process, the structure of preset JSON files, and guidance on creating/modifying presets for developers.
* **`05_Processing_Pipeline.md`**: Step-by-step technical breakdown of the asset processing logic within the `AssetProcessor` class.
* **`06_GUI_Internals.md`**: Technical details of the GUI implementation, including threading, signals/slots, and background task management.
* **`07_Monitor_Internals.md`**: Technical details of the Directory Monitor implementation using `watchdog`.
* **`08_Blender_Integration_Internals.md`**: Technical details of how the Blender scripts are executed and interact with the processed assets.
* **`09_Development_Workflow.md`**: Guidance for developers on contributing, setting up a development environment, and modifying specific parts of the codebase.
* **`10_Coding_Conventions.md`**: Overview of the project's coding standards, object-oriented approach, type hinting, logging, and error handling.
* **`11_Debugging_Notes.md`**: Advanced internal details, state management, error propagation, concurrency models, resource management, and known limitations/edge cases.
This plan provides a solid foundation for organizing the existing documentation and serves as a roadmap for creating the new markdown files.

Binary file not shown.

View File

@@ -1,356 +0,0 @@
# Asset Processor Tool vX.Y
## Overview
This tool processes 3D asset source files (texture sets, models, etc., provided as ZIP archives or folders) into a standardized library format. It uses configurable presets to interpret different asset sources and automates tasks like file classification, image resizing, channel merging, and metadata generation.
The tool offers both a Graphical User Interface (GUI) for interactive use and a Command-Line Interface (CLI) for batch processing and scripting.
This tool is currently work in progress, rewritting features from an original proof of concept, original script can be found at `Deprecated-POC/` for reference
## Features
* **Preset-Driven:** Uses JSON presets (`presets/`) to define rules for different asset suppliers (e.g., `Poliigon.json`).
* **Dual Interface:** Provides both a user-friendly GUI and a powerful CLI.
* **Parallel Processing:** Utilizes multiple CPU cores for faster processing of multiple assets (configurable via `--workers` in CLI or GUI control).
* **Multi-Asset Input Handling:** Correctly identifies and processes multiple distinct assets contained within a single input ZIP or folder, creating separate outputs for each.
* **File Classification:** Automatically identifies map types (Color, Normal, Roughness, etc.), models, explicitly marked extra files, and unrecognised files based on preset rules.
* **Variant Handling:** Map types listed in `RESPECT_VARIANT_MAP_TYPES` (in `config.py`, e.g., `"COL"`) will *always* receive a numeric suffix (`-1`, `-2`, etc.). The numbering priority is determined primarily by the order of keywords listed in the preset's `map_type_mapping`. Alphabetical sorting of filenames is used only as a tie-breaker for files matching the exact same keyword pattern. Other map types will *never* receive a suffix.
* **16-bit Prioritization:** Correctly identifies 16-bit variants defined in preset `bit_depth_variants` (e.g., `*_NRM16.tif`), prioritizes them, and ignores the corresponding 8-bit version (marked as `Ignored` in GUI).
* **Map Processing:**
* Resizes texture maps to configured power of two resolutions (e.g., 4K, 2K, 1K), avoiding upscaling.
* Handles Glossiness map inversion to Roughness.
* Applies bit-depth rules (`respect` source or `force_8bit`).
* Saves maps in appropriate formats. Map types listed in `FORCE_LOSSLESS_MAP_TYPES` (in `config.py`, e.g., `"NRM"`, `"DISP"`) are *always* saved in a lossless format (PNG for 8-bit, configured 16-bit format like EXR/PNG for 16-bit), overriding other rules. For other map types, if the output is 8-bit and the resolution meets or exceeds `RESOLUTION_THRESHOLD_FOR_JPG` (in `config.py`), the output is forced to JPG. Otherwise, the format is based on input type and target bit depth: JPG inputs yield JPG outputs (8-bit); TIF inputs yield PNG/EXR (based on target bit depth and config); other inputs use configured formats (PNG/EXR). Merged maps follow similar logic, checking `FORCE_LOSSLESS_MAP_TYPES` first, then the threshold for 8-bit targets, then using the highest format from inputs (EXR > TIF > PNG > JPG hierarchy, with TIF adjusted to PNG/EXR based on target bit depth).
* Calculates basic image statistics (Min/Max/Mean) for a reference resolution.
* Calculates and stores the relative aspect ratio change string in metadata.
* **Channel Merging:** Combines channels from different maps into packed textures (e.g., NRMRGH) based on preset rules.
* **Metadata Generation:** Creates a `metadata.json` file for each asset containing details about maps, category, archetype, aspect ratio change, processing settings, etc. **Aspect Ratio Metadata:** Calculates the relative aspect ratio change during resizing and stores it in the `metadata.json` file (`aspect_ratio_change_string`). The format indicates if the aspect is unchanged (`EVEN`), scaled horizontally (`X150`, `X110`, etc.), scaled vertically (`Y150`, `Y125`, etc.)
* **Output Organization:** Creates a clean, structured output directory (`<output_base>/<supplier>/<asset_name>/`).
* **Skip/Overwrite:** Can skip processing if the output already exists or force reprocessing with the `--overwrite` flag (CLI) or checkbox (GUI).
* **Blender Integration:** Optionally runs Blender scripts (`create_nodegroups.py`, `create_materials.py`) after asset processing to automate node group and material creation in specified `.blend` files. Available via both CLI and GUI.
* **GUI Features:**
* Drag-and-drop input for assets (ZIPs/folders).
* Integrated preset editor panel for managing `.json` presets.
* Configurable output directory field with a browse button (defaults to path in `config.py`).
* Enhanced live preview table showing predicted file status (Mapped, Model, Extra, Unrecognised, Ignored, Error) based on the selected processing preset.
* Toggleable preview mode (via View menu) to switch between detailed file preview and a simple list of input assets.
* Toggleable log console panel (via View menu) displaying application log messages within the GUI.
* Progress bar, cancellation button, and clear queue button.
* **Blender Post-Processing Controls:** Checkbox to enable/disable Blender script execution and input fields with browse buttons to specify the target `.blend` files for node group and material creation (defaults configurable in `config.py`).
* **Responsive GUI:** Utilizes background threads (`QThread`) for processing (`ProcessPoolExecutor`) and file preview generation (`ThreadPoolExecutor`), ensuring the user interface remains responsive during intensive operations.
* **Optimized Classification:** Pre-compiles regular expressions from presets for faster file identification during classification.
* **Docker Support:** Includes a `Dockerfile` for containerized execution.
## Directory Structure
```
Asset_processor_tool/
├── main.py # CLI Entry Point & processing orchestrator
├── monitor.py # Directory monitoring script for automated processing
├── asset_processor.py # Core class handling single asset processing pipeline
├── configuration.py # Class for loading and accessing configuration
├── config.py # Core settings definition (output paths, resolutions, merge rules etc.)
├── blenderscripts/ # Scripts for integration with Blender
│ └── create_nodegroups.py # Script to create node groups from processed assets
│ └── create_materials.py # Script to create materials linking to node groups
├── gui/ # Contains files related to the Graphical User Interface
│ ├── main_window.py # Main GUI application window and layout
│ ├── processing_handler.py # Handles background processing logic for the GUI
│ ├── prediction_handler.py # Handles background file prediction/preview for the GUI
├── Presets/ # Preset definition files
│ ├── _template.json # Template for creating new presets
│ └── Poliigon.json # Example preset for Poliigon assets
├── Testfiles/ # Directory containing example input assets for testing
├── Tickets/ # Directory for issue and feature tracking (Markdown files)
│ ├── _template.md # Template for creating new tickets
│ └── Ticket-README.md # Explanation of the ticketing system
├── requirements.txt # Python package dependencies for standard execution
├── requirements-docker.txt # Dependencies specifically for the Docker environment
├── Dockerfile # Instructions for building the Docker container image
└── readme.md # This documentation file
```
* **Core Logic:** `main.py`, `monitor.py`, `asset_processor.py`, `configuration.py`, `config.py`
* **Blender Integration:** `blenderscripts/` directory
* **GUI:** `gui/` directory
* **Configuration:** `config.py`, `Presets/` directory
* **Dependencies:** `requirements.txt`, `requirements-docker.txt`
* **Containerization:** `Dockerfile`
* **Documentation/Planning:** `readme.md`, `Project Notes/` directory
* **Issue/Feature Tracking:** `Tickets/` directory (see `Tickets/README.md`)
* **Testing:** `Testfiles/` directory
## Architecture
This section provides a higher-level overview of the tool's internal structure and design, intended for developers or users interested in the technical implementation.
### Core Components
The tool is primarily built around several key Python modules:
* `config.py`: Defines core, global settings (output paths, resolutions, default behaviors, format rules, Blender executable path, default Blender file paths, etc.) that are generally not supplier-specific.
* `Presets/*.json`: Supplier-specific JSON files defining rules for interpreting source assets (filename patterns, map type keywords, model identification, etc.).
* `configuration.py` **(**`Configuration` **class)**: Responsible for loading the core `config.py` settings and merging them with a selected preset JSON file. Crucially, it also **pre-compiles** regular expression patterns defined in the preset (e.g., for map keywords, extra files, 16-bit variants) upon initialization. This pre-compilation significantly speeds up the file classification process.
* `asset_processor.py` **(**`AssetProcessor` **class)**: Contains the core logic for processing a *single* asset. It orchestrates the pipeline steps: workspace setup, extraction, file classification, metadata determination, map processing, channel merging, metadata file generation, and output organization.
* `main.py`: Serves as the entry point for the Command-Line Interface (CLI). It handles argument parsing, sets up logging, manages the parallel processing pool, calls `AssetProcessor` for each input asset via a wrapper function, and optionally triggers Blender script execution after processing.
* `gui/`: Contains modules related to the Graphical User Interface (GUI), built using PySide6.
* `monitor.py`: Implements the directory monitoring functionality for automated processing.
### Parallel Processing (CLI & GUI)
To accelerate the processing of multiple assets, the tool utilizes Python's `concurrent.futures.ProcessPoolExecutor`.
* Both `main.py` (for CLI) and `gui/processing_handler.py` (for GUI background tasks) create a process pool.
* The actual processing for each asset is delegated to the `main.process_single_asset_wrapper` function. This wrapper is executed in a separate worker process within the pool.
* The wrapper function is responsible for instantiating the `Configuration` and `AssetProcessor` classes for the specific asset being processed in that worker. This isolates each asset's processing environment.
* Results (success, skip, failure, error messages) are communicated back from the worker processes to the main coordinating script (either `main.py` or `gui/processing_handler.py`).
### Asset Processing Pipeline (`AssetProcessor` class)
The `AssetProcessor` class executes a sequence of steps for each asset:
1. `_setup_workspace()`: Creates a temporary directory for processing.
2. `_extract_input()`: Extracts the input ZIP archive or copies the input folder contents into the temporary workspace.
3. `_inventory_and_classify_files()`: This is a critical step that scans the workspace and classifies each file based on rules defined in the loaded `Configuration` (which includes the preset). It uses the pre-compiled regex patterns for efficiency. Key logic includes:
* Identifying files explicitly marked for the `Extra/` folder.
* Identifying model files.
* Matching potential texture maps against keyword patterns.
* Identifying and prioritizing 16-bit variants (e.g., `_NRM16.tif`) over their 8-bit counterparts based on `source_naming.bit_depth_variants` patterns. Ignored 8-bit files are tracked.
* Handling map variants (e.g., multiple Color maps) by assigning suffixes (`-1`, `-2`) based on the `RESPECT_VARIANT_MAP_TYPES` setting in `config.py` and the order of keywords defined in the preset's `map_type_mapping`.
* Classifying any remaining files as 'Unrecognised' (which are also moved to the `Extra/` folder).
4. `_determine_base_metadata()`: Determines the asset's base name, category (Texture, Asset, Decal), and archetype (e.g., Wood, Metal) based on classified files and preset rules (`source_naming`, `asset_category_rules`, `archetype_rules`).
5. **Skip Check**: If `overwrite` is false, checks if the final output directory and metadata file already exist. If so, processing for this asset stops early.
6. `_process_maps()`: Iterates through classified texture maps. For each map:
* Loads the image data (handling potential Gloss->Roughness inversion).
* Resizes the map to each target resolution specified in `config.py`, avoiding upscaling.
* Determines the output bit depth based on `MAP_BIT_DEPTH_RULES` (`respect` source or `force_8bit`).
* Determines the output file format (`.jpg`, `.png`, `.exr`) based on a combination of factors:
* The `RESOLUTION_THRESHOLD_FOR_JPG` (forces JPG for 8-bit maps above the threshold).
* The original input file format (e.g., `.jpg` inputs tend to produce `.jpg` outputs if 8-bit and below threshold).
* The target bit depth (16-bit outputs use configured `OUTPUT_FORMAT_16BIT_PRIMARY` or `_FALLBACK`).
* Configured 8-bit format (`OUTPUT_FORMAT_8BIT`).
* The `FORCE_LOSSLESS_MAP_TYPES` list in `config.py` (overrides all other logic for specified map types, ensuring PNG/EXR output).
* Saves the processed map for each resolution, applying appropriate compression/quality settings. Includes fallback logic if saving in the primary format fails (e.g., EXR -> PNG).
* Calculates basic image statistics (Min/Max/Mean) for a reference resolution (`CALCULATE_STATS_RESOLUTION`) and determines the aspect ratio change string (e.g., "EVEN", "X150", "Y075") stored in the metadata.
7. `_merge_maps()`: Combines channels from different processed maps into new textures (e.g., NRMRGH) based on `MAP_MERGE_RULES` defined in `config.py`. It determines the output format for merged maps similarly to `_process_maps` (checking `FORCE_LOSSLESS_MAP_TYPES` first, then threshold, then input hierarchy), considering the formats of the input maps involved.
8. `_generate_metadata_file()`: Collects all gathered information (asset name, maps present, resolutions, stats, aspect ratio change, etc.) and writes it to the `metadata.json` file.
9. `_organize_output_files()`: Moves the processed maps, merged maps, models, metadata file, and any 'Extra'/'Unrecognised'/'Ignored' files from the temporary workspace to the final structured output directory (`<output_base>/<supplier>/<asset_name>/`).
10. `_cleanup_workspace()`: Removes the temporary workspace directory.
### GUI Architecture (`gui/`)
The GUI provides an interactive way to use the tool and manage presets.
* **Framework**: Built using `PySide6`, the official Python bindings for the Qt framework.
* **Main Window (**`main_window.py`**)**: Defines the main application window, which includes:
* An integrated preset editor panel (using `QSplitter`).
* A processing panel with drag-and-drop support, output directory selection, a file preview table, and processing controls.
* **Blender Post-Processing Controls:** A group box containing a checkbox to enable/disable Blender script execution and input fields with browse buttons for specifying the target `.blend` files for node group and material creation.
* **Threading Model**: To prevent the UI from freezing during potentially long operations, background tasks are run in separate `QThread`s:
* `ProcessingHandler` **(**`processing_handler.py`**)**: Manages the execution of the main processing pipeline (using `ProcessPoolExecutor` and `main.process_single_asset_wrapper`, similar to the CLI) and the optional Blender script execution in a background thread. Receives the target output directory and Blender integration settings from the main window.
* `PredictionHandler` **(**`prediction_handler.py`**)**: Manages the generation of file previews in a background thread using a `ThreadPoolExecutor` to parallelize prediction across multiple assets. It calls `AssetProcessor.get_detailed_file_predictions()`, which performs extraction and classification.
* **Communication**: Qt's **signal and slot mechanism** is used for communication between the background threads (`ProcessingHandler`, `PredictionHandler`) and the main GUI thread (`MainWindow`). For example, signals are emitted to update the progress bar, populate the preview table, and report completion status or errors. A custom `QtLogHandler` redirects Python log messages to the UI console via signals.
* **Preset Editor**: The editor allows creating, modifying, and saving preset JSON files directly within the GUI. Changes are tracked, and users are prompted to save before closing or loading another preset if changes are pending. Includes an optional, toggleable log console panel at the top.
### Monitor Architecture (`monitor.py`)
The `monitor.py` script enables automated processing of assets dropped into a designated input directory.
* **File System Watching**: Uses the `watchdog` library (specifically `PollingObserver` for cross-platform compatibility) to monitor the specified `INPUT_DIR`.
* **Event Handling**: A custom `ZipHandler` detects `on_created` events for `.zip` files.
* **Filename Parsing**: It expects filenames in the format `[preset]_filename.zip` and uses a regular expression (`PRESET_FILENAME_REGEX`) to extract the `preset` name.
* **Preset Validation**: Checks if the extracted preset name corresponds to a valid `.json` file in the `Presets/` directory.
* **Processing Trigger**: If the filename format and preset are valid, it calls the `main.run_processing` function (the same core logic used by the CLI) to process the detected ZIP file using the extracted preset.
* **File Management**: Moves the source ZIP file to either a `PROCESSED_DIR` (on success/skip) or an `ERROR_DIR` (on failure or invalid preset) after the processing attempt.
### Error Handling
* Custom exception classes (`ConfigurationError`, `AssetProcessingError`) are defined and used to signal specific types of errors during configuration loading or asset processing.
* Standard Python logging is used throughout the application (CLI, GUI, Monitor, Core Logic) to record information, warnings, and errors. Log levels can be configured.
* Worker processes in the processing pool capture exceptions and report them back to the main process for logging and status updates.
## Requirements
* Python 3.8+
* Required Python Packages (see `requirements.txt`):
* `opencv-python` (for image processing)
* `numpy` (for numerical operations)
* `PySide6` (only needed for the GUI)
* Optional Python Packages:
* `OpenEXR` (provides more robust EXR file handling, recommended if processing EXR sources)
* **Blender:** A working installation of Blender is required for the optional Blender integration features. The path to the executable should be configured in `config.py` or available in the system's PATH.
Install dependencies using pip:
```bash
pip install -r requirements.txt
```
(For GUI, ensure PySide6 is included or install separately: `pip install PySide6`)
## Configuration
The tool's behavior is controlled by two main configuration components:
1. `config.py`**:** Defines core, global settings:
* `OUTPUT_BASE_DIR`: Default root directory for processed assets.
* `DEFAULT_ASSET_CATEGORY`: Fallback category ("Texture", "Asset", "Decal").
* `IMAGE_RESOLUTIONS`: Dictionary mapping resolution keys (e.g., "4K") to pixel dimensions.
* `RESPECT_VARIANT_MAP_TYPES`: List of map type strings (e.g., `["COL"]`) that should always receive a numeric suffix (`-1`, `-2`, etc.) based on preset order, even if only one variant exists.
* `TARGET_FILENAME_PATTERN`: Format string for output filenames.
* `MAP_MERGE_RULES`: List defining how to merge channels (e.g., creating NRMRGH).
* `ARCHETYPE_RULES`: Rules for determining asset usage archetype (e.g., Wood, Metal).
* `RESOLUTION_THRESHOLD_FOR_JPG`: Dimension threshold (pixels) above which 8-bit maps are forced to JPG format, overriding other format logic.
* `FORCE_LOSSLESS_MAP_TYPES`: List of map type strings (e.g., `["NRM", "DISP"]`) that should *always* be saved losslessly (PNG/EXR), overriding the JPG threshold and other format logic.
* `BLENDER_EXECUTABLE_PATH`: Path to the Blender executable (required for Blender integration).
* `DEFAULT_NODEGROUP_BLEND_PATH`: Default path to the .blend file for node group creation (used by GUI if not specified).
* `DEFAULT_MATERIALS_BLEND_PATH`: Default path to the .blend file for material creation (used by GUI if not specified).
* ... and other processing parameters (JPEG quality, PNG compression, 16-bit/8-bit output formats, etc.).
2. `presets/*.json`**:** Define supplier-specific rules. Each JSON file represents a preset (e.g., `Poliigon.json`). Key sections include:
* `supplier_name`: Name of the asset source.
* `map_type_mapping`: A list of dictionaries defining rules to map source filename keywords/patterns to standard map types. Each dictionary should have `"target_type"` (e.g., `"COL"`, `"NRM"`) and `"keywords"` (a list of source filename patterns like `["_col*", "_color"]`). For map types listed in `config.py`'s `RESPECT_VARIANT_MAP_TYPES`, the numbering priority (`-1`, `-2`, etc.) is determined primarily by the order of the keywords within the `"keywords"` list for the matching rule. Alphabetical sorting of filenames is used only as a secondary tie-breaker for files matching the exact same keyword pattern. Other map types do not receive suffixes.
* `bit_depth_variants`: Dictionary mapping standard map types (e.g., `"NRM"`) to fnmatch patterns used to identify their high bit-depth source files (e.g., `"*_NRM16*.tif"`). These take priority over standard keyword matches, and the corresponding 8-bit version will be ignored.
* `bit_depth_rules`: Specifies whether to `respect` source bit depth or `force_8bit` for specific map types (defined in `config.py`).
* `model_patterns`: Regex patterns to identify model files (e.g., `*.fbx`, `*.obj`).
* `move_to_extra_patterns`: Regex patterns for files to move directly to the `Extra/` output folder.
* `source_naming_convention`: Defines separator and indices for extracting base name/archetype from source filenames.
* `asset_category_rules`: Keywords/patterns to identify specific asset categories (e.g., "Decal").
Use `presets/_template.json` as a starting point for creating new presets.
## Usage
### 1. Graphical User Interface (GUI)
* **Run:**
```bash
python -m gui.main_window
```
*(Note: Run this command from the project root directory)*
* **Interface:**
* **Menu Bar:** Contains a "View" menu to toggle visibility of the Log Console and enable/disable the detailed file preview.
* **Preset Editor Panel (Left):**
* Optional **Log Console:** A text area at the top displaying application log messages (toggle via View menu).
* **Preset List:** Allows creating, deleting, loading, editing, and saving presets. Select a preset here to load it into the editor tabs below.
* **Preset Editor Tabs:** Edit preset details ("General & Naming", "Mapping & Rules").
* **Processing Panel (Right):**
* **Preset Selector:** Select the preset to use for *processing* the current queue.
* **Output Directory:** Displays the target output directory. Defaults to the path in `config.py`. Use the "Browse..." button to select a different directory.
* **Drag and Drop Area:** Drag asset ZIP files or folders here to add them to the queue.
* **Preview Table:** Displays information about the assets in the queue. Behavior depends on the "Disable Detailed Preview" option in the View menu:
* **Detailed Preview (Default):** Shows all files found within the dropped assets, their predicted classification status (Mapped, Model, Extra, Unrecognised, Ignored, Error), predicted output name (if applicable), and other details based on the selected *processing* preset. Rows are color-coded by status.
* **Simple View (Preview Disabled):** Shows only the list of top-level input asset paths (ZIPs/folders) added to the queue.
* **Progress Bar:** Shows the overall processing progress.
* **Blender Post-Processing:** A group box containing a checkbox to enable/disable the optional Blender script execution. When enabled, input fields and browse buttons appear to specify the `.blend` files for node group and material creation. These fields default to the paths configured in `config.py`.
* **Options & Controls (Bottom):**
* `Overwrite Existing`: Checkbox to force reprocessing if output already exists.
* `Workers`: Spinbox to set the number of assets to process concurrently.
* `Clear Queue`: Button to remove all assets from the queue and clear the preview.
* `Start Processing`: Button to begin processing all assets in the queue.
* `Cancel`: Button to attempt stopping ongoing processing.
* **Status Bar:** Displays messages about the current state, errors, or completion.
### 2. Command-Line Interface (CLI)
* **Run:**
```bash
python main.py [OPTIONS] INPUT_PATH [INPUT_PATH ...]
```
* **Arguments:**
* `INPUT_PATH`: One or more paths to input ZIP files or folders.
* `-p PRESET`, `--preset PRESET`: (Required) Name of the preset to use (e.g., `Poliigon`).
* `-o OUTPUT_DIR`, `--output-dir OUTPUT_DIR`: Override the `OUTPUT_BASE_DIR` set in `config.py`.
* `-w WORKERS`, `--workers WORKERS`: Number of parallel processes (default: auto-detected based on CPU cores).
* `--overwrite`: Force reprocessing and overwrite existing output.
* `-v`, `--verbose`: Enable detailed DEBUG level logging.
* `--nodegroup-blend NODEGROUP_BLEND`: Path to the .blend file for creating/updating node groups. Overrides `config.py` default. If provided, triggers node group script execution after processing.
* `--materials-blend MATERIALS_BLEND`: Path to the .blend file for creating/updating materials. Overrides `config.py` default. If provided, triggers material script execution after processing.
* **Example:**
```bash
python main.py "C:/Downloads/WoodFine001.zip" -p Poliigon -o "G:/Assets/Processed" --workers 4 --overwrite --nodegroup-blend "G:/Blender/Libraries/NodeGroups.blend" --materials-blend "G:/Blender/Libraries/Materials.blend"
```
### 3. Directory Monitor (Automated Processing)
* **Run:**
```bash
python monitor.py
```
* **Functionality:** This script continuously monitors a specified input directory for new `.zip` files. When a file matching the expected format `[preset]_filename.zip` appears, it automatically triggers the processing pipeline using the extracted preset name. **Note:** The directory monitor currently does *not* support the optional Blender script execution. This feature is only available via the CLI and GUI.
* **Configuration (Environment Variables):**
* `INPUT_DIR`: Directory to monitor for new ZIP files (default: `/data/input`).
* `OUTPUT_DIR`: Base directory for processed asset output (default: `/data/output`).
* `PROCESSED_DIR`: Directory where successfully processed/skipped source ZIPs are moved (default: `/data/processed`).
* `ERROR_DIR`: Directory where source ZIPs that failed processing are moved (default: `/data/error`).
* `LOG_LEVEL`: Logging verbosity (e.g., `INFO`, `DEBUG`) (default: `INFO`).
* `POLL_INTERVAL`: How often to check the input directory (seconds) (default: `5`).
* `PROCESS_DELAY`: Delay after detecting a file before processing starts (seconds) (default: `2`).
* `NUM_WORKERS`: Number of parallel workers for processing (default: auto-detected).
* **Output:**
* Logs processing activity to the console.
* Processed assets are created in the `OUTPUT_DIR` following the standard structure.
* The original input `.zip` file is moved to `PROCESSED_DIR` on success/skip or `ERROR_DIR` on failure.
### 4. Blender Node Group Creation Script (`blenderscripts/create_nodegroups.py`)
* **Purpose:** This script, designed to be run *within* Blender (either manually or triggered by `main.py`/GUI), scans processed assets and creates/updates PBR node groups in the active `.blend` file.
* **Execution:** Typically run via the Asset Processor tool's CLI or GUI after asset processing. Can also be run manually in Blender's Text Editor.
* **Prerequisites (for manual run):**
* A library of assets processed by this tool, located at a known path.
* A Blender file containing two template node groups named exactly `Template_PBRSET` and `Template_PBRTYPE`.
* **Configuration (Inside the script for manual run):**
* `PROCESSED_ASSET_LIBRARY_ROOT`: **Must be updated** within the script to point to the base output directory where the processed supplier folders (e.g., `Poliigon/`) are located. This is overridden by the tool when run via CLI/GUI.
* **Functionality:** Reads metadata, creates/updates node groups, loads textures, sets up nodes, applies metadata-driven settings (aspect ratio, stats, highest resolution), and sets asset previews. Includes an explicit save command at the end.
### 5. Blender Material Creation Script (`blenderscripts/create_materials.py`)
* **Purpose:** This script, designed to be run *within* Blender (either manually or triggered by `main.py`/GUI), scans processed assets and creates/updates materials in the active `.blend` file that link to the PBRSET node groups created by `create_nodegroups.py`.
* **Execution:** Typically run via the Asset Processor tool's CLI or GUI after asset processing. Can also be run manually in Blender's Text Editor.
* **Prerequisites (for manual run):**
* A library of assets processed by this tool, located at a known path.
* A `.blend` file containing the PBRSET node groups created by `create_nodegroups.py`.
* A template material in the *current* Blender file named `Template_PBRMaterial` that uses nodes and contains a Group node labeled `PLACEHOLDER_NODE_LABEL`.
* **Configuration (Inside the script for manual run):**
* `PROCESSED_ASSET_LIBRARY_ROOT`: **Must be updated** within the script to point to the base output directory where the processed supplier folders (e.g., `Poliigon/`) are located. This is overridden by the tool when run via CLI/GUI.
* `NODEGROUP_BLEND_FILE_PATH`: **Must be updated** within the script to point to the `.blend` file containing the PBRSET node groups. This is overridden by the tool when run via CLI/GUI.
* `TEMPLATE_MATERIAL_NAME`, `PLACEHOLDER_NODE_LABEL`, `MATERIAL_NAME_PREFIX`, `PBRSET_GROUP_PREFIX`, etc., can be adjusted if needed.
* **Functionality:** Reads metadata, creates/updates materials by copying the template, links the corresponding PBRSET node group from the specified `.blend` file, marks materials as assets, copies tags, sets custom previews, and sets viewport properties based on metadata. Includes an explicit save command at the end.
## Processing Pipeline (Simplified)
1. **Extraction:** Input ZIP/folder contents are extracted/copied to a temporary workspace.
2. **Classification:** Files are scanned and classified (map, model, extra, ignored) using preset rules.
3. **Metadata Determination:** Asset name, category, and archetype are determined.
4. **Skip Check:** If output exists and overwrite is off, processing stops here.
5. **Map Processing:** Identified maps are loaded, resized, converted (bit depth, format), and saved. Gloss maps are inverted if needed. Stats are calculated.
6. **Merging:** Channels are merged according to preset rules and saved.
7. **Metadata Generation:** `metadata.json` is created with all collected information.
8. **Output Organization:** Processed files are moved to the final structured output directory.
9. **Cleanup:** The temporary workspace is removed.
10. **Optional Blender Script Execution:** If configured via CLI or GUI, Blender is launched in the background to run `create_nodegroups.py` and `create_materials.py` on specified `.blend` files, using the processed asset output directory as input.
## Output Structure
Processed assets are saved to: `<output_base_directory>/<supplier_name>/<asset_name>/`
Each asset directory typically contains:
* Processed texture maps (e.g., `AssetName_Color_4K.png`, `AssetName_NRM_2K.exr`).
* Merged texture maps (e.g., `AssetName_NRMRGH_4K.png`).
* Model files (if present in source).
* `metadata.json`: Detailed information about the asset and processing.
* `Extra/` (subdirectory): Contains source files that were not classified as standard maps or models. This includes files explicitly matched by `move_to_extra_patterns` in the preset (e.g., previews, documentation) as well as any other unrecognised files.
## Docker
A `Dockerfile` and `requirements-docker.txt` are provided for building a container image to run the processor in an isolated environment. Build and run using standard Docker commands.

View File

@@ -12,9 +12,9 @@ This documentation strictly excludes details on environment setup, dependency in
## Architecture and Codebase Summary
For developers interested in contributing, the tool's architecture centers on a **Core Processing Engine** (`processing_engine.py`) executing a pipeline based on a **Hierarchical Rule System** (`rule_structure.py`) and a **Configuration System** (`configuration.py` loading `config/app_settings.json` and `Presets/*.json`). The **Graphical User Interface** (`gui/`) has been significantly refactored: `MainWindow` (`main_window.py`) acts as a coordinator, delegating tasks to specialized widgets (`MainPanelWidget`, `PresetEditorWidget`, `LogConsoleWidget`) and background handlers (`RuleBasedPredictionHandler`, `LLMPredictionHandler`, `LLMInteractionHandler`, `AssetRestructureHandler`). The **Directory Monitor** (`monitor.py`) now processes archives asynchronously using a thread pool and utility functions (`utils/prediction_utils.py`, `utils/workspace_utils.py`). The **Command-Line Interface** entry point (`main.py`) primarily launches the GUI, with core CLI functionality currently non-operational. Optional **Blender Integration** (`blenderscripts/`) remains. A new `utils/` directory houses shared helper functions.
For developers interested in contributing, the tool's architecture centers on a **Core Processing Engine** (`processing_engine.py`) which initializes and runs a **Pipeline Orchestrator** (`processing/pipeline/orchestrator.py::PipelineOrchestrator`). This orchestrator executes a defined sequence of **Processing Stages** (located in `processing/pipeline/stages/`) based on a **Hierarchical Rule System** (`rule_structure.py`) and a **Configuration System** (`configuration.py` loading `config/app_settings.json` and `Presets/*.json`). The **Graphical User Interface** (`gui/`) has been significantly refactored: `MainWindow` (`main_window.py`) acts as a coordinator, delegating tasks to specialized widgets (`MainPanelWidget`, `PresetEditorWidget`, `LogConsoleWidget`) and background handlers (`RuleBasedPredictionHandler`, `LLMPredictionHandler`, `LLMInteractionHandler`, `AssetRestructureHandler`). The **Directory Monitor** (`monitor.py`) now processes archives asynchronously using a thread pool and utility functions (`utils/prediction_utils.py`, `utils/workspace_utils.py`). The **Command-Line Interface** entry point (`main.py`) primarily launches the GUI, with core CLI functionality currently non-operational. Optional **Blender Integration** (`blenderscripts/`) remains. A new `utils/` directory houses shared helper functions.
The codebase reflects this structure. The `gui/` directory contains the refactored UI components, `utils/` holds shared utilities, `Presets/` contains JSON presets, and `blenderscripts/` holds Blender scripts. Core logic resides in `processing_engine.py`, `configuration.py`, `rule_structure.py`, `monitor.py`, and `main.py`. The processing pipeline, executed by `processing_engine.py`, relies entirely on the input `SourceRule` and static configuration for steps like map processing, channel merging, and metadata generation.
The codebase reflects this structure. The `gui/` directory contains the refactored UI components, `utils/` holds shared utilities, `processing/pipeline/` contains the orchestrator and individual processing stages, `Presets/` contains JSON presets, and `blenderscripts/` holds Blender scripts. Core logic resides in `processing_engine.py`, `processing/pipeline/orchestrator.py`, `configuration.py`, `rule_structure.py`, `monitor.py`, and `main.py`. The processing pipeline, initiated by `processing_engine.py` and executed by the `PipelineOrchestrator`, relies entirely on the input `SourceRule` and static configuration. Each stage in the pipeline operates on an `AssetProcessingContext` object (`processing/pipeline/asset_context.py`) to perform specific tasks like map processing, channel merging, and metadata generation.
## Table of Contents

View File

@@ -16,6 +16,7 @@ This document outlines the key features of the Asset Processor Tool.
* Saves maps in appropriate formats (JPG, PNG, EXR) based on complex rules involving map type (`FORCE_LOSSLESS_MAP_TYPES`), resolution (`RESOLUTION_THRESHOLD_FOR_JPG`), bit depth, and source format.
* Calculates basic image statistics (Min/Max/Mean) for a reference resolution.
* Calculates and stores the relative aspect ratio change string in metadata (e.g., `EVEN`, `X150`, `Y125`).
* **Low-Resolution Fallback:** If enabled (`ENABLE_LOW_RESOLUTION_FALLBACK`), automatically saves an additional "LOWRES" variant of source images if their largest dimension is below a configurable threshold (`LOW_RESOLUTION_THRESHOLD`). This "LOWRES" variant uses the original image dimensions and is saved in addition to any standard resolution outputs.
* **Channel Merging:** Combines channels from different maps into packed textures (e.g., NRMRGH) based on preset rules (`MAP_MERGE_RULES` in `config.py`).
* **Metadata Generation:** Creates a `metadata.json` file for each asset containing details about maps, category, archetype, aspect ratio change, processing settings, etc.
* **Output Organization:** Creates a clean, structured output directory (`<output_base>/<supplier>/<asset_name>/`).

View File

@@ -9,13 +9,25 @@ The tool's core settings are now stored in `config/app_settings.json`. This JSON
The `configuration.py` module is responsible for loading the settings from `app_settings.json` (including loading and saving the JSON content), merging them with the rules from the selected preset file, and providing the base configuration via the `load_base_config()` function. Note that the old `config.py` file has been deleted.
The `app_settings.json` file is structured into several key sections, including:
* `FILE_TYPE_DEFINITIONS`: Defines known file types (like different texture maps, models, etc.) and their properties. Each definition now includes a `"standard_type"` key for aliasing to a common type and a `"bit_depth_rule"` key specifying how to handle bit depth for this file type. The separate `MAP_BIT_DEPTH_RULES` section has been removed.
* `FILE_TYPE_DEFINITIONS`: Defines known file types (like different texture maps, models, etc.) and their properties. Each definition now includes a `"standard_type"` key for aliasing to a common type (e.g., "COL" for color maps, "NRM" for normal maps), an `"is_grayscale"` boolean property, and a `"bit_depth_rule"` key specifying how to handle bit depth for this file type. The separate `MAP_BIT_DEPTH_RULES` section has been removed. For users creating or editing presets, it's important to note that internal mapping rules (like `Map_type_Mapping.target_type` within a preset's `FileRule`) now directly use the main keys from these `FILE_TYPE_DEFINITIONS` (e.g., `"MAP_COL"`, `"MAP_RGH"`), not just the `standard_type` aliases.
* `ASSET_TYPE_DEFINITIONS`: Defines known asset types (like Surface, Model, Decal) and their properties.
* `MAP_MERGE_RULES`: Defines how multiple input maps can be merged into a single output map (e.g., combining Normal and Roughness into one).
### Low-Resolution Fallback Settings
These settings control the generation of low-resolution "fallback" variants for source images:
* `ENABLE_LOW_RESOLUTION_FALLBACK` (boolean, default: `true`):
* If `true`, the tool will generate an additional "LOWRES" variant for source images whose largest dimension is smaller than the `LOW_RESOLUTION_THRESHOLD`.
* This "LOWRES" variant uses the original dimensions of the source image and is saved in addition to any other standard resolution outputs (e.g., 1K, PREVIEW).
* If `false`, this feature is disabled.
* `LOW_RESOLUTION_THRESHOLD` (integer, default: `512`):
* Defines the pixel dimension (for the largest side of an image) below which the "LOWRES" fallback variant will be generated (if enabled).
* For example, if set to `512`, any source image smaller than 512x512 (e.g., 256x512, 128x128) will have a "LOWRES" variant created.
### LLM Predictor Settings
For users who wish to utilize the experimental LLM Predictor feature, the following settings are available in `config/app_settings.json`:
For users who wish to utilize the experimental LLM Predictor feature, the following settings are available in `config/llm_settings.json`:
* `llm_endpoint_url`: The URL of the LLM API endpoint. For local LLMs like LM Studio or Ollama, this will typically be `http://localhost:<port>/v1`. Consult your LLM server documentation for the exact endpoint.
* `llm_api_key`: The API key required to access the LLM endpoint. Some local LLM servers may not require a key, in which case this can be left empty.
@@ -23,15 +35,39 @@ For users who wish to utilize the experimental LLM Predictor feature, the follow
* `llm_temperature`: Controls the randomness of the LLM's output. Lower values (e.g., 0.1-0.5) make the output more deterministic and focused, while higher values (e.g., 0.6-1.0) make it more creative and varied. For prediction tasks, lower temperatures are generally recommended.
* `llm_request_timeout`: The maximum time (in seconds) to wait for a response from the LLM API. Adjust this based on the performance of your LLM server and the complexity of the requests.
Note that the `llm_predictor_prompt` and `llm_predictor_examples` settings are also present in `app_settings.json`. These define the instructions and examples provided to the LLM for prediction. While they can be viewed here, they are primarily intended for developer reference and tuning the LLM's behavior, and most users will not need to modify them.
Note that the `llm_predictor_prompt` and `llm_predictor_examples` settings are also present in `config/llm_settings.json`. These define the instructions and examples provided to the LLM for prediction. While they can be viewed here, they are primarily intended for developer reference and tuning the LLM's behavior, and most users will not need to modify them directly via the file. These settings are editable via the LLM Editor panel in the main GUI when the LLM interpretation mode is selected.
## GUI Configuration Editor
## Application Preferences (`config/app_settings.json` overrides)
You can modify the `app_settings.json` file using the built-in GUI editor. Access it via the **Edit** -> **Preferences...** menu.
You can modify user-overridable application settings using the built-in GUI editor. These settings are loaded from `config/app_settings.json` and saved as overrides in `config/user_settings.json`. Access it via the **Edit** -> **Preferences...** menu.
This editor provides a tabbed interface (e.g., "General", "Output & Naming") to view and change the core application settings defined in `app_settings.json`. Settings in the editor directly correspond to the structure and values within the JSON file. Note that any changes made through the GUI editor require an application restart to take effect.
This editor provides a tabbed interface to view and change various application behaviors. The tabs include:
* **General:** Basic settings like output base directory and temporary file prefix.
* **Output & Naming:** Settings controlling output directory and filename patterns, and how variants are handled.
* **Image Processing:** Settings related to image resolution definitions, compression levels, and format choices.
* **Map Merging:** Configuration for how multiple input maps are combined into single output maps.
* **Postprocess Scripts:** Paths to default Blender files for post-processing.
*(Ideally, a screenshot of the GUI Configuration Editor would be included here.)*
Note that this editor focuses on user-specific overrides of core application settings. **Asset Type Definitions, File Type Definitions, and Supplier Settings are managed in a separate Definitions Editor.**
Any changes made through the Preferences editor require an application restart to take effect.
*(Ideally, a screenshot of the Application Preferences editor would be included here.)*
## Definitions Editor (`config/asset_type_definitions.json`, `config/file_type_definitions.json`, `config/suppliers.json`)
Core application definitions that are separate from general user preferences are managed in the dedicated Definitions Editor. This includes defining known asset types, file types, and configuring settings specific to different suppliers. Access it via the **Edit** -> **Edit Definitions...** menu.
The editor is organized into three tabs:
* **Asset Type Definitions:** Define the different categories of assets (e.g., Surface, Model, Decal). For each asset type, you can configure its description, a color for UI representation, and example usage strings.
* **File Type Definitions:** Define the specific types of files the tool recognizes (e.g., MAP_COL, MAP_NRM, MODEL). For each file type, you can configure its description, a color, example keywords/patterns, a standard type alias, bit depth handling rules, whether it's grayscale, and an optional keybind for quick assignment in the GUI.
* **Supplier Settings:** Configure settings that are specific to assets originating from different suppliers. Currently, this includes the "Normal Map Type" (OpenGL or DirectX) used for normal maps from that supplier.
Each tab presents a list of the defined items on the left (Asset Types, File Types, or Suppliers). Selecting an item in the list displays its configurable details on the right. Buttons are provided to add new definitions or remove existing ones.
Changes made in the Definitions Editor are saved directly to their respective configuration files (`config/asset_type_definitions.json`, `config/file_type_definitions.json`, and `config/suppliers.json`). Some changes may require an application restart to take full effect in processing logic.
*(Ideally, screenshots of the Definitions Editor tabs would be included here.)*
## Preset Files (`presets/*.json`)
@@ -40,8 +76,18 @@ Preset files define supplier-specific rules for interpreting asset source files.
* Presets are located in the `presets/` directory.
* Each preset is a JSON file named after the supplier (e.g., `Poliigon.json`).
* Presets contain rules based on filename patterns and keywords to identify map types, models, and other files.
* They also define how variants (like different resolutions or bit depths) are handled and how asset names and categories are determined from the source filename.
* They also define how variants (like different resolutions or bit depths) are handled and how asset names and categories are determined from the source filename. When defining `map_type_mapping` rules within a preset, the `target_type` field must now use a valid key from the `FILE_TYPE_DEFINITIONS` in `config/app_settings.json` (e.g., `"MAP_AO"` instead of a custom alias like `"AO"`).
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.
## Global Output Path Configuration
The structure and naming of the output files generated by the tool are now controlled by two global settings defined exclusively in `config/app_settings.json`:
* `OUTPUT_DIRECTORY_PATTERN`: Defines the directory structure where processed assets will be saved.
* `OUTPUT_FILENAME_PATTERN`: Defines the naming convention for the individual output files within the generated directory.
**Important:** These settings are global and apply to all processing tasks, regardless of the selected preset. They are **not** part of individual preset files and cannot be modified using the Preset Editor. You can view and edit these patterns in the main application preferences (**Edit** -> **Preferences...**).
These patterns use special tokens (e.g., `[assetname]`, `[maptype]`) that are replaced with actual values during processing. For a detailed explanation of how these patterns work together, the available tokens, and examples, please refer to the [Output Structure](./09_Output_Structure.md) section of the User Guide.

View File

@@ -12,7 +12,10 @@ python -m gui.main_window
## Interface Overview
* **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.
* **Menu Bar:** The "Edit" menu contains options to configure application settings and definitions:
* **Preferences...:** Opens the Application Preferences editor for user-overridable settings (saved to `config/user_settings.json`).
* **Edit Definitions...:** Opens the Definitions Editor for managing Asset Type Definitions, File Type Definitions, and Supplier Settings (saved to their respective files).
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.
@@ -28,6 +31,15 @@ python -m gui.main_window
* **Drag-and-Drop Re-parenting:** File rows can be dragged and dropped onto different Asset rows to change their parent asset association.
* **Right-Click Context Menu:** Right-clicking on Source, Asset, or File rows brings up a context menu:
* **Re-interpret selected source:** This sub-menu allows re-running the prediction process for the selected source item(s) using either a specific preset or the LLM predictor. The available presets and the "LLM" option are listed dynamically. This replaces the previous standalone "Re-interpret Selected with LLM" button.
* **Keybinds for Item Management:** When items are selected in the Preview Table, the following keybinds can be used:
* `Ctrl + C`: Sets the file type of selected items to Color/Albedo (`MAP_COL`).
* `Ctrl + R`: Toggles the file type of selected items between Roughness (`MAP_ROUGH`) and Glossiness (`MAP_GLOSS`).
* `Ctrl + N`: Sets the file type of selected items to Normal (`MAP_NRM`).
* `Ctrl + M`: Toggles the file type of selected items between Metalness (`MAP_METAL`) and Reflection/Specular (`MAP_REFL`).
* `Ctrl + D`: Sets the file type of selected items to Displacement/Height (`MAP_DISP`).
* `Ctrl + E`: Sets the file type of selected items to Extra (`EXTRA`).
* `Ctrl + X`: Sets the file type of selected items to Ignore (`FILE_IGNORE`).
* `F2`: Prompts to set the asset name for all selected items. This name propagates to the `AssetRule` name or the `FileRule` `target_asset_name_override` for the files under the selected assets. If individual files are selected, it will affect their `target_asset_name_override`.
* **Prediction Population:** If a valid preset is selected in the Preset Selector (or if re-interpretation is triggered), the table populates with prediction results as they become available. If no preset is selected, added items show empty prediction fields.
* **Columns:** The table displays columns: Name, Target Asset, Supplier, Asset Type, Item Type. The "Target Asset" column stretches to fill available space.
* **Coloring:** The *text color* of file items is determined by their Item Type (colors defined in `config/app_settings.json`). The *background color* of file items is a 30% darker shade of their parent asset's background, helping to visually group files within an asset. Asset rows themselves may use alternating background colors based on the application theme.

View File

@@ -2,20 +2,66 @@
This document describes the directory structure and contents of the processed assets generated by the Asset Processor Tool.
Processed assets are saved to: `<output_base_directory>/<supplier_name>/<asset_name>/`
Processed assets are saved to a location determined by two global settings, `OUTPUT_DIRECTORY_PATTERN` and `OUTPUT_FILENAME_PATTERN`, defined in `config/app_settings.json`. These settings can be overridden by the user via `config/user_settings.json`.
* `<output_base_directory>`: The base output directory configured in `config.py` or specified via CLI/GUI.
* `<supplier_name>`: The name of the asset supplier, determined from the preset used.
* `<asset_name>`: The name of the processed asset, determined from the source filename based on preset rules.
* `OUTPUT_DIRECTORY_PATTERN`: Defines the directory structure *within* the Base Output Directory.
* `OUTPUT_FILENAME_PATTERN`: Defines the naming convention for individual files *within* the directory created by `OUTPUT_DIRECTORY_PATTERN`.
These patterns use special tokens (explained below) that are replaced with actual values during processing. You can configure these patterns via the main application preferences (**Edit** -> **Preferences...** -> **Output & Naming** tab). They are global settings and are not part of individual presets.
### Available Tokens
The following tokens can be used in both `OUTPUT_DIRECTORY_PATTERN` and `OUTPUT_FILENAME_PATTERN`. Note that some tokens make more sense in one pattern than the other (e.g., `[maptype]` and `[ext]` are typically used in the filename pattern).
* `[Assettype]`: The type of asset (e.g., `Texture`, `Model`, `Surface`).
* `[supplier]`: The supplier name (from the preset, e.g., `Poliigon`).
* `[assetname]`: The main asset name (e.g., `RustyMetalPanel`).
* `[resolution]`: Texture resolution (e.g., `1k`, `2k`, `4k`).
* `[ext]`: The output file extension (e.g., `png`, `jpg`, `exr`). (Primarily for filename pattern)
* `[IncrementingValue]` or `[####]`: A numerical value that increments based on existing directories matching the `OUTPUT_DIRECTORY_PATTERN` in the output base path. The number of `#` characters determines the zero-padding (e.g., `[###]` -> `001`, `002`). If `[IncrementingValue]` is used, it defaults to 4 digits of padding (`0001`, `0002`).
* `[Date]`: Current date (`YYYYMMDD`).
* `[Time]`: Current time (`HHMMSS`).
* `[Sha5]`: The first 5 characters of the SHA-256 hash of the original input source file (e.g., the source zip archive).
* `[ApplicationPath]`: Absolute path to the application directory.
* `[maptype]`: The standardized map type identifier (e.g., `COL` for Color/Albedo, `NRM` for Normal, `RGH` for Roughness). This is derived from the `standard_type` defined in the application's `FILE_TYPE_DEFINITIONS` (managed in `config/file_type_definitions.json` via the Definitions Editor) and may include a variant suffix if applicable. (Primarily for filename pattern)
* `[dimensions]`: Pixel dimensions (e.g., `2048x2048`).
* `[bitdepth]`: Output bit depth (e.g., `8bit`, `16bit`).
* `[category]`: Asset category determined by preset rules.
* `[archetype]`: Asset archetype determined by preset rules.
* `[variant]`: Asset variant identifier determined by preset rules.
* `[source_filename]`: The original filename of the source file being processed.
* `[source_basename]`: The original filename without the extension.
* `[source_dirname]`: The directory containing the original source file.
### Example Output Paths
The final output path is constructed by combining the Base Output Directory (set in Preferences or via CLI) with the results of the two patterns.
**Example 1:**
* Base Output Directory: `/home/user/ProcessedAssets`
* `OUTPUT_DIRECTORY_PATTERN`: `[supplier]/[assetname]/[resolution]`
* `OUTPUT_FILENAME_PATTERN`: `[assetname]_[maptype]_[resolution].[ext]`
* Resulting Path for an Albedo map: `/home/user/ProcessedAssets/Poliigon/WoodFloor001/4k/WoodFloor001_Albedo_4k.png`
**Example 2:**
* Base Output Directory: `Output` (relative path)
* `OUTPUT_DIRECTORY_PATTERN`: `[Assettype]/[category]/[assetname]`
* `OUTPUT_FILENAME_PATTERN`: `[maptype].[ext]`
* Resulting Path for a Normal map: `Output/Texture/Wood/WoodFloor001/Normal.exr`
The `<output_base_directory>` (the root folder where processing output starts) is configured separately via the GUI (**Edit** -> **Preferences...** -> **General** tab -> **Output Base Directory**) or the `--output` CLI argument. The `OUTPUT_DIRECTORY_PATTERN` defines the structure *within* this base directory, and `OUTPUT_FILENAME_PATTERN` defines the filenames within that structure.
## Contents of Each Asset Directory
Each asset directory contains the following:
* Processed texture maps (e.g., `AssetName_Color_4K.png`, `AssetName_NRM_2K.exr`). These are the resized, format-converted, and bit-depth adjusted texture files.
* Merged texture maps (e.g., `AssetName_NRMRGH_4K.png`). These are maps created by combining channels from different source maps based on the configured merge rules.
* Processed texture maps (e.g., `WoodFloor_Albedo_4k.png`, `MetalPanel_Normal_2k.exr`). The exact filenames depend on the `OUTPUT_FILENAME_PATTERN`. These are the resized, format-converted, and bit-depth adjusted texture files.
* **LOWRES Variants:** If the "Low-Resolution Fallback" feature is enabled and a source image's dimensions are below the configured threshold, an additional variant with "LOWRES" as its resolution token (e.g., `MyTexture_COL_LOWRES.png`) will be saved. This variant uses the original dimensions of the source image.
* Merged texture maps (e.g., `WoodFloor_Combined_4k.png`). The exact filenames depend on the `OUTPUT_FILENAME_PATTERN`. These are maps created by combining channels from different source maps based on the configured merge rules.
* Model files (if present in the source asset).
* `metadata.json`: A JSON file containing detailed information about the asset and the processing that was performed. This includes details about the maps, resolutions, formats, bit depths, merged map details, calculated image statistics, aspect ratio change information, asset category and archetype, the source preset used, and a list of ignored source files. This file is intended for use by downstream tools or scripts (like the Blender integration scripts).
* `Extra/` (subdirectory): Contains source files that were not classified as maps or models but were explicitly marked to be moved to the extra directory based on preset rules (e.g., previews, documentation files).
* `metadata.json`: A JSON file containing detailed information about the asset and the processing that was performed. This includes details about the maps (resolutions, formats, bit depths, and for roughness maps, a `derived_from_gloss_filename: true` flag if it was inverted from an original gloss map), merged map details, calculated image statistics, aspect ratio change information, asset category and archetype, the source preset used, and a list of ignored source files. This file is intended for use by downstream tools or scripts (like the Blender integration scripts).
* `EXTRA/` (subdirectory): Contains source files not classified as maps or models but marked as "EXTRA" by preset rules (e.g., previews, documentation). These files are placed in an `EXTRA` folder *within* the directory generated by `OUTPUT_DIRECTORY_PATTERN`.
* `Unrecognised/` (subdirectory): Contains source files that were not classified as maps, models, or explicitly marked as extra, and were not ignored.
* `Ignored/` (subdirectory): Contains source files that were explicitly ignored during processing (e.g., an 8-bit Normal map when a 16-bit variant exists and is prioritized).

View File

@@ -0,0 +1,85 @@
# User Guide: Usage - Automated GUI Testing (`autotest.py`)
This document explains how to use the `autotest.py` script for automated sanity checks of the Asset Processor Tool's GUI-driven workflow.
## Overview
The `autotest.py` script provides a way to run predefined test scenarios headlessly (without displaying the GUI). It simulates the core user actions: loading an asset, selecting a preset, allowing rules to be predicted, processing the asset, and then checks the results against expectations. This is primarily intended as a developer tool for regression testing and ensuring core functionality remains stable.
## Running the Autotest Script
From the project root directory, you can run the script using Python:
```bash
python autotest.py [OPTIONS]
```
### Command-Line Options
The script accepts several command-line arguments to configure the test run. If not provided, they use predefined default values.
* `--zipfile PATH_TO_ZIP`:
* Specifies the path to the input asset `.zip` file to be used for the test.
* Default: `TestFiles/BoucleChunky001.zip`
* `--preset PRESET_NAME`:
* Specifies the name of the preset to be selected and used for rule prediction and processing.
* Default: `Dinesen`
* `--expectedrules PATH_TO_JSON`:
* Specifies the path to a JSON file containing the expected rule structure that should be generated after the preset is applied to the input asset.
* Default: `TestFiles/test-BoucleChunky001.json`
* `--outputdir PATH_TO_DIR`:
* Specifies the directory where the processed assets will be written.
* Default: `TestFiles/TestOutputs/DefaultTestOutput`
* `--search "SEARCH_TERM"` (optional):
* A string to search for within the application logs generated during the test run. If found, matching log lines (with context) will be highlighted.
* Default: None
* `--additional-lines NUM_LINES` (optional):
* When using `--search`, this specifies how many lines of context before and after each matching log line should be displayed. A good non-zero value is 1-2.
* Default: `0`
**Example Usage:**
```bash
# Run with default test files and settings
python autotest.py
# Run with specific test files and search for a log message
python autotest.py --zipfile TestFiles/MySpecificAsset.zip --preset MyPreset --expectedrules TestFiles/MySpecificAsset_rules.json --outputdir TestFiles/TestOutputs/MySpecificOutput --search "Processing complete for asset"
```
## `TestFiles` Directory
The autotest script relies on a directory named `TestFiles` located in the project root. This directory should contain:
* **Test Asset `.zip` files:** The actual asset archives used as input for tests (e.g., `default_test_asset.zip`, `MySpecificAsset.zip`).
* **Expected Rules `.json` files:** JSON files defining the expected rule structure for a given asset and preset combination (e.g., `default_test_asset_rules.json`, `MySpecificAsset_rules.json`). The structure of this file is detailed in the main autotest plan (`AUTOTEST_GUI_PLAN.md`).
* **`TestOutputs/` subdirectory:** This is the default parent directory where the autotest script will create specific output folders for each test run (e.g., `TestFiles/TestOutputs/DefaultTestOutput/`).
## Test Workflow
When executed, `autotest.py` performs the following steps:
1. **Initialization:** Parses command-line arguments and initializes the main application components headlessly.
2. **Load Expected Rules:** Loads the `expected_rules.json` file.
3. **Load Asset:** Loads the specified `.zip` file into the application.
4. **Select Preset:** Selects the specified preset. This triggers the internal rule prediction process.
5. **Await Prediction:** Waits for the rule prediction to complete.
6. **Compare Rules:** Retrieves the predicted rules from the application and compares them against the loaded expected rules. If there's a mismatch, the test typically fails at this point.
7. **Start Processing:** If the rules match, it initiates the asset processing pipeline, directing output to the specified output directory.
8. **Await Processing:** Waits for all backend processing tasks to complete.
9. **Check Output:** Verifies the existence of the output directory and lists its contents. Basic checks ensure some output was generated.
10. **Analyze Logs:** Retrieves logs from the application. If a search term was provided, it filters and displays relevant log portions. It also checks for Python tracebacks, which usually indicate a failure.
11. **Report Result:** Prints a summary of the test outcome (success or failure) and exits with an appropriate status code (0 for success, 1 for failure).
## Interpreting Results
* **Console Output:** The script will log its progress and the results of each step to the console.
* **Log Analysis:** Pay attention to the log output, especially if a `--search` term was used or if any tracebacks are reported.
* **Exit Code:**
* `0`: Test completed successfully.
* `1`: Test failed at some point (e.g., rule mismatch, processing error, traceback found).
* **Output Directory:** Inspect the contents of the specified output directory to manually verify the processed assets if needed.
This automated test helps ensure the stability of the core processing logic when driven by GUI-equivalent actions.
Note: Under some conditions, the autotest will exit with errorcode "3221226505". This has no consequence and can therefor be ignore.

View File

@@ -6,17 +6,19 @@ This document provides a high-level overview of the Asset Processor Tool's archi
The Asset Processor Tool is designed to process 3D asset source files into a standardized library format. Its high-level architecture consists of:
1. **Core Processing Engine (`processing_engine.py`):** The primary component responsible for executing the asset processing pipeline for a single input asset based on a provided `SourceRule` object and static configuration. The previous `asset_processor.py` has been removed.
2. **Prediction System:** Responsible for analyzing input files and generating the initial `SourceRule` hierarchy with predicted values. This system utilizes a base handler (`gui/base_prediction_handler.py::BasePredictionHandler`) with specific implementations:
1. **Core Processing Initiation (`processing_engine.py`):** The `ProcessingEngine` class acts as the entry point for an asset processing task. It initializes and runs a `PipelineOrchestrator`.
2. **Pipeline Orchestration (`processing/pipeline/orchestrator.py`):** The `PipelineOrchestrator` manages a sequence of discrete processing stages. It creates an `AssetProcessingContext` for each asset and passes this context through each stage.
3. **Processing Stages (`processing/pipeline/stages/`):** Individual modules, each responsible for a specific task in the pipeline (e.g., filtering files, processing maps, merging channels, organizing output). They operate on the `AssetProcessingContext`.
4. **Prediction System:** Responsible for analyzing input files and generating the initial `SourceRule` hierarchy with predicted values. This system utilizes a base handler (`gui/base_prediction_handler.py::BasePredictionHandler`) with specific implementations:
* **Rule-Based Predictor (`gui/prediction_handler.py::RuleBasedPredictionHandler`):** Uses predefined rules from presets to classify files and determine initial processing parameters.
* **LLM Predictor (`gui/llm_prediction_handler.py::LLMPredictionHandler`):** An experimental alternative that uses a Large Language Model (LLM) to interpret file contents and context to predict processing parameters.
3. **Configuration System (`Configuration`):** Handles loading core settings (including centralized type definitions and LLM-specific configuration) and merging them with supplier-specific rules defined in JSON presets and the persistent `config/suppliers.json` file.
4. **Multiple Interfaces:** Provides different ways to interact with the tool:
5. **Configuration System (`Configuration`):** Handles loading core settings (including centralized type definitions and LLM-specific configuration) and merging them with supplier-specific rules defined in JSON presets and the persistent `config/suppliers.json` file.
6. **Multiple Interfaces:** Provides different ways to interact with the tool:
* Graphical User Interface (GUI)
* Command-Line Interface (CLI) - *Note: The primary CLI execution logic (`run_cli` in `main.py`) is currently non-functional/commented out post-refactoring.*
* Directory Monitor for automated processing.
The GUI acts as the primary source of truth for processing rules, coordinating the generation and management of the `SourceRule` hierarchy before sending it to the processing engine. It accumulates prediction results from multiple input sources before updating the view. The Monitor interface can also generate `SourceRule` objects (using `utils/prediction_utils.py`) to bypass the GUI for automated workflows.
5. **Optional Integration:** Includes scripts (`blenderscripts/`) for integrating with Blender. Logic for executing these scripts was intended to be centralized in `utils/blender_utils.py`, but this utility has not yet been implemented.
The GUI acts as the primary source of truth for processing rules, coordinating the generation and management of the `SourceRule` hierarchy before sending it to the `ProcessingEngine`. It accumulates prediction results from multiple input sources before updating the view. The Monitor interface can also generate `SourceRule` objects (using `utils/prediction_utils.py`) to bypass the GUI for automated workflows.
7. **Optional Integration:** Includes scripts (`blenderscripts/`) for integrating with Blender. Logic for executing these scripts was intended to be centralized in `utils/blender_utils.py`, but this utility has not yet been implemented.
## Hierarchical Rule System
@@ -26,14 +28,14 @@ A key addition to the architecture is the **Hierarchical Rule System**, which pr
* **AssetRule:** Represents rules applied to a specific asset within a source (a source can contain multiple assets).
* **FileRule:** Represents rules applied to individual files within an asset.
This hierarchy allows for fine-grained control over processing parameters. The GUI's prediction logic generates this hierarchy with initial predicted values for overridable fields based on presets and file analysis. The processing engine then operates *solely* on the explicit values provided in this `SourceRule` object and static configuration, without internal prediction or fallback logic.
This hierarchy allows for fine-grained control over processing parameters. The GUI's prediction logic generates this hierarchy with initial predicted values for overridable fields based on presets and file analysis. The `ProcessingEngine` (via the `PipelineOrchestrator` and its stages) then operates *solely* on the explicit values provided in this `SourceRule` object and static configuration, without internal prediction or fallback logic.
## Core Components
* `config/app_settings.json`: Defines core, global settings, constants, and centralized definitions for allowed asset and file types (`ASSET_TYPE_DEFINITIONS`, `FILE_TYPE_DEFINITIONS`), including metadata like colors and descriptions. This replaces the old `config.py` file.
* `config/suppliers.json`: A persistent JSON file storing known supplier names for GUI auto-completion.
* `Presets/*.json`: Supplier-specific JSON files defining rules for file interpretation and initial prediction.
* `configuration.py` (`Configuration` class): Loads `config/app_settings.json` settings and merges them with a selected preset, pre-compiling regex patterns for efficiency. This static configuration is used by the processing engine.
* `configuration.py` (`Configuration` class): Loads `config/app_settings.json` settings and merges them with a selected preset, pre-compiling regex patterns for efficiency. This static configuration is used by the processing pipeline.
* `rule_structure.py`: Defines the `SourceRule`, `AssetRule`, and `FileRule` dataclasses used to represent the hierarchical processing rules.
* `gui/`: Directory containing modules for the Graphical User Interface (GUI), built with PySide6. The `MainWindow` (`main_window.py`) acts as a coordinator, orchestrating interactions between various components. Key GUI components include:
* `main_panel_widget.py::MainPanelWidget`: Contains the primary controls for loading sources, selecting presets, viewing/editing rules, and initiating processing.
@@ -47,7 +49,10 @@ This hierarchy allows for fine-grained control over processing parameters. The G
* `prediction_handler.py::RuleBasedPredictionHandler`: Generates the initial `SourceRule` hierarchy based on presets and file analysis. Inherits from `BasePredictionHandler`.
* `llm_prediction_handler.py::LLMPredictionHandler`: Experimental predictor using an LLM. Inherits from `BasePredictionHandler`.
* `llm_interaction_handler.py::LLMInteractionHandler`: Manages communication with the LLM service for the LLM predictor.
* `processing_engine.py` (`ProcessingEngine` class): The core component that executes the processing pipeline for a single `SourceRule` object using the static `Configuration`. A new instance is created per task for state isolation.
* `processing_engine.py` (`ProcessingEngine` class): The entry-point class that initializes and runs the `PipelineOrchestrator` for a given `SourceRule` and `Configuration`.
* `processing/pipeline/orchestrator.py` (`PipelineOrchestrator` class): Manages the sequence of processing stages, creating and passing an `AssetProcessingContext` through them.
* `processing/pipeline/asset_context.py` (`AssetProcessingContext` class): A dataclass holding all data and state for the processing of a single asset, passed between stages.
* `processing/pipeline/stages/`: Directory containing individual processing stage modules, each handling a specific part of the pipeline (e.g., `IndividualMapProcessingStage`, `MapMergingStage`).
* `main.py`: The main entry point for the application. Primarily launches the GUI. Contains commented-out/non-functional CLI logic (`run_cli`).
* `monitor.py`: Implements the directory monitoring feature using `watchdog`. It now processes archives asynchronously using a `ThreadPoolExecutor`, leveraging `utils.prediction_utils.py` for rule generation and `utils.workspace_utils.py` for workspace management before invoking the `ProcessingEngine`.
* `blenderscripts/`: Contains Python scripts designed to be executed *within* Blender for post-processing tasks.
@@ -56,19 +61,21 @@ This hierarchy allows for fine-grained control over processing parameters. The G
* `prediction_utils.py`: Contains functions like `generate_source_rule_from_archive` used by the monitor for rule-based prediction.
* `blender_utils.py`: (Intended location for Blender script execution logic, currently not implemented).
## Processing Pipeline (Simplified)
## Processing Pipeline (Simplified Overview)
The primary processing engine (`processing_engine.py`) executes a series of steps for each asset based on the provided `SourceRule` object and static configuration:
The asset processing pipeline, initiated by `processing_engine.py` and managed by `PipelineOrchestrator`, executes a series of stages for each asset defined in the `SourceRule`. An `AssetProcessingContext` object carries data between stages. The typical sequence is:
1. Extraction of input to a temporary workspace (using `utils.workspace_utils.py`).
2. Classification of files (map, model, extra, ignored, unrecognised) based *only* on the provided `SourceRule` object (classification/prediction happens *before* the engine is called).
3. Determination of base metadata (asset name, category, archetype).
4. Skip check if output exists and overwrite is not forced.
5. Processing of maps (resize, format/bit depth conversion, inversion, stats calculation).
6. Merging of channels based on rules.
7. Generation of `metadata.json` file.
8. Organization of processed files into the final output structure.
9. Cleanup of the temporary workspace.
10. (Optional) Execution of Blender scripts (currently triggered directly, intended to use `utils.blender_utils.py`).
1. **Supplier Determination**: Identify the effective supplier.
2. **Asset Skip Logic**: Check if the asset should be skipped.
3. **Metadata Initialization**: Set up initial asset metadata.
4. **File Rule Filtering**: Determine which files to process.
5. **Pre-Map Processing**:
* Gloss-to-Roughness Conversion.
* Alpha Channel Extraction.
* Normal Map Green Channel Inversion.
6. **Individual Map Processing**: Handle individual maps (scaling, variants, stats, naming).
7. **Map Merging**: Combine channels from different maps.
8. **Metadata Finalization & Save**: Generate and save `metadata.json` (temporarily).
9. **Output Organization**: Copy all processed files to final output locations.
This architecture allows for a modular design, separating configuration, rule generation/management (GUI, Monitor utilities), and core processing execution. The `SourceRule` object serves as a clear data contract between the rule generation layer and the processing engine. Parallel processing (in Monitor) and background threads (in GUI) are utilized for efficiency and responsiveness.
External steps like workspace preparation/cleanup and optional Blender script execution bracket this core pipeline. This architecture allows for a modular design, separating configuration, rule generation/management, and core processing execution.

View File

@@ -2,17 +2,65 @@
This document describes the major classes and modules that form the core of the Asset Processor Tool.
## `ProcessingEngine` (`processing_engine.py`)
## Core Processing Architecture
The `ProcessingEngine` class is the new core component responsible for executing the asset processing pipeline for a *single* input asset. Unlike the older `AssetProcessor`, this engine operates *solely* based on a complete `SourceRule` object provided to its `process()` method and the static `Configuration` object passed during initialization. It contains no internal prediction, classification, or fallback logic. Its key responsibilities include:
The asset processing pipeline has been refactored into a staged architecture, managed by an orchestrator.
* Setting up and cleaning up a temporary workspace for processing (potentially using `utils.workspace_utils`).
* Extracting or copying input files to the workspace.
* Processing files based on the explicit rules and predicted values contained within the input `SourceRule`.
* Processing texture maps (resizing, format/bit depth conversion, inversion, stats calculation) using parameters from the `SourceRule` or static `Configuration`.
* Merging channels based on rules defined in the static `Configuration` and parameters from the `SourceRule`.
* Generating the `metadata.json` file containing details about the processed asset, incorporating information from the `SourceRule`.
* Organizing the final output files into the structured library directory.
### `ProcessingEngine` (`processing_engine.py`)
The `ProcessingEngine` class serves as the primary entry point for initiating an asset processing task. Its main responsibilities are:
* Initializing a `PipelineOrchestrator` instance.
* Providing the `PipelineOrchestrator` with the global `Configuration` object and a predefined list of processing stages.
* Invoking the orchestrator's `process_source_rule()` method with the input `SourceRule`, workspace path, output path, and other processing parameters.
* Managing a top-level temporary directory for the engine's operations if needed, though individual stages might also use sub-temporary directories via the `AssetProcessingContext`.
It no longer contains the detailed logic for each processing step (like map manipulation, merging, etc.) directly. Instead, it delegates these tasks to the orchestrator and its stages.
### `PipelineOrchestrator` (`processing/pipeline/orchestrator.py`)
The `PipelineOrchestrator` class is responsible for managing the execution of the asset processing pipeline. Its key functions include:
* Receiving a `SourceRule` object, `Configuration`, and a list of `ProcessingStage` objects.
* For each `AssetRule` within the `SourceRule`:
* Creating an `AssetProcessingContext` instance.
* Sequentially executing each registered `ProcessingStage`, passing the `AssetProcessingContext` to each stage.
* Handling exceptions that occur within stages and managing the overall status of asset processing (processed, skipped, failed).
* Managing a temporary directory for the duration of a `SourceRule` processing, which is made available to stages via the `AssetProcessingContext`.
### `AssetProcessingContext` (`processing/pipeline/asset_context.py`)
The `AssetProcessingContext` is a dataclass that acts as a stateful container for all data related to the processing of a single `AssetRule`. An instance of this context is created by the `PipelineOrchestrator` for each asset and is passed through each processing stage. Key information it holds includes:
* The input `SourceRule` and the current `AssetRule`.
* Paths: `workspace_path`, `engine_temp_dir`, `output_base_path`.
* The `Configuration` object.
* `effective_supplier`: Determined by an early stage.
* `asset_metadata`: A dictionary to accumulate metadata about the asset.
* `processed_maps_details`: Stores details about individually processed maps (paths, dimensions, etc.).
* `merged_maps_details`: Stores details about merged maps.
* `files_to_process`: A list of `FileRule` objects to be processed for the current asset.
* `loaded_data_cache`: For caching loaded image data within an asset's processing.
* `status_flags`: For signaling conditions like `skip_asset` or `asset_failed`.
* `incrementing_value`, `sha5_value`: Optional values for path generation.
Each stage reads from and writes to this context, allowing data and state to flow through the pipeline.
### `Processing Stages` (`processing/pipeline/stages/`)
The actual processing logic is broken down into a series of discrete stages, each inheriting from `ProcessingStage` (`processing/pipeline/stages/base_stage.py`). Each stage implements an `execute(context: AssetProcessingContext)` method. Key stages include (in typical execution order):
* **`SupplierDeterminationStage`**: Determines the effective supplier.
* **`AssetSkipLogicStage`**: Checks if the asset processing should be skipped.
* **`MetadataInitializationStage`**: Initializes basic asset metadata.
* **`FileRuleFilterStage`**: Filters `FileRule`s to decide which files to process.
* **`GlossToRoughConversionStage`**: Handles gloss-to-roughness map inversion.
* **`AlphaExtractionToMaskStage`**: Extracts alpha channels to create masks.
* **`NormalMapGreenChannelStage`**: Inverts normal map green channels if required.
* **`IndividualMapProcessingStage`**: Processes individual maps (POT scaling, resolution variants, color conversion, stats, aspect ratio, filename conventions).
* **`MapMergingStage`**: Merges map channels based on rules.
* **`MetadataFinalizationAndSaveStage`**: Collects all metadata and saves `metadata.json` to a temporary location.
* **`OutputOrganizationStage`**: Copies all processed files and metadata to the final output directory structure.
## `Rule Structure` (`rule_structure.py`)
@@ -22,19 +70,19 @@ This module defines the data structures used to represent the hierarchical proce
* `AssetRule`: A dataclass representing rules applied at the asset level. It contains nested `FileRule` objects.
* `FileRule`: A dataclass representing rules applied at the file level.
These classes hold specific rule parameters (e.g., `supplier_identifier`, `asset_type`, `asset_type_override`, `item_type`, `item_type_override`, `target_asset_name_override`). Attributes like `asset_type` and `item_type_override` now use string types, which are validated against centralized lists in `config/app_settings.json`. These structures support serialization (Pickle, JSON) to allow them to be passed between different parts of the application, including across process boundaries.
These classes hold specific rule parameters (e.g., `supplier_identifier`, `asset_type`, `asset_type_override`, `item_type`, `item_type_override`, `target_asset_name_override`, `resolution_override`, `channel_merge_instructions`). Attributes like `asset_type` and `item_type_override` now use string types, which are validated against centralized lists in `config/app_settings.json`. These structures support serialization (Pickle, JSON) to allow them to be passed between different parts of theapplication, including across process boundaries. The `PipelineOrchestrator` and its stages heavily rely on the information within these rule objects, passed via the `AssetProcessingContext`.
## `Configuration` (`configuration.py`)
The `Configuration` class manages the tool's settings. It is responsible for:
* Loading the core default settings defined in `config/app_settings.json`.
* Loading the core default settings defined in `config/app_settings.json` (e.g., `FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`, `image_resolutions`, `map_merge_rules`, `output_filename_pattern`).
* Loading the supplier-specific rules from a selected preset JSON file (`Presets/*.json`).
* Merging the core settings and preset rules into a single, unified configuration object.
* Validating the loaded configuration to ensure required settings are present.
* Pre-compiling regular expression patterns defined in the preset for efficient file classification by the `PredictionHandler`.
* Pre-compiling regular expression patterns defined in the preset for efficient file classification by the prediction handlers.
An instance of the `Configuration` class is typically created once per application run (or per processing batch) and passed to the `ProcessingEngine`.
An instance of the `Configuration` class is typically created once per application run (or per processing batch) and passed to the `ProcessingEngine`, which then makes it available to the `PipelineOrchestrator` and subsequently to each stage via the `AssetProcessingContext`.
## GUI Components (`gui/`)
@@ -44,16 +92,19 @@ The GUI has been refactored into several key components:
The `MainWindow` class acts as the main application window and **coordinator** for the GUI. Its primary responsibilities now include:
* Setting up the main window structure and menu bar.
* Setting up the main window structure (using a `QSplitter`) and menu bar.
* Instantiating and arranging the major GUI widgets:
* `MainPanelWidget` (containing core controls and the rule editor)
* `PresetEditorWidget`
* `PresetEditorWidget` (providing selector and JSON editor parts)
* `LLMEditorWidget` (for LLM settings)
* `MainPanelWidget` (containing the rule view and processing controls)
* `LogConsoleWidget`
* Connecting signals and slots between these widgets, the underlying models (`UnifiedViewModel`), and background handlers (`RuleBasedPredictionHandler`, `LLMPredictionHandler`, `LLMInteractionHandler`).
* **Layout Management:** Placing the preset selector statically and using a `QStackedWidget` to switch between the `PresetEditorWidget`'s JSON editor and the `LLMEditorWidget`.
* **Editor Switching:** Handling the `preset_selection_changed_signal` from `PresetEditorWidget` to switch the stacked editor view (`_on_preset_selection_changed` slot).
* Connecting signals and slots between widgets, models (`UnifiedViewModel`), and handlers (`LLMInteractionHandler`, `AssetRestructureHandler`).
* Managing the overall application state related to GUI interactions (e.g., enabling/disabling controls).
* Handling top-level actions like loading sources (drag-and-drop), initiating predictions, and starting the processing task (via `main.ProcessingTask`).
* Managing the `QThreadPool` for running background tasks (prediction).
* Implementing slots like `_handle_prediction_completion` to update the model/view when prediction results are ready.
* Handling top-level actions like loading sources (drag-and-drop), initiating predictions (`update_preview`), and starting the processing task (`_on_process_requested`).
* Managing background prediction threads (Rule-Based via `QThread`, LLM via `LLMInteractionHandler`).
* Implementing slots (`_on_rule_hierarchy_ready`, `_on_llm_prediction_ready_from_handler`, `_on_prediction_error`, `_handle_prediction_completion`) to update the model/view when prediction results/errors arrive.
### `MainPanelWidget` (`gui/main_panel_widget.py`)
@@ -69,7 +120,10 @@ This widget contains the central part of the GUI, including:
This widget provides the interface for managing presets:
* Loading, saving, and editing preset files (`Presets/*.json`).
* Displaying preset rules and settings.
* Displaying preset rules and settings in a tabbed JSON editor.
* Providing the preset selection list (`QListWidget`) including the "LLM Interpretation" option.
* **Refactored:** Exposes its selector (`selector_container`) and JSON editor (`json_editor_container`) as separate widgets for use by `MainWindow`.
* Emits `preset_selection_changed_signal` when the selection changes.
### `LogConsoleWidget` (`gui/log_console_widget.py`)
@@ -79,6 +133,15 @@ This widget displays application logs within the GUI:
* Integrates with Python's `logging` system via a custom `QtLogHandler`.
* Can be shown/hidden via the main window's "View" menu.
### `LLMEditorWidget` (`gui/llm_editor_widget.py`)
A new widget dedicated to editing LLM settings:
* Provides a tabbed interface ("Prompt Settings", "API Settings") to edit `config/llm_settings.json`.
* Allows editing the main prompt, managing examples (add/delete/edit JSON), and configuring API details (URL, key, model, temperature, timeout).
* Loads settings via `load_settings()` and saves them using `_save_settings()` (which calls `configuration.save_llm_config()`).
* Placed within `MainWindow`'s `QStackedWidget`.
### `UnifiedViewModel` (`gui/unified_view_model.py`)
The `UnifiedViewModel` implements a `QAbstractItemModel` for use with Qt's model-view architecture. It is specifically designed to:
@@ -136,16 +199,19 @@ An experimental predictor (inheriting from `BasePredictionHandler`) that uses a
* Takes an input source identifier, file list, and `Configuration` object.
* Interacts with the `LLMInteractionHandler` to send data to the LLM and receive predictions.
* Parses the LLM response to construct a `SourceRule` hierarchy.
* Emits the `prediction_signal` with the generated `SourceRule` object.
* **Parses the LLM's JSON response**: It expects a specific two-part JSON structure (see `12_LLM_Predictor_Integration.md`). It first sanitizes the response (removing comments/markdown) and then parses the JSON.
* **Constructs `SourceRule`**: It groups files based on the `proposed_asset_group_name` from the JSON, assigns the final `asset_type` using the `asset_group_classifications` map, and builds the complete `SourceRule` hierarchy.
* Emits the `prediction_signal` with the generated `SourceRule` object or `error_signal` on failure.
### `LLMInteractionHandler` (`gui/llm_interaction_handler.py`)
This class manages the specifics of communicating with the configured LLM API:
This class now acts as the central manager for LLM prediction tasks:
* Handles constructing prompts based on templates and input data.
* Sends requests to the LLM endpoint.
* Receives and potentially pre-processes the LLM's response before returning it to the `LLMPredictionHandler`.
* **Manages the LLM prediction queue** and processes items sequentially.
* **Loads LLM configuration** directly from `config/llm_settings.json` and `config/app_settings.json`.
* **Instantiates and manages** the `LLMPredictionHandler` and its `QThread`.
* **Handles LLM task state** (running/idle) and signals changes to the GUI.
* Receives results/errors from `LLMPredictionHandler` and **emits signals** (`llm_prediction_ready`, `llm_prediction_error`, `llm_status_update`, `llm_processing_state_changed`) to `MainWindow`.
## Utility Modules (`utils/`)
@@ -173,10 +239,10 @@ The `monitor.py` script implements the directory monitoring feature. It has been
* Loads the necessary `Configuration`.
* Calls `utils.prediction_utils.generate_source_rule_from_archive` to get the `SourceRule`.
* Calls `utils.workspace_utils.prepare_processing_workspace` to set up the workspace.
* Instantiates and runs the `ProcessingEngine`.
* Instantiates and runs the `ProcessingEngine` (which in turn uses the `PipelineOrchestrator`).
* Handles moving the source archive to 'processed' or 'error' directories.
* Cleans up the workspace.
## Summary
These key components, along with the refactored GUI structure and new utility modules, work together to provide the tool's functionality. The architecture emphasizes separation of concerns (configuration, rule generation, processing, UI), utilizes background processing for responsiveness (GUI prediction, Monitor tasks), and relies on the `SourceRule` object as the central data structure passed between different stages of the workflow.
These key components, along with the refactored GUI structure and new utility modules, work together to provide the tool's functionality. The architecture emphasizes separation of concerns (configuration, rule generation, processing, UI), utilizes background processing for responsiveness (GUI prediction, Monitor tasks), and relies on the `SourceRule` object as the central data structure passed between different stages of the workflow. The processing core is now a staged pipeline managed by the `PipelineOrchestrator`, enhancing modularity and maintainability.

View File

@@ -2,14 +2,144 @@
This document provides technical details about the configuration system and the structure of preset files for developers working on the Asset Processor Tool.
## Configuration Flow
## Configuration System Overview
The tool utilizes a two-tiered configuration system managed by the `configuration.py` module:
The tool's configuration is managed by the `configuration.py` module and loaded from several JSON files, providing a layered approach for defaults, user overrides, definitions, and source-specific presets.
1. **Application Settings (`config/app_settings.json`):** This JSON file defines the core global default settings, constants, and rules that apply generally across different asset sources. Examples include default output paths, standard image resolutions, map merge rules, output format rules, Blender executable paths, and default map types. It also centrally defines metadata for allowed asset and file types. Key sections include `FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`, and `MAP_MERGE_RULES`.
2. **Preset Files (`Presets/*.json`):** These JSON files define supplier-specific rules and overrides. They contain patterns (often regular expressions) to interpret filenames, classify map types, handle variants, define naming conventions, and specify other source-specific behaviors.
### Configuration Files
The `configuration.py` module is responsible for loading the base settings from `config/app_settings.json` (including loading and saving the JSON content), merging them with the rules from the selected preset file, and providing the base configuration via the `load_base_config()` function. Preset values generally override core settings where applicable. Note that the old `config.py` file has been deleted.
The tool's configuration is loaded from several JSON files, providing a layered approach for defaults, user overrides, definitions, and source-specific presets.
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 (e.g., the global `OUTPUT_DIRECTORY_PATTERN` and `OUTPUT_FILENAME_PATTERN`, standard image resolutions, map merge rules, output format rules, Blender paths, temporary directory prefix, initial scaling mode, merge dimension mismatch strategy). See the [User Guide: Output Structure](../01_User_Guide/09_Output_Structure.md#available-tokens) for a list of available tokens for these patterns.
* *Note:* `ASSET_TYPE_DEFINITIONS` and `FILE_TYPE_DEFINITIONS` are no longer stored here; they have been moved to dedicated files.
* It also includes settings for new features like the "Low-Resolution Fallback":
* `ENABLE_LOW_RESOLUTION_FALLBACK` (boolean): Enables or disables the generation of "LOWRES" variants for small source images. Defaults to `true`.
* `LOW_RESOLUTION_THRESHOLD` (integer): The pixel dimension threshold (largest side) below which a "LOWRES" variant is created if the feature is enabled. Defaults to `512`.
2. **User Settings (`config/user_settings.json`):** This optional JSON file allows users to override specific settings defined in `config/app_settings.json`. If this file exists, its values for corresponding keys will take precedence over the base application settings. This file is primarily managed through the GUI's Application Preferences Editor.
3. **Asset Type Definitions (`config/asset_type_definitions.json`):** This dedicated JSON file contains the definitions for different asset types (e.g., Surface, Model, Decal), including their descriptions, colors for UI representation, and example usage strings.
4. **File Type Definitions (`config/file_type_definitions.json`):** This dedicated JSON file contains the definitions for different file types (specifically texture maps and models), including descriptions, colors for UI representation, examples of keywords/patterns, a standard alias (`standard_type`), bit depth handling rules (`bit_depth_rule`), a grayscale flag (`is_grayscale`), and an optional GUI keybind (`keybind`).
* **`keybind` Property:** Each file type object within `FILE_TYPE_DEFINITIONS` can optionally include a `keybind` property. This property accepts a single character string (e.g., `"C"`, `"R"`) representing the keyboard key. In the GUI, this key (typically combined with `Ctrl`) is used as a shortcut to set or toggle the corresponding file type for selected items in the Preview Table.
*Example:*
```json
"MAP_COL": {
"description": "Color/Albedo Map",
"color": "#ffaa00",
"examples": ["_col.", "_basecolor.", "albedo", "diffuse"],
"standard_type": "COL",
"bit_depth_rule": "force_8bit",
"is_grayscale": false,
"keybind": "C"
},
```
Note: The `bit_depth_rule` property in `FILE_TYPE_DEFINITIONS` is the primary source for determining bit depth handling for a given map type.
5. **Supplier Settings (`config/suppliers.json`):** This JSON file stores settings specific to different asset suppliers. It is now structured as a dictionary where keys are supplier names and values are objects containing supplier-specific configurations.
* **Structure:**
```json
{
"SupplierName1": {
"setting_key1": "value",
"setting_key2": "value"
},
"SupplierName2": {
"setting_key1": "value"
}
}
```
* **`normal_map_type` Property:** A key setting within each supplier's object is `normal_map_type`, specifying whether normal maps from this supplier use "OpenGL" or "DirectX" conventions.
*Example:*
```json
{
"Poliigon": {
"normal_map_type": "DirectX"
},
"Dimensiva": {
"normal_map_type": "OpenGL"
}
}
```
6. **LLM Settings (`config/llm_settings.json`):** This JSON file contains settings specifically related to the LLM predictor, such as the API endpoint, model name, prompt template, and examples. These settings are managed through the GUI using the `LLMEditorWidget`.
7. **Preset Files (`Presets/*.json`):** These JSON files define source-specific rules and overrides. They contain patterns to interpret filenames, classify map types, handle variants, define naming conventions, and specify other source-specific behaviors. Preset settings override values from `app_settings.json` and `user_settings.json` where applicable.
### Configuration Loading and Access
The `configuration.py` module contains the `Configuration` class and standalone functions for loading and saving settings.
* **`Configuration` Class:** This is the primary class used by the processing engine and other core components. When initialized with a `preset_name`, it loads settings in the following order, with later files overriding earlier ones for shared keys:
1. `config/app_settings.json` (Base Defaults)
2. `config/user_settings.json` (User Overrides - if exists)
3. `config/asset_type_definitions.json` (Asset Type Definitions)
4. `config/file_type_definitions.json` (File Type Definitions)
5. `config/llm_settings.json` (LLM Settings)
6. `Presets/{preset_name}.json` (Preset Overrides)
The loaded settings are merged into internal dictionaries, and most are accessible via instance properties (e.g., `config.output_base_dir`, `config.llm_endpoint_url`, `config.get_asset_type_definitions()`). Regex patterns defined in the merged configuration are pre-compiled for performance.
* **`load_base_config()` function:** This standalone function is primarily used by the GUI for initial setup and displaying default/user-overridden settings before a specific preset is selected. It loads and merges the following files:
1. `config/app_settings.json`
2. `config/user_settings.json` (if exists)
3. `config/asset_type_definitions.json`
4. `config/file_type_definitions.json`
It returns a single dictionary containing the combined settings and definitions.
* **Saving Functions:**
* `save_base_config(settings_dict)`: Saves the provided dictionary to `config/app_settings.json`. (Used less frequently now for user-driven saves).
* `save_user_config(settings_dict)`: Saves the provided dictionary to `config/user_settings.json`. Used by `ConfigEditorDialog`.
* `save_llm_config(settings_dict)`: Saves the provided dictionary to `config/llm_settings.json`. Used by `LLMEditorWidget`.
## Supplier Management (`config/suppliers.json`)
A file, `config/suppliers.json`, is used to store a persistent list of known supplier names. This file is a simple JSON array of strings.
* **Purpose:** Provides a list of suggestions for the "Supplier" field in the GUI's Unified View, enabling auto-completion.
* **Management:** The GUI's `SupplierSearchDelegate` is responsible for loading this list on startup, adding new, unique supplier names entered by the user, and saving the updated list back to the file.
## GUI Configuration Editors
The GUI provides dedicated editors for modifying configuration files:
* **`ConfigEditorDialog` (`gui/config_editor_dialog.py`):** Edits user-configurable application settings.
* **`LLMEditorWidget` (`gui/llm_editor_widget.py`):** Edits the LLM-specific settings.
### `ConfigEditorDialog` (`gui/config_editor_dialog.py`)
The GUI includes a dedicated editor for modifying user-configurable settings. This is implemented in `gui/config_editor_dialog.py`.
* **Purpose:** Provides a user-friendly interface for viewing the effective application settings (defaults + user overrides + definitions) and editing the user-specific overrides.
* **Implementation:** The dialog loads the effective settings using `load_base_config()`. It presents relevant settings in a tabbed layout ("General", "Output & Naming", etc.). When saving, it now performs a **granular save**: it loads the current content of `config/user_settings.json`, identifies only the settings that were changed by the user during the current dialog session (by comparing against the initial state), updates only those specific values in the loaded `user_settings.json` content, and saves the modified content back to `config/user_settings.json` using `save_user_config()`. This preserves any other settings in `user_settings.json` that were not touched. The dialog displays definitions from `asset_type_definitions.json` and `file_type_definitions.json` but does not save changes to these files.
* **Limitations:** Currently, editing complex fields like `IMAGE_RESOLUTIONS` or the full details of `MAP_MERGE_RULES` via the UI is not fully supported for saving to `user_settings.json`.
### `LLMEditorWidget` (`gui/llm_editor_widget.py`)
* **Purpose:** Provides a user-friendly interface for viewing and editing the LLM settings defined in `config/llm_settings.json`.
* **Implementation:** Uses tabs for "Prompt Settings" and "API Settings". Allows editing the prompt, managing examples, and configuring API details. When saving, it also performs a **granular save**: it loads the current content of `config/llm_settings.json`, identifies only the settings changed by the user in the current session, updates only those values, and saves the modified content back to `config/llm_settings.json` using `configuration.save_llm_config()`.
## Preset File Structure (`Presets/*.json`)
Preset files are the primary way to adapt the tool to new asset sources. Developers should use `Presets/_template.json` as a starting point. Key fields include:
* `supplier_name`: The name of the asset source (e.g., `"Poliigon"`). Used for output directory naming.
* `map_type_mapping`: A list of dictionaries, each mapping source filename patterns/keywords to a specific file type. The `target_type` for this mapping **must** be a key from the `FILE_TYPE_DEFINITIONS` now located in `config/file_type_definitions.json`.
* `target_type`: The specific file type key from `FILE_TYPE_DEFINITIONS` (e.g., `"MAP_COL"`, `"MAP_NORM_GL"`, `"MAP_RGH"`). This replaces previous alias-based systems. The common aliases like "COL" or "NRM" are now derived from the `standard_type` property within `FILE_TYPE_DEFINITIONS` but are not used directly for `target_type`.
* `keywords`: A list of filename patterns (regex or fnmatch-style wildcards) used to identify this map type. The order of keywords within this list, and the order of dictionaries in the `map_type_mapping` list, determines the priority for assigning variant suffixes (`-1`, `-2`, etc.) when multiple files match the same `target_type`.
* `bit_depth_variants`: A dictionary mapping standard map types (e.g., `"NRM"`) to a pattern identifying its high bit-depth variant (e.g., `"*_NRM16*.tif"`). Files matching these patterns are prioritized over their standard counterparts.
* `map_bit_depth_rules`: Defines how to handle the bit depth of source maps. Can specify a default behavior (`"respect"` or `"force_8bit"`) and overrides for specific map types.
* `model_patterns`: A list of regex patterns to identify model files (e.g., `".*\\.fbx"`, `".*\\.obj"`).
* `move_to_extra_patterns`: A list of regex patterns for files that should be moved directly to the `Extra/` output subdirectory without further processing.
* `source_naming_convention`: Rules for extracting the base asset name and potentially the archetype from source filenames or directory structures (e.g., using separators and indices).
* `asset_category_rules`: Keywords or patterns used to determine the asset category (e.g., identifying `"Decal"` based on keywords).
* `archetype_rules`: Keywords or patterns used to determine the asset archetype (e.g., identifying `"Wood"` or `"Metal"`).
Careful definition of these patterns and rules, especially the regex in `map_type_mapping`, `bit_depth_variants`, `model_patterns`, and `move_to_extra_patterns`, is essential for correct asset processing.
**Note on Data Passing:** As mentioned in the Architecture documentation, major changes to the data passing mechanisms between the GUI, Main (CLI orchestration), and `AssetProcessor` modules are currently being planned. The descriptions of how configuration data is handled and passed within this document reflect the current state and will require review and updates once the plan for these changes is finalized.
## Supplier Management (`config/suppliers.json`)
@@ -24,31 +154,45 @@ The `Configuration` class is central to the new configuration system. It is resp
* **Initialization:** An instance is created with a specific `preset_name`.
* **Loading:**
* It first loads the base application settings from `config/app_settings.json`. This file now also contains the LLM-specific settings (`llm_endpoint_url`, `llm_api_key`, `llm_model_name`, `llm_temperature`, `llm_request_timeout`, `llm_predictor_prompt`, `llm_predictor_examples`).
* 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 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.
* **LLM Settings Access:** The `Configuration` class provides getter methods (e.g., `get_llm_endpoint_url()`, `get_llm_api_key()`, `get_llm_model_name()`, `get_llm_temperature()`, `get_llm_request_timeout()`, `get_llm_predictor_prompt()`, `get_llm_predictor_examples()`) to allow components like the `LLMPredictionHandler` to easily access the necessary LLM configuration values loaded from `app_settings.json`.
* It first loads the base application settings from `config/app_settings.json`.
* It then loads the LLM-specific settings from `config/llm_settings.json`.
* Finally, it loads the specified preset JSON file from the `Presets/` directory.
* **Merging & Access:** The base settings from `app_settings.json` are merged with the preset rules. LLM settings are stored separately. Most settings are accessible via instance properties (e.g., `config.llm_endpoint_url`). Preset values generally override the base settings where applicable. **Exception:** The `OUTPUT_DIRECTORY_PATTERN` and `OUTPUT_FILENAME_PATTERN` are loaded *only* from `app_settings.json` and are accessed via `config.output_directory_pattern` and `config.output_filename_pattern` respectively; they are not defined in or overridden by presets.
* **Validation (`_validate_configs`):** Performs basic structural validation on the loaded settings (base, LLM, and preset), checking for the presence of required keys and basic data types. Logs warnings for missing optional LLM keys.
* **Regex Compilation (`_compile_regex_patterns`):** Compiles regex patterns defined in the merged configuration (from base settings and the preset) for performance. Compiled regex objects are stored as instance attributes (e.g., `self.compiled_map_keyword_regex`).
* **LLM Settings Access:** The `Configuration` class provides direct property access (e.g., `config.llm_endpoint_url`, `config.llm_api_key`, `config.llm_model_name`, `config.llm_temperature`, `config.llm_request_timeout`, `config.llm_predictor_prompt`, `config.get_llm_examples()`) to allow components like the `LLMPredictionHandler` to easily access the necessary LLM configuration values loaded from `config/llm_settings.json`.
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.
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. The `LLMInteractionHandler` loads LLM settings directly using helper functions or file access, not the `Configuration` class.
## GUI Configuration Editor (`gui/config_editor_dialog.py`)
## GUI Configuration Editors
The GUI provides dedicated editors for modifying configuration files:
* **`ConfigEditorDialog` (`gui/config_editor_dialog.py`):** Edits the core `config/app_settings.json`.
* **`LLMEditorWidget` (`gui/llm_editor_widget.py`):** Edits the LLM-specific `config/llm_settings.json`.
### `ConfigEditorDialog` (`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 a tabbed layout ("General", "Output & Naming", etc.) using standard GUI widgets mapped to the JSON structure, and saves the changes back to the file. It supports editing basic fields, tables for definitions (`FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`), and a list/detail view for merge rules (`MAP_MERGE_RULES`). The definitions tables include dynamic color editing features.
* **Implementation:** The dialog loads the JSON content of `app_settings.json`, presents it in a tabbed layout ("General", "Output & Naming", etc.) using standard GUI widgets mapped to the JSON structure, and saves the changes back to the file. The "Output & Naming" tab specifically handles the global `OUTPUT_DIRECTORY_PATTERN` and `OUTPUT_FILENAME_PATTERN`. It supports editing basic fields, tables for definitions (`FILE_TYPE_DEFINITIONS`, `ASSET_TYPE_DEFINITIONS`), and a list/detail view for merge rules (`MAP_MERGE_RULES`). The definitions tables include dynamic color editing features.
* **Limitations:** Currently, editing complex fields like `IMAGE_RESOLUTIONS` or the full details of `MAP_MERGE_RULES` via the UI is not fully supported.
* **Note:** Changes made through the GUI editor are written directly to `config/app_settings.json` but require an application restart to be loaded and applied by the `Configuration` class.
* **Note:** Changes made through the `ConfigEditorDialog` are written directly to `config/app_settings.json` (using `save_base_config`) but require an application restart to be loaded and applied by the `Configuration` class during processing.
### `LLMEditorWidget` (`gui/llm_editor_widget.py`)
* **Purpose:** Provides a user-friendly interface for viewing and editing the LLM settings defined in `config/llm_settings.json`.
* **Implementation:** Uses tabs for "Prompt Settings" and "API Settings". Allows editing the prompt, managing examples, and configuring API details.
* **Persistence:** Saves changes directly to `config/llm_settings.json` using the `configuration.save_llm_config()` function. Changes are loaded by the `LLMInteractionHandler` the next time an LLM task is initiated.
## Preset File Structure (`Presets/*.json`)
Preset files are the primary way to adapt the tool to new asset sources. Developers should use `Presets/_template.json` as a starting point. Key fields include:
* `supplier_name`: The name of the asset source (e.g., `"Poliigon"`). Used for output directory naming.
* `map_type_mapping`: A list of dictionaries, each mapping source filename patterns/keywords to a standard internal map type (defined in `config.py`).
* `target_type`: The standard internal map type (e.g., `"COL"`, `"NRM"`).
* `map_type_mapping`: A list of dictionaries, each mapping source filename patterns/keywords to a specific file type. The `target_type` for this mapping **must** be a key from `FILE_TYPE_DEFINITIONS` located in `config/app_settings.json`.
* `target_type`: The specific file type key from `FILE_TYPE_DEFINITIONS` (e.g., `"MAP_COL"`, `"MAP_NORM_GL"`, `"MAP_RGH"`). This replaces previous alias-based systems. The common aliases like "COL" or "NRM" are now derived from the `standard_type` property within `FILE_TYPE_DEFINITIONS` but are not used directly for `target_type`.
* `keywords`: A list of filename patterns (regex or fnmatch-style wildcards) used to identify this map type. The order of keywords within this list, and the order of dictionaries in the `map_type_mapping` list, determines the priority for assigning variant suffixes (`-1`, `-2`, etc.) when multiple files match the same `target_type`.
* `bit_depth_variants`: A dictionary mapping standard map types (e.g., `"NRM"`) to a pattern identifying its high bit-depth variant (e.g., `"*_NRM16*.tif"`). Files matching these patterns are prioritized over their standard counterparts.
* `map_bit_depth_rules`: Defines how to handle the bit depth of source maps. Can specify a default behavior (`"respect"` or `"force_8bit"`) and overrides for specific map types.

View File

@@ -1,67 +1,115 @@
# Developer Guide: Processing Pipeline
Cl# Developer Guide: Processing Pipeline
This document details the step-by-step technical process executed by the `ProcessingEngine` class (`processing_engine.py`) when processing a single asset. A new instance of `ProcessingEngine` is created for each processing task to ensure state isolation.
This document details the step-by-step technical process executed by the asset processing pipeline, which is initiated by the [`ProcessingEngine`](processing_engine.py:73) class (`processing_engine.py`) and orchestrated by the [`PipelineOrchestrator`](processing/pipeline/orchestrator.py:36) (`processing/pipeline/orchestrator.py`).
The `ProcessingEngine.process()` method orchestrates the following pipeline based *solely* on the provided `SourceRule` object and the static `Configuration` object passed during engine initialization. It contains no internal prediction, classification, or fallback logic. All necessary overrides and static configuration values are accessed directly from these inputs.
The [`ProcessingEngine.process()`](processing_engine.py:131) method serves as the main entry point. It initializes a [`PipelineOrchestrator`](processing/pipeline/orchestrator.py:36) instance, providing it with the application's [`Configuration`](configuration.py:68) object and predefined lists of pre-item and post-item processing stages. The [`PipelineOrchestrator.process_source_rule()`](processing/pipeline/orchestrator.py:95) method then manages the execution of these stages for each asset defined in the input [`SourceRule`](rule_structure.py:40).
The pipeline steps are:
A crucial component in this architecture is the [`AssetProcessingContext`](processing/pipeline/asset_context.py:86) (`processing/pipeline/asset_context.py`). An instance of this dataclass is created for each [`AssetRule`](rule_structure.py:22) being processed. It acts as a stateful container, carrying all relevant data (source files, rules, configuration, intermediate results, metadata) and is passed sequentially through each stage. Each stage can read from and write to the context, allowing data to flow and be modified throughout the pipeline.
1. **Workspace Preparation (External)**:
* Before the `ProcessingEngine` is invoked, the calling code (e.g., `main.ProcessingTask`, `monitor._process_archive_task`) is responsible for setting up a temporary workspace.
* This typically involves using `utils.workspace_utils.prepare_processing_workspace`, which creates a temporary directory and extracts the input source (archive or folder) into it.
* The path to this prepared workspace is passed to the `ProcessingEngine` during initialization.
The pipeline execution for each asset follows this general flow:
2. **Prediction and Rule Generation (External)**:
* Also handled before the `ProcessingEngine` is invoked.
* Either the `RuleBasedPredictionHandler`, `LLMPredictionHandler` (triggered by the GUI), or `utils.prediction_utils.generate_source_rule_from_archive` (used by the Monitor) analyzes the input files and generates a `SourceRule` object.
* This `SourceRule` contains predicted classifications and initial overrides.
* If using the GUI, the user can modify these rules.
* The final `SourceRule` object is the primary input to the `ProcessingEngine.process()` method.
1. **Pre-Item Stages:** A sequence of stages executed once per asset before the core item processing loop. These stages typically perform initial setup, filtering, and asset-level transformations.
2. **Core Item Processing Loop:** The [`PipelineOrchestrator`](processing/pipeline/orchestrator.py:36) iterates through a list of "processing items" (individual files or merge tasks) prepared by a dedicated stage. For each item, a sequence of core processing stages is executed.
3. **Post-Item Stages:** A sequence of stages executed once per asset after the core item processing loop is complete. These stages handle final tasks like organizing output files and saving metadata.
3. **File Inventory (`_inventory_and_classify_files`)**:
* Scans the contents of the *already prepared* temporary workspace.
* This step primarily inventories the files present. The *classification* (determining `item_type`, etc.) is taken directly from the input `SourceRule`.
* Stores the file paths and their associated rules from the `SourceRule` in `self.classified_files`.
## Pipeline Stages
4. **Base Metadata Determination (`_determine_base_metadata`, `_determine_single_asset_metadata`)**:
* Determines the base asset name, category, and archetype using the explicit values provided in the input `SourceRule` and the static `Configuration`. Overrides (like `supplier_identifier`, `asset_type`, `asset_name_override`) are taken directly from the `SourceRule`.
The stages are executed in the following order for each asset:
5. **Skip Check**:
* If the `overwrite` flag is `False`, checks if the final output directory already exists and contains `metadata.json`.
* If so, processing for this asset is skipped.
### Pre-Item Stages
6. **Map Processing (`_process_maps`)**:
* Iterates through files classified as maps in the `SourceRule`.
* Loads images (`cv2.imread`).
* Handles Glossiness-to-Roughness inversion.
* Resizes images based on `Configuration`.
* Determines output bit depth and format based on `Configuration` and `SourceRule`.
* Converts data types and saves images (`cv2.imwrite`).
* The output filename uses the `standard_type` alias (e.g., `COL`, `NRM`) retrieved from the `Configuration.FILE_TYPE_DEFINITIONS` based on the file's effective `item_type`.
* Calculates image statistics.
* Stores processed map details.
These stages are executed sequentially once for each asset before the core item processing loop begins.
7. **Map Merging (`_merge_maps_from_source`)**:
* Iterates through `MAP_MERGE_RULES` in `Configuration`.
* Identifies required source maps by checking the `item_type_override` within the `SourceRule` (specifically in the `FileRule` for each file). Files with a base `item_type` of `"FILE_IGNORE"` are explicitly excluded from consideration.
* Loads source channels, handling missing inputs with defaults from `Configuration` or `SourceRule`.
* Merges channels (`cv2.merge`).
* Determines output format/bit depth and saves the merged map.
* Stores merged map details.
1. **[`SupplierDeterminationStage`](processing/pipeline/stages/supplier_determination.py:6)** (`processing/pipeline/stages/supplier_determination.py`):
* **Responsibility**: Determines the effective supplier for the asset based on the [`SourceRule`](rule_structure.py:40)'s `supplier_override`, `supplier_identifier`, and validation against configured suppliers.
* **Context Interaction**: Sets `context.effective_supplier` and may set a `supplier_error` flag in `context.status_flags`.
8. **Metadata File Generation (`_generate_metadata_file`)**:
* Collects asset metadata, processed/merged map details, ignored files list, etc., primarily from the `SourceRule` and internal processing results.
* Writes data to `metadata.json` in the temporary workspace.
2. **[`AssetSkipLogicStage`](processing/pipeline/stages/asset_skip_logic.py:5)** (`processing/pipeline/stages/asset_skip_logic.py`):
* **Responsibility**: Checks if the entire asset should be skipped based on conditions like a missing/invalid supplier, a "SKIP" status in asset metadata, or if the asset is already processed and overwrite is disabled.
* **Context Interaction**: Sets the `skip_asset` flag and `skip_reason` in `context.status_flags` if the asset should be skipped.
9. **Output Organization (`_organize_output_files`)**:
* Creates the final structured output directory (`<output_base_dir>/<supplier_name>/<asset_name>/`), using the supplier name from the `SourceRule`.
* Moves processed maps, merged maps, models, metadata, and other classified files from the temporary workspace to the final output directory.
3. **[`MetadataInitializationStage`](processing/pipeline/stages/metadata_initialization.py:81)** (`processing/pipeline/stages/metadata_initialization.py`):
* **Responsibility**: Initializes the `context.asset_metadata` dictionary with base information derived from the [`AssetRule`](rule_structure.py:22), [`SourceRule`](rule_structure.py:40), and [`Configuration`](configuration.py:68). This includes asset name, IDs, source/output paths, timestamps, and initial status.
* **Context Interaction**: Populates `context.asset_metadata`. Initializes `context.processed_maps_details` and `context.merged_maps_details` as empty dictionaries (these are used internally by subsequent stages but are not directly part of the final `metadata.json` in their original form).
10. **Workspace Cleanup (External)**:
* After the `ProcessingEngine.process()` method completes (successfully or with errors), the *calling code* is responsible for cleaning up the temporary workspace directory created in Step 1. This is often done in a `finally` block where `utils.workspace_utils.prepare_processing_workspace` was called.
4. **[`FileRuleFilterStage`](processing/pipeline/stages/file_rule_filter.py:10)** (`processing/pipeline/stages/file_rule_filter.py`):
* **Responsibility**: Filters the [`FileRule`](rule_structure.py:5) objects associated with the asset to determine which individual files should be considered for processing. It identifies and excludes files matching "FILE_IGNORE" rules based on their `item_type`.
* **Context Interaction**: Populates `context.files_to_process` with the list of [`FileRule`](rule_structure.py:5) objects that are not ignored.
11. **(Optional) Blender Script Execution (External)**:
* If triggered (e.g., via CLI arguments or GUI controls), the orchestrating code (e.g., `main.ProcessingTask`) executes the corresponding Blender scripts (`blenderscripts/*.py`) using `subprocess.run` *after* the `ProcessingEngine.process()` call completes successfully.
* *Note: Centralized logic for this was intended for `utils/blender_utils.py`, but this utility has not yet been implemented.* See `Developer Guide: Blender Integration Internals` for more details.
5. **[`GlossToRoughConversionStage`](processing/pipeline/stages/gloss_to_rough_conversion.py:15)** (`processing/pipeline/stages/gloss_to_rough_conversion.py`):
* **Responsibility**: Identifies processed maps in `context.processed_maps_details` whose `internal_map_type` starts with "MAP_GLOSS". If found, it loads the temporary image data, inverts it using the shared utility function [`apply_common_map_transformations`](processing/utils/image_processing_utils.py), saves a new temporary roughness map ("MAP_ROUGH"), and updates the corresponding details in `context.processed_maps_details` (setting `internal_map_type` to "MAP_ROUGH") and the relevant [`FileRule`](rule_structure.py:5) in `context.files_to_process` (setting `item_type` to "MAP_ROUGH").
* **Context Interaction**: Reads from and updates `context.processed_maps_details` (specifically `internal_map_type` and `temp_processed_file`) and `context.files_to_process` (specifically `item_type`).
This pipeline, executed by the `ProcessingEngine`, provides a clear and explicit processing flow based on the complete rule set provided by the GUI or other interfaces.
6. **[`AlphaExtractionToMaskStage`](processing/pipeline/stages/alpha_extraction_to_mask.py:16)** (`processing/pipeline/stages/alpha_extraction_to_mask.py`):
* **Responsibility**: If no mask map is explicitly defined for the asset (as a [`FileRule`](rule_structure.py:5) with `item_type="MAP_MASK"`), this stage searches `context.processed_maps_details` for a suitable source map (e.g., a "MAP_COL" with an alpha channel, based on its `internal_map_type`). If found, it extracts the alpha channel, saves it as a new temporary mask map, and adds a new [`FileRule`](rule_structure.py:5) (with `item_type="MAP_MASK"`) and corresponding details (with `internal_map_type="MAP_MASK"`) to the context.
* **Context Interaction**: Reads from `context.processed_maps_details`, adds a new [`FileRule`](rule_structure.py:5) to `context.files_to_process`, and adds a new entry to `context.processed_maps_details` (setting `internal_map_type`).
7. **[`NormalMapGreenChannelStage`](processing/pipeline/stages/normal_map_green_channel.py:14)** (`processing/pipeline/stages/normal_map_green_channel.py`):
* **Responsibility**: Identifies processed normal maps in `context.processed_maps_details` (those with an `internal_map_type` starting with "MAP_NRM"). If the global `invert_normal_map_green_channel_globally` configuration is true, it loads the temporary image data, inverts the green channel using the shared utility function [`apply_common_map_transformations`](processing/utils/image_processing_utils.py), saves a new temporary modified normal map, and updates the `temp_processed_file` path in `context.processed_maps_details`.
* **Context Interaction**: Reads from and updates `context.processed_maps_details` (specifically `temp_processed_file` and `notes`).
### Core Item Processing Loop
The [`PipelineOrchestrator`](processing/pipeline/orchestrator.py:36) iterates through the `context.processing_items` list (populated by the [`PrepareProcessingItemsStage`](processing/pipeline/stages/prepare_processing_items.py:10)). Each `item` in this list is now either a [`ProcessingItem`](rule_structure.py:0) (representing a specific variant of a source map, e.g., Color at 1K, or Color at LOWRES) or a [`MergeTaskDefinition`](processing/pipeline/asset_context.py:16).
1. **[`PrepareProcessingItemsStage`](processing/pipeline/stages/prepare_processing_items.py:10)** (`processing/pipeline/stages/prepare_processing_items.py`):
* **Responsibility**: (Executed once before the loop) This stage is now responsible for "exploding" each relevant [`FileRule`](rule_structure.py:5) into one or more [`ProcessingItem`](rule_structure.py:0) objects.
* For each [`FileRule`](rule_structure.py:5) that represents an image map:
* It loads the source image data and determines its original dimensions and bit depth.
* It creates standard [`ProcessingItem`](rule_structure.py:0)s for each required output resolution (e.g., "1K", "PREVIEW"), populating them with a copy of the source image data and the respective `resolution_key`.
* If the "Low-Resolution Fallback" feature is enabled (`ENABLE_LOW_RESOLUTION_FALLBACK` in config) and the source image's largest dimension is below `LOW_RESOLUTION_THRESHOLD`, it creates an additional [`ProcessingItem`](rule_structure.py:0) with `resolution_key="LOWRES"`, using the original image data and dimensions.
* It also adds [`MergeTaskDefinition`](processing/pipeline/asset_context.py:16)s derived from global `map_merge_rules`.
* **Context Interaction**: Reads `context.files_to_process` and `context.config_obj`. Populates `context.processing_items` with a list of [`ProcessingItem`](rule_structure.py:0) and [`MergeTaskDefinition`](processing/pipeline/asset_context.py:16) objects. Initializes `context.intermediate_results`.
For each `item` in `context.processing_items`:
2. **Transformations (Implicit or via a dedicated stage - formerly `RegularMapProcessorStage` logic):**
* **Responsibility**: If the `item` is a [`ProcessingItem`](rule_structure.py:0), its `image_data` (loaded by `PrepareProcessingItemsStage`) may need transformations (Gloss-to-Rough, Normal Green Invert). This logic, previously in `RegularMapProcessorStage`, might be integrated into `PrepareProcessingItemsStage` before `ProcessingItem` creation, or handled by a new dedicated transformation stage that operates on `ProcessingItem.image_data`. The `item.map_type_identifier` would be updated if a transformation like Gloss-to-Rough occurs.
* **Context Interaction**: Modifies `item.image_data` and `item.map_type_identifier` within the [`ProcessingItem`](rule_structure.py:0) object.
3. **[`MergedTaskProcessorStage`](processing/pipeline/stages/merged_task_processor.py:68)** (`processing/pipeline/stages/merged_task_processor.py`):
* **Responsibility**: (Executed if `item` is a [`MergeTaskDefinition`](processing/pipeline/asset_context.py:16)) Same as before: validates inputs, loads source map data (likely from `ProcessingItem`s in `context.processing_items` or a cache populated from them), applies transformations, merges channels, and returns [`ProcessedMergedMapData`](processing/pipeline/asset_context.py:35).
* **Context Interaction**: Reads [`MergeTaskDefinition`](processing/pipeline/asset_context.py:16), potentially `context.processing_items` (or a cache derived from it) for input image data. Returns [`ProcessedMergedMapData`](processing/pipeline/asset_context.py:35).
4. **[`InitialScalingStage`](processing/pipeline/stages/initial_scaling.py:14)** (`processing/pipeline/stages/initial_scaling.py`):
* **Responsibility**: (Executed per item)
* If `item` is a [`ProcessingItem`](rule_structure.py:0): Takes `item.image_data`, `item.current_dimensions`, and `item.resolution_key` as input. If `item.resolution_key` is "LOWRES", POT scaling is skipped. Otherwise, applies POT scaling if configured.
* If `item` is from a `MergeTaskDefinition` (i.e., `processed_data` from `MergedTaskProcessorStage`): Applies POT scaling as before.
* **Context Interaction**: Takes [`InitialScalingInput`](processing/pipeline/asset_context.py:46) (now including `resolution_key`). Returns [`InitialScalingOutput`](processing/pipeline/asset_context.py:54) (also including `resolution_key`), which updates `context.intermediate_results`. The `current_image_data` and `current_dimensions` for saving are taken from this output.
5. **[`SaveVariantsStage`](processing/pipeline/stages/save_variants.py:15)** (`processing/pipeline/stages/save_variants.py`):
* **Responsibility**: (Executed per item) Saves the (potentially scaled) `current_image_data`.
* **Context Interaction**:
* Takes [`SaveVariantsInput`](processing/pipeline/asset_context.py:61).
* `internal_map_type` is set from `item.map_type_identifier` (for `ProcessingItem`) or `processed_data.output_map_type` (for merged).
* `output_filename_pattern_tokens['resolution']` is set to the `resolution_key` obtained from `scaled_data_output.resolution_key` (which originates from `item.resolution_key` for `ProcessingItem`s, or is `None` for merged items that get all standard resolutions).
* `image_resolutions` argument for `SaveVariantsInput`:
* If `resolution_key == "LOWRES"`: Set to `{"LOWRES": width_of_lowres_data}`.
* If `resolution_key` is a standard key (e.g., "1K"): Set to `{resolution_key: configured_dimension}`.
* For merged items (where `resolution_key` from scaling is likely `None`): Set to the full `config.image_resolutions` map to generate all applicable standard sizes.
* Returns [`SaveVariantsOutput`](processing/pipeline/asset_context.py:79). Orchestrator stores details in `context.processed_maps_details`.
### Post-Item Stages
These stages are executed sequentially once for each asset after the core item processing loop has finished for all items.
1. **[`OutputOrganizationStage`](processing/pipeline/stages/output_organization.py:14)** (`processing/pipeline/stages/output_organization.py`):
* **Responsibility**: Determines the final output paths for all processed maps (including variants) and extra files based on configured patterns. It copies the temporary files generated by the core stages to these final destinations, creating directories as needed and respecting overwrite settings.
* **Context Interaction**: Reads from `context.processed_maps_details`, `context.files_to_process` (for 'EXTRA' files), `context.output_base_path`, and [`Configuration`](configuration.py:68). Updates entries in `context.processed_maps_details` with organization status. Populates `context.asset_metadata['maps']` with the final map structure:
* The `maps` object is a dictionary where keys are standard map types (e.g., "COL", "REFL").
* Each entry contains a `variant_paths` dictionary, where keys are resolution strings (e.g., "8K", "4K") and values are the filenames of the map variants (relative to the asset's output directory).
It also populates `context.asset_metadata['final_output_files']` with a list of absolute paths to all generated files (this list itself is not saved in the final `metadata.json`).
2. **[`MetadataFinalizationAndSaveStage`](processing/pipeline/stages/metadata_finalization_save.py:14)** (`processing/pipeline/stages/metadata_finalization_save.py`):
* **Responsibility**: Finalizes the `context.asset_metadata` (setting final status based on flags). It determines the save path for the metadata file based on configuration and patterns, serializes the `context.asset_metadata` (which now contains the structured `maps` data from `OutputOrganizationStage`) to JSON, and saves the `metadata.json` file.
* **Context Interaction**: Reads from `context.asset_metadata` (including the `maps` structure), `context.output_base_path`, and [`Configuration`](configuration.py:68). Before saving, it explicitly removes the `final_output_files` key from `context.asset_metadata`. The `processing_end_time` is also no longer added. The `metadata.json` file is written, and `context.asset_metadata` is updated with its final path and status. The older `processed_maps_details` and `merged_maps_details` from the context are not directly included in the JSON.
## External Steps
Certain steps are integral to the overall asset processing workflow but are handled outside the [`PipelineOrchestrator`](processing/pipeline/orchestrator.py:36)'s direct execution loop:
* **Workspace Preparation and Cleanup**: Handled by the code that invokes [`ProcessingEngine.process()`](processing_engine.py:131) (e.g., `main.ProcessingTask`, `monitor._process_archive_task`), typically involving extracting archives and setting up temporary directories. The engine itself manages a sub-temporary directory (`engine_temp_dir`) for intermediate processing files.
* **Prediction and Rule Generation**: Performed before the [`ProcessingEngine`](processing_engine.py:73) is called. This involves analyzing source files and generating the [`SourceRule`](rule_structure.py:40) object with its nested [`AssetRule`](rule_structure.py:22)s and [`FileRule`](rule_structure.py:5)s, often involving prediction logic (potentially using LLMs).
* **Optional Blender Script Execution**: Can be triggered externally after successful processing to perform tasks like material setup in Blender using the generated output files and metadata.
This staged pipeline provides a modular and extensible architecture for asset processing, with clear separation of concerns for each step. The [`AssetProcessingContext`](processing/pipeline/asset_context.py:86) ensures that data flows consistently between these stages.

View File

@@ -10,22 +10,35 @@ The GUI is built using `PySide6`, which provides Python bindings for the Qt fram
The `MainWindow` class acts as the central **coordinator** for the GUI application. It is responsible for:
* Setting up the main application window structure and menu bar.
* Instantiating and arranging the major GUI widgets:
* `MainPanelWidget` (`gui/main_panel_widget.py`): Contains the core controls, preset selection, and the rule editor.
* `PresetEditorWidget` (`gui/preset_editor_widget.py`): Handles preset loading, saving, and editing.
* Setting up the main application window structure and menu bar, including actions to launch configuration and definition editors.
* **Layout:** Arranging the main GUI components using a `QSplitter`.
* **Left Pane:** Contains the preset selection controls (from `PresetEditorWidget`) permanently displayed at the top. Below this, a `QStackedWidget` switches between the preset JSON editor (also from `PresetEditorWidget`) and the `LLMEditorWidget`.
* **Right Pane:** Contains the `MainPanelWidget`.
* Instantiating and managing the major GUI widgets:
* `PresetEditorWidget` (`gui/preset_editor_widget.py`): Provides the preset selector and the JSON editor parts.
* `LLMEditorWidget` (`gui/llm_editor_widget.py`): Provides the editor for LLM settings (from `config/llm_settings.json`).
* `MainPanelWidget` (`gui/main_panel_widget.py`): Contains the rule hierarchy view and processing controls.
* `LogConsoleWidget` (`gui/log_console_widget.py`): Displays application logs.
* Instantiating key models and handlers:
* `UnifiedViewModel` (`gui/unified_view_model.py`): The model for the rule hierarchy view.
* `LLMInteractionHandler` (`gui/llm_interaction_handler.py`): Manages communication with the LLM service.
* `AssetRestructureHandler` (`gui/asset_restructure_handler.py`): Handles rule restructuring.
* Connecting signals and slots between these components to orchestrate the application flow.
* **Editor Switching:** Handling the `preset_selection_changed_signal` from `PresetEditorWidget` in its `_on_preset_selection_changed` slot. This slot:
* Switches the `QStackedWidget` (`editor_stack`) to display either the `PresetEditorWidget`'s JSON editor or the `LLMEditorWidget` based on the selected mode ("preset", "llm", "placeholder").
* Calls `llm_editor_widget.load_settings()` when switching to LLM mode.
* Updates the window title.
* Triggers `update_preview()`.
* Handling top-level user interactions like drag-and-drop for loading sources (`add_input_paths`). This method now handles the "placeholder" state (no preset selected) by scanning directories or inspecting archives (ZIP) and creating placeholder `SourceRule`/`AssetRule`/`FileRule` objects to immediately populate the `UnifiedViewModel` with the file structure.
* Initiating predictions based on the selected preset mode (Rule-Based or LLM) when presets change or sources are added.
* Initiating predictions based on the selected preset mode (Rule-Based or LLM) when presets change or sources are added (`update_preview`).
* Starting the processing task (`_on_process_requested`): This slot now filters the `SourceRule` list obtained from the `UnifiedViewModel`, excluding sources where no asset has a `Target Asset` name assigned, before emitting the `start_backend_processing` signal. It also manages enabling/disabling controls.
* Managing the `QThreadPool` for running background prediction tasks (`RuleBasedPredictionHandler`, `LLMPredictionHandler`).
* Managing the background prediction threads (`RuleBasedPredictionHandler` via `QThread`, `LLMPredictionHandler` via `LLMInteractionHandler`).
* Implementing slots to handle results from background tasks:
* `_handle_prediction_completion(source_id, source_rule_list)`: Receives results from either prediction handler via the `prediction_signal`. It calls `self.unified_view_model.update_rules_for_sources()` to update the view model, preserving user overrides where possible. For LLM predictions, it also triggers processing the next item in the queue.
* Slots to handle status updates from the LLM handler.
* `_on_rule_hierarchy_ready`: Handles results from `RuleBasedPredictionHandler`.
* `_on_llm_prediction_ready_from_handler`: Handles results from `LLMInteractionHandler`.
* `_on_prediction_error`: Handles errors from both prediction paths.
* `_handle_prediction_completion`: Centralized logic to track completion and update UI state after each prediction result or error.
* Slots to handle status and state changes from `LLMInteractionHandler`.
## Threading and Background Tasks
@@ -53,7 +66,26 @@ Communication between the `MainWindow` (main UI thread) and the background predi
## Preset Editor (`gui/preset_editor_widget.py`)
The `PresetEditorWidget` provides a dedicated interface for managing presets. It handles loading, displaying, editing, and saving preset `.json` files. It communicates with the `MainWindow` (e.g., via signals) when a preset is loaded or saved.
The `PresetEditorWidget` provides a dedicated interface for managing presets. It handles loading, displaying, editing, and saving preset `.json` files.
* **Refactoring:** This widget has been refactored to expose its main components:
* `selector_container`: A `QWidget` containing the preset list (`QListWidget`) and New/Delete buttons. Used statically by `MainWindow`.
* `json_editor_container`: A `QWidget` containing the tabbed editor (`QTabWidget`) for preset JSON details and the Save/Save As buttons. Placed in `MainWindow`'s `QStackedWidget`.
* **Functionality:** Still manages the logic for populating the preset list, loading/saving presets, handling unsaved changes, and providing the editor UI for preset details.
* **Communication:** Emits `preset_selection_changed_signal(mode, preset_name)` when the user selects a preset, the LLM option, or the placeholder. This signal is crucial for `MainWindow` to switch the editor stack and trigger preview updates.
## LLM Settings Editor (`gui/llm_editor_widget.py`)
This new widget provides a dedicated interface for editing LLM-specific settings stored in `config/llm_settings.json`.
* **Purpose:** Allows users to configure the LLM predictor's behavior without directly editing the JSON file.
* **Structure:** Uses a `QTabWidget` with two tabs:
* **"Prompt Settings":** Contains a `QPlainTextEdit` for the main prompt and a nested `QTabWidget` for managing examples (add/delete/edit JSON in `QTextEdit` widgets).
* **"API Settings":** Contains fields (`QLineEdit`, `QDoubleSpinBox`, `QSpinBox`) for endpoint URL, API key, model name, temperature, and timeout.
* **Functionality:**
* `load_settings()`: Reads `config/llm_settings.json` and populates the UI fields. Handles file not found or JSON errors. Called by `MainWindow` when switching to LLM mode.
* `_save_settings()`: Gathers data from the UI, validates example JSON, constructs the settings dictionary, and calls `configuration.save_llm_config()` to write back to the file. Emits `settings_saved` signal on success.
* Manages unsaved changes state and enables/disables the "Save LLM Settings" button accordingly.
## Unified Hierarchical View
@@ -80,36 +112,44 @@ The core rule editing interface is built around a `QTreeView` managed within the
graph TD
subgraph MainWindow [MainWindow Coordinator]
direction LR
MW_Input[User Input (Drag/Drop, Preset Select)] --> MW(MainWindow);
MW -- Initiates --> PredPool{QThreadPool};
MW -- Connects Signals --> VM(UnifiedViewModel);
MW -- Connects Signals --> ARH(AssetRestructureHandler);
MW -- Owns/Manages --> MPW(MainPanelWidget);
MW -- Owns/Manages --> PEW(PresetEditorWidget);
MW -- Owns/Manages --> LCW(LogConsoleWidget);
MW_Input[User Input (Drag/Drop)] --> MW(MainWindow);
MW -- Owns/Manages --> Splitter(QSplitter);
MW -- Owns/Manages --> LLMIH(LLMInteractionHandler);
MW -- Owns/Manages --> ARH(AssetRestructureHandler);
MW -- Owns/Manages --> VM(UnifiedViewModel);
MW -- Owns/Manages --> LCW(LogConsoleWidget);
MW -- Initiates --> PredPool{Prediction Threads};
MW -- Connects Signals --> VM;
MW -- Connects Signals --> ARH;
MW -- Connects Signals --> LLMIH;
MW -- Connects Signals --> PEW(PresetEditorWidget);
MW -- Connects Signals --> LLMEDW(LLMEditorWidget);
end
subgraph MainPanel [MainPanelWidget]
subgraph LeftPane [Left Pane Widgets]
direction TB
MPW_UI[UI Controls (Load, Predict, Process Btns)];
Splitter -- Adds Widget --> LPW(Left Pane Container);
LPW -- Contains --> PEW_Sel(PresetEditorWidget - Selector);
LPW -- Contains --> Stack(QStackedWidget);
Stack -- Contains --> PEW_Edit(PresetEditorWidget - JSON Editor);
Stack -- Contains --> LLMEDW;
end
subgraph RightPane [Right Pane Widgets]
direction TB
Splitter -- Adds Widget --> MPW(MainPanelWidget);
MPW -- Contains --> TV(QTreeView - Rule View);
MPW_UI[UI Controls (Process Btn, etc)];
MPW_UI --> MPW;
MPW -- Contains --> REW(RuleEditorWidget);
end
subgraph RuleEditor [RuleEditorWidget]
direction TB
REW -- Contains --> TV(QTreeView - Rule View);
end
subgraph Prediction [Background Prediction]
direction TB
PredPool -- Runs --> RBP(RuleBasedPredictionHandler);
PredPool -- Runs --> LLMP(LLMPredictionHandler);
LLMP -- Uses --> LLMIH;
RBP -- prediction_signal --> MW;
LLMP -- prediction_signal --> MW;
LLMP -- status_signal --> MW;
LLMIH -- Manages/Starts --> LLMP;
RBP -- prediction_ready/error/status --> MW;
LLMIH -- llm_prediction_ready/error/status --> MW;
end
subgraph ModelView [Model/View Components]
@@ -126,17 +166,24 @@ graph TD
Del -- Get/Set Data --> VM;
end
%% MainWindow Interactions
MW_Input -- Triggers --> MW;
PEW -- preset_selection_changed_signal --> MW;
LLMEDW -- settings_saved --> MW;
MPW -- process_requested/etc --> MW;
MW -- _on_preset_selection_changed --> Stack;
MW -- _on_preset_selection_changed --> LLMEDW;
MW -- _handle_prediction_completion --> VM;
MW -- Triggers Processing --> ProcTask(main.ProcessingTask);
%% Connections between subgraphs
MPW --> MW;
PEW --> MW;
LCW --> MW;
PEW --> LPW; %% PresetEditorWidget parts are in Left Pane
LLMEDW --> Stack; %% LLMEditorWidget is in Stack
MPW --> Splitter; %% MainPanelWidget is in Right Pane
VM --> MW;
ARH --> MW;
LLMIH --> MW;
REW --> MPW;
LCW --> MW;
```
## Application Styling
@@ -151,13 +198,24 @@ The `LogConsoleWidget` displays logs captured by a custom `QtLogHandler` from Py
The GUI provides a "Cancel" button. Cancellation logic for the actual processing is now likely handled within the `main.ProcessingTask` or the code that manages it, as the `ProcessingHandler` has been removed. The GUI button would signal this external task manager.
## GUI Configuration Editor (`gui/config_editor_dialog.py`)
## Application Preferences Editor (`gui/config_editor_dialog.py`)
A dedicated dialog for editing `config/app_settings.json`.
A dedicated dialog for editing user-overridable application settings. It loads base settings from `config/app_settings.json` and saves user overrides to `config/user_settings.json`.
* **Functionality:** Loads `config/app_settings.json`, presents in tabs, allows editing basic fields, definitions tables (with color editing), and merge rules list/detail.
* **Limitations:** Editing complex fields like `IMAGE_RESOLUTIONS` or full `MAP_MERGE_RULES` details might still be limited.
* **Integration:** Launched by `MainWindow` ("Edit" -> "Preferences...").
* **Persistence:** Saves changes to `config/app_settings.json`. Requires application restart for changes to affect processing logic loaded by the `Configuration` class.
* **Functionality:** Provides a tabbed interface to edit various application settings, including general paths, output/naming patterns, image processing options (like resolutions and compression), and map merging rules. It no longer includes editors for Asset Type or File Type Definitions.
* **Integration:** Launched by `MainWindow` via the "Edit" -> "Preferences..." menu.
* **Persistence:** Saves changes to `config/user_settings.json`. Changes require an application restart to take effect in processing logic.
The refactored GUI separates concerns into distinct widgets and handlers, coordinated by the `MainWindow`. Background tasks use `QThreadPool` and `QRunnable`. The `UnifiedViewModel` focuses on data presentation and simple edits, delegating complex restructuring to the `AssetRestructureHandler`.
## Definitions Editor (`gui/definitions_editor_dialog.py`)
A new dedicated dialog for managing core application definitions that are separate from general user preferences.
* **Purpose:** Provides a structured UI for editing Asset Type Definitions, File Type Definitions, and Supplier Settings.
* **Structure:** Uses a `QTabWidget` with three tabs:
* **Asset Type Definitions:** Manages definitions from `config/asset_type_definitions.json`. Presents a list of asset types and allows editing their description, color, and examples.
* **File Type Definitions:** Manages definitions from `config/file_type_definitions.json`. Presents a list of file types and allows editing their description, color, examples, standard type, bit depth rule, grayscale status, and keybind.
* **Supplier Settings:** Manages settings from `config/suppliers.json`. Presents a list of suppliers and allows editing supplier-specific settings (e.g., Normal Map Type).
* **Integration:** Launched by `MainWindow` via the "Edit" -> "Edit Definitions..." menu.
* **Persistence:** Saves changes directly to the respective configuration files (`config/asset_type_definitions.json`, `config/file_type_definitions.json`, `config/suppliers.json`). Some changes may require an application restart.

View File

@@ -20,10 +20,10 @@ This document outlines the coding conventions and general practices followed wit
* Use Qt's signals and slots mechanism for communication between objects, especially across threads.
* Run long-running or blocking tasks in separate `QThread`s to keep the main UI thread responsive.
* Perform UI updates only from the main UI thread.
* **Configuration:** Core settings are managed in `config.py` (Python module). Supplier-specific rules are managed in JSON files (`Presets/`). The `Configuration` class handles loading and merging these.
* **Configuration:** Core application settings are defined in `config/app_settings.json`. Supplier-specific rules are managed in JSON files within the `Presets/` directory. The `Configuration` class (`configuration.py`) is responsible for loading `app_settings.json` and merging it with the selected preset file.
* **File Paths:** Use `pathlib.Path` objects for handling file system paths. Avoid using string manipulation for path joining or parsing.
* **Docstrings:** Write clear and concise docstrings for modules, classes, methods, and functions, explaining their purpose, arguments, and return values.
* **Comments:** Use comments to explain complex logic or non-obvious parts of the code.
* **Comments:** Use comments to explain complex logic or non-obvious parts of the code. Avoid obsolete comments (e.g., commented-out old code) and redundant comments (e.g., comments stating the obvious, like `# Import module` or `# Initialize variable`). The goal is to maintain clarity while minimizing unnecessary token usage for LLM tools.
* **Imports:** Organize imports at the top of the file, grouped by standard library, third-party libraries, and local modules.
* **Naming:**
* Use `snake_case` for function and variable names.
@@ -31,4 +31,51 @@ This document outlines the coding conventions and general practices followed wit
* Use `UPPER_CASE` for constants.
* Use a leading underscore (`_`) for internal or "protected" methods/attributes.
## Terminology and Data Standards
To ensure consistency and clarity across the codebase, particularly concerning asset and file classifications, the following standards must be adhered to. These primarily revolve around definitions stored in `config/app_settings.json`.
### `FILE_TYPE_DEFINITIONS`
`FILE_TYPE_DEFINITIONS` in `config/app_settings.json` is the **single source of truth** for all file type identifiers used within the application.
* **`FileRule.item_type` and `FileRule.item_type_override`**: When defining or interpreting `SourceRule` objects (and their constituent `FileRule` instances), the `item_type` and `item_type_override` attributes **must** always use a key directly from `FILE_TYPE_DEFINITIONS`.
* Example: `file_rule.item_type = "MAP_COL"` (for a color map) or `file_rule.item_type = "MODEL_FBX"` (for an FBX model).
* **`standard_type` Property**: Each entry in `FILE_TYPE_DEFINITIONS` includes a `standard_type` property. This provides a common, often abbreviated, alias for the file type.
* Example: `FILE_TYPE_DEFINITIONS["MAP_COL"]["standard_type"]` might be `"COL"`.
* Example: `FILE_TYPE_DEFINITIONS["MAP_NORM_GL"]["standard_type"]` might be `"NRM"`.
* **Removal of `STANDARD_MAP_TYPES`**: The global constant `STANDARD_MAP_TYPES` (previously in `config.py`) has been **removed**. Standard map type aliases (e.g., "COL", "NRM", "RGH") are now derived dynamically from the `standard_type` property of the relevant entry in `FILE_TYPE_DEFINITIONS`.
* **`map_type` Usage**:
* **Filename Tokens**: When used as a token in output filename patterns (e.g., `[maptype]`), `map_type` is typically derived from the `standard_type` of the file's effective `item_type`. It may also include a variant suffix if applicable (e.g., "COL", "COL_var01").
* **General Classification**: For precise classification within code logic or rules, developers should refer to the full `FILE_TYPE_DEFINITIONS` key (e.g., `"MAP_COL"`, `"MAP_METAL"`). The `standard_type` can be used for broader categorization or when a common alias is needed.
* **`Map_type_Mapping.target_type` in Presets**: Within preset files (e.g., `Presets/Poliigon.json`), the `map_type_mapping` rules found inside a `FileRule`'s `map_processing_options` now use keys from `FILE_TYPE_DEFINITIONS` for the `target_type` field.
* Example:
```json
// Inside a FileRule in a preset
"map_processing_options": {
"map_type_mapping": {
"source_type_pattern": ".*ambient occlusion.*",
"target_type": "MAP_AO", // Uses FILE_TYPE_DEFINITIONS key
"source_channels": "RGB"
}
}
```
This replaces old aliases like `"AO"` or `"OCC"`.
* **`is_grayscale` Property**: `FILE_TYPE_DEFINITIONS` entries can now include an `is_grayscale` boolean property. This flag indicates whether the file type is inherently grayscale (e.g., a roughness map). It can be used by the processing engine to inform decisions about channel handling, compression, or specific image operations.
* Example: `FILE_TYPE_DEFINITIONS["MAP_RGH"]["is_grayscale"]` might be `true`.
### `ASSET_TYPE_DEFINITIONS`
Similarly, `ASSET_TYPE_DEFINITIONS` in `config/app_settings.json` is the **single source of truth** for all asset type identifiers.
* **`AssetRule.asset_type`, `AssetRule.asset_type_override`, and `AssetRule.asset_category`**: When defining or interpreting `SourceRule` objects (and their constituent `AssetRule` instances), the `asset_type`, `asset_type_override`, and `asset_category` attributes **must** always use a key directly from `ASSET_TYPE_DEFINITIONS`.
* Example: `asset_rule.asset_type = "SURFACE_3D"` or `asset_rule.asset_category = "FABRIC"`.
Adherence to these definitions ensures that terminology remains consistent throughout the application, from configuration to core logic and output.
Adhering to these conventions will make the codebase more consistent, easier to understand, and more maintainable for all contributors.

View File

@@ -6,7 +6,7 @@ The LLM Predictor feature provides an alternative method for classifying asset t
## Configuration
The LLM Predictor is configured via new settings in the `config/app_settings.json` file. These settings control the behavior of the LLM interaction:
The LLM Predictor is configured via settings in the dedicated `config/llm_settings.json` file. These settings control the behavior of the LLM interaction:
- `llm_predictor_prompt`: The template for the prompt sent to the LLM. This prompt should guide the LLM to classify the asset based on its name and potentially other context. It can include placeholders that will be replaced with actual data during processing.
- `llm_endpoint_url`: The URL of the LLM API endpoint.
@@ -16,48 +16,108 @@ The LLM Predictor is configured via new settings in the `config/app_settings.jso
- `llm_request_timeout`: The maximum time (in seconds) to wait for a response from the LLM API.
- `llm_predictor_examples`: A list of example input/output pairs to include in the prompt for few-shot learning, helping the LLM understand the desired output format and classification logic.
The prompt structure is crucial for effective classification. It should clearly instruct the LLM on the task and the expected output format. Placeholders within the prompt template (e.g., `{asset_name}`) are dynamically replaced with relevant data before the request is sent.
**Editing:** These settings can be edited directly through the GUI using the **`LLMEditorWidget`** (`gui/llm_editor_widget.py`), which provides a user-friendly interface for modifying the prompt, examples, and API parameters. Changes are saved back to `config/llm_settings.json` via the `configuration.save_llm_config()` function.
## `LLMPredictionHandler`
**Loading:** The `LLMInteractionHandler` now loads these settings directly from `config/llm_settings.json` and relevant parts of `config/app_settings.json` when it needs to start an `LLMPredictionHandler` task. It no longer relies on the main `Configuration` class for LLM-specific settings. The prompt structure remains crucial for effective classification. Placeholders within the prompt template (e.g., `{FILE_LIST}`) are dynamically replaced with relevant data before the request is sent.
The `gui/llm_prediction_handler.py` module contains the `LLMPredictionHandler` class, which is responsible for interacting with the LLM API. It operates in a separate thread to avoid blocking the GUI during potentially long API calls.
## Expected LLM Output Format (Refactored)
Key methods:
The LLM is now expected to return a JSON object containing two distinct parts. This structure helps the LLM maintain context across multiple files belonging to the same conceptual asset and allows for a more robust grouping mechanism.
- `run()`: The main method executed when the thread starts. It processes prediction requests from a queue.
- `_prepare_prompt(asset_name)`: Constructs the final prompt string by loading the template from settings, including examples, and replacing placeholders like `{asset_name}`.
- `_call_llm(prompt)`: Sends the prepared prompt to the configured LLM API endpoint using the `requests` library and handles the HTTP communication.
- `_parse_llm_response(response)`: Parses the response received from the LLM API to extract the predicted classification.
**Rationale:** The previous implicit format made it difficult for the LLM to consistently group related files (e.g., different texture maps for the same material) under a single asset, especially in complex archives. The new two-part structure explicitly separates file-level analysis from asset-level classification, improving accuracy and consistency.
Signals:
**Structure:**
- `prediction_ready(asset_name, prediction_result)`: Emitted when a prediction is successfully received and parsed for a given asset.
- `prediction_error(asset_name, error_message)`: Emitted if an error occurs during the prediction process (e.g., API call failure, parsing error).
```json
{
"individual_file_analysis": [
{
"relative_file_path": "Textures/Wood_Floor_01/Wood_Floor_01_BaseColor.png",
"classified_file_type": "BaseColor",
"proposed_asset_group_name": "Wood_Floor_01"
},
{
"relative_file_path": "Textures/Wood_Floor_01/Wood_Floor_01_Roughness.png",
"classified_file_type": "Roughness",
"proposed_asset_group_name": "Wood_Floor_01"
},
{
"relative_file_path": "Textures/Metal_Plate_03/Metal_Plate_03_Metallic.jpg",
"classified_file_type": "Metallic",
"proposed_asset_group_name": "Metal_Plate_03"
}
],
"asset_group_classifications": {
"Wood_Floor_01": "PBR Material",
"Metal_Plate_03": "PBR Material"
}
}
```
The handler uses the `requests` library to make HTTP POST requests to the LLM endpoint, including the API key in the headers for authentication.
- **`individual_file_analysis`**: A list where each object represents a single file within the source.
- `relative_file_path`: The path of the file relative to the source root.
- `classified_file_type`: The LLM's prediction for the *type* of this specific file (e.g., "BaseColor", "Normal", "Model"). This corresponds to the `item_type` in the `FileRule`.
- `proposed_asset_group_name`: A name suggested by the LLM to group this file with others belonging to the same conceptual asset. This is used internally by the parser.
- **`asset_group_classifications`**: A dictionary mapping the `proposed_asset_group_name` values from the list above to a final `asset_type` (e.g., "PBR Material", "HDR Environment").
## `LLMInteractionHandler` (Refactored)
The `gui/llm_interaction_handler.py` module contains the `LLMInteractionHandler` class, which now acts as the central manager for LLM prediction tasks.
Key Responsibilities & Methods:
- **Queue Management:** Maintains a queue (`llm_processing_queue`) of pending prediction requests (input path, file list). Handles adding single (`queue_llm_request`) or batch (`queue_llm_requests_batch`) requests.
- **State Management:** Tracks whether an LLM task is currently running (`_is_processing`) and emits `llm_processing_state_changed(bool)` to update the GUI (e.g., disable preset editor). Includes `force_reset_state()` for recovery.
- **Task Orchestration:** Processes the queue sequentially (`_process_next_llm_item`). For each item:
* Loads required settings directly from `config/llm_settings.json` and `config/app_settings.json`.
* Instantiates an `LLMPredictionHandler` in a new `QThread`.
* Passes the loaded settings dictionary to the `LLMPredictionHandler`.
* Connects signals from the handler (`prediction_ready`, `prediction_error`, `status_update`) to internal slots (`_handle_llm_result`, `_handle_llm_error`) or directly re-emits them (`llm_status_update`).
* Starts the thread.
- **Result/Error Handling:** Internal slots (`_handle_llm_result`, `_handle_llm_error`) receive results/errors from the `LLMPredictionHandler`, remove the completed/failed item from the queue, emit the corresponding public signal (`llm_prediction_ready`, `llm_prediction_error`), and trigger processing of the next queue item.
- **Communication:** Emits signals to `MainWindow`:
* `llm_prediction_ready(input_path, source_rule_list)`
* `llm_prediction_error(input_path, error_message)`
* `llm_status_update(status_message)`
* `llm_processing_state_changed(is_processing)`
## `LLMPredictionHandler` (Refactored)
The `gui/llm_prediction_handler.py` module contains the `LLMPredictionHandler` class (inheriting from `BasePredictionHandler`), which performs the actual LLM prediction for a *single* input source. It runs in a background thread managed by the `LLMInteractionHandler`.
Key Responsibilities & Methods:
- **Initialization**: Takes the source identifier, file list, and a **`settings` dictionary** (passed from `LLMInteractionHandler`) containing all necessary configuration (LLM endpoint, prompt, examples, API details, type definitions, etc.).
- **`_perform_prediction()`**: Implements the core prediction logic:
* **Prompt Preparation (`_prepare_prompt`)**: Uses the passed `settings` dictionary to access the prompt template, type definitions, and examples to build the final prompt string.
* **API Call (`_call_llm`)**: Uses the passed `settings` dictionary to get the endpoint URL, API key, model name, temperature, and timeout to make the API request.
* **Parsing (`_parse_llm_response`)**: Parses the LLM's JSON response (using type definitions from the `settings` dictionary for validation) and constructs the `SourceRule` hierarchy based on the two-part format (`individual_file_analysis`, `asset_group_classifications`). Includes sanitization logic for comments and markdown fences.
- **Signals (Inherited):** Emits `prediction_ready(input_path, source_rule_list)` or `prediction_error(input_path, error_message)` upon completion or failure, which are connected to the `LLMInteractionHandler`. Also emits `status_update(message)`.
## GUI Integration
The `gui/main_window.py` module integrates the LLM Predictor feature into the main application window.
- The LLM predictor mode is selected via the preset dropdown in `PresetEditorWidget`.
- Selecting "LLM Interpretation" triggers `MainWindow._on_preset_selection_changed`, which switches the editor view to the `LLMEditorWidget` and calls `update_preview`.
- `MainWindow.update_preview` (or `add_input_paths`) delegates the LLM prediction request(s) to the `LLMInteractionHandler`'s queue.
- `LLMInteractionHandler` manages the background tasks and signals results/errors/status back to `MainWindow`.
- `MainWindow` slots (`_on_llm_prediction_ready_from_handler`, `_on_prediction_error`, `show_status_message`, `_on_llm_processing_state_changed`) handle these signals to update the `UnifiedViewModel` and the UI state (status bar, progress, button enablement).
- The `LLMEditorWidget` allows users to modify settings, saving them via `configuration.save_llm_config()`. `MainWindow` listens for the `settings_saved` signal to provide user feedback.
Integration points:
## Model Integration (Refactored)
- **Preset Dropdown Option:** A new option is added to the preset dropdown to enable LLM prediction as the classification method.
- **Re-interpret Button:** The "Re-interpret" button's functionality is extended to trigger LLM prediction when the LLM method is selected.
- `llm_processing_queue`: A queue (`Queue` object) is used to hold asset names that require LLM prediction. The `LLMPredictionHandler` thread consumes items from this queue.
- `_start_llm_prediction(asset_name)`: A method to add an asset name to the `llm_processing_queue` and ensure the `LLMPredictionHandler` thread is running.
- `_process_next_llm_item()`: A slot connected to the `prediction_ready` and `prediction_error` signals. It processes the results received from the `LLMPredictionHandler` and updates the GUI accordingly.
- **Signal Handling:** Connections are established between the `LLMPredictionHandler`'s signals (`prediction_ready`, `prediction_error`) and slots in `main_window.py` to handle prediction results and errors asynchronously.
The `gui/unified_view_model.py` module's `update_rules_for_sources` method still incorporates the results.
## Model Integration
- When the `prediction_signal` is received from `LLMPredictionHandler`, the accompanying `SourceRule` object (which has already been constructed based on the new two-part JSON parsing logic) is passed to `update_rules_for_sources`.
- This method then merges the new `SourceRule` hierarchy into the existing model data, preserving user overrides where applicable. The internal structure of the received `SourceRule` now directly reflects the groupings and classifications determined by the LLM and the new parser.
The `gui/unified_view_model.py` module, specifically the `update_rules_for_sources` method, is responsible for incorporating the prediction results into the application's data model. When a prediction is received via the `prediction_ready` signal, the `update_rules_for_sources` method is called to update the classification rules for the corresponding asset source based on the LLM's output.
## Error Handling (Updated)
## Error Handling
Error handling is distributed:
Error handling for the LLM Predictor includes:
- **Configuration Loading:** `LLMInteractionHandler` handles errors loading `llm_settings.json` or `app_settings.json` before starting a task.
- **LLM API Errors:** Handled within `LLMPredictionHandler._call_llm` (e.g., `requests.exceptions.RequestException`, `HTTPError`) and propagated via the `prediction_error` signal.
- **Sanitization/Parsing Errors:** `LLMPredictionHandler._parse_llm_response` catches errors during comment/markdown removal and `json.loads()`.
- **Structure/Validation Errors:** `LLMPredictionHandler._parse_llm_response` includes explicit checks for the required two-part JSON structure and data consistency.
- **Task Management Errors:** `LLMInteractionHandler` handles errors during thread setup/start.
- **LLM API Errors:** The `_call_llm` method in `LLMPredictionHandler` catches exceptions during the HTTP request and emits the `prediction_error` signal with a relevant error message.
- **Parsing Errors:** The `_parse_llm_response` method handles potential errors during the parsing of the LLM's response, emitting `prediction_error` if the response format is unexpected or invalid.
These errors are then handled in `main_window.py` by the slot connected to the `prediction_error` signal, typically by displaying an error message to the user.
All errors ultimately result in the `llm_prediction_error` signal being emitted by `LLMInteractionHandler`, allowing `MainWindow` to inform the user via the status bar and handle the completion state.

View File

@@ -1,55 +0,0 @@
# Developer Guide: LLM Integration Progress
This document summarizes the goals, approach, and current progress on integrating Large Language Model (LLM) capabilities into the Asset Processor Tool for handling irregularly named asset inputs.
## 1. Initial Goal
The primary goal is to enhance the Asset Processor Tool's ability to process asset sources with irregular or non-standard naming conventions that cannot be reliably handled by the existing regex and keyword-based preset system. This involves leveraging an LLM to interpret lists of filenames and determine asset metadata and file classifications.
## 2. Agreed Approach
After initial discussion and exploring several options, the agreed approach for developing this feature is as follows:
* **Dedicated LLM Preset:** The LLM classification logic will be triggered by selecting a specific preset type (or flag) in the main tool, indicating that standard rule-based processing should be bypassed in favor of the LLM.
* **Standalone Prototype:** The core LLM interaction and classification logic is being developed as a standalone Python prototype within the `llm_prototype/` directory. This allows for focused development, testing, and refinement in isolation before integration into the main application.
* **Configurable LLM Endpoint:** The prototype is designed to allow users to configure the LLM API endpoint, supporting various providers including local LLMs (e.g., via LM Studio) and commercial APIs. API keys are handled via environment variables for security.
* **Multi-Asset Handling:** The prototype is being built to handle input sources that contain multiple distinct assets within a single directory or archive. The LLM is expected to identify these separate assets and return a JSON **list**, where each item in the list represents one asset.
* **Chain of Thought (CoT) Prompting:** To improve the LLM's ability to handle the complex task of identifying multiple assets and classifying files, the prompt includes instructions for the LLM to output its reasoning process within <thinking> tags before generating the final JSON list.
* **Unified Asset Category:** The asset classification uses a single `asset_category` field with defined valid values: `Model`, `Surface`, `Decal`, `ATLAS`, `Imperfection`.
* **Robust JSON Extraction & Validation:** The prototype includes logic to extract the JSON list from the LLM's response (handling potential extra text) and validate its structure and content against expected schemas and values.
## 3. Prototype Development Progress
The initial structure for the standalone prototype has been created in the `llm_prototype/` directory:
* `llm_prototype/PLAN.md`: This document outlines the detailed plan.
* `llm_prototype/config_llm.py`: Configuration file for LLM settings, expected values, and placeholders.
* `llm_prototype/llm_classifier.py`: Main script containing the core logic (loading config/input/prompt, formatting prompt, calling LLM API, extracting/validating JSON).
* `llm_prototype/requirements_llm.txt`: Lists the `requests` library dependency.
* `llm_prototype/prompt_template.txt`: Contains the Chain of Thought prompt template with placeholders and few-shot examples.
* `llm_prototype/README.md`: Provides setup and running instructions for the prototype.
* `llm_prototype/test_inputs/`: Contains example input JSON files (`dinesen_example.json`, `imperfections_example.json`) representing file lists from asset sources.
Code has been added to `llm_classifier.py` for loading inputs/config/prompt, formatting the prompt, calling the API, and extracting/validating the JSON response. The JSON extraction logic has been made more robust to handle potential variations in LLM output format.
## 4. Current Status and Challenges
Initial testing of the prototype revealed the following:
* Successful communication with the configured LLM API endpoint.
* The LLM is attempting to follow the Chain of Thought structure and generate the list-based JSON output.
* **Challenge:** The LLM is currently failing to consistently produce complete and valid JSON output, leading to JSON decoding errors in the prototype script.
* **Challenge:** The LLM is not strictly adhering to the specified classification values (e.g., returning "Map" instead of "PBRMap"), despite the prompt explicitly listing the allowed values and including few-shot examples.
To address these challenges, few-shot examples demonstrating the expected JSON structure and exact classification values were added to the `prompt_template.txt`. The JSON extraction logic in `llm_classifier.py` was also updated to be more resilient.
## 5. Next Steps
The immediate next steps are focused on debugging and improving the LLM's output reliability:
1. Continue testing the prototype with the updated `prompt_template.txt` (including examples) using the example input files.
2. Analyze the terminal output to determine if the few-shot examples and improved extraction logic have resolved the JSON completeness and classification value issues.
3. Based on the results, iterate on the prompt template (e.g., further emphasizing strict adherence to output format and values) and/or the JSON extraction/validation logic in `llm_classifier.py` as needed.
4. Repeat testing and iteration until the prototype reliably produces valid JSON output with correct classifications for the test cases.
Once the prototype demonstrates reliable classification, we can proceed to evaluate its performance and plan the integration into the main Asset Processor Tool.

View File

@@ -1,5 +1,5 @@
{
"preset_name": "Dinesen Custom",
"preset_name": "Dinesen",
"supplier_name": "Dinesen",
"notes": "Preset for standard Poliigon downloads. Prioritizes _xxx16 files. Moves previews etc. to Extra/. Assumes Metal/Rough workflow.",
"source_naming": {
@@ -10,11 +10,7 @@
},
"glossiness_keywords": [
"GLOSS"
],
"bit_depth_variants": {
"NRM": "*_NRM16*",
"DISP": "*_DISP16*"
}
]
},
"move_to_extra_patterns": [
"*_Preview*",
@@ -25,11 +21,12 @@
"*.pdf",
"*.url",
"*.htm*",
"*_Fabric.*"
"*_Fabric.*",
"*_DISP_*METALNESS*"
],
"map_type_mapping": [
{
"target_type": "COL",
"target_type": "MAP_COL",
"keywords": [
"COLOR*",
"COL",
@@ -40,45 +37,58 @@
]
},
{
"target_type": "NRM",
"target_type": "MAP_NRM",
"keywords": [
"NORMAL*",
"NORM*",
"NRM*",
"N"
],
"priority_keywords": [
"*_NRM16*",
"*_NM16*",
"*Normal16*"
]
},
{
"target_type": "ROUGH",
"target_type": "MAP_ROUGH",
"keywords": [
"ROUGHNESS",
"ROUGH"
]
},
{
"target_type": "ROUGH",
"target_type": "MAP_GLOSS",
"keywords": [
"GLOSS"
]
},
{
"target_type": "AO",
"target_type": "MAP_AO",
"keywords": [
"AMBIENTOCCLUSION",
"AO"
]
},
{
"target_type": "DISP",
"target_type": "MAP_DISP",
"keywords": [
"DISPLACEMENT",
"DISP",
"HEIGHT",
"BUMP"
],
"priority_keywords": [
"*_DISP16*",
"*_DSP16*",
"*DSP16*",
"*DISP16*",
"*Displacement16*",
"*Height16*"
]
},
{
"target_type": "REFL",
"target_type": "MAP_REFL",
"keywords": [
"REFLECTION",
"REFL",
@@ -87,26 +97,26 @@
]
},
{
"target_type": "SSS",
"target_type": "MAP_SSS",
"keywords": [
"SSS",
"SUBSURFACE*"
]
},
{
"target_type": "FUZZ",
"target_type": "MAP_FUZZ",
"keywords": [
"FUZZ"
]
},
{
"target_type": "IDMAP",
"target_type": "MAP_IDMAP",
"keywords": [
"IDMAP"
]
},
{
"target_type": "MASK",
"target_type": "MAP_MASK",
"keywords": [
"OPAC*",
"TRANSP*",
@@ -115,7 +125,7 @@
]
},
{
"target_type": "METAL",
"target_type": "MAP_METAL",
"keywords": [
"METAL*",
"METALLIC"

View File

@@ -10,11 +10,7 @@
},
"glossiness_keywords": [
"GLOSS"
],
"bit_depth_variants": {
"NRM": "*_NRM16*",
"DISP": "*_DISP16*"
}
]
},
"move_to_extra_patterns": [
"*_Preview*",
@@ -25,59 +21,74 @@
"*.pdf",
"*.url",
"*.htm*",
"*_Fabric.*"
"*_Fabric.*",
"*_Albedo*"
],
"map_type_mapping": [
{
"target_type": "COL",
"target_type": "MAP_COL",
"keywords": [
"COLOR*",
"COL",
"COL-*",
"DIFFUSE",
"DIF",
"ALBEDO"
]
},
{
"target_type": "NRM",
"target_type": "MAP_NRM",
"keywords": [
"NORMAL*",
"NORM*",
"NRM*"
"NRM*",
"N"
],
"priority_keywords": [
"*_NRM16*",
"*_NM16*",
"*Normal16*"
]
},
{
"target_type": "ROUGH",
"target_type": "MAP_ROUGH",
"keywords": [
"ROUGHNESS",
"ROUGH"
]
},
{
"target_type": "ROUGH",
"target_type": "MAP_GLOSS",
"keywords": [
"GLOSS"
],
"is_gloss_source": true
]
},
{
"target_type": "AO",
"target_type": "MAP_AO",
"keywords": [
"AMBIENTOCCLUSION",
"AO"
]
},
{
"target_type": "DISP",
"target_type": "MAP_DISP",
"keywords": [
"DISPLACEMENT",
"DISP",
"HEIGHT",
"BUMP"
],
"priority_keywords": [
"*_DISP16*",
"*_DSP16*",
"*DSP16*",
"*DISP16*",
"*Displacement16*",
"*Height16*"
]
},
{
"target_type": "REFL",
"target_type": "MAP_REFL",
"keywords": [
"REFLECTION",
"REFL",
@@ -86,26 +97,26 @@
]
},
{
"target_type": "SSS",
"target_type": "MAP_SSS",
"keywords": [
"SSS",
"SUBSURFACE*"
]
},
{
"target_type": "FUZZ",
"target_type": "MAP_FUZZ",
"keywords": [
"FUZZ"
]
},
{
"target_type": "IDMAP",
"target_type": "MAP_IDMAP",
"keywords": [
"IDMAP"
]
},
{
"target_type": "MASK",
"target_type": "MAP_MASK",
"keywords": [
"OPAC*",
"TRANSP*",
@@ -114,7 +125,7 @@
]
},
{
"target_type": "METAL",
"target_type": "MAP_METAL",
"keywords": [
"METAL*",
"METALLIC"

View File

@@ -29,7 +29,7 @@
],
"map_type_mapping": [
{
"target_type": "COL",
"target_type": "MAP_COL",
"keywords": [
"COLOR*",
"COL",
@@ -39,7 +39,7 @@
]
},
{
"target_type": "NRM",
"target_type": "MAP_NRM",
"keywords": [
"NORMAL*",
"NORM*",
@@ -48,27 +48,27 @@
]
},
{
"target_type": "ROUGH",
"target_type": "MAP_ROUGH",
"keywords": [
"ROUGHNESS",
"ROUGH"
]
},
{
"target_type": "ROUGH",
"target_type": "MAP_ROUGH",
"keywords": [
"GLOSS"
]
},
{
"target_type": "AO",
"target_type": "MAP_AO",
"keywords": [
"AMBIENTOCCLUSION",
"AO"
]
},
{
"target_type": "DISP",
"target_type": "MAP_DISP",
"keywords": [
"DISPLACEMENT",
"DISP",
@@ -77,7 +77,7 @@
]
},
{
"target_type": "REFL",
"target_type": "MAP_REFL",
"keywords": [
"REFLECTION",
"REFL",
@@ -86,27 +86,27 @@
]
},
{
"target_type": "SSS",
"target_type": "MAP_SSS",
"keywords": [
"SSS",
"SUBSURFACE*"
]
},
{
"target_type": "FUZZ",
"target_type": "MAP_FUZZ",
"keywords": [
"FUZZ"
]
},
{
"target_type": "IDMAP",
"target_type": "MAP_IDMAP",
"keywords": [
"ID*",
"IDMAP"
]
},
{
"target_type": "MASK",
"target_type": "MAP_MASK",
"keywords": [
"OPAC*",
"TRANS*",
@@ -115,7 +115,7 @@
]
},
{
"target_type": "METAL",
"target_type": "MAP_METAL",
"keywords": [
"METALNESS_",
"METALLIC"

View File

@@ -1,106 +0,0 @@
# DRAFT README Enhancements - Architecture Section & Refinements
**(Note: This is a draft. Integrate the "Architecture" section and the refinements into the main `readme.md` file.)**
---
## Refinements to Existing Sections
**(Suggest adding these points or similar wording to the relevant existing sections)**
* **In Features:**
* Add: **Responsive GUI:** Utilizes background threads for processing and file preview generation, ensuring the user interface remains responsive.
* Add: **Optimized Classification:** Pre-compiles regular expressions from presets for faster file identification during classification.
* **In Directory Structure:**
* Update Core Logic bullet: `* **Core Logic:** main.py, monitor.py, asset_processor.py, configuration.py, config.py` (explicitly add `configuration.py`).
---
## Architecture
**(Suggest adding this new section, perhaps after "Features" or "Directory Structure")**
This section provides a higher-level overview of the tool's internal structure and design, intended for developers or users interested in the technical implementation.
### Core Components
The tool is primarily built around several key Python modules:
* **`config.py`**: Defines core, global settings (output paths, resolutions, default behaviors, format rules, etc.) that are generally not supplier-specific.
* **`Presets/*.json`**: Supplier-specific JSON files defining rules for interpreting source assets (filename patterns, map type keywords, model identification, etc.).
* **`configuration.py` (`Configuration` class)**: Responsible for loading the core `config.py` settings and merging them with a selected preset JSON file. Crucially, it also **pre-compiles** regular expression patterns defined in the preset (e.g., for map keywords, extra files, 16-bit variants) upon initialization. This pre-compilation significantly speeds up the file classification process.
* **`asset_processor.py` (`AssetProcessor` class)**: Contains the core logic for processing a *single* asset. It orchestrates the pipeline steps: workspace setup, extraction, file classification, metadata determination, map processing, channel merging, metadata file generation, and output organization.
* **`main.py`**: Serves as the entry point for the Command-Line Interface (CLI). It handles argument parsing, sets up logging, manages the parallel processing pool, and calls `AssetProcessor` for each input asset via a wrapper function.
* **`gui/`**: Contains modules related to the Graphical User Interface (GUI), built using PySide6.
* **`monitor.py`**: Implements the directory monitoring functionality for automated processing.
### Parallel Processing (CLI & GUI)
To accelerate the processing of multiple assets, the tool utilizes Python's `concurrent.futures.ProcessPoolExecutor`.
* Both `main.py` (for CLI) and `gui/processing_handler.py` (for GUI background tasks) create a process pool.
* The actual processing for each asset is delegated to the `main.process_single_asset_wrapper` function. This wrapper is executed in a separate worker process within the pool.
* The wrapper function is responsible for instantiating the `Configuration` and `AssetProcessor` classes for the specific asset being processed in that worker. This isolates each asset's processing environment.
* Results (success, skip, failure, error messages) are communicated back from the worker processes to the main coordinating script (either `main.py` or `gui/processing_handler.py`).
### Asset Processing Pipeline (`AssetProcessor` class)
The `AssetProcessor` class executes a sequence of steps for each asset:
1. **`_setup_workspace()`**: Creates a temporary directory for processing.
2. **`_extract_input()`**: Extracts the input ZIP archive or copies the input folder contents into the temporary workspace.
3. **`_inventory_and_classify_files()`**: This is a critical step that scans the workspace and classifies each file based on rules defined in the loaded `Configuration` (which includes the preset). It uses the pre-compiled regex patterns for efficiency. Key logic includes:
* Identifying files explicitly marked for the `Extra/` folder.
* Identifying model files.
* Matching potential texture maps against keyword patterns.
* Identifying and prioritizing 16-bit variants (e.g., `_NRM16.tif`) over their 8-bit counterparts based on `source_naming.bit_depth_variants` patterns. Ignored 8-bit files are tracked.
* Handling map variants (e.g., multiple Color maps) by assigning suffixes (`-1`, `-2`) based on the `RESPECT_VARIANT_MAP_TYPES` setting in `config.py` and the order of keywords defined in the preset's `map_type_mapping`.
* Classifying any remaining files as 'Unrecognised' (which are also moved to the `Extra/` folder).
4. **`_determine_base_metadata()`**: Determines the asset's base name, category (Texture, Asset, Decal), and archetype (e.g., Wood, Metal) based on classified files and preset rules (`source_naming`, `asset_category_rules`, `archetype_rules`).
5. **Skip Check**: If `overwrite` is false, checks if the final output directory and metadata file already exist. If so, processing for this asset stops early.
6. **`_process_maps()`**: Iterates through classified texture maps. For each map:
* Loads the image data (handling potential Gloss->Roughness inversion).
* Resizes the map to each target resolution specified in `config.py`, avoiding upscaling.
* Determines the output bit depth based on `MAP_BIT_DEPTH_RULES` (`respect` source or `force_8bit`).
* Determines the output file format (`.jpg`, `.png`, `.exr`) based on a combination of factors:
* The `RESOLUTION_THRESHOLD_FOR_JPG` (forces JPG for 8-bit maps above the threshold).
* The original input file format (e.g., `.jpg` inputs tend to produce `.jpg` outputs if 8-bit and below threshold).
* The target bit depth (16-bit outputs use configured `OUTPUT_FORMAT_16BIT_PRIMARY` or `_FALLBACK`).
* Configured 8-bit format (`OUTPUT_FORMAT_8BIT`).
* Saves the processed map for each resolution, applying appropriate compression/quality settings. Includes fallback logic if saving in the primary format fails (e.g., EXR -> PNG).
* Calculates basic image statistics (Min/Max/Mean) for a reference resolution (`CALCULATE_STATS_RESOLUTION`).
7. **`_merge_maps()`**: Combines channels from different processed maps into new textures (e.g., NRMRGH) based on `MAP_MERGE_RULES` defined in `config.py`. It determines the output format for merged maps similarly to `_process_maps`, considering the formats of the input maps involved.
8. **`_generate_metadata_file()`**: Collects all gathered information (asset name, maps present, resolutions, stats, etc.) and writes it to the `metadata.json` file.
9. **`_organize_output_files()`**: Moves the processed maps, merged maps, models, metadata file, and any 'Extra'/'Unrecognised'/'Ignored' files from the temporary workspace to the final structured output directory (`<output_base>/<supplier>/<asset_name>/`).
10. **`_cleanup_workspace()`**: Removes the temporary workspace directory.
### GUI Architecture (`gui/`)
The GUI provides an interactive way to use the tool and manage presets.
* **Framework**: Built using `PySide6`, the official Python bindings for the Qt framework.
* **Main Window (`main_window.py`)**: Defines the main application window, which includes:
* An integrated preset editor panel (using `QSplitter`).
* A processing panel with drag-and-drop support, a file preview table, and processing controls.
* **Threading Model**: To prevent the UI from freezing during potentially long operations, background tasks are run in separate `QThread`s:
* **`ProcessingHandler` (`processing_handler.py`)**: Manages the execution of the main processing pipeline (using `ProcessPoolExecutor` and `main.process_single_asset_wrapper`, similar to the CLI) in a background thread.
* **`PredictionHandler` (`prediction_handler.py`)**: Manages the generation of file previews in a background thread. It calls `AssetProcessor.get_detailed_file_predictions()`, which performs the extraction and classification steps without full image processing, making it much faster.
* **Communication**: Qt's **signal and slot mechanism** is used for communication between the background threads (`ProcessingHandler`, `PredictionHandler`) and the main GUI thread (`MainWindow`). For example, signals are emitted to update the progress bar, populate the preview table, and report completion status or errors.
* **Preset Editor**: The editor allows creating, modifying, and saving preset JSON files directly within the GUI. Changes are tracked, and users are prompted to save before closing or loading another preset if changes are pending.
### Monitor Architecture (`monitor.py`)
The `monitor.py` script enables automated processing of assets dropped into a designated input directory.
* **File System Watching**: Uses the `watchdog` library (specifically `PollingObserver` for cross-platform compatibility) to monitor the specified `INPUT_DIR`.
* **Event Handling**: A custom `ZipHandler` detects `on_created` events for `.zip` files.
* **Filename Parsing**: It expects filenames in the format `[preset]_filename.zip` and uses a regular expression (`PRESET_FILENAME_REGEX`) to extract the `preset` name.
* **Preset Validation**: Checks if the extracted preset name corresponds to a valid `.json` file in the `Presets/` directory.
* **Processing Trigger**: If the filename format and preset are valid, it calls the `main.run_processing` function (the same core logic used by the CLI) to process the detected ZIP file using the extracted preset.
* **File Management**: Moves the source ZIP file to either a `PROCESSED_DIR` (on success/skip) or an `ERROR_DIR` (on failure or invalid preset) after the processing attempt.
### Error Handling
* Custom exception classes (`ConfigurationError`, `AssetProcessingError`) are defined and used to signal specific types of errors during configuration loading or asset processing.
* Standard Python logging is used throughout the application (CLI, GUI, Monitor, Core Logic) to record information, warnings, and errors. Log levels can be configured.
* Worker processes in the processing pool capture exceptions and report them back to the main process for logging and status updates.

View File

@@ -1,124 +0,0 @@
# Blender Integration Plan: Node Groups from Processed Assets
**Objective:** Develop a Python script (`blenderscripts/create_nodegroups.py`) to run manually inside Blender. This script will scan the output directory generated by the Asset Processor Tool, read `metadata.json` files, and create/update corresponding PBR node groups in the active Blender file, leveraging the pre-calculated metadata.
**Key Principles:**
* **Leverage Existing Tool Output:** Rely entirely on the structured output and `metadata.json` from the Asset Processor Tool. Avoid reprocessing or recalculating data already available.
* **Blender Environment:** The script is designed solely for Blender's Python environment (`bpy`).
* **Manual Execution:** Users will manually run this script from Blender's Text Editor.
* **Target Active File:** All operations modify the currently open `.blend` file.
* **Assume Templates:** The script will assume node group templates (`Template_PBRSET`, `Template_PBRTYPE`) exist in the active file. Error handling will be added if they are missing.
* **Focus on Node Groups:** The script's scope is limited to creating and updating the node groups, not materials.
**Detailed Plan:**
1. **Script Setup & Configuration:**
* Create a new Python file named `create_nodegroups.py` (intended location: `blenderscripts/`).
* Import necessary modules (`bpy`, `os`, `json`, `pathlib`).
* Define user-configurable variables at the top:
* `PROCESSED_ASSET_LIBRARY_ROOT`: Path to the root output directory of the Asset Processor Tool.
* `PARENT_TEMPLATE_NAME`: Name of the parent node group template (e.g., `"Template_PBRSET"`).
* `CHILD_TEMPLATE_NAME`: Name of the child node group template (e.g., `"Template_PBRTYPE"`).
* `ASPECT_RATIO_NODE_LABEL`: Label of the Value node in the parent template for aspect ratio correction (e.g., `"AspectRatioCorrection"`).
* `STATS_NODE_PREFIX`: Prefix for Combine XYZ nodes storing stats in the parent template (e.g., `"Histogram-"`).
* `ENABLE_MANIFEST`: Boolean flag to enable/disable the manifest system (default: `True`).
2. **Manifest Handling:**
* **Location:** Use a separate JSON file named `[ActiveBlendFileName]_manifest.json`, located in the same directory as the active `.blend` file.
* **Loading:** Implement `load_manifest(context)` that finds and reads the manifest JSON file. If not found or invalid, return an empty dictionary.
* **Saving:** Implement `save_manifest(context, manifest_data)` that writes the `manifest_data` dictionary to the manifest JSON file.
* **Checking:** Implement helper functions `is_asset_processed(manifest_data, asset_name)` and `is_map_processed(manifest_data, asset_name, map_type, resolution)` to check against the loaded manifest.
* **Updating:** Update the manifest dictionary in memory as assets/maps are processed. Save the manifest once at the end of the script.
3. **Core Logic - `process_library()` function:**
* Get Blender context.
* Load manifest data (if enabled).
* Validate that `PROCESSED_ASSET_LIBRARY_ROOT` exists.
* Validate that template node groups exist in `bpy.data.node_groups`. Exit gracefully with an error message if not found.
* Initialize counters (new groups, updated groups, etc.).
* **Scan Directory:** Use `os.walk` or `pathlib.rglob` to find all `metadata.json` files within the `PROCESSED_ASSET_LIBRARY_ROOT`.
* **Iterate Metadata:** For each `metadata.json` found:
* Parse the JSON data. Extract key information: `asset_name`, `supplier_name`, `archetype`, `maps` (dictionary of maps with resolutions, paths, stats), `aspect_ratio_change_string`.
* Check manifest if asset is already processed (if enabled). Skip if true.
* **Parent Group Handling:**
* Determine target parent group name (e.g., `f"PBRSET_{asset_name}"`).
* Find existing group or create a copy from `PARENT_TEMPLATE_NAME`.
* Mark group as asset (`asset_mark()`) if not already.
* **Apply Metadata:**
* Find the aspect ratio node using `ASPECT_RATIO_NODE_LABEL`. Calculate the correction factor based on the `aspect_ratio_change_string` from metadata (using helper function `calculate_factor_from_string`) and set the node's default value.
* For relevant map types (e.g., ROUGH, DISP), find the stats node (`STATS_NODE_PREFIX` + map type). Set the X, Y, Z inputs using the `min`, `max`, `mean` values stored in the map's metadata entry for the reference resolution.
* **Apply Asset Tags:** Use `asset_data.tags.new()` to add the `supplier_name` and `archetype` tags (checking for existence first).
* **Child Group Handling (Iterate through `maps` in metadata):**
* For each `map_type` (e.g., "COL", "NRM") and its data in the metadata:
* Determine target child group name (e.g., `f"PBRTYPE_{asset_name}_{map_type}"`).
* Find existing child group or create a copy from `CHILD_TEMPLATE_NAME`.
* Find the corresponding placeholder node in the *parent* group (by label matching `map_type`). Assign the child node group to this placeholder (`placeholder_node.node_tree = child_group`).
* Link the child group's output to the corresponding parent group's output socket. Ensure the parent output socket type is `NodeSocketColor`.
* **Image Node Handling (Iterate through resolutions for the map type):**
* For each `resolution` (e.g., "4K", "2K") and its `image_path` in the metadata:
* Check manifest if this specific map/resolution is processed (if enabled). Skip if true.
* Find the corresponding Image Texture node in the *child* group (by label matching `resolution`, e.g., "4K").
* Load the image using `bpy.data.images.load(image_path, check_existing=True)`. Handle potential file-not-found errors.
* Assign the loaded image to the `image_node.image`.
* Set the `image_node.image.colorspace_settings.name` based on the `map_type` (using a helper function `get_color_space`).
* Update manifest dictionary for this map/resolution (if enabled).
* Update manifest dictionary for the processed asset (if enabled).
* Save manifest data (if enabled and changes were made).
* Print summary (duration, groups created/updated, etc.).
4. **Helper Functions:**
* `find_nodes_by_label(node_tree, label, node_type)`: Reusable function to find nodes.
* `calculate_factor_from_string(aspect_string)`: Parses the `aspect_ratio_change_string` from metadata and returns the appropriate UV X-scaling factor.
* `get_color_space(map_type)`: Returns the appropriate Blender color space name for a given map type string.
* `add_tag_if_new(asset_data, tag_name)`: Adds a tag if it doesn't exist.
* Manifest loading/saving/checking functions.
5. **Execution Block (`if __name__ == "__main__":`)**
* Add pre-run checks (templates exist, library path valid, blend file saved if manifest enabled).
* Call the main `process_library()` function.
* Include basic timing and print statements for start/end.
**Mermaid Diagram:**
```mermaid
graph TD
A[Start Script in Blender] --> B(Load Config: Lib Path, Template Names, Node Labels);
B --> C{Check Templates Exist};
C -- Templates OK --> D(Load Manifest from adjacent .json file);
C -- Templates Missing --> X(Error & Exit);
D --> E{Scan Processed Library for metadata.json};
E --> F{For each metadata.json};
F --> G{Parse Metadata (Asset Name, Supplier, Archetype, Maps, Aspect Str, Stats)};
G --> H{Is Asset in Manifest?};
H -- Yes --> F;
H -- No --> I{Find/Create Parent Group (PBRSET_)};
I --> J(Mark as Asset & Apply Supplier + Archetype Tags);
J --> K(Find Aspect Node & Set Value from Aspect String);
K --> M{For each Map Type in Metadata};
M --> N(Find Stats Node & Set Values from Stats in Metadata);
N --> O{Find/Create Child Group (PBRTYPE_)};
O --> P(Assign Child to Parent Placeholder);
P --> Q(Link Child Output to Parent Output);
Q --> R{For each Resolution of Map};
R --> S{Is Map/Res in Manifest?};
S -- Yes --> R;
S -- No --> T(Find Image Node in Child);
T --> U(Load Processed Image);
U --> V(Assign Image to Node);
V --> W(Set Image Color Space);
W --> W1(Update Manifest Dict for Map/Res);
W1 --> R;
R -- All Resolutions Done --> M;
M -- All Map Types Done --> X1(Update Manifest Dict for Asset);
X1 --> F;
F -- All metadata.json Processed --> Y(Save Manifest Dict to adjacent .json file);
Y --> Z(Print Summary & Finish);
subgraph "Manifest Operations (External File)"
D; H; S; W1; X1; Y;
end
subgraph "Node/Asset Operations"
I; J; K; N; O; P; Q; T; U; V; W;
end

View File

@@ -1,52 +0,0 @@
# Blender Integration Plan v2
## Goal
Add an optional step to `main.py` to run `blenderscripts/create_nodegroups.py` and `blenderscripts/create_materials.py` on specified `.blend` files after asset processing is complete.
## Proposed Plan
1. **Update `config.py`:**
* Add two new optional configuration variables: `DEFAULT_NODEGROUP_BLEND_PATH` and `DEFAULT_MATERIALS_BLEND_PATH`. These will store the default paths to the Blender files.
2. **Update `main.py` Argument Parser:**
* Add two new optional command-line arguments: `--nodegroup-blend` and `--materials-blend`.
* These arguments will accept file paths to the respective `.blend` files.
* If provided, these arguments will override the default paths specified in `config.py`.
3. **Update `blenderscripts/create_nodegroups.py` and `blenderscripts/create_materials.py`:**
* Modify both scripts to accept the processed asset library root path (`PROCESSED_ASSET_LIBRARY_ROOT`) as a command-line argument. This will be passed to the script when executed by Blender using the `--` separator.
* Update the scripts to read this path from `sys.argv` instead of using the hardcoded variable.
4. **Update `main.py` Execution Flow:**
* After the main asset processing loop (`run_processing`) completes and the summary is reported, check if the `--nodegroup-blend` or `--materials-blend` arguments (or their fallbacks from `config.py`) were provided.
* If a path for the nodegroup `.blend` file is available:
* Construct a command to execute Blender in the background (`-b`), load the specified nodegroup `.blend` file, run the `create_nodegroups.py` script using `--python`, pass the processed asset root directory as an argument after `--`, and save the `.blend` file (`-S`).
* Execute this command using the `execute_command` tool.
* If a path for the materials `.blend` file is available:
* Construct a similar command to execute Blender in the background, load the specified materials `.blend` file, run the `create_materials.py` script using `--python`, pass the processed asset root directory as an argument after `--`, and save the `.blend` file (`-S`).
* Execute this command using the `execute_command` tool.
* Include error handling for the execution of the Blender commands.
## Execution Flow Diagram
```mermaid
graph TD
A[Asset Processing Complete] --> B[Report Summary];
B --> C{Nodegroup Blend Path Specified?};
C -- Yes --> D[Get Nodegroup Blend Path (Arg or Config)];
D --> E[Construct Blender Command for Nodegroups];
E --> F[Execute Command: blender -b nodegroup.blend --python create_nodegroups.py -- <asset_root> -S];
F --> G{Command Successful?};
G -- Yes --> H{Materials Blend Path Specified?};
G -- No --> I[Log Nodegroup Error];
I --> H;
H -- Yes --> J[Get Materials Blend Path (Arg or Config)];
J --> K[Construct Blender Command for Materials];
K --> L[Execute Command: blender -b materials.blend --python create_materials.py -- <asset_root> -S];
L --> M{Command Successful?};
M -- Yes --> N[End main.py];
M -- No --> O[Log Materials Error];
O --> N;
H -- No --> N;
C -- No --> H;

View File

@@ -1,131 +0,0 @@
# Blender Material Creation Script Plan
This document outlines the plan for creating a new Blender script (`create_materials.py`) in the `blenderscripts/` directory. This script will scan the processed asset library output by the Asset Processor Tool, read the `metadata.json` files, and create or update Blender materials that link to the corresponding PBRSET node groups found in a specified Blender Asset Library. The script will also set the material's viewport properties using pre-calculated statistics from the metadata. The script will skip processing an asset if the corresponding material already exists in the current Blender file.
## 1. Script Location and Naming
* Create a new file: `blenderscripts/create_materials.py`.
## 2. Script Structure
The script will follow a similar structure to `blenderscripts/create_nodegroups.py`, including:
* Import statements (`bpy`, `os`, `json`, `pathlib`, `time`, `base64`).
* A `--- USER CONFIGURATION ---` section at the top.
* Helper functions.
* A main processing function (e.g., `process_library_for_materials`).
* An execution block (`if __name__ == "__main__":`) to run the main function.
## 3. Configuration Variables
The script will include the following configuration variables in the `--- USER CONFIGURATION ---` section:
* `PROCESSED_ASSET_LIBRARY_ROOT`: Path to the root output directory of the Asset Processor Tool (same as in `create_nodegroups.py`). This is used to find the `metadata.json` files and reference images for previews.
* `PBRSET_ASSET_LIBRARY_NAME`: The name of the Blender Asset Library (configured in Blender Preferences) that contains the PBRSET node groups created by `create_nodegroups.py`.
* `TEMPLATE_MATERIAL_NAME`: Name of the required template material in the Blender file (e.g., "Template_PBRMaterial").
* `PLACEHOLDER_NODE_LABEL`: Label of the placeholder Group node within the template material's node tree where the PBRSET node group will be linked (e.g., "PBRSET_PLACEHOLDER").
* `MATERIAL_NAME_PREFIX`: Prefix for the created materials (e.g., "Mat_").
* `PBRSET_GROUP_PREFIX`: Prefix used for the PBRSET node groups created by `create_nodegroups.py` (e.g., "PBRSET_").
* `REFERENCE_MAP_TYPES`: List of map types to look for to find a reference image for the material preview (e.g., `["COL", "COL-1"]`).
* `REFERENCE_RESOLUTION_ORDER`: Preferred resolution order for the reference image (e.g., `["1K", "512", "2K", "4K"]`).
* `IMAGE_FILENAME_PATTERN`: Assumed filename pattern for processed images (same as in `create_nodegroups.py`).
* `FALLBACK_IMAGE_EXTENSIONS`: Fallback extensions for finding image files (same as in `create_nodegroups.py`).
* `VIEWPORT_COLOR_MAP_TYPES`: List of map types to check in metadata's `image_stats_1k` for viewport diffuse color.
* `VIEWPORT_ROUGHNESS_MAP_TYPES`: List of map types to check in metadata's `image_stats_1k` for viewport roughness.
* `VIEWPORT_METALLIC_MAP_TYPES`: List of map types to check in metadata's `image_stats_1k` for viewport metallic.
## 4. Helper Functions
The script will include the following helper functions:
* `find_nodes_by_label(node_tree, label, node_type=None)`: Reusable from `create_nodegroups.py` to find nodes in a node tree.
* `add_tag_if_new(asset_data, tag_name)`: Reusable from `create_nodegroups.py` to add asset tags.
* `reconstruct_image_path_with_fallback(...)`: Reusable from `create_nodegroups.py` to find image paths (needed for setting the custom preview).
* `get_stat_value(stats_dict, map_type_list, stat_key)`: A helper function to safely retrieve a specific statistic from the `image_stats_1k` dictionary.
## 5. Main Processing Logic (`process_library_for_materials`)
The main function will perform the following steps:
* **Pre-run Checks:**
* Verify `PROCESSED_ASSET_LIBRARY_ROOT` exists and is a directory.
* Verify the `PBRSET_ASSET_LIBRARY_NAME` exists in Blender's user preferences (`bpy.context.preferences.filepaths.asset_libraries`).
* Verify the `TEMPLATE_MATERIAL_NAME` material exists and uses nodes.
* Verify the `PLACEHOLDER_NODE_LABEL` Group node exists in the template material's node tree.
* **Scan for Metadata:**
* Iterate through supplier directories within `PROCESSED_ASSET_LIBRARY_ROOT`.
* Iterate through asset directories within each supplier directory.
* Identify `metadata.json` files.
* **Process Each Metadata File:**
* Load the `metadata.json` file.
* Extract `asset_name`, `supplier_name`, `archetype`, `processed_map_resolutions`, `merged_map_resolutions`, `map_details`, and `image_stats_1k`.
* Determine the expected PBRSET node group name: `f"{PBRSET_GROUP_PREFIX}{asset_name}"`.
* Determine the target material name: `f"{MATERIAL_NAME_PREFIX}{asset_name}"`.
* **Find or Create Material:**
* Check if a material with the `target_material_name` already exists in `bpy.data.materials`.
* If it exists, log a message indicating the asset is being skipped and move to the next metadata file.
* If it doesn't exist, copy the `TEMPLATE_MATERIAL_NAME` material and rename the copy to `target_material_name` (create mode). Handle potential copy failures.
* **Find Placeholder Node:**
* Find the node with `PLACEHOLDER_NODE_LABEL` in the target material's node tree using `find_nodes_by_label`. Handle cases where the node is not found or is not a Group node.
* **Find and Link PBRSET Node Group from Asset Library:**
* Get the path to the `.blend` file associated with the `PBRSET_ASSET_LIBRARY_NAME` from user preferences.
* Use `bpy.data.libraries.load(filepath, link=True)` to link the node group with `target_pbrset_group_name` from the external `.blend` file into the current file. Handle cases where the library or the node group is not found.
* Once linked, get the reference to the newly linked node group in `bpy.data.node_groups`.
* **Link Linked Node Group to Placeholder:**
* If both the placeholder node and the *newly linked* PBRSET node group are found, assign the linked node group to the `node_tree` property of the placeholder node.
* **Mark Material as Asset:**
* If the material is new or not already marked, call `material.asset_mark()`.
* **Copy Asset Tags:**
* If both the material and the *linked* PBRSET node group have asset data, copy tags (supplier, archetype) from the node group to the material using `add_tag_if_new`.
* **Set Custom Preview:**
* Find a suitable reference image path (e.g., lowest resolution COL map) using `reconstruct_image_path_with_fallback` and the `REFERENCE_MAP_TYPES` and `REFERENCE_RESOLUTION_ORDER` configurations.
* If a reference image path is found, use `bpy.ops.ed.lib_id_load_custom_preview` to set the custom preview for the material. This operation requires overriding the context.
* **Set Viewport Properties (using metadata stats):**
* Check if `image_stats_1k` is present and valid in the metadata.
* **Diffuse Color:** Use the `get_stat_value` helper to get the 'mean' stat for map types in `VIEWPORT_COLOR_MAP_TYPES`. If found and is a valid color list `[R, G, B]`, set `material.diffuse_color` to this value.
* **Roughness:** Use the `get_stat_value` helper to get the 'mean' stat for map types in `VIEWPORT_ROUGHNESS_MAP_TYPES`. If found, get the first value (for grayscale). Check if stats for `VIEWPORT_METALLIC_MAP_TYPES` exist. If metallic stats are *not* found, invert the roughness value (`1.0 - value`) before assigning it to `material.roughness`. Clamp the final value between 0.0 and 1.0.
* **Metallic:** Use the `get_stat_value` helper to get the 'mean' stat for map types in `VIEWPORT_METALLIC_MAP_TYPES`. If found, get the first value (for grayscale) and assign it to `material.metallic`. If metallic stats are *not* found, set `material.metallic` to 0.0.
* **Error Handling and Reporting:**
* Include `try...except` blocks to catch errors during file reading, JSON parsing, Blender operations, linking, etc.
* Print informative messages about progress, creation/update status, and errors.
* **Summary Report:**
* Print a summary of how many metadata files were processed, materials created/updated, node groups linked, errors encountered, and assets skipped.
## 6. Process Flow Diagram (Updated)
```mermaid
graph TD
A[Start Script] --> B{Pre-run Checks Pass?};
B -- No --> C[Abort Script];
B -- Yes --> D[Scan Processed Asset Root];
D --> E{Found metadata.json?};
E -- No --> F[Finish Script (No Assets)];
E -- Yes --> G[Loop through metadata.json files];
G --> H[Read metadata.json];
H --> I{Metadata Valid?};
I -- No --> J[Log Error, Skip Asset];
I -- Yes --> K[Extract Asset Info & Stats];
K --> L[Find or Create Material];
L --> M{Material Exists?};
M -- Yes --> N[Log Skip, Continue Loop];
M -- No --> O[Find Placeholder Node in Material];
O --> P[Find PBRSET NG in Library & Link];
P --> Q{Linking Successful?};
Q -- No --> R[Log Error, Skip Asset];
Q -- Yes --> S[Link Linked NG to Placeholder];
S --> T[Mark Material as Asset];
T --> U[Copy Asset Tags];
U --> V[Find Reference Image Path for Preview];
V --> W{Reference Image Found?};
W -- Yes --> X[Set Custom Material Preview];
W -- No --> Y[Log Warning (No Preview)];
X --> Z[Set Viewport Properties from Stats];
Y --> Z;
Z --> AA[Increment Counters];
AA --> G;
G --> AB[Print Summary Report];
AB --> AC[End Script];
J --> G;
N --> G;
R --> G;

View File

@@ -1,103 +0,0 @@
# Blender Addon Plan: Material Merger
**Version:** 1.1 (Includes Extensibility Consideration)
**1. Goal:**
Create a standalone Blender addon that allows users to select two existing materials (generated by the Asset Processor Tool, or previously merged by this addon) and merge them into a new material. The merge should preserve their individual node structures (including custom tweaks) and combine their final outputs using a dedicated `MaterialMerge` node group.
**2. Core Functionality (Approach 2 - Node Copying):**
* **Trigger:** User selects two materials in Blender and invokes an operator (e.g., via a button in the Shader Editor's UI panel).
* **New Material Creation:** The addon creates a new Blender material, named appropriately (e.g., `MAT_Merged_<NameA>_<NameB>`).
* **Node Copying:**
* For *each* selected source material:
* Iterate through its node tree.
* Copy all nodes *except* the `Material Output` node into the *new* material's node tree, attempting to preserve relative layout and offsetting subsequent copies.
* **Identify Final Outputs:** Determine the node providing the final BSDF shader output and the node providing the final Displacement output *before* the original `Material Output` node.
* In a base material (from Asset Processor), these are expected to be the `PBR_BSDF` node group (BSDF output) and the `PBR_Handler` node group (Displacement output).
* In an already-merged material, these will be the outputs of its top-level `MaterialMerge` node group.
* Store references to these final output nodes and their relevant sockets.
* **MaterialMerge Node:**
* **Link/Append** the `MaterialMerge` node group into the new material's node tree.
* **Assumption:** This node group exists in `blender_files/utility_nodegroups.blend` relative to the addon's location.
* **Assumption:** Socket names are `Shader A`, `Shader B`, `Displacement A`, `Displacement B` (inputs) and `BSDF`, `Displacement` (outputs).
* **Connections:**
* Connect the identified final BSDF output of the *first* source material's copied structure to the `MaterialMerge` node's `Shader A` input.
* Connect the identified final Displacement output of the *first* source material's copied structure to the `MaterialMerge` node's `Displacement A` input.
* Connect the identified final BSDF output of the *second* source material's copied structure to the `MaterialMerge` node's `Shader B` input.
* Connect the identified final Displacement output of the *second* source material's copied structure to the `MaterialMerge` node's `Displacement B` input.
* Connect the `MaterialMerge` node's `BSDF` output to the new material's `Material Output` node's `Surface` input.
* Connect the `MaterialMerge` node's `Displacement` output to the new material's `Material Output` node's `Displacement` input.
* **Layout:** Optionally, attempt a basic auto-layout (`node_tree.nodes.update()`) or arrange the key nodes logically.
**3. User Interface (UI):**
* A simple panel in the Blender Shader Editor (Properties region - 'N' panel).
* Two dropdowns or search fields allowing the user to select existing materials from the current `.blend` file.
* A button labeled "Merge Selected Materials".
* Status messages/feedback (e.g., "Merged material created: [Name]", "Error: Could not find required nodes in [Material Name]").
**4. Addon Structure (Python):**
* `__init__.py`: Registers the addon, panel, and operator classes.
* `operator.py`: Contains the `OT_MergeMaterials` operator class implementing the core logic.
* `panel.py`: Contains the `PT_MaterialMergePanel` class defining the UI layout.
* (Optional) `utils.py`: Helper functions for node finding, copying, linking, identifying final outputs, etc.
**5. Error Handling:**
* Check if two valid materials are selected.
* Verify that the selected materials have node trees.
* Handle cases where the expected final BSDF/Displacement output nodes cannot be reliably identified in one or both source materials.
* Handle potential errors during node copying.
* Handle errors if the `utility_nodegroups.blend` file or the `MaterialMerge` node group within it cannot be found/linked.
**6. Assumptions to Verify (Based on User Feedback):**
* **Node Identification:**
* Base Material Handler: Node named `PBR_Handler`.
* Base Material BSDF: Node named `PBR_BSDF`.
* Merged Material Outputs: The `BSDF` and `Displacement` outputs of the top-level `MaterialMerge` node.
* **`MaterialMerge` Node:**
* Location: `blender_files/utility_nodegroups.blend` (relative path).
* Input Sockets: `Shader A`, `Shader B`, `Displacement A`, `Displacement B`.
* Output Sockets: `BSDF`, `Displacement`.
**7. Future Extensibility - Recursive Merging:**
* The core merging logic (copying nodes, identifying final outputs, connecting to a new `MaterialMerge` node) is designed to inherently support selecting an already-merged material as an input without requiring separate code paths initially. The identification of final BSDF/Displacement outputs needs to correctly handle both base materials and merged materials (checking for `PBR_BSDF`/`PBR_Handler` or the outputs of an existing `MaterialMerge` node).
**8. Mermaid Diagram of Node Flow:**
```mermaid
graph TD
subgraph New Merged Material
subgraph Copied from Source A (Mat_A or Merge_A)
%% Nodes representing the structure of Source A
Structure_A[...]
Final_BSDF_A[Final BSDF Output A]
Final_Disp_A[Final Displacement Output A]
Structure_A --> Final_BSDF_A
Structure_A --> Final_Disp_A
end
subgraph Copied from Source B (Mat_B or Merge_B)
%% Nodes representing the structure of Source B
Structure_B[...]
Final_BSDF_B[Final BSDF Output B]
Final_Disp_B[Final Displacement Output B]
Structure_B --> Final_BSDF_B
Structure_B --> Final_Disp_B
end
Merge[MaterialMerge]
Output[Material Output]
Final_BSDF_A -- BSDF --> Merge -- Shader A --> Merge
Final_Disp_A -- Displacement --> Merge -- Displacement A --> Merge
Final_BSDF_B -- BSDF --> Merge -- Shader B --> Merge
Final_Disp_B -- Displacement --> Merge -- Displacement B --> Merge
Merge -- BSDF --> Output -- Surface --> Output
Merge -- Displacement --> Output -- Displacement --> Output
end

View File

@@ -0,0 +1,107 @@
# Configuration System Refactoring Plan
This document outlines the plan for refactoring the configuration system of the Asset Processor Tool.
## Overall Goals
1. **Decouple Definitions:** Separate `ASSET_TYPE_DEFINITIONS` and `FILE_TYPE_DEFINITIONS` from the main `config/app_settings.json` into dedicated files.
2. **Introduce User Overrides:** Allow users to override base settings via a new `config/user_settings.json` file.
3. **Improve GUI Saving:** (Lower Priority) Make GUI configuration saving more targeted to avoid overwriting unrelated settings when saving changes from `ConfigEditorDialog` or `LLMEditorWidget`.
## Proposed Plan Phases
**Phase 1: Decouple Definitions**
1. **Create New Definition Files:**
* Create `config/asset_type_definitions.json`.
* Create `config/file_type_definitions.json`.
2. **Migrate Content:**
* Move `ASSET_TYPE_DEFINITIONS` object from `config/app_settings.json` to `config/asset_type_definitions.json`.
* Move `FILE_TYPE_DEFINITIONS` object from `config/app_settings.json` to `config/file_type_definitions.json`.
3. **Update `configuration.py`:**
* Add constants for new definition file paths.
* Modify `Configuration` class to load these new files.
* Update property methods (e.g., `get_asset_type_definitions`, `get_file_type_definitions_with_examples`) to use data from the new definition dictionaries.
* Adjust validation (`_validate_configs`) as needed.
4. **Update GUI & `load_base_config()`:**
* Modify `load_base_config()` to load and return a combined dictionary including `app_settings.json` and the two new definition files.
* Update GUI components relying on `load_base_config()` to ensure they receive the necessary definition data.
**Phase 2: Implement User Overrides**
1. **Define `user_settings.json`:**
* Establish `config/user_settings.json` for user-specific overrides, mirroring parts of `app_settings.json`.
2. **Update `configuration.py` Loading:**
* In `Configuration.__init__`, load `app_settings.json`, then definition files, then attempt to load and deep merge `user_settings.json` (user settings override base).
* Load presets *after* the base+user merge (presets override combined base+user).
* Modify `load_base_config()` to also load and merge `user_settings.json` after `app_settings.json`.
3. **Update GUI Editors:**
* Modify `ConfigEditorDialog` to load the effective settings (base+user) but save changes *only* to `config/user_settings.json`.
* `LLMEditorWidget` continues targeting `llm_settings.json`.
**Phase 3: Granular GUI Saving (Lower Priority)**
1. **Refactor Saving Logic:**
* In `ConfigEditorDialog` and `LLMEditorWidget`:
* Load the current target file (`user_settings.json` or `llm_settings.json`).
* Identify specific setting(s) changed by the user in the GUI session.
* Update only those specific key(s) in the loaded dictionary.
* Write the entire modified dictionary back to the target file, preserving untouched settings.
## Proposed File Structure & Loading Flow
```mermaid
graph LR
subgraph Config Files
A[config/asset_type_definitions.json]
B[config/file_type_definitions.json]
C[config/app_settings.json (Base Defaults)]
D[config/user_settings.json (User Overrides)]
E[config/llm_settings.json]
F[config/suppliers.json]
G[Presets/*.json]
end
subgraph Code
H[configuration.py]
I[GUI]
J[Processing Engine / Pipeline]
K[LLM Handlers]
end
subgraph Loading Flow (Configuration Class)
L(Load Asset Types) --> H
M(Load File Types) --> H
N(Load Base Settings) --> P(Merge Base + User)
O(Load User Settings) --> P
P --> R(Merge Preset Overrides)
Q(Load LLM Settings) --> H
R --> T(Final Config Object)
G -- Load Preset --> R
H -- Contains --> T
end
subgraph Loading Flow (GUI - load_base_config)
L2(Load Asset Types) --> U(Return Merged Defaults + Defs)
M2(Load File Types) --> U
N2(Load Base Settings) --> V(Merge Base + User)
O2(Load User Settings) --> V
V --> U
I -- Calls --> U
end
T -- Used by --> J
T -- Used by --> K
I -- Edits --> D
I -- Edits --> E
I -- Manages --> F
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#9cf,stroke:#333,stroke-width:2px
style E fill:#ccf,stroke:#333,stroke-width:2px
style F fill:#9cf,stroke:#333,stroke-width:2px
style G fill:#ffc,stroke:#333,stroke-width:2px

View File

@@ -1,124 +0,0 @@
# Architectural Plan: Data Flow Refinement (v3)
**Date:** 2025-04-30
**Author:** Roo (Architect Mode)
**Status:** Approved
## 1. Goal
Refine the application's data flow to establish the GUI as the single source of truth for processing rules. This involves moving prediction/preset logic upstream from the backend processor and ensuring the backend receives a *complete* `SourceRule` object for processing, thereby simplifying the processor itself. This version of the plan involves creating a new processing module (`processing_engine.py`) instead of refactoring the existing `asset_processor.py`.
## 2. Proposed Data Flow
The refined data flow centralizes rule generation and modification within the GUI components before passing a complete, explicit rule set to the backend. The `SourceRule` object structure serves as a consistent data contract throughout the pipeline.
```mermaid
sequenceDiagram
participant User
participant GUI_MainWindow as GUI (main_window.py)
participant GUI_Predictor as Predictor (prediction_handler.py)
participant GUI_UnifiedView as Unified View (unified_view_model.py)
participant Main as main.py
participant ProcessingEngine as New Backend (processing_engine.py)
participant Config as config.py
User->>+GUI_MainWindow: Selects Input & Preset
Note over GUI_MainWindow: Scans input, gets file list
GUI_MainWindow->>+GUI_Predictor: Request Prediction(File List, Preset Name, Input ID)
GUI_Predictor->>+Config: Load Preset Rules & Canonical Types
Config-->>-GUI_Predictor: Return Rules & Types
%% Prediction Logic (Internal to Predictor)
Note over GUI_Predictor: Perform file analysis (based on list), apply preset rules, generate COMPLETE SourceRule hierarchy (only overridable fields populated)
GUI_Predictor-->>-GUI_MainWindow: Return List[SourceRule] (Initial Rules)
GUI_MainWindow->>+GUI_UnifiedView: Populate View(List[SourceRule])
GUI_UnifiedView->>+Config: Read Allowed Asset/File Types for Dropdowns
Config-->>-GUI_UnifiedView: Return Allowed Types
Note over GUI_UnifiedView: Display rules, allow user edits
User->>GUI_UnifiedView: Modifies Rules (Overrides)
GUI_UnifiedView-->>GUI_MainWindow: Update SourceRule Objects in Memory
User->>+GUI_MainWindow: Trigger Processing
GUI_MainWindow->>+Main: Send Final List[SourceRule]
Main->>+ProcessingEngine: Queue Task(SourceRule) for each input
Note over ProcessingEngine: Execute processing based *solely* on the provided SourceRule and static config. No internal prediction/fallback.
ProcessingEngine-->>-Main: Processing Result
Main-->>-GUI_MainWindow: Update Status
GUI_MainWindow-->>User: Show Result/Status
```
## 3. Module-Specific Changes
* **`config.py`:**
* **Add Canonical Lists:** Introduce `ALLOWED_ASSET_TYPES` (e.g., `["Surface", "Model", "Decal", "Atlas", "UtilityMap"]`) and `ALLOWED_FILE_TYPES` (e.g., `["MAP_COL", "MAP_NRM", ..., "MODEL", "EXTRA", "FILE_IGNORE"]`).
* **Purpose:** Single source of truth for GUI dropdowns and validation.
* **Existing Config:** Retains static definitions like `IMAGE_RESOLUTIONS`, `MAP_MERGE_RULES`, `JPG_QUALITY`, etc.
* **`rule_structure.py`:**
* **Remove Enums:** Remove `AssetType` and `ItemType` Enums. Update `AssetRule.asset_type`, `FileRule.item_type_override`, etc., to use string types validated against `config.py` lists.
* **Field Retention:** Keep `FileRule.resolution_override` and `FileRule.channel_merge_instructions` fields for structural consistency, but they will not be populated or used for overrides in this flow.
* **`gui/prediction_handler.py` (or equivalent):**
* **Enhance Prediction Logic:** Modify `run_prediction` method.
* **Input:** Accept `input_source_identifier` (string), `file_list` (List[str] of relative paths), and `preset_name` (string) when called from GUI.
* **Load Config:** Read `ALLOWED_ASSET_TYPES`, `ALLOWED_FILE_TYPES`, and preset rules.
* **Relocate Classification:** Integrate classification/naming logic (previously in `asset_processor.py`) to operate on the provided `file_list`.
* **Generate Complete Rules:** Populate `SourceRule`, `AssetRule`, and `FileRule` objects.
* Set initial values only for *overridable* fields (e.g., `asset_type`, `item_type_override`, `target_asset_name_override`, `supplier_identifier`, `output_format_override`) based on preset rules/defaults.
* Explicitly **do not** populate static config fields like `FileRule.resolution_override` or `FileRule.channel_merge_instructions`.
* **Temporary Files (If needed for non-GUI):** May need logic later to handle direct path inputs (CLI/Docker) involving temporary extraction/cleanup, but the primary GUI flow uses the provided list.
* **Output:** Emit `rule_hierarchy_ready` signal with the `List[SourceRule]`.
* **NEW: `processing_engine.py` (New Module):**
* **Purpose:** Contains a new class (e.g., `ProcessingEngine`) for executing the processing pipeline based solely on a complete `SourceRule` and static configuration. Replaces `asset_processor.py` in the main workflow.
* **Initialization (`__init__`):** Takes the static `Configuration` object as input.
* **Core Method (`process`):** Accepts a single, complete `SourceRule` object. Orchestrates processing steps (workspace setup, extraction, map processing, merging, metadata, organization, cleanup).
* **Helper Methods (Refactored Logic):** Implement simplified versions of processing helpers (e.g., `_process_individual_maps`, `_merge_maps_from_source`, `_generate_metadata_file`, `_organize_output_files`, `_load_and_transform_source`, `_save_image`).
* Retrieve *overridable* parameters directly from the input `SourceRule`.
* Retrieve *static configuration* parameters (resolutions, merge rules) **only** from the stored `Configuration` object.
* Contain **no** prediction, classification, or fallback logic.
* **Dependencies:** `rule_structure.py`, `configuration.py`, `config.py`, cv2, numpy, etc.
* **`asset_processor.py` (Old Module):**
* **Status:** Remains in the codebase **unchanged** for reference.
* **Usage:** No longer called by `main.py` or GUI for standard processing.
* **`gui/main_window.py`:**
* **Scan Input:** Perform initial directory/archive scan to get the file list for each directory/archieve.
* **Initiate Prediction:** Call `PredictionHandler` with the file list, preset, and input identifier.
* **Receive/Pass Rules:** Handle `rule_hierarchy_ready`, pass `SourceRule` list to `UnifiedViewModel`.
* **Send Final Rules:** Send the final `SourceRule` list to `main.py`.
* **`gui/unified_view_model.py` / `gui/delegates.py`:**
* **Load Dropdown Options:** Source dropdowns (`AssetType`, `ItemType`) from `config.py`.
* **Data Handling:** Read/write user modifications to overridable fields in `SourceRule` objects.
* **No UI for Static Config:** Do not provide UI editing for resolution or merge instructions.
* **`main.py`:**
* **Receive Rule List:** Accept `List[SourceRule]` from GUI.
* **Instantiate New Engine:** Import and instantiate the new `ProcessingEngine` from `processing_engine.py`.
* **Queue Tasks:** Iterate `SourceRule` list, queue tasks.
* **Call New Engine:** Pass the individual `SourceRule` object to `ProcessingEngine.process` for each task.
## 4. Rationale / Benefits
* **Single Source of Truth:** GUI holds the final `SourceRule` objects.
* **Backend Simplification:** New `processing_engine.py` is focused solely on execution based on explicit rules and static config.
* **Decoupling:** Reduced coupling between GUI/prediction and backend processing.
* **Clarity:** Clearer data flow and component responsibilities.
* **Maintainability:** Easier maintenance and debugging.
* **Centralized Definitions:** `config.py` centralizes allowed types.
* **Preserves Reference:** Keeps `asset_processor.py` available for comparison.
* **Consistent Data Contract:** `SourceRule` structure is consistent from predictor output to engine input, enabling potential GUI bypass.
## 5. Potential Issues / Considerations
* **`PredictionHandler` Complexity:** Will require careful implementation of classification/rule population logic.
* **Performance:** Prediction logic needs to remain performant (threading).
* **Rule Structure Completeness:** Ensure `SourceRule` dataclasses hold all necessary *overridable* fields.
* **Preset Loading:** Robust preset loading/interpretation needed in `PredictionHandler`.
* **Static Config Loading:** Ensure the new `ProcessingEngine` correctly loads and uses the static `Configuration` object.
## 6. Documentation
This document (`ProjectNotes/Data_Flow_Refinement_Plan.md`) serves as the architectural plan. Relevant sections of the Developer Guide will need updating upon implementation.

View File

@@ -1,118 +0,0 @@
# Data Interface: GUI Preview Edits to Processing Handler
## 1. Purpose
This document defines the data structures and interface used to pass user edits made in the GUI's file preview table (specifically changes to 'Status' and 'Predicted Asset' output name) to the backend `ProcessingHandler`. It also incorporates a structure for future asset-level properties.
## 2. Data Structures
Two primary data structures are used:
### 2.1. File Data (`file_list`)
* **Type:** `list[dict]`
* **Description:** A flat list where each dictionary represents a single file identified during the prediction phase. This list is modified by the GUI to reflect user edits to file status and output names.
* **Dictionary Keys (per file):**
* `original_path` (`str`): The full path to the source file. (Read-only by GUI)
* `predicted_asset_name` (`str | None`): The name of the asset group the file belongs to, derived from input. (Read-only by GUI)
* `predicted_output_name` (`str | None`): The backend's predicted final output filename. **EDITABLE** by the user in the GUI ('Predicted Asset' column).
* `status` (`str`): The backend's predicted status (e.g., 'Mapped', 'Ignored'). **EDITABLE** by the user in the GUI ('Status' column).
* `details` (`str | None`): Additional information or error messages. (Read-only by GUI, potentially updated by model validation).
* `source_asset` (`str`): An identifier for the source asset group (e.g., input folder/zip name). (Read-only by GUI)
### 2.2. Asset Properties (`asset_properties`)
* **Type:** `dict[str, dict]`
* **Description:** A dictionary mapping the `source_asset` identifier (string key) to a dictionary of asset-level properties determined by the backend prediction/preset. This structure is initially read-only in the GUI but designed for future expansion (e.g., editing asset category).
* **Asset Properties Dictionary Keys (Example):**
* `asset_category` (`str`): The determined category (e.g., 'surface', 'model').
* `asset_tags` (`list[str]`): Any relevant tags associated with the asset.
* *(Other future asset-level properties can be added here)*
## 3. Data Flow & Interface
```mermaid
graph LR
subgraph Backend
A[Prediction Logic] -- Generates --> B(file_list);
A -- Generates --> C(asset_properties);
end
subgraph GUI Components
D[PredictionHandler] -- prediction_results_ready(file_list, asset_properties) --> E(PreviewTableModel);
E -- Stores & Allows Edits --> F(Internal file_list);
E -- Stores --> G(Internal asset_properties);
H[MainWindow] -- Retrieves --> F;
H -- Retrieves --> G;
H -- Passes (file_list, asset_properties) --> I[ProcessingHandler];
end
subgraph Backend
I -- Uses Edited --> F;
I -- Uses Read-Only --> G;
end
style A fill:#lightblue,stroke:#333,stroke-width:1px
style B fill:#lightgreen,stroke:#333,stroke-width:1px
style C fill:#lightyellow,stroke:#333,stroke-width:1px
style D fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#ccf,stroke:#333,stroke-width:2px
style F fill:#lightgreen,stroke:#333,stroke-width:1px
style G fill:#lightyellow,stroke:#333,stroke-width:1px
style H fill:#f9f,stroke:#333,stroke-width:2px
style I fill:#lightblue,stroke:#333,stroke-width:2px
```
1. **Prediction:** The `PredictionHandler` generates both the `file_list` and `asset_properties`.
2. **Signal:** It emits a signal (e.g., `prediction_results_ready`) containing both structures, likely as a tuple `(file_list, asset_properties)`.
3. **Table Model:** The `PreviewTableModel` receives the tuple. It stores `asset_properties` (read-only for now). It stores `file_list` and allows user edits to the `status` and `predicted_output_name` values within this list.
4. **Processing Trigger:** When the user initiates processing, the `MainWindow` retrieves the (potentially modified) `file_list` and the (unmodified) `asset_properties` from the `PreviewTableModel`.
5. **Processing Execution:** The `MainWindow` passes both structures to the `ProcessingHandler`.
6. **Handler Logic:** The `ProcessingHandler` iterates through the `file_list`. For each file, it uses the potentially edited `status` and `predicted_output_name`. If asset-level information is needed, it uses the file's `source_asset` key to look up the data in the `asset_properties` dictionary.
## 4. Example Data Passed to `ProcessingHandler`
* **`file_list` (Example):**
```python
[
{
'original_path': 'C:/Path/To/AssetA/AssetA_Diffuse.png',
'predicted_asset_name': 'AssetA',
'predicted_output_name': 'T_AssetA_BC.tga',
'status': 'Ignored', # <-- User Edit
'details': 'User override: Ignored',
'source_asset': 'AssetA'
},
{
'original_path': 'C:/Path/To/AssetA/AssetA_Normal.png',
'predicted_asset_name': 'AssetA',
'predicted_output_name': 'T_AssetA_Normals_DX.tga', # <-- User Edit
'status': 'Mapped',
'details': None,
'source_asset': 'AssetA'
},
# ... other files
]
```
* **`asset_properties` (Example):**
```python
{
'AssetA': {
'asset_category': 'surface',
'asset_tags': ['wood', 'painted']
},
'AssetB': {
'asset_category': 'model',
'asset_tags': ['metal', 'sci-fi']
}
# ... other assets
}
```
## 5. Implications
* **`PredictionHandler`:** Needs modification to generate and emit both `file_list` and `asset_properties`. Signal signature changes.
* **`PreviewTableModel`:** Needs modification to receive, store, and provide both structures. Must implement editing capabilities (`flags`, `setData`) for the relevant columns using the `file_list`. Needs methods like `get_edited_data()` returning both structures.
* **`MainWindow`:** Needs modification to retrieve both structures from the table model and pass them to the `ProcessingHandler`.
* **`ProcessingHandler`:** Needs modification to accept both structures in its processing method signature. Must update logic to use the edited `status` and `predicted_output_name` from `file_list` and look up data in `asset_properties` using `source_asset` when needed.

View File

@@ -1,37 +0,0 @@
# FEAT-003: Selective Nodegroup Generation and Category Tagging - Implementation Plan
**Objective:** Modify `blenderscripts/create_nodegroups.py` to read the asset category from `metadata.json`, conditionally create nodegroups for "Surface" and "Decal" assets, and add the category as a tag to the Blender asset.
**Plan:**
1. **Modify `blenderscripts/create_nodegroups.py`:**
* Locate the main loop in `blenderscripts/create_nodegroups.py` that iterates through the processed assets.
* Inside this loop, for each asset directory, construct the path to the `metadata.json` file.
* Read the `metadata.json` file using Python's `json` module.
* Extract the `category` value from the parsed JSON data.
* Implement a conditional check: If the extracted `category` is *not* "Surface" and *not* "Decal", skip the existing nodegroup creation logic for this asset and proceed to the tagging step.
* If the `category` *is* "Surface" or "Decal", execute the existing nodegroup creation logic.
* After the conditional nodegroup creation (or skipping), use the Blender Python API (`bpy`) to find the corresponding Blender asset (likely the created node group, if applicable, or potentially the asset representation even if the nodegroup was skipped).
* Add the extracted `category` string as a tag to the found Blender asset.
2. **Testing:**
* Prepare a test set of processed assets that includes examples of "Surface", "Decal", and "Asset" categories, each with a corresponding `metadata.json` file.
* Run the modified `create_nodegroups.py` script within Blender, pointing it to the test asset library root.
* Verify in Blender that:
* Node groups were created only for the "Surface" and "Decal" assets.
* No node groups were created for "Asset" category assets.
* All processed assets (Surface, Decal, and Asset) have a tag corresponding to their category ("Surface", "Decal", or "Asset") in the Blender asset browser.
```mermaid
graph TD
A[Start Script] --> B{Iterate Assets};
B --> C[Read metadata.json];
C --> D[Get Category];
D --> E{Category in ["Surface", "Decal"]?};
E -- Yes --> F[Create Nodegroup];
E -- No --> G[Skip Nodegroup];
F --> H[Find Blender Asset];
G --> H[Find Blender Asset];
H --> I[Add Category Tag];
I --> B;
B -- No More Assets --> J[End Script];

View File

@@ -1,49 +0,0 @@
# Plan for Adding .rar and .7z Support
**Goal:** Extend the Asset Processor Tool to accept `.rar` and `.7z` files as input sources, in addition to the currently supported `.zip` files and folders.
**Plan:**
1. **Add Required Libraries:**
* Update the `requirements.txt` file to include `py7zr` and `rarfile` as dependencies. This will ensure these libraries are installed when setting up the project.
2. **Modify Input Extraction Logic:**
* Locate the `_extract_input` method within the `AssetProcessor` class in `asset_processor.py`.
* Modify this method to check the file extension of the input source.
* If the extension is `.zip`, retain the existing extraction logic using Python's built-in `zipfile` module.
* If the extension is `.rar`, implement extraction using the `rarfile` library.
* If the extension is `.7z`, implement extraction using the `py7zr` library.
* Include error handling for cases where the archive might be corrupted, encrypted (since we are not implementing password support at this stage, these should likely be skipped or logged as errors), or uses an unsupported compression method. Log appropriate warnings or errors in such cases.
* If the input is a directory, retain the existing logic to copy its contents to the temporary workspace.
3. **Update CLI and Monitor Input Handling:**
* Review `main.py` (CLI entry point) and `monitor.py` (Directory Monitor).
* Ensure that the argument parsing in `main.py` can accept `.rar` and `.7z` file paths as valid inputs.
* In `monitor.py`, modify the `ZipHandler` (or create a new handler) to watch for `.rar` and `.7z` file creation events in the watched directory, in addition to `.zip` files. The logic for triggering processing via `main.run_processing` should then be extended to handle these new file types.
4. **Update Documentation:**
* Edit `Documentation/00_Overview.md` to explicitly mention `.rar` and `.7z` as supported input formats in the overview section.
* Update `Documentation/01_User_Guide/02_Features.md` to list `.rar` and `.7z` alongside `.zip` and folders in the features list.
* Modify `Documentation/01_User_Guide/03_Installation.md` to include instructions for installing the new `py7zr` and `rarfile` dependencies (likely via `pip install -r requirements.txt`).
* Revise `Documentation/02_Developer_Guide/05_Processing_Pipeline.md` to accurately describe the updated `_extract_input` method, detailing how `.zip`, `.rar`, `.7z`, and directories are handled.
5. **Testing:**
* Prepare sample `.rar` and `.7z` files (including nested directories and various file types) to test the extraction logic thoroughly.
* Test processing of these new archive types via both the CLI and the Directory Monitor.
* Verify that the subsequent processing steps (classification, map processing, metadata generation, etc.) work correctly with files extracted from `.rar` and `.7z` archives.
Here is a simplified flow diagram illustrating the updated input handling:
```mermaid
graph TD
A[Input Source] --> B{Is it a file or directory?};
B -- Directory --> C[Copy Contents to Workspace];
B -- File --> D{What is the file extension?};
D -- .zip --> E[Extract using zipfile];
D -- .rar --> F[Extract using rarfile];
D -- .7z --> G[Extract using py7zr];
E --> H[Temporary Workspace];
F --> H;
G --> H;
C --> H;
H --> I[Processing Pipeline Starts];

View File

@@ -1,51 +0,0 @@
# Plan: Implement "Force Lossless" Format for Specific Map Types
**Goal:** Modify the asset processor to ensure specific map types ("NRM", "DISP") are always saved in a lossless format (PNG or EXR based on bit depth), overriding the JPG threshold and input format rules. This rule should apply to both individually processed maps and merged maps.
**Steps:**
1. **Add Configuration Setting (`config.py`):**
* Introduce a new list named `FORCE_LOSSLESS_MAP_TYPES` in `config.py`.
* Populate this list: `FORCE_LOSSLESS_MAP_TYPES = ["NRM", "DISP"]`.
2. **Expose Setting in `Configuration` Class (`configuration.py`):**
* Add a default value for `FORCE_LOSSLESS_MAP_TYPES` in the `_load_core_config` method's `default_core_settings` dictionary.
* Add a new property to the `Configuration` class to access this list:
```python
@property
def force_lossless_map_types(self) -> list:
"""Gets the list of map types that must always be saved losslessly."""
return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])
```
3. **Modify `_process_maps` Method (`asset_processor.py`):**
* Locate the section determining the output format (around line 805).
* **Before** the `if output_bit_depth == 8 and target_dim >= threshold:` check (line 811), insert the new logic:
* Check if the current `map_type` is in `self.config.force_lossless_map_types`.
* If yes, determine the appropriate lossless format (`png` or configured 16-bit format like `exr`) based on `output_bit_depth`, set `output_format`, `output_ext`, `save_params`, and `needs_float16` accordingly, and skip the subsequent `elif` / `else` blocks for format determination.
* Use an `elif` for the existing JPG threshold check and the final `else` for the rule-based logic, ensuring they only run if `force_lossless` is false.
4. **Modify `_merge_maps` Method (`asset_processor.py`):**
* Locate the section determining the output format for the merged map (around line 1151).
* **Before** the `if output_bit_depth == 8 and target_dim >= threshold:` check (line 1158), insert similar logic as in step 3:
* Check if the `output_map_type` (the type of the *merged* map) is in `self.config.force_lossless_map_types`.
* If yes, determine the appropriate lossless format based on the merged map's `output_bit_depth`, set `output_format`, `output_ext`, `save_params`, and `needs_float16`, and skip the subsequent `elif` / `else` blocks.
* Use `elif` and `else` for the existing threshold and hierarchy logic.
**Process Flow Diagram:**
```mermaid
graph TD
subgraph Format Determination (per resolution, _process_maps & _merge_maps)
A[Start] --> B(Get map_type / output_map_type);
B --> C{Determine output_bit_depth};
C --> D{Is map_type in FORCE_LOSSLESS_MAP_TYPES?};
D -- Yes --> E[Set format = Lossless (PNG/EXR based on bit_depth)];
D -- No --> F{Is output_bit_depth == 8 AND target_dim >= threshold?};
F -- Yes --> G[Set format = JPG];
F -- No --> H[Set format based on input/hierarchy/rules];
G --> I[Set Save Params];
H --> I;
E --> I;
I --> J[Save Image];
end

View File

@@ -1,17 +0,0 @@
Here is a summary of our session focused on adding OpenEXR support:
Goal:
Enable the Asset Processor Tool to reliably write .exr files for 16-bit maps, compatible with Windows executables and Docker.
Approach:
We decided to use the dedicated openexr Python library for writing EXR files, rather than compiling a custom OpenCV build. The plan involved modifying asset_processor.py to use this library, updating the Dockerfile with necessary system libraries, and outlining steps for PyInstaller on Windows.
Issues & Fixes:
Initial Changes: Modified asset_processor.py and Dockerfile.
NameError: name 'log' is not defined: Fixed by moving the logger initialization earlier in asset_processor.py.
AttributeError: module 'Imath' has no attribute 'Header' & NameError: name 'fallback_fmt' is not defined: Attempted fixes by changing Imath.Header to OpenEXR.Header and correcting one instance of fallback_fmt.
TypeError: __init__(): incompatible constructor arguments... Invoked with: HALF & NameError: name 'fallback_fmt' is not defined (again): Corrected the OpenEXR.Channel constructor call and fixed the remaining fallback_fmt typo.
concurrent.futures.process.BrokenProcessPool: The script crashed when attempting the EXR save, likely due to an internal OpenEXR library error or a Windows dependency issue.
Next Steps (when resuming):
Investigate the BrokenProcessPool error, possibly by testing EXR saving with a simple array or verifying Windows dependencies for the OpenEXR library.

View File

@@ -1,52 +0,0 @@
# GUI Blender Integration Plan
## Goal
Add a checkbox and input fields to the GUI (`gui/main_window.py`) to enable/disable Blender script execution and specify the `.blend` file paths, defaulting to `config.py` values. Integrate this control into the processing logic (`gui/processing_handler.py`).
## Proposed Plan
1. **Modify `gui/main_window.py`:**
* Add a `QCheckBox` (e.g., `self.blender_integration_checkbox`) to the processing panel layout.
* Add two pairs of `QLineEdit` widgets and `QPushButton` browse buttons for the nodegroup `.blend` path (`self.nodegroup_blend_path_input`, `self.browse_nodegroup_blend_button`) and the materials `.blend` path (`self.materials_blend_path_input`, `self.browse_materials_blend_button`).
* Initialize the text of the `QLineEdit` widgets by reading the `DEFAULT_NODEGROUP_BLEND_PATH` and `DEFAULT_MATERIALS_BLEND_PATH` values from `config.py` when the GUI starts.
* Connect signals from the browse buttons to new methods that open a `QFileDialog` to select `.blend` files and update the corresponding input fields.
* Modify the slot connected to the "Start Processing" button to:
* Read the checked state of `self.blender_integration_checkbox`.
* Read the text from `self.nodegroup_blend_path_input` and `self.materials_blend_path_input`.
* Pass these three pieces of information (checkbox state, nodegroup path, materials path) to the `ProcessingHandler` when initiating the processing task.
2. **Modify `gui/processing_handler.py`:**
* Add parameters to the method that starts the processing (likely `start_processing`) to accept the Blender integration flag (boolean), the nodegroup `.blend` path (string), and the materials `.blend` path (string).
* Implement the logic for finding the Blender executable (reading `BLENDER_EXECUTABLE_PATH` from `config.py` or checking PATH) within `processing_handler.py`.
* Implement the logic for executing the Blender scripts using `subprocess.run` within `processing_handler.py`. This logic should be similar to the `run_blender_script` function added to `main.py` in the previous step.
* Ensure this Blender script execution logic is conditional based on the received integration flag and runs *after* the main asset processing (handled by the worker pool) is complete.
## Execution Flow Diagram (GUI)
```mermaid
graph TD
A[GUI: User Clicks Start Processing] --> B{Blender Integration Checkbox Checked?};
B -- Yes --> C[Get Blend File Paths from Input Fields];
C --> D[Pass Paths and Flag to ProcessingHandler];
D --> E[ProcessingHandler: Start Asset Processing (Worker Pool)];
E --> F[ProcessingHandler: Asset Processing Complete];
F --> G{Blender Integration Flag True?};
G -- Yes --> H[ProcessingHandler: Find Blender Executable];
H --> I{Blender Executable Found?};
I -- Yes --> J{Nodegroup Blend Path Valid?};
J -- Yes --> K[ProcessingHandler: Run Nodegroup Script in Blender];
K --> L{Script Successful?};
L -- Yes --> M{Materials Blend Path Valid?};
L -- No --> N[ProcessingHandler: Report Nodegroup Error];
N --> M;
M -- Yes --> O[ProcessingHandler: Run Materials Script in Blender];
O --> P{Script Successful?};
P -- Yes --> Q[ProcessingHandler: Report Completion];
P -- No --> R[ProcessingHandler: Report Materials Error];
R --> Q;
M -- No --> Q;
J -- No --> M[Skip Nodegroup Script];
I -- No --> Q[Skip Blender Scripts];
G -- No --> Q;
B -- No --> E;

View File

@@ -1,43 +0,0 @@
# GUI Enhancement Plan
## Objective
Implement two new features in the Graphical User Interface (GUI) of the Asset Processor Tool:
1. Automatically switch the preview to "simple view" when more than 10 input files (ZIPs or folders) are added to the queue.
2. Remove the specific visual area labeled "Drag and drop folders here" while keeping the drag-and-drop functionality active for the main processing panel.
## Implementation Plan
The changes will be made in the `gui/main_window.py` file.
1. **Implement automatic preview switch:**
* Locate the `add_input_paths` method.
* After adding the `newly_added_paths` to `self.current_asset_paths`, check the total number of items in `self.current_asset_paths`.
* If the count is greater than 10, programmatically set the state of the "Disable Detailed Preview" menu action (`self.toggle_preview_action`) to `checked=True`. This will automatically trigger the `update_preview` method, which will then render the simple list view.
2. **Remove the "Drag and drop folders here" visual area:**
* Locate the `setup_main_panel_ui` method.
* Find the creation of the `self.drag_drop_area` QFrame and its associated QLabel (`drag_drop_label`).
* Add a line after the creation of `self.drag_drop_area` to hide this widget (`self.drag_drop_area.setVisible(False)`). This will remove the visual box and label while keeping the drag-and-drop functionality enabled for the main window.
## Workflow Diagram
```mermaid
graph TD
A[User drops files/folders] --> B{Call add_input_paths}
B --> C[Add paths to self.current_asset_paths]
C --> D{Count items in self.current_asset_paths}
D{Count > 10?} -->|Yes| E[Set toggle_preview_action.setChecked(True)]
D{Count > 10?} -->|No| F[Keep current preview state]
E --> G[update_preview triggered]
F --> G[update_preview triggered]
G --> H{Check toggle_preview_action state}
H{Checked (Simple)?} -->|Yes| I[Display simple list in preview_table]
H{Checked (Simple)?} -->|No| J[Run PredictionHandler for detailed preview]
J --> K[Display detailed results in preview_table]
L[GUI Initialization] --> M[Call setup_main_panel_ui]
M --> N[Create drag_drop_area QFrame]
N --> O[Hide drag_drop_area QFrame]
O --> P[Main window accepts drops]
P --> B

View File

@@ -1,103 +0,0 @@
# GUI Feature Enhancement Plan
**Overall Goal:** Modify the GUI (`gui/main_window.py`, `gui/prediction_handler.py`) to make the output path configurable, improve UI responsiveness during preview generation, and add a toggle to switch between detailed file preview and a simple input path list.
**Detailed Plan:**
1. **Feature: Configurable Output Path**
* **File:** `gui/main_window.py`
* **Changes:**
* **UI Addition:**
* Below the `preset_combo` layout, add a new `QHBoxLayout`.
* Inside this layout, add:
* A `QLabel` with text "Output Directory:".
* A `QLineEdit` (e.g., `self.output_path_edit`) to display/edit the path. Make it read-only initially if preferred, or editable.
* A `QPushButton` (e.g., `self.browse_output_button`) with text "Browse...".
* **Initialization (`__init__` or `setup_main_panel_ui`):**
* Read the default `OUTPUT_BASE_DIR` from `core_config`.
* Resolve this path relative to the project root (`project_root / output_base_dir_config`).
* Set the initial text of `self.output_path_edit` to this resolved default path.
* **Browse Button Logic:**
* Connect the `clicked` signal of `self.browse_output_button` to a new method (e.g., `_browse_for_output_directory`).
* Implement `_browse_for_output_directory`:
* Use `QFileDialog.getExistingDirectory` to let the user select a folder.
* If a directory is selected, update the text of `self.output_path_edit`.
* **Processing Logic (`start_processing`):**
* Instead of reading/resolving the path from `core_config`, get the path string directly from `self.output_path_edit.text()`.
* Convert this string to a `Path` object.
* **Add Validation:** Before passing the path to the handler, check if the directory exists. If not, attempt to create it using `output_dir.mkdir(parents=True, exist_ok=True)`. Handle potential `OSError` exceptions during creation and show an error message if it fails. Also, consider adding a basic writability check if possible.
* Pass the validated `output_dir_str` to `self.processing_handler.run_processing`.
2. **Feature: Responsive UI (Address Prediction Bottleneck)**
* **File:** `gui/prediction_handler.py`
* **Changes:**
* **Import:** Add `from concurrent.futures import ThreadPoolExecutor, as_completed`.
* **Modify `run_prediction`:**
* Inside the `try` block (after loading `config`), create a `ThreadPoolExecutor` (e.g., `with ThreadPoolExecutor(max_workers=...) as executor:`). Determine a reasonable `max_workers` count (e.g., `os.cpu_count() // 2` or a fixed number like 4 or 8).
* Instead of iterating through `input_paths` sequentially, submit a task to the executor for each `input_path_str`.
* The task submitted should be a helper method (e.g., `_predict_single_asset`) that takes `input_path_str` and the loaded `config` object as arguments.
* `_predict_single_asset` will contain the logic currently inside the loop: instantiate `AssetProcessor`, call `get_detailed_file_predictions`, handle exceptions, and return the list of prediction dictionaries for that *single* asset (or an error dictionary).
* Store the `Future` objects returned by `executor.submit`.
* Use `as_completed(futures)` to process results as they become available.
* Append the results from each completed future to the `all_file_results` list.
* Emit `prediction_results_ready` once at the very end with the complete `all_file_results` list.
* **File:** `gui/main_window.py`
* **Changes:**
* No changes needed in the `on_prediction_results_ready` slot itself, as the handler will still emit the full list at the end.
3. **Feature: Preview Toggle**
* **File:** `gui/main_window.py`
* **Changes:**
* **UI Addition:**
* Add a `QCheckBox` (e.g., `self.disable_preview_checkbox`) with text "Disable Detailed Preview". Place it logically, perhaps near the `overwrite_checkbox` or above the `preview_table`. Set its default state to unchecked.
* **Modify `update_preview`:**
* At the beginning of the method, check `self.disable_preview_checkbox.isChecked()`.
* **If Checked (Simple View):**
* Clear the `preview_table`.
* Set simplified table headers (e.g., `self.preview_table.setColumnCount(1); self.preview_table.setHorizontalHeaderLabels(["Input Path"])`). Adjust column resize modes.
* Iterate through `self.current_asset_paths`. For each path, add a row to the table containing just the path string.
* Set status bar message (e.g., "Preview disabled. Showing input list.").
* **Crucially:** `return` from the method here to prevent the `PredictionHandler` from being started.
* **If Unchecked (Detailed View):**
* Ensure the table headers and column count are set back to the detailed view configuration (Status, Original Path, Predicted Name, Details).
* Continue with the rest of the existing `update_preview` logic to start the `PredictionHandler`.
* **Connect Signal:** In `__init__` or `setup_main_panel_ui`, connect the `toggled` signal of `self.disable_preview_checkbox` to the `self.update_preview` slot.
* **Initial State:** Ensure the first call to `update_preview` (if any) respects the initial unchecked state of the checkbox.
**Mermaid Diagram:**
```mermaid
graph TD
subgraph MainWindow
A[User Action: Add Asset / Change Preset / Toggle Preview] --> B{Update Preview Triggered};
B --> C{Is 'Disable Preview' Checked?};
C -- Yes --> D[Show Simple List View in Table];
C -- No --> E[Set Detailed Table Headers];
E --> F[Start PredictionHandler Thread];
F --> G[PredictionHandler Runs];
G --> H[Slot: Populate Table with Detailed Results];
I[User Clicks Start Processing] --> J{Get Output Path from UI LineEdit};
J --> K[Validate/Create Output Path];
K -- Path OK --> L[Start ProcessingHandler Thread];
K -- Path Error --> M[Show Error Message];
L --> N[ProcessingHandler Runs];
N --> O[Update UI (Progress, Status)];
P[User Clicks Browse...] --> Q[Show QFileDialog];
Q --> R[Update Output Path LineEdit];
end
subgraph PredictionHandler [Background Thread]
style PredictionHandler fill:#f9f,stroke:#333,stroke-width:2px
F --> S{Use ThreadPoolExecutor};
S --> T[Run _predict_single_asset Concurrently];
T --> U[Collect Results];
U --> V[Emit prediction_results_ready (Full List)];
V --> H;
end
subgraph ProcessingHandler [Background Thread]
style ProcessingHandler fill:#ccf,stroke:#333,stroke-width:2px
L --> N;
end

View File

@@ -1,78 +0,0 @@
# GUI Log Console Feature Plan
**Overall Goal:** Add a log console panel to the GUI's editor panel, controlled by a "View" menu action. Move the "Disable Detailed Preview" control to the same "View" menu.
**Detailed Plan:**
1. **Create Custom Log Handler:**
* **New File/Location:** Potentially add this to a new `gui/log_handler.py` or keep it within `gui/main_window.py` if simple enough.
* **Implementation:**
* Define a class `QtLogHandler(logging.Handler, QObject)` that inherits from both `logging.Handler` and `QObject` (for signals).
* Add a Qt signal, e.g., `log_record_received = Signal(str)`.
* Override the `emit(self, record)` method:
* Format the log record using `self.format(record)`.
* Emit the `log_record_received` signal with the formatted string.
2. **Modify `gui/main_window.py`:**
* **Imports:** Add `QMenuBar`, `QMenu`, `QAction` from `PySide6.QtWidgets`. Import the new `QtLogHandler`.
* **UI Elements (`__init__` / `setup_editor_panel_ui`):**
* **Menu Bar:**
* Create `self.menu_bar = self.menuBar()`.
* Create `view_menu = self.menu_bar.addMenu("&View")`.
* **Log Console:**
* Create `self.log_console_output = QTextEdit()`. Set it to read-only (`self.log_console_output.setReadOnly(True)`).
* Create a container widget, e.g., `self.log_console_widget = QWidget()`. Create a layout for it (e.g., `QVBoxLayout`) and add `self.log_console_output` to this layout.
* In `setup_editor_panel_ui`, insert `self.log_console_widget` into the `editor_layout` *before* adding the `list_layout` (the preset list).
* Initially hide the console: `self.log_console_widget.setVisible(False)`.
* **Menu Actions:**
* Create `self.toggle_log_action = QAction("Show Log Console", self, checkable=True)`. Connect `self.toggle_log_action.toggled.connect(self._toggle_log_console_visibility)`. Add it to `view_menu`.
* Create `self.toggle_preview_action = QAction("Disable Detailed Preview", self, checkable=True)`. Connect `self.toggle_preview_action.toggled.connect(self.update_preview)`. Add it to `view_menu`.
* **Remove Old Checkbox:** Delete the lines creating and adding `self.disable_preview_checkbox`.
* **Logging Setup (`__init__`):**
* Instantiate the custom handler: `self.log_handler = QtLogHandler()`.
* Connect its signal: `self.log_handler.log_record_received.connect(self._append_log_message)`.
* Add the handler to the logger: `log.addHandler(self.log_handler)`. Set an appropriate level if needed (e.g., `self.log_handler.setLevel(logging.INFO)`).
* **New Slots:**
* Implement `_toggle_log_console_visibility(self, checked)`: This slot will simply call `self.log_console_widget.setVisible(checked)`.
* Implement `_append_log_message(self, message)`:
* Append the `message` string to `self.log_console_output`.
* Optional: Add logic to limit the number of lines in the text edit to prevent performance issues.
* Optional: Add basic HTML formatting for colors based on log level.
* **Modify `update_preview`:**
* Replace the check for `self.disable_preview_checkbox.isChecked()` with `self.toggle_preview_action.isChecked()`.
* Update the log messages within this method to reflect checking the action state.
**Mermaid Diagram:**
```mermaid
graph TD
subgraph MainWindow
A[Initialization] --> B(Create Menu Bar);
B --> C(Add View Menu);
C --> D(Add 'Show Log Console' Action);
C --> E(Add 'Disable Detailed Preview' Action);
A --> F(Create Log Console QTextEdit);
F --> G(Place Log Console Widget in Layout [Hidden]);
A --> H(Create & Add QtLogHandler);
H --> I(Connect Log Handler Signal to _append_log_message);
D -- Toggled --> J[_toggle_log_console_visibility];
J --> K(Show/Hide Log Console Widget);
E -- Toggled --> L[update_preview];
M[update_preview] --> N{Is 'Disable Preview' Action Checked?};
N -- Yes --> O[Show Simple List View];
N -- No --> P[Start PredictionHandler];
Q[Any Log Message] -- Emitted by Logger --> H;
I --> R[_append_log_message];
R --> S(Append Message to Log Console QTextEdit);
end
subgraph QtLogHandler
style QtLogHandler fill:#lightgreen,stroke:#333,stroke-width:2px
T1[emit(record)] --> T2(Format Record);
T2 --> T3(Emit log_record_received Signal);
T3 --> I;
end

View File

@@ -1,65 +0,0 @@
# GUI Overhaul Plan: Unified Hierarchical View
**Task:** Implement a UI overhaul for the Asset Processor Tool GUI to address usability issues and streamline the workflow for viewing and editing processing rules.
**Context:**
* A hierarchical rule system (`SourceRule`, `AssetRule`, `FileRule` in `rule_structure.py`) is used by the core engine (`asset_processor.py`).
* The current GUI (`gui/main_window.py`, `gui/rule_hierarchy_model.py`, `gui/rule_editor_widget.py`) uses a `QTreeView` for hierarchy, a separate `RuleEditorWidget` for editing selected items, and a `QTableView` (`PreviewTableModel`) for previewing file classifications.
* Relevant files analyzed: `gui/main_window.py`, `gui/rule_editor_widget.py`, `gui/rule_hierarchy_model.py`.
**Identified Issues with Current UI:**
1. **Window Resizing:** Selecting Source/Asset items causes window expansion because `RuleEditorWidget` displays large child lists (`assets`, `files`) as simple labels.
2. **GUI Not Updating on Add:** Potential regression where adding new inputs doesn't reliably update the preview/hierarchy.
3. **Incorrect Source Display:** Tree view shows "Source: None" instead of the input path (likely `SourceRule.input_path` is None when model receives it).
4. **Preview Table Stale:** Changes made in `RuleEditorWidget` (e.g., overrides) are not reflected in the `PreviewTableModel` because the `_on_rule_updated` slot in `main_window.py` doesn't trigger a refresh.
**Agreed-Upon Overhaul Plan:**
The goal is to create a more unified and streamlined experience by merging the hierarchy, editing overrides, and preview aspects into a single view, reducing redundancy.
1. **UI Structure Redesign:**
* **Left Panel:** Retain the existing Preset Editor panel (`main_window.py`'s `editor_panel`) for managing preset files (`.json`) and their complex rules (naming patterns, map type mappings, archetype rules, etc.).
* **Right Panel:** Replace the current three-part splitter (Hierarchy Tree, Rule Editor, Preview Table) with a **single Unified Hierarchical View**.
* Implementation: Use a `QTreeView` with a custom `QAbstractItemModel` and custom `QStyledItemDelegate`s for inline editing.
* Hierarchy Display: Show Input Source(s) -> Assets -> Files.
* Visual Cues: Use distinct background colors for rows representing Inputs, Assets, and Files.
2. **Unified View Columns & Functionality:**
* **Column 1: Name/Hierarchy:** Displays input path, asset name, or file name with indentation.
* **Column 2+: Editable Attributes (Context-Dependent):** Implement inline editors using delegates:
* **Input Row:** Optional editable field for `Supplier` override.
* **Asset Row:** `QComboBox` delegate for `Asset-Type` override (e.g., `GENERIC`, `DECAL`, `MODEL`).
* **File Row:**
* `QLineEdit` delegate for `Target Asset Name` override.
* `QComboBox` delegate for `Item-Type` override (e.g., `MAP-COL`, `MAP-NRM`, `EXTRA`, `MODEL_FILE`).
* **Column X: Status (Optional, Post-Processing):** Non-editable column showing processing status icon/text (Pending, Success, Warning, Error).
* **Column Y: Output Path (Optional, Post-Processing):** Non-editable column showing the final output path after successful processing.
3. **Data Flow and Initialization:**
* When inputs are added and a preset selected, `PredictionHandler` runs.
* `PredictionHandler` generates the `SourceRule` hierarchy *and* predicts initial `Asset-Type`, `Item-Type`, and `Target Asset Name`.
* The Unified View's model is populated with this `SourceRule`.
* *Initial values* in inline editors are set based on these *predicted* values.
* User edits in the Unified View directly modify attributes on the `SourceRule`, `AssetRule`, or `FileRule` objects held by the model.
4. **Dropdown Options Source:**
* Available options in dropdowns (`Asset-Type`, `Item-Type`) should be sourced from globally defined lists or Enums (e.g., in `rule_structure.py` or `config.py`).
5. **Addressing Original Issues (How the Plan Fixes Them):**
* **Window Resizing:** Resolved by removing `RuleEditorWidget`.
* **GUI Not Updating on Add:** Fix requires ensuring `add_input_paths` triggers `PredictionHandler` and updates the new Unified View model correctly.
* **Incorrect Source Display:** Fix requires ensuring `PredictionHandler` correctly populates `SourceRule.input_path`.
* **Preview Table Stale:** Resolved by merging preview/editing; edits are live in the main view.
**Implementation Tasks:**
* Modify `gui/main_window.py`: Remove the right-side splitter, `RuleEditorWidget`, `PreviewTableModel`/`View`. Instantiate the new Unified View. Adapt `add_input_paths`, `start_processing`, `_on_rule_hierarchy_ready`, etc., to interact with the new view/model.
* Create/Modify Model (`gui/rule_hierarchy_model.py` or new file): Implement a `QAbstractItemModel` supporting multiple columns, hierarchical data, and providing data/flags for inline editing.
* Create Delegates (`gui/delegates.py`?): Implement `QStyledItemDelegate` subclasses for `QComboBox` and `QLineEdit` editors in the tree view.
* Modify `gui/prediction_handler.py`: Ensure it predicts initial override values (`Asset-Type`, `Item-Type`, `Target Asset Name`) and includes them in the data passed back to the main window (likely within the `SourceRule` structure or alongside it). Ensure `SourceRule.input_path` is correctly set.
* Modify `gui/processing_handler.py`: Update it to potentially signal back status/output path updates that can be reflected in the new Unified View model's optional columns.
* Define Dropdown Sources: Add necessary Enums or lists to `rule_structure.py` or `config.py`.
This plan provides a clear path forward for implementing the UI overhaul.

View File

@@ -1,119 +0,0 @@
# Asset Processor GUI Development Plan
This document outlines the plan for developing a Graphical User Interface (GUI) for the Asset Processor Tool.
**I. Foundation & Framework Choice**
1. **Choose GUI Framework:** PySide6 (LGPL license, powerful features).
2. **Project Structure:** Create `gui/` directory. Core logic remains separate.
3. **Dependencies:** Add `PySide6` to `requirements.txt`.
**II. Core GUI Layout & Basic Functionality**
1. **Main Window:** `gui/main_window.py`.
2. **Layout Elements:**
* Drag & Drop Area (`QWidget` subclass).
* Preset Dropdown (`QComboBox`).
* Preview Area (`QListWidget`).
* Progress Bar (`QProgressBar`).
* Control Buttons (`QPushButton`: Start, Cancel, Manage Presets).
* Status Bar (`QStatusBar`).
**III. Input Handling & Predictive Preview**
1. **Connect Drag & Drop:** Validate drops, add valid paths to Preview List.
2. **Refactor for Prediction:** Create/refactor methods in `AssetProcessor`/`Configuration` to predict output names without full processing.
3. **Implement Preview Update:** On preset change or file add, load config, call prediction logic, update Preview List items (e.g., "Input: ... -> Output: ...").
4. **Responsive Preview:** Utilize PySide6 list widget efficiency (consider model/view for very large lists).
**IV. Processing Integration & Progress Reporting**
1. **Adapt Processing Logic:** Refactor `main.py`'s `ProcessPoolExecutor` loop into a callable function/class (`gui/processing_handler.py`).
2. **Background Execution:** Run processing logic in a `QThread` to keep GUI responsive.
3. **Progress Updates (Signals & Slots):**
* Background thread emits signals: `progress_update(current, total)`, `file_status_update(path, status, msg)`, `processing_finished(stats)`.
* Main window connects slots to these signals to update UI elements (`QProgressBar`, `QListWidget` items, status bar).
4. **Completion/Error Handling:** Re-enable controls, display summary stats, report errors on `processing_finished`.
**V. Preset Management Interface (Sub-Task)**
1. **New Window/Dialog:** `gui/preset_editor.py`.
2. **Functionality:** List, load, edit (tree view/form), create, save/save as presets (`.json` in `presets/`). Basic validation.
**VI. Refinements & Additional Features (Ideas)**
1. **Cancel Button:** Implement cancellation signal to background thread/workers.
2. **Log Viewer:** Add `QTextEdit` to display `logging` output.
3. **Output Directory Selection:** Add browse button/field.
4. **Configuration Options:** Expose key `config.py` settings.
5. **Clearer Error Display:** Tooltips, status bar updates, or error panel.
**VII. Packaging (Deployment)**
1. **Tooling:** PyInstaller or cx_Freeze.
2. **Configuration:** Build scripts (`.spec`) to bundle code, dependencies, assets, and `presets/`.
**High-Level Mermaid Diagram:**
```mermaid
graph TD
subgraph GUI Application (PySide6)
A[Main Window] --> B(Drag & Drop Area);
A --> C(Preset Dropdown);
A --> D(Preview List Widget);
A --> E(Progress Bar);
A --> F(Start Button);
A -- Contains --> SB(Status Bar);
A --> CB(Cancel Button);
A --> MB(Manage Presets Button);
B -- fileDroppedSignal --> X(Handle Input Files);
C -- currentIndexChangedSignal --> Y(Update Preview);
X -- Adds paths --> D;
X -- Triggers --> Y;
Y -- Reads --> C;
Y -- Uses --> J(Prediction Logic);
Y -- Updates --> D;
F -- clickedSignal --> Z(Start Processing);
CB -- clickedSignal --> AA(Cancel Processing);
MB -- clickedSignal --> L(Preset Editor Dialog);
Z -- Starts --> BB(Processing Thread: QThread);
AA -- Signals --> BB;
BB -- progressSignal(curr, total) --> E;
BB -- fileStatusSignal(path, status, msg) --> D;
BB -- finishedSignal(stats) --> AB(Handle Processing Finished);
AB -- Updates --> SB;
AB -- Enables/Disables --> F;
AB -- Enables/Disables --> CB;
AB -- Enables/Disables --> C;
AB -- Enables/Disables --> B;
L -- Modifies --> H(Presets Dir: presets/*.json);
end
subgraph Backend Logic (Existing + Refactored)
H -- Loaded by --> C;
H -- Loaded/Saved by --> L;
J -- Reads --> M(configuration.py / asset_processor.py);
BB -- Runs --> K(Adapted main.py Logic);
K -- Uses --> N(ProcessPoolExecutor);
N -- Runs --> O(process_single_asset_wrapper);
O -- Uses --> M;
O -- Reports Status --> K;
K -- Reports Progress --> BB;
end
classDef gui fill:#f9f,stroke:#333,stroke-width:2px;
classDef backend fill:#ccf,stroke:#333,stroke-width:2px;
classDef thread fill:#ffc,stroke:#333,stroke-width:1px;
class A,B,C,D,E,F,CB,MB,L,SB,X,Y,Z,AA,AB gui;
class H,J,K,M,N,O backend;
class BB thread;
```
This plan provides a roadmap for the GUI development.

View File

@@ -1,42 +0,0 @@
# GUI Preset Selection Plan
## Objective
Modify the GUI so that no preset is selected by default, and the preview table is only populated after the user explicitly selects a preset. This aims to prevent accidental processing with an unintended preset and clearly indicate to the user that a preset selection is required for preview.
## Plan
1. **Modify `gui/main_window.py`:**
* Remove the logic that selects a default preset during initialization.
* Initialize the preview table to display the text "please select a preset".
* Disable the mechanism that triggers the `PredictionHandler` for preview generation until a preset is selected.
* Update the slot connected to the preset selection signal:
* When a preset is selected, clear the placeholder text and enable the preview generation mechanism.
* Pass the selected preset configuration to the `PredictionHandler`.
* Trigger the `PredictionHandler` to generate and display the preview.
* (Optional but recommended) Add logic to handle the deselection of a preset, which should clear the preview table and display the "please select a preset" text again, and disable preview generation.
2. **Review `gui/prediction_handler.py`:**
* Verify that the `PredictionHandler`'s methods that generate predictions (`get_detailed_file_predictions`) correctly handle being called only when a valid preset is provided. No major changes are expected here, but it's good practice to confirm.
3. **Update Preview Table Handling (`gui/preview_table_model.py` and `gui/main_window.py`):**
* Ensure the `PreviewTableModel` can gracefully handle having no data when no preset is selected.
* In `gui/main_window.py`, configure the `QTableView` or its parent widget to display the placeholder text "please select a preset" when the model is empty or no preset is active.
## Data Flow Change
The current data flow for preview generation is roughly:
Initialization -> Default Preset Loaded -> Trigger PredictionHandler -> Update Preview Table
The proposed data flow would be:
Initialization -> No Preset Selected -> Preview Table Empty/Placeholder -> User Selects Preset -> Trigger PredictionHandler with Selected Preset -> Update Preview Table
```mermaid
graph TD
A[GUI Initialization] --> B{Is a Preset Selected?};
B -- Yes (Current) --> C[Load Default Preset];
B -- No (Proposed) --> D[Preview Table Empty/Placeholder];
C --> E[Trigger PredictionHandler];
D --> F[User Selects Preset];
F --> E;
E --> G[Update Preview Table];

View File

@@ -1,59 +0,0 @@
# Plan: Implement Alternating Row Colors Per Asset Group in GUI Preview Table
## Objective
Modify the GUI preview table to display alternating background colors for rows based on the asset group they belong to, rather than alternating colors for each individual row. The visual appearance should be similar to the default alternating row colors (dark greys, no border, no rounded corners).
## Current State
The preview table in the GUI uses a `QTableView` with `setAlternatingRowColors(True)` enabled, which applies alternating background colors based on the row index. The `PreviewTableModel` groups file prediction data by `source_asset` in its internal `_table_rows` structure and provides data to the view.
## Proposed Plan
To achieve alternating colors per asset group, we will implement custom coloring logic within the `PreviewTableModel`.
1. **Disable Default Alternating Colors:**
* In `gui/main_window.py`, locate the initialization of the `preview_table_view` (a `QTableView`).
* Change `self.preview_table_view.setAlternatingRowColors(True)` to `self.preview_table_view.setAlternatingRowColors(False)`.
2. **Modify `PreviewTableModel.data()`:**
* Open `gui/preview_table_model.py`.
* In the `data()` method, add a case to handle the `Qt.ItemDataRole.BackgroundRole`.
* Inside this case, retrieve the `source_asset` for the current row from the `self._table_rows` structure.
* Maintain a sorted list of unique `source_asset` values. This can be done when the data is set in `set_data()`.
* Find the index of the current row's `source_asset` within this sorted list.
* Based on whether the index is even or odd, return a `QColor` object representing one of the two desired grey colors.
* Ensure that the `Qt.ItemDataRole.BackgroundRole` is handled correctly for all columns in the row.
3. **Define Colors:**
* Define two `QColor` objects within the `PreviewTableModel` class to represent the two grey colors for alternating groups. These should be chosen to be visually similar to the default alternating row colors.
## Visual Representation of Data Flow with Custom Coloring
```mermaid
graph TD
A[QTableView] --> B{Requests Data for Row/Column};
B --> C[PreviewSortFilterProxyModel];
C --> D[PreviewTableModel];
D -- data(index, role) --> E{Check Role};
E -- Qt.ItemDataRole.BackgroundRole --> F{Get source_asset for row};
F --> G{Determine Asset Group Index};
G --> H{Assign Color based on Index Parity};
H --> I[Return QColor];
E -- Other Roles --> J[Return Display/Tooltip/Foreground Data};
I --> C;
J --> C;
C --> A{Displays Data with Custom Background Color};
style D fill:#f9f,stroke:#333,stroke-width:2px
style H fill:#ccf,stroke:#333,stroke-width:1px
style I fill:#ccf,stroke:#333,stroke-width:1px
```
## Implementation Steps (for Code Mode)
1. Modify `gui/main_window.py` to disable default alternating row colors.
2. Modify `gui/preview_table_model.py` to:
* Define the two grey `QColor` objects.
* Update `set_data()` to create and store a sorted list of unique asset groups.
* Implement the `Qt.ItemDataRole.BackgroundRole` logic in the `data()` method to return alternating colors based on the asset group index.

View File

@@ -1,49 +0,0 @@
# Plan: Enhance GUI Preview Table Coloring
## Objective
Modify the GUI preview table to apply status-based text coloring to all relevant cells in a row, providing a more consistent visual indication of a file's status.
## Current State
The `PreviewTableModel` in `gui/preview_table_model.py` currently applies status-based text colors only to the "Status" column (based on the main file's status) and the "Additional Files" column (based on the additional file's status). Other cells in the row do not have status-based coloring.
## Proposed Change
Extend the status-based text coloring logic in the `PreviewTableModel`'s `data()` method to apply the relevant status color to any cell that corresponds to either the main file or an additional file in that row.
## Plan
1. **Modify the `data()` method in `gui/preview_table_model.py`:**
* Locate the section handling the `Qt.ItemDataRole.ForegroundRole`.
* Currently, this section checks the column index (`col`) to decide which file's status to use for coloring (main file for `COL_STATUS`, additional file for `COL_ADDITIONAL_FILES`).
* We will change this logic to determine which file (main or additional) the *current row and column* corresponds to, and then use that file's status to look up the color.
* For columns related to the main file (`COL_STATUS`, `COL_PREDICTED_ASSET`, `COL_ORIGINAL_PATH`, `COL_PREDICTED_OUTPUT`, `COL_DETAILS`), if the row contains a `main_file`, use the `main_file`'s status for coloring.
* For the `COL_ADDITIONAL_FILES` column, if the row contains `additional_file_details`, use the `additional_file_details`' status for coloring.
* If a cell does not correspond to a file (e.g., a main file column in a row that only has an additional file), return `None` for the `ForegroundRole` to use the default text color.
## Detailed Steps
1. Open `gui/preview_table_model.py`.
2. Navigate to the `data()` method.
3. Find the `if role == Qt.ItemDataRole.ForegroundRole:` block.
4. Inside this block, modify the logic to determine the `status` variable based on the current `col` and the presence of `main_file` or `additional_file_details` in `row_data`.
5. Use the determined `status` to look up the color in `self.STATUS_COLORS`.
6. Return the color if found, otherwise return `None`.
## Modified Color Logic Flow
```mermaid
graph TD
A[data(index, role)] --> B{role == Qt.ItemDataRole.ForegroundRole?};
B -- Yes --> C{Determine relevant file for cell (row, col)};
C -- Cell corresponds to Main File --> D{Get main_file status};
C -- Cell corresponds to Additional File --> E{Get additional_file_details status};
C -- Cell is empty --> F[status = None];
D --> G{Lookup color in STATUS_COLORS};
E --> G;
F --> H[Return None];
G -- Color found --> I[Return Color];
G -- No color found --> H;
B -- No --> J[Handle other roles];
J --> K[Return data based on role];

View File

@@ -1,90 +0,0 @@
# GUI Preview Table Restructure Plan
## Objective
Restructure the Graphical User Interface (GUI) preview table to group files by source asset and display "Ignored" and "Extra" files in a new "Additional Files" column, aligned with the mapped files of the same asset.
## Analysis
Based on the review of `gui/prediction_handler.py` and `gui/preview_table_model.py`:
* The `PredictionHandler` provides a flat list of file prediction dictionaries.
* The `PreviewTableModel` currently stores and displays this flat list directly.
* The `PreviewSortFilterProxyModel` sorts this flat list.
* The data transformation to achieve the desired grouped layout must occur within the `PreviewTableModel`.
## Proposed Plan
1. **Modify `gui/preview_table_model.py`:**
* **Add New Column:**
* Define a new constant: `COL_ADDITIONAL_FILES = 5`.
* Add "Additional Files" to the `_headers_detailed` list.
* **Introduce New Internal Data Structure:**
* Create a new internal list, `self._table_rows`, to store dictionaries representing the final rows to be displayed in the table.
* **Update `set_data(self, data: list)`:**
* Process the incoming flat `data` list (received from `PredictionHandler`).
* Group file dictionaries by their `source_asset`.
* Within each asset group, separate files into two lists:
* `main_files`: Files with status "Mapped", "Model", or "Error".
* `additional_files`: Files with status "Ignored", "Extra", "Unrecognised", or "Unmatched Extra".
* Determine the maximum number of rows needed for this asset block: `max(len(main_files), len(additional_files))`.
* Build the row dictionaries for `self._table_rows` for this asset block:
* For `i` from 0 to `max_rows - 1`:
* Get the `i`-th file from `main_files` (or `None` if `i` is out of bounds).
* Get the `i`-th file from `additional_files` (or `None` if `i` is out of bounds).
* Create a row dictionary containing:
* `source_asset`: The asset name.
* `predicted_asset`: From the `main_file` (if exists).
* `details`: From the `main_file` (if exists).
* `original_path`: From the `main_file` (if exists).
* `additional_file_path`: Path from the `additional_file` (if exists).
* `additional_file_details`: The original dictionary of the `additional_file` (if exists, for tooltips).
* `is_main_row`: Boolean flag (True if this row corresponds to a file in `main_files`, False otherwise).
* Append these row dictionaries to `self._table_rows`.
* After processing all assets, call `self.beginResetModel()` and `self.endResetModel()`.
* **Update `rowCount`:** Return `len(self._table_rows)` when in detailed mode.
* **Update `columnCount`:** Return `len(self._headers_detailed)`.
* **Update `data(self, index, role)`:**
* Retrieve the row dictionary: `row_data = self._table_rows[index.row()]`.
* For `Qt.ItemDataRole.DisplayRole`:
* If `index.column()` is `COL_ADDITIONAL_FILES`, return `row_data.get('additional_file_path', '')`.
* For other columns (`COL_STATUS`, `COL_PREDICTED_ASSET`, `COL_ORIGINAL_PATH`, `COL_DETAILS`), return data from the `main_file` part of `row_data` if `row_data['is_main_row']` is True, otherwise return an empty string or appropriate placeholder.
* For `Qt.ItemDataRole.ToolTipRole`:
* If `index.column()` is `COL_ADDITIONAL_FILES` and `row_data.get('additional_file_details')` exists, generate a tooltip using the status and details from `additional_file_details`.
* For other columns, use the existing tooltip logic based on the `main_file` data.
* For `Qt.ItemDataRole.ForegroundRole`:
* Apply existing color-coding based on the status of the `main_file` if `row_data['is_main_row']` is True.
* For the `COL_ADDITIONAL_FILES` cell and for rows where `row_data['is_main_row']` is False, use neutral styling (default text color).
* **Update `headerData`:** Return the correct header for `COL_ADDITIONAL_FILES`.
2. **Modify `gui/preview_table_model.py` (`PreviewSortFilterProxyModel`):**
* **Update `lessThan(self, left, right)`:**
* Retrieve the row dictionaries for `left` and `right` indices from the source model (`model._table_rows[left.row()]`, etc.).
* **Level 1: Source Asset:** Compare `source_asset` from the row dictionaries.
* **Level 2: Row Type:** If assets are the same, compare `is_main_row` (True sorts before False).
* **Level 3 (Main Rows):** If both are main rows (`is_main_row` is True), compare `original_path`.
* **Level 4 (Additional-Only Rows):** If both are additional-only rows (`is_main_row` is False), compare `additional_file_path`.
## Clarifications & Decisions
* **Error Handling:** "Error" files will remain in the main columns, similar to "Mapped" files, with their current "Error" status.
* **Sorting within Asset:** The proposed sorting logic within an asset block is acceptable (mapped rows by original path, additional-only rows by additional file path).
* **Styling of Additional Column:** Use neutral text and background styling for the "Additional Files" column, relying on tooltips for specific file details.
## Mermaid Diagram (Updated Data Flow)
```mermaid
graph LR
A[PredictionHandler] -- prediction_results_ready(flat_list) --> B(PreviewTableModel);
subgraph PreviewTableModel
C[set_data] -- Processes flat_list --> D{Internal Grouping & Transformation};
D -- Creates --> E[_table_rows (Structured List)];
F[data()] -- Reads from --> E;
end
B -- Provides data via data() --> G(QTableView via Proxy);
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:1px
style D fill:#lightgrey,stroke:#333,stroke-width:1px
style E fill:#ccf,stroke:#333,stroke-width:1px
style F fill:#ccf,stroke:#333,stroke-width:1px

View File

@@ -1,123 +0,0 @@
# Asset Processor GUI Refactor Plan
This document outlines the plan to refactor the Asset Processor GUI based on user requirements.
## Goals
1. **Improve File Visibility:** Display all files found within an asset in the preview list, including those that don't match the preset, are moved to 'Extra', or have errors, along with their status.
2. **Integrate Preset Editor:** Move the preset editing functionality from the separate dialog into a collapsible panel within the main window.
## Goal 1: Improve File Visibility in Preview List
**Problem:** The current preview (`PredictionHandler` calling `AssetProcessor.predict_output_structure`) only shows files that successfully match a map type rule and get a predicted output name. It doesn't show files that are ignored, moved to 'Extra', or encounter errors during classification.
**Solution:** Leverage the more comprehensive classification logic already present in `AssetProcessor._inventory_and_classify_files` for the GUI preview.
**Plan Steps:**
1. **Modify `asset_processor.py`:**
* Create a new method in `AssetProcessor`, perhaps named `get_detailed_file_predictions()`.
* This new method will perform the core steps of `_setup_workspace()`, `_extract_input()`, and `_inventory_and_classify_files()`.
* It will then iterate through *all* categories in `self.classified_files` ('maps', 'models', 'extra', 'ignored').
* For each file, it will determine a 'status' (e.g., "Mapped", "Model", "Extra", "Ignored", "Error") and attempt to predict the output name (similar to `predict_output_structure` for maps, maybe just the original name for others).
* It will return a more detailed list of dictionaries, each containing: `{'original_path': str, 'predicted_name': str | None, 'status': str, 'details': str | None}`.
* Crucially, this method will *not* perform the actual processing (`_process_maps`, `_merge_maps`, etc.) or file moving, only the classification and prediction. It should also include cleanup (`_cleanup_workspace`).
2. **Modify `gui/prediction_handler.py`:**
* Update `PredictionHandler.run_prediction` to call the new `AssetProcessor.get_detailed_file_predictions()` method instead of `predict_output_structure()`.
* Adapt the code that processes the results to handle the new dictionary format (including the 'status' and 'details' fields).
* Emit this enhanced list via the `prediction_results_ready` signal.
3. **Modify `gui/main_window.py`:**
* In `setup_ui`, add a new column to `self.preview_table` for "Status". Adjust column count and header labels.
* In `on_prediction_results_ready`, populate the new "Status" column using the data received from `PredictionHandler`.
* Consider adding tooltips to the status column to show the 'details' (e.g., the reason for being ignored or moved to extra).
* Optionally, use background colors or icons in the status column for better visual distinction.
## Goal 2: Integrate Preset Editor into Main Window
**Problem:** Preset editing requires opening a separate modal dialog, interrupting the main workflow.
**Solution:** Embed the preset editing controls directly into the main window within a collapsible panel.
**Plan Steps:**
1. **Modify `gui/main_window.py` - UI Changes:**
* Remove the "Manage Presets" button (`self.manage_presets_button`).
* Add a collapsible panel (potentially using a `QFrame` with show/hide logic triggered by a button, or a `QDockWidget` if more appropriate) to the left side of the main layout.
* Inside this panel:
* Add the `QListWidget` for displaying presets (`self.preset_list`).
* Add the "New" and "Delete" buttons below the list. (The "Load" button becomes implicit - selecting a preset in the list loads it into the editor).
* Recreate the `QTabWidget` (`self.preset_editor_tabs`) with the "General & Naming" and "Mapping & Rules" tabs.
* Recreate *all* the widgets currently inside the `PresetEditorDialog` tabs (QLineEdit, QTextEdit, QSpinBox, QListWidget+controls, QTableWidget+controls) within the corresponding tabs in the main window's panel. Give them appropriate instance names (e.g., `self.editor_preset_name`, `self.editor_supplier_name`, etc.).
* Add "Save" and "Save As..." buttons within the collapsible panel, likely at the bottom.
2. **Modify `gui/main_window.py` - Logic Integration:**
* Adapt the `populate_presets` method to populate the new `self.preset_list` in the panel.
* Connect `self.preset_list.currentItemChanged` to a new method `load_selected_preset_for_editing`. This method will handle checking for unsaved changes in the editor panel and then load the selected preset's data into the editor widgets (similar to `PresetEditorDialog.load_preset`).
* Implement `save_preset`, `save_preset_as`, `new_preset`, `delete_preset` methods directly within `MainWindow`, adapting the logic from `PresetEditorDialog`. These will interact with the editor widgets in the panel.
* Implement `check_unsaved_changes` logic for the editor panel, prompting the user if they try to load/create/delete a preset or close the application with unsaved edits in the panel.
* Connect the editor widgets' change signals (`textChanged`, `valueChanged`, `itemChanged`, etc.) to a `mark_editor_unsaved` method in `MainWindow`.
* Ensure the main preset selection `QComboBox` (`self.preset_combo`) is repopulated when presets are saved/deleted via the editor panel.
3. **Cleanup:**
* Delete the `gui/preset_editor_dialog.py` file.
* Remove imports and references to `PresetEditorDialog` from `gui/main_window.py`.
## Visual Plan
**Current Layout:**
```mermaid
graph TD
subgraph "Current Main Window Layout"
A[Preset Combo + Manage Button] --> B(Drag & Drop Area);
B --> C{File Preview Table};
C --> D[Progress Bar];
D --> E[Options + Start/Cancel Buttons];
end
subgraph "Current Preset Editor (Separate Dialog)"
F[Preset List + Load/New/Delete] --> G{Tab Widget};
subgraph "Tab Widget"
G1[General & Naming Tab]
G2[Mapping & Rules Tab]
end
G --> H[Save / Save As / Close Buttons];
end
A -- Manage Button Click --> F;
style F fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#f9f,stroke:#333,stroke-width:2px
style H fill:#f9f,stroke:#333,stroke-width:2px
```
**Proposed Layout:**
```mermaid
graph TD
subgraph "Proposed Main Window Layout"
direction LR
subgraph "Collapsible Preset Editor Panel (Left)"
P_List[Preset List] --> P_Buttons[New / Delete Buttons]
P_Buttons --> P_Tabs{Tab Widget}
subgraph "Editor Tabs"
P_Tab1[General & Naming]
P_Tab2[Mapping & Rules]
end
P_Tabs --> P_Save[Save / Save As Buttons]
end
subgraph "Main Area (Right)"
M_Preset[Preset Combo (for processing)] --> M_DragDrop(Drag & Drop Area)
M_DragDrop --> M_Preview{File Preview Table (with Status Column)}
M_Preview --> M_Progress[Progress Bar]
M_Progress --> M_Controls[Options + Start/Cancel Buttons]
end
P_List -- Selection Loads --> P_Tabs;
P_Save -- Updates --> P_List;
P_List -- Updates --> M_Preset;
style M_Preview fill:#ccf,stroke:#333,stroke-width:2px
style P_List fill:#cfc,stroke:#333,stroke-width:2px
style P_Tabs fill:#cfc,stroke:#333,stroke-width:2px
style P_Save fill:#cfc,stroke:#333,stroke-width:2px
end

View File

@@ -1,63 +0,0 @@
# Plan: Update GUI Preview Status
**Objective:** Modify the Asset Processor GUI preview to distinguish between files explicitly marked as "Extra" by preset patterns and those that are simply unclassified.
**Current Statuses:**
* Mapped
* Ignored
* Extra (Includes both explicitly matched and unclassified files)
**Proposed Statuses:**
* Mapped
* Ignored
* Extra (Files explicitly matched by `move_to_extra_patterns` in the preset)
* Unrecognised (Files not matching any map, model, or explicit extra pattern)
**Visual Plan:**
```mermaid
graph TD
A[Start: User Request] --> B{Analyze Request: Split 'Extra' status};
B --> C{Info Gathering};
C --> D[Read gui/prediction_handler.py];
D --> E[Read asset_processor.py];
E --> F[Read Presets/Poliigon.json];
F --> G[Read gui/main_window.py];
G --> H{Identify Code Locations};
H --> I[asset_processor.py: get_detailed_file_predictions()];
H --> J[gui/main_window.py: on_prediction_results_ready()];
I --> K{Plan Code Changes};
J --> K;
K --> L[Modify asset_processor.py: Differentiate status based on 'reason'];
K --> M[Modify gui/main_window.py: Add color rule for 'Unrecognised' (#92371f)];
L --> N{Final Plan};
M --> N;
N --> O[Present Plan to User];
O --> P{User Approval + Color Choice};
P --> Q[Switch to Code Mode for Implementation];
subgraph "Code Modification"
L
M
end
subgraph "Information Gathering"
D
E
F
G
end
```
**Implementation Steps:**
1. **Modify `asset_processor.py` (`get_detailed_file_predictions` method):**
* Locate the loop processing the `self.classified_files["extra"]` list (around line 1448).
* Inside this loop, check the `reason` associated with each file:
* If `reason == 'Unclassified'`, set the output `status` to `"Unrecognised"`.
* Otherwise (if the reason indicates an explicit pattern match), set the output `status` to `"Extra"`.
* Adjust the `details` string provided in the output for clarity (e.g., show pattern match reason for "Extra", maybe just "[Unrecognised]" for the new status).
2. **Modify `gui/main_window.py` (`on_prediction_results_ready` method):**
* Locate the section where text color is applied based on the `status` (around line 673).
* Add a new `elif` condition to handle `status == "Unrecognised"` and assign it the color `QColor("#92371f")`.

View File

@@ -1,194 +0,0 @@
# Implementation Plan: GUI User-Friendliness Enhancements
This document outlines the plan for implementing three key GUI improvements for the Asset Processor Tool, focusing on user-friendliness and workflow efficiency.
**Target Audience:** Developers implementing these features.
**Status:** Planning Phase
## Feature 1: Editable Asset Name
**Goal:** Allow users to edit the name of an asset directly in the main view, and automatically update the 'Target Asset' field of all associated child files to reflect the new name.
**Affected Components:**
* `gui/unified_view_model.py` (`UnifiedViewModel`)
* `gui/delegates.py` (`LineEditDelegate`)
* `gui/main_window.py` (or view setup location)
* `rule_structure.py` (`AssetRule`, `FileRule`)
* Potentially a new handler or modifications to `gui/asset_restructure_handler.py`
**Implementation Steps:**
1. **Enable Editing in Model (`UnifiedViewModel`):**
* Modify `flags()`: For an index pointing to an `AssetRule`, return `Qt.ItemIsEditable` in addition to default flags when `index.column()` is `COL_NAME`.
* Modify `setData()`:
* Add logic to handle `isinstance(item, AssetRule)` and `column == self.COL_NAME`.
* Get the `new_asset_name` from the `value`.
* **Validation:** Before proceeding, check if an `AssetRule` with `new_asset_name` already exists within the same parent `SourceRule`. If so, log a warning and return `False` to prevent duplicate names.
* Store the `old_asset_name = item.asset_name`.
* If `new_asset_name` is valid and different from `old_asset_name`:
* Update `item.asset_name = new_asset_name`.
* Set `changed = True`.
* **Crucial - Child Update:** Iterate through *all* `SourceRule`s, `AssetRule`s, and `FileRule`s in the model (`self._source_rules`). For each `FileRule` found where `file_rule.target_asset_name_override == old_asset_name`, update `file_rule.target_asset_name_override = new_asset_name`. Emit `dataChanged` for the `COL_TARGET_ASSET` index of each modified `FileRule`. (See Potential Challenges regarding performance).
* Emit `dataChanged` for the edited `AssetRule`'s `COL_NAME` index.
* Return `changed`.
* **(Alternative Signal Approach):** Instead of performing the child update directly in `setData`, emit a new signal like `assetNameChanged = Signal(QModelIndex, str, str)` carrying the `AssetRule` index, old name, and new name. A dedicated handler would connect to this signal to perform the child updates. This improves separation of concerns.
2. **Assign Delegate (`main_window.py` / View Setup):**
* Ensure the `LineEditDelegate` is assigned to the view for the `COL_NAME` using `view.setItemDelegateForColumn(UnifiedViewModel.COL_NAME, line_edit_delegate_instance)`.
3. **Handling Child Updates (if using Signal Approach):**
* Create a new handler class (e.g., `AssetNameChangeHandler`) or add a slot to `AssetRestructureHandler`.
* Connect the `UnifiedViewModel.assetNameChanged` signal to this slot.
* The slot receives the `AssetRule` index, old name, and new name. It iterates through the model's `FileRule`s, updates their `target_asset_name_override` where it matches the old name, and emits `dataChanged` for those files.
**Data Model Impact:**
* `AssetRule.asset_name` becomes directly mutable via the GUI.
* The relationship between files and their intended parent asset (represented by `FileRule.target_asset_name_override`) is maintained automatically when the parent asset's name changes.
**Potential Challenges/Considerations:**
* **Performance:** The child update logic requires iterating through potentially all files in the model. For very large datasets, this could be slow. Consider optimizing by maintaining an index/lookup map (`Dict[str, List[FileRule]]`) mapping target asset override names to the list of `FileRule`s using them. This map would need careful updating whenever overrides change or files are moved.
* **Duplicate Asset Names:** The plan includes basic validation in `setData`. Robust handling (e.g., user feedback, preventing the edit) is needed.
* **Undo/Redo:** Reversing an asset name change requires reverting the name *and* reverting all the child `target_asset_name_override` changes, adding complexity.
* **Scope of Child Update:** The current plan updates *any* `FileRule` whose override matches the old name. Confirm if this update should be restricted only to files originally under the renamed asset or within the same `SourceRule`. The current approach seems most logical based on how `target_asset_name_override` works.
```mermaid
sequenceDiagram
participant User
participant View
participant LineEditDelegate
participant UnifiedViewModel
participant AssetNameChangeHandler
User->>View: Edits AssetRule Name in COL_NAME
View->>LineEditDelegate: setModelData(editor, model, index)
LineEditDelegate->>UnifiedViewModel: setData(index, new_name, EditRole)
UnifiedViewModel->>UnifiedViewModel: Validate new_name (no duplicates)
UnifiedViewModel->>UnifiedViewModel: Update AssetRule.asset_name
alt Signal Approach
UnifiedViewModel->>AssetNameChangeHandler: emit assetNameChanged(index, old_name, new_name)
AssetNameChangeHandler->>UnifiedViewModel: Iterate through FileRules
loop For each FileRule where target_override == old_name
AssetNameChangeHandler->>UnifiedViewModel: Update FileRule.target_asset_name_override = new_name
UnifiedViewModel->>View: emit dataChanged(file_rule_target_index)
end
else Direct Approach in setData
UnifiedViewModel->>UnifiedViewModel: Iterate through FileRules
loop For each FileRule where target_override == old_name
UnifiedViewModel->>UnifiedViewModel: Update FileRule.target_asset_name_override = new_name
UnifiedViewModel->>View: emit dataChanged(file_rule_target_index)
end
end
UnifiedViewModel->>View: emit dataChanged(asset_rule_name_index)
```
## Feature 2: Item Type Field Conversion
**Goal:** Replace the `QComboBox` delegate for the "Item Type" column (for `FileRule`s) with a `QLineEdit` that provides auto-suggestions based on defined file types, similar to the existing "Supplier" field.
**Affected Components:**
* `gui/main_window.py` (or view setup location)
* `gui/delegates.py` (Requires a new delegate)
* `gui/unified_view_model.py` (`UnifiedViewModel`)
* `config/app_settings.json` (Source of file type definitions)
**Implementation Steps:**
1. **Create New Delegate (`delegates.py`):**
* Create a new class `ItemTypeSearchDelegate(QStyledItemDelegate)`.
* **`createEditor(self, parent, option, index)`:**
* Create a `QLineEdit` instance.
* Get the list of valid item type keys: `item_keys = index.model()._file_type_keys` (add error handling).
* Create a `QCompleter` using `item_keys` and set it on the `QLineEdit` (configure case sensitivity, filter mode, completion mode as in `SupplierSearchDelegate`).
* Return the editor.
* **`setEditorData(self, editor, index)`:**
* Get the current value using `index.model().data(index, Qt.EditRole)`.
* Set the editor's text (`editor.setText(str(value) if value is not None else "")`).
* **`setModelData(self, editor, model, index)`:**
* Get the `final_text = editor.text().strip()`.
* Determine the `value_to_set = final_text if final_text else None`.
* Call `model.setData(index, value_to_set, Qt.EditRole)`.
* **Important:** Unlike `SupplierSearchDelegate`, do *not* add `final_text` to the list of known types or save anything back to config. Suggestions are strictly based on `config/app_settings.json`.
* **`updateEditorGeometry(self, editor, option, index)`:**
* Standard implementation: `editor.setGeometry(option.rect)`.
2. **Assign Delegate (`main_window.py` / View Setup):**
* Instantiate the new `ItemTypeSearchDelegate`.
* Find where delegates are set for the view.
* Replace the `ComboBoxDelegate` assignment for `UnifiedViewModel.COL_ITEM_TYPE` with the new `ItemTypeSearchDelegate` instance: `view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, item_type_search_delegate_instance)`.
**Data Model Impact:**
* None. The underlying data (`FileRule.item_type_override`) and its handling remain the same. Only the GUI editor changes.
**Potential Challenges/Considerations:**
* None significant. This is a relatively straightforward replacement of one delegate type with another, leveraging existing patterns from `SupplierSearchDelegate` and data loading from `UnifiedViewModel`.
## Feature 3: Drag-and-Drop File Re-parenting
**Goal:** Enable users to drag one or more `FileRule` rows and drop them onto an `AssetRule` row to change the parent asset of the dragged files.
**Affected Components:**
* `gui/main_panel_widget.py` or `gui/main_window.py` (View management)
* `gui/unified_view_model.py` (`UnifiedViewModel`)
**Implementation Steps:**
1. **Enable Drag/Drop in View (`main_panel_widget.py` / `main_window.py`):**
* Get the `QTreeView` instance (`view`).
* `view.setSelectionMode(QAbstractItemView.ExtendedSelection)` (Allow selecting multiple files)
* `view.setDragEnabled(True)`
* `view.setAcceptDrops(True)`
* `view.setDropIndicatorShown(True)`
* `view.setDefaultDropAction(Qt.MoveAction)`
* `view.setDragDropMode(QAbstractItemView.InternalMove)`
2. **Implement Drag/Drop Support in Model (`UnifiedViewModel`):**
* **`flags(self, index)`:**
* Modify to include `Qt.ItemIsDragEnabled` if `index.internalPointer()` is a `FileRule`.
* Modify to include `Qt.ItemIsDropEnabled` if `index.internalPointer()` is an `AssetRule`.
* Return the combined flags.
* **`supportedDropActions(self)`:**
* Return `Qt.MoveAction`.
* **`mimeData(self, indexes)`:**
* Create `QMimeData`.
* Encode information about the dragged rows (which must be `FileRule`s). Store a list of tuples, each containing `(source_parent_row, source_parent_col, source_row)` for each valid `FileRule` index in `indexes`. Use a custom MIME type (e.g., `"application/x-filerule-index-list"`).
* Return the `QMimeData`.
* **`canDropMimeData(self, data, action, row, column, parent)`:**
* Check if `action == Qt.MoveAction`.
* Check if `data.hasFormat("application/x-filerule-index-list")`.
* Check if `parent.isValid()` and `parent.internalPointer()` is an `AssetRule`.
* Return `True` if all conditions met, `False` otherwise.
* **`dropMimeData(self, data, action, row, column, parent)`:**
* Check `action` and MIME type again for safety.
* Get the target `AssetRule` item: `target_asset = parent.internalPointer()`. If not an `AssetRule`, return `False`.
* Decode the `QMimeData` to get the list of source index information.
* Create a list `files_to_move = []` containing the actual `QModelIndex` objects for the source `FileRule`s (reconstruct them using the decoded info and `self.index()`).
* Iterate through `files_to_move`:
* Get the `source_file_index`.
* Get the `file_item = source_file_index.internalPointer()`.
* Get the `old_parent_asset = getattr(file_item, 'parent_asset', None)`.
* If `target_asset != old_parent_asset`:
* Call `self.moveFileRule(source_file_index, parent)`. This handles the actual move within the model structure and emits `beginMoveRows`/`endMoveRows`.
* **After successful move:** Update the file's override: `file_item.target_asset_name_override = target_asset.asset_name`.
* Emit `self.dataChanged.emit(moved_file_index, moved_file_index, [Qt.DisplayRole, Qt.EditRole])` for the `COL_TARGET_ASSET` column of the *now moved* file (get its new index).
* **Cleanup:** After the loop, identify any original parent `AssetRule`s that became empty as a result of the moves. Call `self.removeAssetRule(empty_asset_rule)` for each.
* Return `True`.
**Data Model Impact:**
* Changes the parentage of `FileRule` items within the model's internal structure.
* Updates `FileRule.target_asset_name_override` to match the `asset_name` of the new parent `AssetRule`, ensuring consistency between the visual structure and the override field.
**Potential Challenges/Considerations:**
* **MIME Data Encoding/Decoding:** Ensure the index information is reliably encoded and decoded, especially handling potential model changes between drag start and drop. Using persistent IDs instead of row/column numbers might be more robust if available.
* **Cleanup Logic:** Reliably identifying and removing empty parent assets after potentially moving multiple files from different original parents requires careful tracking.
* **Transactionality:** If moving multiple files and one part fails, should the whole operation roll back? The current plan doesn't explicitly handle this; errors are logged, and subsequent steps might proceed.
* **Interaction with `AssetRestructureHandler`:** The plan suggests handling the move and override update directly within `dropMimeData`. This means the existing `AssetRestructureHandler` won't be triggered by the override change *during* the drop. Ensure the cleanup logic (removing empty parents) is correctly handled either in `dropMimeData` or by ensuring `moveFileRule` emits signals that the handler *can* use for cleanup.

View File

@@ -1,34 +0,0 @@
# Plan to Resolve ISSUE-011: Blender nodegroup script creates empty assets for skipped items
**Issue:** The Blender nodegroup creation script (`blenderscripts/create_nodegroups.py`) creates empty asset entries in the target .blend file for assets belonging to categories that the script is designed to skip, even though it correctly identifies them as skippable.
**Root Cause:** The script creates the parent node group, marks it as an asset, and applies tags *before* checking if the asset category is one that should be skipped for full nodegroup generation.
**Plan:**
1. **Analyze `blenderscripts/create_nodegroups.py` (Completed):** Confirmed that parent group creation and asset marking occur before the asset category skip check.
2. **Modify `blenderscripts/create_nodegroups.py`:**
* Relocate the code block responsible for creating/updating the parent node group, marking it as an asset, and applying tags (currently lines 605-645) to *after* the conditional check `if asset_category not in CATEGORIES_FOR_NODEGROUP_GENERATION:` (line 646).
* This ensures that if an asset's category is in the list of categories to be skipped, the `continue` statement will be hit before any actions are taken to create the parent asset entry in the Blender file.
3. **Testing:**
* Use test assets that represent both categories that *should* and *should not* result in full nodegroup generation based on the `CATEGORIES_FOR_NODEGROUP_GENERATION` list.
* Run the asset processor with these test assets, ensuring the Blender script is executed.
* Inspect the resulting `.blend` file to confirm:
* No `PBRSET_` node groups are created for assets belonging to skipped categories.
* `PBRSET_` node groups are correctly created and populated for assets belonging to categories in `CATEGORIES_FOR_NODEGROUP_GENERATION`.
4. **Update Ticket Status:**
* Once the fix is implemented and verified, update the `Status` field in `Tickets/ISSUE-011-blender-nodegroup-empty-assets.md` to `Resolved`.
**Logic Flow:**
```mermaid
graph TD
A[create_nodegroups.py] --> B{Load Asset Metadata};
B --> C{Determine Asset Category};
C --> D{Is Category Skipped?};
D -- Yes --> E[Exit Processing for Asset];
D -- No --> F{Create/Update Parent Group};
F --> G{Mark as Asset & Add Tags};
G --> H{Proceed with Child Group Creation etc.};

View File

@@ -1,68 +0,0 @@
# Map Variant Handling Plan (Revised)
**Goal:**
1. Ensure map types listed in a new `RESPECT_VARIANT_MAP_TYPES` config setting (initially just "COL") always receive a numeric suffix (`-1`, `-2`, etc.), based on their order determined by preset keywords and alphabetical sorting within keywords.
2. Ensure all other map types *never* receive a numeric suffix.
3. Correctly prioritize 16-bit map variants (identified by `bit_depth_variants` in presets) over their 8-bit counterparts, ensuring the 8-bit version is ignored/marked as extra and the 16-bit version is correctly classified ("Mapped") in the GUI preview.
**Affected Files:**
* `config.py`: To define the `RESPECT_VARIANT_MAP_TYPES` list.
* `asset_processor.py`: To modify the classification and suffix assignment logic according to the new rule.
* `Presets/Poliigon.json`: To remove the conflicting pattern.
**Plan Details:**
```mermaid
graph TD
A[Start] --> B(Modify config.py);
B --> C(Modify asset_processor.py);
C --> D(Modify Presets/Poliigon.json);
D --> E{Review Revised Plan};
E -- Approve --> F(Optional: Write Plan to MD);
F --> G(Switch to Code Mode);
E -- Request Changes --> B;
G --> H[End Plan];
subgraph Modifications
B[1. Add RESPECT_VARIANT_MAP_TYPES list to config.py]
C[2. Update suffix logic in asset_processor.py (_inventory_and_classify_files)]
D[3. Remove "*_16BIT*" pattern from move_to_extra_patterns in Presets/Poliigon.json]
end
```
1. **Modify `config.py`:**
* **Action:** Introduce a new configuration list named `RESPECT_VARIANT_MAP_TYPES`.
* **Value:** Initialize it as `RESPECT_VARIANT_MAP_TYPES = ["COL"]`.
* **Location:** Add this near other map-related settings like `STANDARD_MAP_TYPES`.
* **Purpose:** To explicitly define which map types should always respect variant numbering via suffixes.
2. **Modify `asset_processor.py`:**
* **File:** `asset_processor.py`
* **Method:** `_inventory_and_classify_files`
* **Location:** Within Step 5, replacing the suffix assignment logic (currently lines ~470-474).
* **Action:** Implement the new conditional logic for assigning the `final_map_type`.
* **New Logic:** Inside the loop iterating through `final_ordered_candidates` (for each `base_map_type`):
```python
# Determine final map type based on the new rule
if base_map_type in self.config.respect_variant_map_types: # Check the new config list
# Always assign suffix for types in the list
final_map_type = f"{base_map_type}-{i + 1}"
else:
# Never assign suffix for types NOT in the list
final_map_type = base_map_type
# Assign to the final map list entry
final_map_list.append({
"map_type": final_map_type,
# ... rest of the dictionary assignment ...
})
```
* **Purpose:** To implement the strict rule: only types in `RESPECT_VARIANT_MAP_TYPES` get a suffix; all others do not.
3. **Modify `Presets/Poliigon.json`:**
* **File:** `Presets/Poliigon.json`
* **Location:** Within the `move_to_extra_patterns` list (currently line ~28).
* **Action:** Remove the string `"*_16BIT*"`.
* **Purpose:** To prevent premature classification of 16-bit variants as "Extra", allowing the specific 16-bit prioritization logic to function correctly.

View File

@@ -1,90 +0,0 @@
# Memory Optimization Plan: Strategy 2 - Load Grayscale Directly
This plan outlines the steps to implement memory optimization strategy #2, which involves loading known grayscale map types directly as grayscale images using OpenCV's `IMREAD_GRAYSCALE` flag. This reduces the memory footprint compared to loading them with `IMREAD_UNCHANGED` and then potentially converting later.
## 1. Identify Target Grayscale Map Types
Define a list of map type names (case-insensitive check recommended during implementation) that should always be treated as single-channel grayscale data.
**Initial List:**
```python
GRAYSCALE_MAP_TYPES = ['HEIGHT', 'ROUGH', 'METAL', 'AO', 'OPC', 'MASK']
```
*(Note: This list might need adjustment based on specific preset configurations or workflow requirements.)*
## 2. Modify `_process_maps` Loading Logic
Locate the primary image loading section within the `_process_maps` method in `asset_processor.py` (around line 608).
**Change:** Before calling `cv2.imread`, determine the correct flag based on the `map_type`:
```python
# (Define GRAYSCALE_MAP_TYPES list earlier in the scope or class)
# ... inside the loop ...
full_source_path = self.temp_dir / source_path_rel
# Determine the read flag
read_flag = cv2.IMREAD_GRAYSCALE if map_type.upper() in GRAYSCALE_MAP_TYPES else cv2.IMREAD_UNCHANGED
log.debug(f"Loading source {source_path_rel.name} with flag: {'GRAYSCALE' if read_flag == cv2.IMREAD_GRAYSCALE else 'UNCHANGED'}")
# Load the image using the determined flag
img_loaded = cv2.imread(str(full_source_path), read_flag)
if img_loaded is None:
raise AssetProcessingError(f"Failed to load image file: {full_source_path.name} with flag {read_flag}")
# ... rest of the processing logic ...
```
## 3. Modify `_merge_maps` Loading Logic
Locate the image loading section within the resolution loop in the `_merge_maps` method (around line 881).
**Change:** Apply the same conditional logic to determine the `imread` flag when loading input maps for merging:
```python
# ... inside the loop ...
input_file_path = self.temp_dir / res_details['path']
# Determine the read flag (reuse GRAYSCALE_MAP_TYPES list)
read_flag = cv2.IMREAD_GRAYSCALE if map_type.upper() in GRAYSCALE_MAP_TYPES else cv2.IMREAD_UNCHANGED
log.debug(f"Loading merge input {input_file_path.name} ({map_type}) with flag: {'GRAYSCALE' if read_flag == cv2.IMREAD_GRAYSCALE else 'UNCHANGED'}")
# Load the image using the determined flag
img = cv2.imread(str(input_file_path), read_flag)
if img is None:
raise AssetProcessingError(f"Failed to load merge input {input_file_path.name} with flag {read_flag}")
# ... rest of the merging logic ...
```
## 4. Verification
During implementation in `code` mode:
* Ensure the `GRAYSCALE_MAP_TYPES` list is defined appropriately (e.g., as a class constant or module-level constant).
* Confirm that downstream code (e.g., stats calculation, channel extraction, data type conversions) correctly handles numpy arrays that might be 2D (grayscale) instead of 3D (BGR/BGRA). The existing code appears to handle this, but it's important to verify.
## Mermaid Diagram of Change
```mermaid
graph TD
subgraph _process_maps
A[Loop through map_info] --> B{Is map_type Grayscale?};
B -- Yes --> C[imread(path, GRAYSCALE)];
B -- No --> D[imread(path, UNCHANGED)];
C --> E[Process Image];
D --> E;
end
subgraph _merge_maps
F[Loop through resolutions] --> G[Loop through required_input_types];
G --> H{Is map_type Grayscale?};
H -- Yes --> I[imread(path, GRAYSCALE)];
H -- No --> J[imread(path, UNCHANGED)];
I --> K[Use Image in Merge];
J --> K;
end
style B fill:#f9f,stroke:#333,stroke-width:2px
style H fill:#f9f,stroke:#333,stroke-width:2px

View File

@@ -1,103 +0,0 @@
# Plan: Implement Input-Based Output Format Logic
This plan outlines the steps to modify the Asset Processor Tool to determine the output format of texture maps based on the input file format and specific rules.
## Requirements Summary
Based on user clarifications:
1. **JPG Input -> JPG Output:** If the original source map is a JPG, the output for that map (at all processed resolutions) will also be JPG (8-bit).
2. **TIF Input -> PNG/EXR Output:** If the original source map is a TIF, the output will be PNG (if the target bit depth is 8-bit, or if 16-bit PNG is the configured preference) or EXR (if the target bit depth is 16-bit and EXR is the configured preference).
3. **Other Inputs (PNG, etc.) -> Configured Output:** For other input formats, the output will follow the existing logic based on target bit depth (using configured 16-bit or 8-bit formats, typically EXR/PNG).
4. **`force_8bit` Rule:** If a map type has a `force_8bit` rule, it overrides the input format. Even if the input was 16-bit TIF, the output will be 8-bit PNG.
5. **Merged Maps:** The output format is determined by the highest format in the hierarchy (EXR > TIF > PNG > JPG) based on the *original* formats of the input files used in the merge. However, if the highest format is TIF, the actual output will be PNG/EXR based on the target bit depth. The target bit depth itself is determined separately by the merge rule's `output_bit_depth` setting.
6. **JPG Resizing:** Resized JPGs will be saved as JPG.
## Implementation Plan
**Phase 1: Data Gathering Enhancement**
1. **Modify `_inventory_and_classify_files` in `asset_processor.py`:**
* When classifying map files, extract and store the original file extension (e.g., `.jpg`, `.tif`, `.png`) along with the `source_path`, `map_type`, etc., within the `self.classified_files["maps"]` list.
**Phase 2: Implement New Logic for Individual Maps (`_process_maps`)**
1. **Modify `_process_maps` in `asset_processor.py`:**
* Inside the loop processing each `map_info`:
* Retrieve the stored original file extension.
* Determine the target output bit depth (8 or 16) using the existing logic (`config.get_bit_depth_rule`, source data type).
* **Implement New Format Determination:**
* Initialize `output_format` and `output_ext`.
* Check the `force_8bit` rule first: If the rule is `force_8bit`, set `output_format = 'png'` and `output_ext = '.png'`, regardless of input format.
* If not `force_8bit`:
* If `original_extension == '.jpg'` and `target_bit_depth == 8`: Set `output_format = 'jpg'`, `output_ext = '.jpg'`.
* If `original_extension == '.tif'`:
* If `target_bit_depth == 16`: Determine format (EXR/PNG) and extension based on `config.get_16bit_output_formats()`.
* If `target_bit_depth == 8`: Set `output_format = 'png'`, `output_ext = '.png'`.
* If `original_extension` is neither `.jpg` nor `.tif` (e.g., `.png`):
* If `target_bit_depth == 16`: Determine format (EXR/PNG) and extension based on `config.get_16bit_output_formats()`.
* If `target_bit_depth == 8`: Set `output_format = config.get_8bit_output_format()` (likely 'png'), `output_ext = f".{output_format}"`.
* **Remove Old Logic:** Delete the code block that checks `self.config.resolution_threshold_for_jpg`.
* Set `save_params` based on the *newly determined* `output_format` (e.g., `cv2.IMWRITE_JPEG_QUALITY` for JPG, `cv2.IMWRITE_PNG_COMPRESSION` for PNG).
* Proceed with data type conversion (if needed based on target bit depth and format requirements like EXR needing float16) and saving using `cv2.imwrite` with the determined `output_path_temp` (using the new `output_ext`) and `save_params`. Ensure fallback logic (e.g., EXR -> PNG) still functions correctly if needed.
**Phase 3: Implement New Logic for Merged Maps (`_merge_maps`)**
1. **Modify `_merge_maps` in `asset_processor.py`:**
* Inside the loop for each `current_res_key`:
* When loading input maps (`loaded_inputs`), also retrieve and store their *original* file extensions (obtained during classification and now available via `self.classified_files["maps"]`, potentially needing a lookup based on `res_details['path']` or storing it earlier).
* **Determine Highest Input Format:** Iterate through the original extensions of the loaded inputs for this resolution. Use the hierarchy (EXR > TIF > PNG > JPG) to find the highest format present.
* **Determine Final Output Format:**
* Start with the `highest_input_format`.
* If `highest_input_format == 'tif'`:
* Check the target bit depth determined by the merge rule (`output_bit_depth`).
* If `target_bit_depth == 16`: Set final format based on `config.get_16bit_output_formats()` (EXR/PNG).
* If `target_bit_depth == 8`: Set final format to `png`.
* Otherwise (JPG, PNG, EXR), the final format is the `highest_input_format`.
* Set `output_ext` based on the `final_output_format`.
* Set `save_params` based on the `final_output_format`.
* Proceed with merging channels, converting the merged data to the target bit depth specified by the *merge rule*, and saving using `cv2.imwrite` with the determined `merged_output_path_temp` (using the new `output_ext`) and `save_params`.
**Phase 4: Configuration and Documentation**
1. **Modify `config.py`:**
* Comment out or remove the `RESOLUTION_THRESHOLD_FOR_JPG` variable as it's no longer used. Add a comment explaining why it was removed.
2. **Update `readme.md`:**
* Modify the "Features" section (around line 21) and the "Configuration" section (around lines 36-40, 86) to accurately describe the new output format logic:
* Explain that JPG inputs result in JPG outputs.
* Explain that TIF inputs result in PNG/EXR outputs based on target bit depth and config.
* Explain the merged map format determination based on input hierarchy (with the TIF->PNG/EXR adjustment).
* Mention the removal of the JPG resolution threshold.
## Visual Plan (Mermaid)
```mermaid
graph TD
A[Start] --> B(Phase 1: Enhance Classification);
B --> B1(Store original extension in classified_files['maps']);
B1 --> C(Phase 2: Modify _process_maps);
C --> C1(Get original extension);
C --> C2(Determine target bit depth);
C --> C3(Apply New Format Logic);
C3 -- force_8bit --> C3a[Format=PNG];
C3 -- input=.jpg, 8bit --> C3b[Format=JPG];
C3 -- input=.tif, 16bit --> C3c[Format=EXR/PNG (Config)];
C3 -- input=.tif, 8bit --> C3d[Format=PNG];
C3 -- input=other, 16bit --> C3c;
C3 -- input=other, 8bit --> C3e[Format=PNG (Config)];
C3a --> C4(Remove JPG Threshold Check);
C3b --> C4;
C3c --> C4;
C3d --> C4;
C3e --> C4;
C4 --> C5(Set Save Params & Save);
C5 --> D(Phase 3: Modify _merge_maps);
D --> D1(Get original extensions of inputs);
D --> D2(Find highest format via hierarchy);
D2 --> D3(Adjust TIF -> PNG/EXR based on target bit depth);
D3 --> D4(Determine target bit depth from rule);
D4 --> D5(Set Save Params & Save Merged);
D5 --> E(Phase 4: Config & Docs);
E --> E1(Update config.py - Remove threshold);
E --> E2(Update readme.md);
E2 --> F[End];

View File

@@ -1,127 +0,0 @@
# Revised Plan: Implement Input-Based Output Format Logic with JPG Threshold Override
This plan outlines the steps to modify the Asset Processor Tool to determine the output format of texture maps based on the input file format, specific rules, and a JPG resolution threshold override.
## Requirements Summary (Revised)
Based on user clarifications:
1. **JPG Threshold Override:** If the target output bit depth is 8-bit AND the image resolution is greater than or equal to `RESOLUTION_THRESHOLD_FOR_JPG` (defined in `config.py`), the output format **must** be JPG.
2. **Input-Based Logic (if threshold not met):**
* **JPG Input -> JPG Output:** If the original source map is JPG and the target is 8-bit (and below threshold), output JPG.
* **TIF Input -> PNG/EXR Output:** If the original source map is TIF:
* If target is 16-bit, output EXR or PNG based on `OUTPUT_FORMAT_16BIT_PRIMARY` config.
* If target is 8-bit (and below threshold), output PNG.
* **Other Inputs (PNG, etc.) -> Configured Output:** For other input formats (and below threshold if 8-bit):
* If target is 16-bit, output EXR or PNG based on `OUTPUT_FORMAT_16BIT_PRIMARY` config.
* If target is 8-bit, output PNG (or format specified by `OUTPUT_FORMAT_8BIT`).
3. **`force_8bit` Rule:** If a map type has a `force_8bit` rule, the target bit depth is 8-bit. The output format will then be determined by the JPG threshold override or the input-based logic (resulting in JPG or PNG).
4. **Merged Maps:**
* Determine target `output_bit_depth` from the merge rule (`respect_inputs`, `force_8bit`, etc.).
* **Check JPG Threshold Override:** If target `output_bit_depth` is 8-bit AND resolution >= threshold, the final output format is JPG.
* **Else (Hierarchy Logic):** Determine the highest format among original inputs (EXR > TIF > PNG > JPG).
* If highest was TIF, adjust based on target bit depth (16-bit -> EXR/PNG config; 8-bit -> PNG).
* Otherwise, use the highest format found (EXR, PNG, JPG).
* **JPG 8-bit Check:** If the final format is JPG but the target bit depth was 16, force the merged data to 8-bit before saving.
5. **JPG Resizing:** Resized JPGs will be saved as JPG if the logic determines JPG as the output format.
## Implementation Plan (Revised)
**Phase 1: Data Gathering Enhancement** (Already Done & Correct)
* `_inventory_and_classify_files` stores the original file extension in `self.classified_files["maps"]`.
**Phase 2: Modify `_process_maps`**
1. Retrieve the `original_extension` from `map_info`.
2. Determine the target `output_bit_depth` (8 or 16).
3. Get the `threshold = self.config.resolution_threshold_for_jpg`.
4. Get the `target_dim` for the current resolution loop iteration.
5. **New Format Logic (Revised):**
* Initialize `output_format`, `output_ext`, `save_params`, `needs_float16`.
* **Check JPG Threshold Override:**
* If `output_bit_depth == 8` AND `target_dim >= threshold`: Set format to JPG, set JPG params.
* **Else (Apply Input/Rule-Based Logic):**
* If `bit_depth_rule == 'force_8bit'`: Set format to PNG (8-bit), set PNG params.
* Else if `original_extension == '.jpg'` and `output_bit_depth == 8`: Set format to JPG, set JPG params.
* Else if `original_extension == '.tif'`:
* If `output_bit_depth == 16`: Set format to EXR/PNG (16-bit config), set params, set `needs_float16` if EXR.
* If `output_bit_depth == 8`: Set format to PNG, set PNG params.
* Else (other inputs like `.png`):
* If `output_bit_depth == 16`: Set format to EXR/PNG (16-bit config), set params, set `needs_float16` if EXR.
* If `output_bit_depth == 8`: Set format to PNG (8-bit config), set PNG params.
6. Proceed with data type conversion and saving.
**Phase 3: Modify `_merge_maps`**
1. Retrieve original extensions of inputs (`input_original_extensions`).
2. Determine target `output_bit_depth` from the merge rule.
3. Get the `threshold = self.config.resolution_threshold_for_jpg`.
4. Get the `target_dim` for the current resolution loop iteration.
5. **New Format Logic (Revised):**
* Initialize `final_output_format`, `output_ext`, `save_params`, `needs_float16`.
* **Check JPG Threshold Override:**
* If `output_bit_depth == 8` AND `target_dim >= threshold`: Set `final_output_format = 'jpg'`.
* **Else (Apply Hierarchy/Rule-Based Logic):**
* Determine `highest_input_format` (EXR > TIF > PNG > JPG).
* Start with `final_output_format = highest_input_format`.
* If `highest_input_format == 'tif'`: Adjust based on target bit depth (16->EXR/PNG config; 8->PNG).
* Set `output_format = final_output_format`.
* Set `output_ext`, `save_params`, `needs_float16` based on `output_format`.
* **JPG 8-bit Check:** If `output_format == 'jpg'` and `output_bit_depth == 16`, force final merged data to 8-bit before saving and update `output_bit_depth` variable.
6. Proceed with merging, data type conversion, and saving.
**Phase 4: Configuration and Documentation**
1. **Modify `config.py`:** Ensure `RESOLUTION_THRESHOLD_FOR_JPG` is uncommented and set correctly (revert previous change).
2. **Update `readme.md`:** Clarify the precedence: 8-bit maps >= threshold become JPG, otherwise the input-based logic applies.
## Visual Plan (Mermaid - Revised)
```mermaid
graph TD
A[Start] --> B(Phase 1: Enhance Classification - Done);
B --> C(Phase 2: Modify _process_maps);
C --> C1(Get original extension);
C --> C2(Determine target bit depth);
C --> C3(Get target_dim & threshold);
C --> C4{8bit AND >= threshold?};
C4 -- Yes --> C4a[Format=JPG];
C4 -- No --> C5(Apply Input/Rule Logic);
C5 -- force_8bit --> C5a[Format=PNG];
C5 -- input=.jpg, 8bit --> C5b[Format=JPG];
C5 -- input=.tif, 16bit --> C5c[Format=EXR/PNG (Config)];
C5 -- input=.tif, 8bit --> C5d[Format=PNG];
C5 -- input=other, 16bit --> C5c;
C5 -- input=other, 8bit --> C5e[Format=PNG (Config)];
C4a --> C6(Set Save Params & Save);
C5a --> C6;
C5b --> C6;
C5c --> C6;
C5d --> C6;
C5e --> C6;
C6 --> D(Phase 3: Modify _merge_maps);
D --> D1(Get original extensions of inputs);
D --> D2(Determine target bit depth from rule);
D --> D3(Get target_dim & threshold);
D --> D4{8bit AND >= threshold?};
D4 -- Yes --> D4a[FinalFormat=JPG];
D4 -- No --> D5(Apply Hierarchy Logic);
D5 --> D5a(Find highest input format);
D5a --> D5b{Highest = TIF?};
D5b -- Yes --> D5c{Target 16bit?};
D5c -- Yes --> D5d[FinalFormat=EXR/PNG (Config)];
D5c -- No --> D5e[FinalFormat=PNG];
D5b -- No --> D5f[FinalFormat=HighestInput];
D4a --> D6(Set Save Params);
D5d --> D6;
D5e --> D6;
D5f --> D6;
D6 --> D7{Format=JPG AND Target=16bit?};
D7 -- Yes --> D7a(Force data to 8bit);
D7 -- No --> D8(Save Merged);
D7a --> D8;
D8 --> E(Phase 4: Config & Docs);
E --> E1(Uncomment threshold in config.py);
E --> E2(Update readme.md);
E2 --> F[End];

View File

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

View File

@@ -0,0 +1,62 @@
# Issue: List item selection not working in Definitions Editor
**Date:** 2025-05-13
**Affected File:** [`gui/definitions_editor_dialog.py`](gui/definitions_editor_dialog.py)
**Problem Description:**
User mouse clicks on items within the `QListWidget` instances (for Asset Types, File Types, and Suppliers) in the Definitions Editor dialog do not trigger item selection or the `currentItemChanged` signal. The first item is selected by default and its details are displayed correctly. Programmatic selection of items (e.g., via a diagnostic button) *does* correctly trigger the `currentItemChanged` signal and updates the UI detail views. The issue is specific to user-initiated mouse clicks for selection after the initial load.
**Debugging Steps Taken & Findings:**
1. **Initial Analysis:**
* Reviewed GUI internals documentation ([`Documentation/02_Developer_Guide/06_GUI_Internals.md`](Documentation/02_Developer_Guide/06_GUI_Internals.md)) and [`gui/definitions_editor_dialog.py`](gui/definitions_editor_dialog.py) source code.
* Confirmed signal connections (`currentItemChanged` to display slots) are made.
2. **Logging in Display Slots (`_display_*_details`):**
* Added logging to display slots. Confirmed they are called for the initial (default) item selection.
* No further calls to these slots occur on user clicks, indicating `currentItemChanged` is not firing.
3. **Color Swatch Palette Role:**
* Investigated and corrected `QPalette.ColorRole` for color swatches (reverted from `Background` to `Window`). This fixed an `AttributeError` but did not resolve the selection issue.
4. **Robust Error Handling in Display Slots:**
* Wrapped display slot logic in `try...finally` blocks with detailed logging. Confirmed slots complete without error for initial selection and signals for detail widgets are reconnected.
5. **Diagnostic Lambda for `currentItemChanged`:**
* Added a lambda logger to `currentItemChanged` alongside the main display slot.
* Confirmed both lambda and display slot fire for initial programmatic selection.
* Neither fires for subsequent user clicks. This proved the `QListWidget` itself was not emitting the signal.
6. **Explicit `setEnabled` and `setSelectionMode` on `QListWidget`:**
* Explicitly set these properties. No change in behavior.
7. **Explicit `setEnabled` and `setFocusPolicy(Qt.ClickFocus)` on `tab_page` (parent of `QListWidget` layout):**
* This change **allowed programmatic selection via a diagnostic button to correctly fire `currentItemChanged` and update the UI**.
* However, user mouse clicks still did not work and did not fire the signal.
8. **Event Filter Investigation:**
* **Filter on `QListWidget`:** Did NOT receive mouse press/release events from user clicks.
* **Filter on `tab_page` (parent of `QListWidget`'s layout):** Did NOT receive mouse press/release events.
* **Filter on `self.tab_widget` (QTabWidget):** DID receive mouse press/release events.
* Modified `self.tab_widget`'s event filter to return `False` for events over the current page, attempting to ensure propagation.
* **Result:** With the modified `tab_widget` filter, an event filter re-added to `asset_type_list_widget` *did* start receiving mouse press/release events. **However, `asset_type_list_widget` still did not emit `currentItemChanged` from these user clicks.**
9. **`DebugListWidget` (Subclassing `QListWidget`):**
* Created `DebugListWidget` overriding `mousePressEvent` with logging.
* Used `DebugListWidget` for `asset_type_list_widget`.
* **Initial user report indicated that `DebugListWidget.mousePressEvent` logs were NOT appearing for user clicks.** This means that even with the `QTabWidget` event filter attempting to propagate events, and the `asset_type_list_widget`'s filter (from step 8) confirming it received them, the `mousePressEvent` of the `QListWidget` itself was not being triggered by those propagated events. This is the current mystery.
**Current Status:**
- Programmatic selection works and fires signals.
- User clicks are received by an event filter on `asset_type_list_widget` (after `QTabWidget` filter modification) but do not result in `mousePressEvent` being called on the `QListWidget` (or `DebugListWidget`) itself, and thus no `currentItemChanged` signal is emitted.
- The issue seems to be a very low-level event processing problem specifically for user mouse clicks within the `QListWidget` instances when they are children of the `QTabWidget` pages, even when events appear to reach the list widget via an event filter.
**Next Steps (When Resuming):**
1. Re-verify the logs from the `DebugListWidget.mousePressEvent` test. If it's truly not being called despite its event filter seeing events, this is extremely unusual.
2. Simplify the `_create_tab_pane` method drastically for one tab:
* Remove the right-hand pane.
* Add the `DebugListWidget` directly to the `tab_page`'s layout without the intermediate `left_pane_layout`.
3. Consider if any styles applied to `QListWidget` or its parents via stylesheets could be interfering with hit testing or event processing (unlikely for this specific symptom, but possible).
4. Explore alternative ways to populate/manage the `QListWidget` or its items if a subtle corruption is occurring.
5. If all else fails, consider replacing the `QListWidget` with a `QListView` and a `QStringListModel` as a more fundamental change to see if the issue is specific to `QListWidget` in this context.

View File

@@ -1,73 +0,0 @@
---
title: Python Args and Kwargs - Python Cheatsheet
description: args and kwargs may seem scary, but the truth is that they are not that difficult to grasp and have the power to grant your functions with flexibility and readability
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Args and Kwargs
</base-title>
<base-disclaimer>
<base-disclaimer-title>
<a href="https://docs.python.org/3/tutorial/index.html">Python args and kwargs Made Easy</a>
</base-disclaimer-title>
<base-disclaimer-content>
<code>*args</code> and <code>**kwargs</code> may seem scary, but the truth is that they are not that difficult to grasp and have the power to grant your functions with lots of flexibility.
</base-disclaimer-content>
</base-disclaimer>
Read the article <router-link to="/blog/python-easy-args-kwargs">Python \*args and \*\*kwargs Made Easy</router-link> for a more in deep introduction.
## Args and Kwargs
`*args` and `**kwargs` allow you to pass an undefined number of arguments and keywords when calling a function.
```python
>>> def some_function(*args, **kwargs):
... pass
...
>>> # call some_function with any number of arguments
>>> some_function(arg1, arg2, arg3)
>>> # call some_function with any number of keywords
>>> some_function(key1=arg1, key2=arg2, key3=arg3)
>>> # call both, arguments and keywords
>>> some_function(arg, key1=arg1)
>>> # or none
>>> some_function()
```
<base-warning>
<base-warning-title>
Python conventions
</base-warning-title>
<base-warning-content>
The words <code>*args</code> and <code>**kwargs</code> are conventions. They are not imposed by the interpreter, but considered good practice by the Python community.
</base-warning-content>
</base-warning>
## args
You can access the _arguments_ through the `args` variable:
```python
>>> def some_function(*args):
... print(f'Arguments passed: {args} as {type(args)}')
...
>>> some_function('arg1', 'arg2', 'arg3')
# Arguments passed: ('arg1', 'arg2', 'arg3') as <class 'tuple'>
```
## kwargs
Keywords are accessed through the `kwargs` variable:
```python
>>> def some_function(**kwargs):
... print(f'keywords: {kwargs} as {type(kwargs)}')
...
>>> some_function(key1='arg1', key2='arg2')
# keywords: {'key1': 'arg1', 'key2': 'arg2'} as <class 'dict'>
```

View File

@@ -1,340 +0,0 @@
---
title: Python Basics - Python Cheatsheet
description: The basics of python. We all need to start somewhere, so how about doing it here.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Basics
</base-title>
We all need to start somewhere, so how about doing it here.
<base-disclaimer>
<base-disclaimer-title>
From the <a href="https://docs.python.org/3/tutorial/index.html">Python 3 tutorial</a>
</base-disclaimer-title>
<base-disclaimer-content>
Python is an easy to learn, powerful programming language [...] Pythons elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development.
</base-disclaimer-content>
</base-disclaimer>
## Math Operators
From **highest** to **lowest** precedence:
| Operators | Operation | Example |
| --------- | ----------------- | --------------- |
| \*\* | Exponent | `2 ** 3 = 8` |
| % | Modulus/Remainder | `22 % 8 = 6` |
| // | Integer division | `22 // 8 = 2` |
| / | Division | `22 / 8 = 2.75` |
| \* | Multiplication | `3 * 3 = 9` |
| - | Subtraction | `5 - 2 = 3` |
| + | Addition | `2 + 2 = 4` |
Examples of expressions:
```python
>>> 2 + 3 * 6
# 20
>>> (2 + 3) * 6
# 30
>>> 2 ** 8
#256
>>> 23 // 7
# 3
>>> 23 % 7
# 2
>>> (5 - 1) * ((7 + 1) / (3 - 1))
# 16.0
```
## Augmented Assignment Operators
| Operator | Equivalent |
| ----------- | ---------------- |
| `var += 1` | `var = var + 1` |
| `var -= 1` | `var = var - 1` |
| `var *= 1` | `var = var * 1` |
| `var /= 1` | `var = var / 1` |
| `var //= 1` | `var = var // 1` |
| `var %= 1` | `var = var % 1` |
| `var **= 1` | `var = var ** 1` |
Examples:
```python
>>> greeting = 'Hello'
>>> greeting += ' world!'
>>> greeting
# 'Hello world!'
>>> number = 1
>>> number += 1
>>> number
# 2
>>> my_list = ['item']
>>> my_list *= 3
>>> my_list
# ['item', 'item', 'item']
```
## Walrus Operator
The Walrus Operator allows assignment of variables within an expression while returning the value of the variable
Example:
```python
>>> print(my_var:="Hello World!")
# 'Hello world!'
>>> my_var="Yes"
>>> print(my_var)
# 'Yes'
>>> print(my_var:="Hello")
# 'Hello'
```
The _Walrus Operator_, or **Assignment Expression Operator** was firstly introduced in 2018 via [PEP 572](https://peps.python.org/pep-0572/), and then officially released with **Python 3.8** in October 2019.
<base-disclaimer>
<base-disclaimer-title>
Syntax Semantics & Examples
</base-disclaimer-title>
<base-disclaimer-content>
The <a href="https://peps.python.org/pep-0572/" target="_blank">PEP 572</a> provides the syntax, semantics and examples for the Walrus Operator.
</base-disclaimer-content>
</base-disclaimer>
## Data Types
| Data Type | Examples |
| ---------------------- | ----------------------------------------- |
| Integers | `-2, -1, 0, 1, 2, 3, 4, 5` |
| Floating-point numbers | `-1.25, -1.0, --0.5, 0.0, 0.5, 1.0, 1.25` |
| Strings | `'a', 'aa', 'aaa', 'Hello!', '11 cats'` |
## Concatenation and Replication
String concatenation:
```python
>>> 'Alice' 'Bob'
# 'AliceBob'
```
String replication:
```python
>>> 'Alice' * 5
# 'AliceAliceAliceAliceAlice'
```
## Variables
You can name a variable anything as long as it obeys the following rules:
1. It can be only one word.
```python
>>> # bad
>>> my variable = 'Hello'
>>> # good
>>> var = 'Hello'
```
2. It can use only letters, numbers, and the underscore (`_`) character.
```python
>>> # bad
>>> %$@variable = 'Hello'
>>> # good
>>> my_var = 'Hello'
>>> # good
>>> my_var_2 = 'Hello'
```
3. It cant begin with a number.
```python
>>> # this wont work
>>> 23_var = 'hello'
```
4. Variable name starting with an underscore (`_`) are considered as "unuseful".
```python
>>> # _spam should not be used again in the code
>>> _spam = 'Hello'
```
## Comments
Inline comment:
```python
# This is a comment
```
Multiline comment:
```python
# This is a
# multiline comment
```
Code with a comment:
```python
a = 1 # initialization
```
Please note the two spaces in front of the comment.
Function docstring:
```python
def foo():
"""
This is a function docstring
You can also use:
''' Function Docstring '''
"""
```
## The print() Function
The `print()` function writes the value of the argument(s) it is given. [...] it handles multiple arguments, floating point-quantities, and strings. Strings are printed without quotes, and a space is inserted between items, so you can format things nicely:
```python
>>> print('Hello world!')
# Hello world!
>>> a = 1
>>> print('Hello world!', a)
# Hello world! 1
```
### The end keyword
The keyword argument `end` can be used to avoid the newline after the output, or end the output with a different string:
```python
phrase = ['printed', 'with', 'a', 'dash', 'in', 'between']
>>> for word in phrase:
... print(word, end='-')
...
# printed-with-a-dash-in-between-
```
### The sep keyword
The keyword `sep` specify how to separate the objects, if there is more than one:
```python
print('cats', 'dogs', 'mice', sep=',')
# cats,dogs,mice
```
## The input() Function
This function takes the input from the user and converts it into a string:
```python
>>> print('What is your name?') # ask for their name
>>> my_name = input()
>>> print('Hi, {}'.format(my_name))
# What is your name?
# Martha
# Hi, Martha
```
`input()` can also set a default message without using `print()`:
```python
>>> my_name = input('What is your name? ') # default message
>>> print('Hi, {}'.format(my_name))
# What is your name? Martha
# Hi, Martha
```
It is also possible to use formatted strings to avoid using .format:
```python
>>> my_name = input('What is your name? ') # default message
>>> print(f'Hi, {my_name}')
# What is your name? Martha
# Hi, Martha
```
## The len() Function
Evaluates to the integer value of the number of characters in a string, list, dictionary, etc.:
```python
>>> len('hello')
# 5
>>> len(['cat', 3, 'dog'])
# 3
```
<base-warning>
<base-warning-title>Test of emptiness</base-warning-title>
<base-warning-content>
Test of emptiness of strings, lists, dictionaries, etc., should not use
<code>len</code>, but prefer direct boolean evaluation.
</base-warning-content>
</base-warning>
Test of emptiness example:
```python
>>> a = [1, 2, 3]
# bad
>>> if len(a) > 0: # evaluates to True
... print("the list is not empty!")
...
# the list is not empty!
# good
>>> if a: # evaluates to True
... print("the list is not empty!")
...
# the list is not empty!
```
## The str(), int(), and float() Functions
These functions allow you to change the type of variable. For example, you can transform from an `integer` or `float` to a `string`:
```python
>>> str(29)
# '29'
>>> str(-3.14)
# '-3.14'
```
Or from a `string` to an `integer` or `float`:
```python
>>> int('11')
# 11
>>> float('3.14')
# 3.14
```

View File

@@ -1,83 +0,0 @@
---
title: Python built-in functions - Python Cheatsheet
description: The Python interpreter has a number of functions and types built into it that are always available.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Built-in Functions
</base-title>
The Python interpreter has a number of functions and types built into it that are always available.
## Python built-in Functions
| Function | Description |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| <router-link to='/builtin/abs'>abs()</router-link> | Return the absolute value of a number. |
| <router-link to='/builtin/aiter'>aiter()</router-link> | Return an asynchronous iterator for an asynchronous iterable. |
| <router-link to='/builtin/all'>all()</router-link> | Return True if all elements of the iterable are true. |
| <router-link to='/builtin/any'>any()</router-link> | Return True if any element of the iterable is true. |
| <router-link to='/builtin/ascii'>ascii()</router-link> | Return a string with a printable representation of an object. |
| <router-link to='/builtin/bin'>bin()</router-link> | Convert an integer number to a binary string. |
| <router-link to='/builtin/bool'>bool()</router-link> | Return a Boolean value. |
| <router-link to='/builtin/breakpoint'>breakpoint()</router-link> | Drops you into the debugger at the call site. |
| <router-link to='/builtin/bytearray'>bytearray()</router-link> | Return a new array of bytes. |
| <router-link to='/builtin/bytes'>bytes()</router-link> | Return a new “bytes” object. |
| <router-link to='/builtin/callable'>callable()</router-link> | Return True if the object argument is callable, False if not. |
| <router-link to='/builtin/chr'>chr()</router-link> | Return the string representing a character. |
| <router-link to='/builtin/classmethod'>classmethod()</router-link> | Transform a method into a class method. |
| <router-link to='/builtin/compile'>compile()</router-link> | Compile the source into a code or AST object. |
| <router-link to='/builtin/complex'>complex()</router-link> | Return a complex number with the value real + imag\*1j. |
| <router-link to='/builtin/delattr'>delattr()</router-link> | Deletes the named attribute, provided the object allows it. |
| <router-link to='/builtin/dict'>dict()</router-link> | Create a new dictionary. |
| <router-link to='/builtin/dir'>dir()</router-link> | Return the list of names in the current local scope. |
| <router-link to='/builtin/divmod'>divmod()</router-link> | Return a pair of numbers consisting of their quotient and remainder. |
| <router-link to='/builtin/enumerate'>enumerate()</router-link> | Return an enumerate object. |
| <router-link to='/builtin/eval'>eval()</router-link> | Evaluates and executes an expression. |
| <router-link to='/builtin/exec'>exec()</router-link> | This function supports dynamic execution of Python code. |
| <router-link to='/builtin/filter'>filter()</router-link> | Construct an iterator from an iterable and returns true. |
| <router-link to='/builtin/float'>float()</router-link> | Return a floating point number from a number or string. |
| <router-link to='/builtin/format'>format()</router-link> | Convert a value to a “formatted” representation. |
| <router-link to='/builtin/frozenset'>frozenset()</router-link> | Return a new frozenset object. |
| <router-link to='/builtin/getattr'>getattr()</router-link> | Return the value of the named attribute of object. |
| <router-link to='/builtin/globals'>globals()</router-link> | Return the dictionary implementing the current module namespace. |
| <router-link to='/builtin/hasattr'>hasattr()</router-link> | True if the string is the name of one of the objects attributes. |
| <router-link to='/builtin/hash'>hash()</router-link> | Return the hash value of the object. |
| <router-link to='/builtin/help'>help()</router-link> | Invoke the built-in help system. |
| <router-link to='/builtin/hex'>hex()</router-link> | Convert an integer number to a lowercase hexadecimal string. |
| <router-link to='/builtin/id'>id()</router-link> | Return the “identity” of an object. |
| <router-link to='/builtin/input'>input()</router-link> | This function takes an input and converts it into a string. |
| <router-link to='/builtin/int'>int()</router-link> | Return an integer object constructed from a number or string. |
| <router-link to='/builtin/isinstance'>isinstance()</router-link> | Return True if the object argument is an instance of an object. |
| <router-link to='/builtin/issubclass'>issubclass()</router-link> | Return True if class is a subclass of classinfo. |
| <router-link to='/builtin/iter'>iter()</router-link> | Return an iterator object. |
| <router-link to='/builtin/len'>len()</router-link> | Return the length (the number of items) of an object. |
| <router-link to='/builtin/list'>list()</router-link> | Rather than being a function, list is a mutable sequence type. |
| <router-link to='/builtin/locals'>locals()</router-link> | Update and return a dictionary with the current local symbol table. |
| <router-link to='/builtin/map'>map()</router-link> | Return an iterator that applies function to every item of iterable. |
| <router-link to='/builtin/max'>max()</router-link> | Return the largest item in an iterable. |
| <router-link to='/builtin/min'>min()</router-link> | Return the smallest item in an iterable. |
| <router-link to='/builtin/next'>next()</router-link> | Retrieve the next item from the iterator. |
| <router-link to='/builtin/object'>object()</router-link> | Return a new featureless object. |
| <router-link to='/builtin/oct'>oct()</router-link> | Convert an integer number to an octal string. |
| <router-link to='/builtin/open'>open()</router-link> | Open file and return a corresponding file object. |
| <router-link to='/builtin/ord'>ord()</router-link> | Return an integer representing the Unicode code point of a character. |
| <router-link to='/builtin/pow'>pow()</router-link> | Return base to the power exp. |
| <router-link to='/builtin/print'>print()</router-link> | Print objects to the text stream file. |
| <router-link to='/builtin/property'>property()</router-link> | Return a property attribute. |
| <router-link to='/builtin/repr'>repr()</router-link> | Return a string containing a printable representation of an object. |
| <router-link to='/builtin/reversed'>reversed()</router-link> | Return a reverse iterator. |
| <router-link to='/builtin/round'>round()</router-link> | Return number rounded to ndigits precision after the decimal point. |
| <router-link to='/builtin/set'>set()</router-link> | Return a new set object. |
| <router-link to='/builtin/setattr'>setattr()</router-link> | This is the counterpart of getattr(). |
| <router-link to='/builtin/slice'>slice()</router-link> | Return a sliced object representing a set of indices. |
| <router-link to='/builtin/sorted'>sorted()</router-link> | Return a new sorted list from the items in iterable. |
| <router-link to='/builtin/staticmethod'>staticmethod()</router-link> | Transform a method into a static method. |
| <router-link to='/builtin/str'>str()</router-link> | Return a str version of object. |
| <router-link to='/builtin/sum'>sum()</router-link> | Sums start and the items of an iterable. |
| <router-link to='/builtin/super'>super()</router-link> | Return a proxy object that delegates method calls to a parent or sibling. |
| <router-link to='/builtin/tuple'>tuple()</router-link> | Rather than being a function, is actually an immutable sequence type. |
| <router-link to='/builtin/type'>type()</router-link> | Return the type of an object. |
| <router-link to='/builtin/vars'>vars()</router-link> | Return the dict attribute for any other object with a dict attribute. |
| <router-link to='/builtin/zip'>zip()</router-link> | Iterate over several iterables in parallel. |
| <router-link to='/builtin/import'>**import**()</router-link> | This function is invoked by the import statement. |

View File

@@ -1,120 +0,0 @@
---
title: Python Comprehensions - Python Cheatsheet
description: List comprehensions provide a concise way to create lists
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Comprehensions
</base-title>
List Comprehensions are a special kind of syntax that let us create lists out of other lists, and are incredibly useful when dealing with numbers and with one or two levels of nested for loops.
<base-disclaimer>
<base-disclaimer-title>
From the Python 3 <a target="_blank" href="https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions">tutorial</a>
</base-disclaimer-title>
<base-disclaimer-content>
List comprehensions provide a concise way to create lists. [...] or to create a subsequence of those elements that satisfy a certain condition.
</base-disclaimer-content>
</base-disclaimer>
Read <router-link to="/blog/python-comprehensions-step-by-step">Python Comprehensions: A step by step Introduction</router-link> for a more in-depth introduction.
## List comprehension
This is how we create a new list from an existing collection with a For Loop:
```python
>>> names = ['Charles', 'Susan', 'Patrick', 'George']
>>> new_list = []
>>> for n in names:
... new_list.append(n)
...
>>> new_list
# ['Charles', 'Susan', 'Patrick', 'George']
```
And this is how we do the same with a List Comprehension:
```python
>>> names = ['Charles', 'Susan', 'Patrick', 'George']
>>> new_list = [n for n in names]
>>> new_list
# ['Charles', 'Susan', 'Patrick', 'George']
```
We can do the same with numbers:
```python
>>> n = [(a, b) for a in range(1, 3) for b in range(1, 3)]
>>> n
# [(1, 1), (1, 2), (2, 1), (2, 2)]
```
## Adding conditionals
If we want `new_list` to have only the names that start with C, with a for loop, we would do it like this:
```python
>>> names = ['Charles', 'Susan', 'Patrick', 'George', 'Carol']
>>> new_list = []
>>> for n in names:
... if n.startswith('C'):
... new_list.append(n)
...
>>> print(new_list)
# ['Charles', 'Carol']
```
In a List Comprehension, we add the `if` statement at the end:
```python
>>> new_list = [n for n in names if n.startswith('C')]
>>> print(new_list)
# ['Charles', 'Carol']
```
To use an `if-else` statement in a List Comprehension:
```python
>>> nums = [1, 2, 3, 4, 5, 6]
>>> new_list = [num*2 if num % 2 == 0 else num for num in nums]
>>> print(new_list)
# [1, 4, 3, 8, 5, 12]
```
<base-disclaimer>
<base-disclaimer-title>
Set and Dict comprehensions
</base-disclaimer-title>
<base-disclaimer-content>
The basics of `list` comprehensions also apply to <b>sets</b> and <b>dictionaries</b>.
</base-disclaimer-content>
</base-disclaimer>
## Set comprehension
```python
>>> b = {"abc", "def"}
>>> {s.upper() for s in b}
{"ABC", "DEF"}
```
## Dict comprehension
```python
>>> c = {'name': 'Pooka', 'age': 5}
>>> {v: k for k, v in c.items()}
{'Pooka': 'name', 5: 'age'}
```
A List comprehension can be generated from a dictionary:
```python
>>> c = {'name': 'Pooka', 'age': 5}
>>> ["{}:{}".format(k.upper(), v) for k, v in c.items()]
['NAME:Pooka', 'AGE:5']
```

View File

@@ -1,68 +0,0 @@
---
title: Python Context Manager - Python Cheatsheet
description: While Python's context managers are widely used, few understand the purpose behind their use. These statements, commonly used with reading and writing files, assist the application in conserving system memory and improve resource management by ensuring specific resources are only in use for certain processes.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Context Manager
</base-title>
While Python's context managers are widely used, few understand the purpose behind their use. These statements, commonly used with reading and writing files, assist the application in conserving system memory and improve resource management by ensuring specific resources are only in use for certain processes.
## The with statement
A context manager is an object that is notified when a context (a block of code) starts and ends. You commonly use one with the `with` statement. It takes care of the notifying.
For example, file objects are context managers. When a context ends, the file object is closed automatically:
```python
>>> with open(filename) as f:
... file_contents = f.read()
...
>>> # the open_file object has automatically been closed.
```
Anything that ends execution of the block causes the context manager's exit method to be called. This includes exceptions, and can be useful when an error causes you to prematurely exit an open file or connection. Exiting a script without properly closing files/connections is a bad idea, that may cause data loss or other problems. By using a context manager, you can ensure that precautions are always taken to prevent damage or loss in this way.
## Writing your own context manager
It is also possible to write a context manager using generator syntax thanks to the `contextlib.contextmanager` decorator:
```python
>>> import contextlib
>>> @contextlib.contextmanager
... def context_manager(num):
... print('Enter')
... yield num + 1
... print('Exit')
...
>>> with context_manager(2) as cm:
... # the following instructions are run when
... # the 'yield' point of the context manager is
... # reached. 'cm' will have the value that was yielded
... print('Right in the middle with cm = {}'.format(cm))
...
# Enter
# Right in the middle with cm = 3
# Exit
```
## Class based context manager
You can define class based context manager. The key methods are `__enter__` and `__exit__`
```python
class ContextManager:
def __enter__(self, *args, **kwargs):
print("--enter--")
def __exit__(self, *args):
print("--exit--")
with ContextManager():
print("test")
#--enter--
#test
#--exit--
```

View File

@@ -1,490 +0,0 @@
---
title: Python Control Flow - Python Cheatsheet
description: Control flow is the order in which individual statements, instructions or function calls are executed or evaluated. The control flow of a Python program is regulated by conditional statements, loops, and function calls.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Control Flow
</base-title>
<base-disclaimer>
<base-disclaimer-title>
Python control flow
</base-disclaimer-title>
<base-disclaimer-content>
Control flow is the order in which individual statements, instructions, or function calls are executed or evaluated. The control flow of a Python program is regulated by conditional statements, loops, and function calls.
</base-disclaimer-content>
</base-disclaimer>
## Comparison Operators
| Operator | Meaning |
| -------- | ------------------------ |
| `==` | Equal to |
| `!=` | Not equal to |
| `<` | Less than |
| `>` | Greater Than |
| `<=` | Less than or Equal to |
| `>=` | Greater than or Equal to |
These operators evaluate to True or False depending on the values you give them.
Examples:
```python
>>> 42 == 42
True
>>> 40 == 42
False
>>> 'hello' == 'hello'
True
>>> 'hello' == 'Hello'
False
>>> 'dog' != 'cat'
True
>>> 42 == 42.0
True
>>> 42 == '42'
False
```
## Boolean Operators
There are three Boolean operators: `and`, `or`, and `not`.
In the order of precedence, highest to lowest they are `not`, `and` and `or`.
The `and` Operators _Truth_ Table:
| Expression | Evaluates to |
| ----------------- | ------------ |
| `True and True` | `True` |
| `True and False` | `False` |
| `False and True` | `False` |
| `False and False` | `False` |
The `or` Operators _Truth_ Table:
| Expression | Evaluates to |
| ---------------- | ------------ |
| `True or True` | `True` |
| `True or False` | `True` |
| `False or True` | `True` |
| `False or False` | `False` |
The `not` Operators _Truth_ Table:
| Expression | Evaluates to |
| ----------- | ------------ |
| `not True` | `False` |
| `not False` | `True` |
## Mixing Operators
You can mix boolean and comparison operators:
```python
>>> (4 < 5) and (5 < 6)
True
>>> (4 < 5) and (9 < 6)
False
>>> (1 == 2) or (2 == 2)
True
```
Also, you can mix use multiple Boolean operators in an expression, along with the comparison operators:
```python
>>> 2 + 2 == 4 and not 2 + 2 == 5 and 2 * 2 == 2 + 2
True
>>> # In the statement below 3 < 4 and 5 > 5 gets executed first evaluating to False
>>> # Then 5 > 4 returns True so the results after True or False is True
>>> 5 > 4 or 3 < 4 and 5 > 5
True
>>> # Now the statement within parentheses gets executed first so True and False returns False.
>>> (5 > 4 or 3 < 4) and 5 > 5
False
```
## if Statements
The `if` statement evaluates an expression, and if that expression is `True`, it then executes the following indented code:
```python
>>> name = 'Debora'
>>> if name == 'Debora':
... print('Hi, Debora')
...
# Hi, Debora
>>> if name != 'George':
... print('You are not George')
...
# You are not George
```
The `else` statement executes only if the evaluation of the `if` and all the `elif` expressions are `False`:
```python
>>> name = 'Debora'
>>> if name == 'George':
... print('Hi, George.')
... else:
... print('You are not George')
...
# You are not George
```
Only after the `if` statement expression is `False`, the `elif` statement is evaluated and executed:
```python
>>> name = 'George'
>>> if name == 'Debora':
... print('Hi Debora!')
... elif name == 'George':
... print('Hi George!')
...
# Hi George!
```
the `elif` and `else` parts are optional.
```python
>>> name = 'Antony'
>>> if name == 'Debora':
... print('Hi Debora!')
... elif name == 'George':
... print('Hi George!')
... else:
... print('Who are you?')
...
# Who are you?
```
## Ternary Conditional Operator
Many programming languages have a ternary operator, which define a conditional expression. The most common usage is to make a terse, simple conditional assignment statement. In other words, it offers one-line code to evaluate the first expression if the condition is true, and otherwise it evaluates the second expression.
```
<expression1> if <condition> else <expression2>
```
Example:
```python
>>> age = 15
>>> # this if statement:
>>> if age < 18:
... print('kid')
... else:
... print('adult')
...
# output: kid
>>> # is equivalent to this ternary operator:
>>> print('kid' if age < 18 else 'adult')
# output: kid
```
Ternary operators can be chained:
```python
>>> age = 15
>>> # this ternary operator:
>>> print('kid' if age < 13 else 'teen' if age < 18 else 'adult')
>>> # is equivalent to this if statement:
>>> if age < 18:
... if age < 13:
... print('kid')
... else:
... print('teen')
... else:
... print('adult')
...
# output: teen
```
## Switch-Case Statement
<base-disclaimer>
<base-disclaimer-title>
Switch-Case statements
</base-disclaimer-title>
<base-disclaimer-content>
In computer programming languages, a switch statement is a type of selection control mechanism used to allow the value of a variable or expression to change the control flow of program execution via search and map.
</base-disclaimer-content>
</base-disclaimer>
The _Switch-Case statements_, or **Structural Pattern Matching**, was firstly introduced in 2020 via [PEP 622](https://peps.python.org/pep-0622/), and then officially released with **Python 3.10** in September 2022.
<base-disclaimer>
<base-disclaimer-title>
Official Tutorial
</base-disclaimer-title>
<base-disclaimer-content>
The <a href="https://peps.python.org/pep-0636/" target="_blank">PEP 636</a> provides an official tutorial for the Python Pattern matching or Switch-Case statements.
</base-disclaimer-content>
</base-disclaimer>
### Matching single values
```python
>>> response_code = 201
>>> match response_code:
... case 200:
... print("OK")
... case 201:
... print("Created")
... case 300:
... print("Multiple Choices")
... case 307:
... print("Temporary Redirect")
... case 404:
... print("404 Not Found")
... case 500:
... print("Internal Server Error")
... case 502:
... print("502 Bad Gateway")
...
# Created
```
### Matching with the or Pattern
In this example, the pipe character (`|` or `or`) allows python to return the same response for two or more cases.
```python
>>> response_code = 502
>>> match response_code:
... case 200 | 201:
... print("OK")
... case 300 | 307:
... print("Redirect")
... case 400 | 401:
... print("Bad Request")
... case 500 | 502:
... print("Internal Server Error")
...
# Internal Server Error
```
### Matching by the length of an Iterable
```python
>>> today_responses = [200, 300, 404, 500]
>>> match today_responses:
... case [a]:
... print(f"One response today: {a}")
... case [a, b]:
... print(f"Two responses today: {a} and {b}")
... case [a, b, *rest]:
... print(f"All responses: {a}, {b}, {rest}")
...
# All responses: 200, 300, [404, 500]
```
### Default value
The underscore symbol (`_`) is used to define a default case:
```python
>>> response_code = 800
>>> match response_code:
... case 200 | 201:
... print("OK")
... case 300 | 307:
... print("Redirect")
... case 400 | 401:
... print("Bad Request")
... case 500 | 502:
... print("Internal Server Error")
... case _:
... print("Invalid Code")
...
# Invalid Code
```
### Matching Builtin Classes
```python
>>> response_code = "300"
>>> match response_code:
... case int():
... print('Code is a number')
... case str():
... print('Code is a string')
... case _:
... print('Code is neither a string nor a number')
...
# Code is a string
```
### Guarding Match-Case Statements
```python
>>> response_code = 300
>>> match response_code:
... case int():
... if response_code > 99 and response_code < 500:
... print('Code is a valid number')
... case _:
... print('Code is an invalid number')
...
# Code is a valid number
```
## while Loop Statements
The while statement is used for repeated execution as long as an expression is `True`:
```python
>>> spam = 0
>>> while spam < 5:
... print('Hello, world.')
... spam = spam + 1
...
# Hello, world.
# Hello, world.
# Hello, world.
# Hello, world.
# Hello, world.
```
## break Statements
If the execution reaches a `break` statement, it immediately exits the `while` loops clause:
```python
>>> while True:
... name = input('Please type your name: ')
... if name == 'your name':
... break
...
>>> print('Thank you!')
# Please type your name: your name
# Thank you!
```
## continue Statements
When the program execution reaches a `continue` statement, the program execution immediately jumps back to the start of the loop.
```python
>>> while True:
... name = input('Who are you? ')
... if name != 'Joe':
... continue
... password = input('Password? (It is a fish.): ')
... if password == 'swordfish':
... break
...
>>> print('Access granted.')
# Who are you? Charles
# Who are you? Debora
# Who are you? Joe
# Password? (It is a fish.): swordfish
# Access granted.
```
## For loop
The `for` loop iterates over a `list`, `tuple`, `dictionary`, `set` or `string`:
```python
>>> pets = ['Bella', 'Milo', 'Loki']
>>> for pet in pets:
... print(pet)
...
# Bella
# Milo
# Loki
```
## The range() function
The `range()` function returns a sequence of numbers. It starts from 0, increments by 1, and stops before a specified number:
```python
>>> for i in range(5):
... print(f'Will stop at 5! or 4? ({i})')
...
# Will stop at 5! or 4? (0)
# Will stop at 5! or 4? (1)
# Will stop at 5! or 4? (2)
# Will stop at 5! or 4? (3)
# Will stop at 5! or 4? (4)
```
The `range()` function can also modify its 3 defaults arguments. The first two will be the `start` and `stop` values, and the third will be the `step` argument. The step is the amount that the variable is increased by after each iteration.
```python
# range(start, stop, step)
>>> for i in range(0, 10, 2):
... print(i)
...
# 0
# 2
# 4
# 6
# 8
```
You can even use a negative number for the step argument to make the for loop count down instead of up.
```python
>>> for i in range(5, -1, -1):
... print(i)
...
# 5
# 4
# 3
# 2
# 1
# 0
```
## For else statement
This allows to specify a statement to execute in case of the full loop has been executed. Only
useful when a `break` condition can occur in the loop:
```python
>>> for i in [1, 2, 3, 4, 5]:
... if i == 3:
... break
... else:
... print("only executed when no item is equal to 3")
```
## Ending a Program with sys.exit()
`exit()` function allows exiting Python.
```python
>>> import sys
>>> while True:
... feedback = input('Type exit to exit: ')
... if feedback == 'exit':
... print(f'You typed {feedback}.')
... sys.exit()
...
# Type exit to exit: open
# Type exit to exit: close
# Type exit to exit: exit
# You typed exit
```

View File

@@ -1,77 +0,0 @@
---
title: Python Dataclasses - Python Cheatsheet
description: Dataclasses are python classes, but are suited for storing data objects. This module provides a decorator and functions for automatically adding generated special methods such as __init__() and __repr__() to user-defined classes.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Dataclasses
</base-title>
`Dataclasses` are python classes, but are suited for storing data objects.
This module provides a decorator and functions for automatically adding generated special methods such as `__init__()` and `__repr__()` to user-defined classes.
## Features
1. They store data and represent a certain data type. Ex: A number. For people familiar with ORMs, a model instance is a data object. It represents a specific kind of entity. It holds attributes that define or represent the entity.
2. They can be compared to other objects of the same type. Ex: A number can be greater than, less than, or equal to another number.
Python 3.7 provides a decorator dataclass that is used to convert a class into a dataclass.
```python
>>> class Number:
... def __init__(self, val):
... self.val = val
...
>>> obj = Number(2)
>>> obj.val
# 2
```
with dataclass
```python
>>> @dataclass
... class Number:
... val: int
...
>>> obj = Number(2)
>>> obj.val
# 2
```
## Default values
It is easy to add default values to the fields of your data class.
```python
>>> @dataclass
... class Product:
... name: str
... count: int = 0
... price: float = 0.0
...
>>> obj = Product("Python")
>>> obj.name
# Python
>>> obj.count
# 0
>>> obj.price
# 0.0
```
## Type hints
It is mandatory to define the data type in dataclass. However, If you would rather not specify the datatype then, use `typing.Any`.
```python
>>> from dataclasses import dataclass
>>> from typing import Any
>>> @dataclass
... class WithoutExplicitTypes:
... name: Any
... value: Any = 42
```

View File

@@ -1,197 +0,0 @@
---
title: Python Debugging - Python Cheatsheet
description: In computer programming and software development, debugging is the process of finding and resolving bugs (defects or problems that prevent correct operation) within computer programs, software, or systems.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Debugging
</base-title>
<base-disclaimer>
<base-disclaimer-title>
<a target="_blank" href="https://en.wikipedia.org/wiki/Debugging">Finding and resolving bugs</a>
</base-disclaimer-title>
<base-disclaimer-content>
In computer programming and software development, debugging is the process of finding and resolving bugs (defects or problems that prevent correct operation) within computer programs, software, or systems.
</base-disclaimer-content>
</base-disclaimer>
## Raising Exceptions
Exceptions are raised with a raise statement. In code, a raise statement consists of the following:
- The `raise` keyword
- A call to the `Exception()` function
- A string with a helpful error message passed to the `Exception()` function
```python
>>> raise Exception('This is the error message.')
# Traceback (most recent call last):
# File "<pyshell#191>", line 1, in <module>
# raise Exception('This is the error message.')
# Exception: This is the error message.
```
Typically, its the code that calls the function, not the function itself, that knows how to handle an exception. So, you will commonly see a raise statement inside a function and the `try` and `except` statements in the code calling the function.
```python
>>> def box_print(symbol, width, height):
... if len(symbol) != 1:
... raise Exception('Symbol must be a single character string.')
... if width <= 2:
... raise Exception('Width must be greater than 2.')
... if height <= 2:
... raise Exception('Height must be greater than 2.')
... print(symbol * width)
... for i in range(height - 2):
... print(symbol + (' ' * (width - 2)) + symbol)
... print(symbol * width)
...
>>> for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
... try:
... box_print(sym, w, h)
... except Exception as err:
... print('An exception happened: ' + str(err))
...
# ****
# * *
# * *
# ****
# OOOOOOOOOOOOOOOOOOOO
# O O
# O O
# O O
# OOOOOOOOOOOOOOOOOOOO
# An exception happened: Width must be greater than 2.
# An exception happened: Symbol must be a single character string.
```
Read more about [Exception Handling](/cheatsheet/exception-handling).
## Getting the Traceback as a string
The `traceback` is displayed by Python whenever a raised exception goes unhandled. But can also obtain it as a string by calling traceback.format_exc(). This function is useful if you want the information from an exceptions traceback but also want an except statement to gracefully handle the exception. You will need to import Pythons traceback module before calling this function.
```python
>>> import traceback
>>> try:
... raise Exception('This is the error message.')
>>> except:
... with open('errorInfo.txt', 'w') as error_file:
... error_file.write(traceback.format_exc())
... print('The traceback info was written to errorInfo.txt.')
...
# 116
# The traceback info was written to errorInfo.txt.
```
The 116 is the return value from the `write()` method, since 116 characters were written to the file. The `traceback` text was written to errorInfo.txt.
Traceback (most recent call last):
File "<pyshell#28>", line 2, in <module>
Exception: This is the error message.
## Assertions
An assertion is a sanity check to make sure your code isnt doing something obviously wrong. These sanity checks are performed by `assert` statements. If the sanity check fails, then an `AssertionError` exception is raised. In code, an `assert` statement consists of the following:
- The `assert` keyword
- A condition (that is, an expression that evaluates to `True` or `False`)
- A comma
- A `string` to display when the condition is `False`
```python
>>> pod_bay_door_status = 'open'
>>> assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
>>> pod_bay_door_status = 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'
>>> assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
# Traceback (most recent call last):
# File "<pyshell#10>", line 1, in <module>
# assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
# AssertionError: The pod bay doors need to be "open".
```
In plain English, an assert statement says, “I assert that this condition holds true, and if not, there is a bug somewhere in the program.” Unlike exceptions, your code should not handle assert statements with try and except; if an assert fails, your program should crash. By failing fast like this, you shorten the time between the original cause of the bug and when you first notice the bug. This will reduce the amount of code you will have to check before finding the code thats causing the bug.
### Disabling Assertions
Assertions can be disabled by passing the `-O` option when running Python.
## Logging
To enable the `logging` module to display log messages on your screen as your program runs, copy the following to the top of your program:
```python
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s- %(message)s')
```
Say you wrote a function to calculate the factorial of a number. In mathematics, factorial 4 is 1 × 2 × 3 × 4, or 24. Factorial 7 is 1 × 2 × 3 × 4 × 5 × 6 × 7, or 5,040. Open a new file editor window and enter the following code. It has a bug in it, but you will also enter several log messages to help yourself figure out what is going wrong. Save the program as factorialLog.py.
```python
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s- %(message)s')
>>> logging.debug('Start of program')
>>> def factorial(n):
... logging.debug('Start of factorial(%s)' % (n))
... total = 1
... for i in range(1, n + 1):
... total *= i
... logging.debug('i is ' + str(i) + ', total is ' + str(total))
... logging.debug('End of factorial(%s)' % (n))
... return total
...
>>> print(factorial(5))
>>> logging.debug('End of program')
# 2015-05-23 16:20:12,664 - DEBUG - Start of program
# 2015-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
# 2015-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
# 2015-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
# 2015-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
# 2015-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
# 2015-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
# 2015-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
# 2015-05-23 16:20:12,680 - DEBUG - End of factorial(5)
# 0
# 2015-05-23 16:20:12,684 - DEBUG - End of program
```
## Logging Levels
Logging levels provide a way to categorize your log messages by importance. There are five logging levels, described in Table 10-1 from least to most important. Messages can be logged at each level using a different logging function.
| Level | Logging Function | Description |
| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `DEBUG` | `logging.debug()` | The lowest level. Used for small details. Usually you care about these messages only when diagnosing problems. |
| `INFO` | `logging.info()` | Used to record information on general events in your program or confirm that things are working at their point in the program. |
| `WARNING` | `logging.warning()` | Used to indicate a potential problem that doesnt prevent the program from working but might do so in the future. |
| `ERROR` | `logging.error()` | Used to record an error that caused the program to fail to do something. |
| `CRITICAL` | `logging.critical()` | The highest level. Used to indicate a fatal error that has caused or is about to cause the program to stop running entirely. |
## Disabling Logging
After youve debugged your program, you probably dont want all these log messages cluttering the screen. The logging.disable() function disables these so that you dont have to go into your program and remove all the logging calls by hand.
```python
>>> import logging
>>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s -%(levelname)s - %(message)s')
>>> logging.critical('Critical error! Critical error!')
# 2015-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error!
>>> logging.disable(logging.CRITICAL)
>>> logging.critical('Critical error! Critical error!')
>>> logging.error('Error! Error!')
```
## Logging to a File
Instead of displaying the log messages to the screen, you can write them to a text file. The `logging.basicConfig()` function takes a filename keyword argument, like so:
```python
>>> import logging
>>> logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
```

View File

@@ -1,188 +0,0 @@
---
title: Python Decorators - Python Cheatsheet
description: A Python Decorator is a syntax that provide a concise and reusable way for extending a function or a class.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Decorators
</base-title>
A Python Decorator provides a concise and reusable way for extending
a function or a class.
## Bare bone decorator
A decorator in its simplest form is a function that takes another
function as an argument and returns a wrapper. The following example
shows the creation of a decorator and its usage.
```python
def your_decorator(func):
def wrapper():
# Do stuff before func...
print("Before func!")
func()
# Do stuff after func...
print("After func!")
return wrapper
@your_decorator
def foo():
print("Hello World!")
foo()
# Before func!
# Hello World!
# After func!
```
## Decorator for a function with parameters
```python
def your_decorator(func):
def wrapper(*args,**kwargs):
# Do stuff before func...
print("Before func!")
func(*args,**kwargs)
# Do stuff after func...
print("After func!")
return wrapper
@your_decorator
def foo(bar):
print("My name is " + bar)
foo("Jack")
# Before func!
# My name is Jack
# After func!
```
## Template for a basic decorator
This template is useful for most decorator use-cases. It is valid for functions
with or without parameters, and with or without a return value.
```python
import functools
def your_decorator(func):
@functools.wraps(func) # For preserving the metadata of func.
def wrapper(*args,**kwargs):
# Do stuff before func...
result = func(*args,**kwargs)
# Do stuff after func..
return result
return wrapper
```
## Decorator with parameters
You can also define parameters for the decorator to use.
```python
import functools
def your_decorator(arg):
def decorator(func):
@functools.wraps(func) # For preserving the metadata of func.
def wrapper(*args,**kwargs):
# Do stuff before func possibly using arg...
result = func(*args,**kwargs)
# Do stuff after func possibly using arg...
return result
return wrapper
return decorator
```
To use this decorator:
```python
@your_decorator(arg = 'x')
def foo(bar):
return bar
```
## Class based decorators
To decorate a class method, you must define the decorator within the class. When
only the implicit argument `self` is passed to the method, without any explicit
additional arguments, you must make a separate decorator for only those methods
without any additional arguments. An example of this, shown below, is when you
want to catch and print exceptions in a certain way.
```python
class DecorateMyMethod:
def decorator_for_class_method_with_no_args(method):
def wrapper_for_class_method(self)
try:
return method(self)
except Exception as e:
print("\nWARNING: Please make note of the following:\n")
print(e)
return wrapper_for_class_method
def __init__(self,succeed:bool):
self.succeed = succeed
@decorator_for_class_method_with_no_args
def class_action(self):
if self.succeed:
print("You succeeded by choice.")
else:
raise Exception("Epic fail of your own creation.")
test_succeed = DecorateMyMethods(True)
test_succeed.class_action()
# You succeeded by choice.
test_fail = DecorateMyMethod(False)
test_fail.class_action()
# Exception: Epic fail of your own creation.
```
A decorator can also be defined as a class instead of a method. This is useful
for maintaining and updating a state, such as in the following example, where we
count the number of calls made to a method:
```python
class CountCallNumber:
def __init__(self, func):
self.func = func
self.call_number = 0
def __call__(self, *args, **kwargs):
self.call_number += 1
print("This is execution number " + str(self.call_number))
return self.func(*args, **kwargs)
@CountCallNumber
def say_hi(name):
print("Hi! My name is " + name)
say_hi("Jack")
# This is execution number 1
# Hi! My name is Jack
say_hi("James")
# This is execution number 2
# Hi! My name is James
```
<base-disclaimer>
<base-disclaimer-title>
Count Example
</base-disclaimer-title>
<base-disclaimer-content>
This count example is inspired by Patrick Loeber's <a href="https://youtu.be/HGOBQPFzWKo?si=IUvFzeQbzTmeEgKV" target="_blank">YouTube tutorial</a>.
</base-disclaimer-content>
</base-disclaimer>

View File

@@ -1,269 +0,0 @@
---
title: Python Dictionaries - Python Cheatsheet
description: In Python, a dictionary is an insertion-ordered (from Python > 3.7) collection of key, value pairs.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Dictionaries
</base-title>
In Python, a dictionary is an _ordered_ (from Python > 3.7) collection of `key`: `value` pairs.
<base-disclaimer>
<base-disclaimer-title>
From the Python 3 <a target="_blank" href="https://docs.python.org/3/tutorial/datastructures.html#dictionaries">documentation</a>
</base-disclaimer-title>
<base-disclaimer-content>
The main operations on a dictionary are storing a value with some key and extracting the value given the key. It is also possible to delete a key:value pair with <code>del</code>.
</base-disclaimer-content>
</base-disclaimer>
Example Dictionary:
```python
my_cat = {
'size': 'fat',
'color': 'gray',
'disposition': 'loud'
}
```
## Set key, value using subscript operator `[]`
```python
>>> my_cat = {
... 'size': 'fat',
... 'color': 'gray',
... 'disposition': 'loud',
... }
>>> my_cat['age_years'] = 2
>>> print(my_cat)
...
# {'size': 'fat', 'color': 'gray', 'disposition': 'loud', 'age_years': 2}
```
## Get value using subscript operator `[]`
In case the key is not present in dictionary <a target="_blank" href="https://docs.python.org/3/library/exceptions.html#KeyError">`KeyError`</a> is raised.
```python
>>> my_cat = {
... 'size': 'fat',
... 'color': 'gray',
... 'disposition': 'loud',
... }
>>> print(my_cat['size'])
...
# fat
>>> print(my_cat['eye_color'])
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# KeyError: 'eye_color'
```
## values()
The `values()` method gets the **values** of the dictionary:
```python
>>> pet = {'color': 'red', 'age': 42}
>>> for value in pet.values():
... print(value)
...
# red
# 42
```
## keys()
The `keys()` method gets the **keys** of the dictionary:
```python
>>> pet = {'color': 'red', 'age': 42}
>>> for key in pet.keys():
... print(key)
...
# color
# age
```
There is no need to use **.keys()** since by default you will loop through keys:
```python
>>> pet = {'color': 'red', 'age': 42}
>>> for key in pet:
... print(key)
...
# color
# age
```
## items()
The `items()` method gets the **items** of a dictionary and returns them as a <router-link to=/cheatsheet/lists-and-tuples#the-tuple-data-type>Tuple</router-link>:
```python
>>> pet = {'color': 'red', 'age': 42}
>>> for item in pet.items():
... print(item)
...
# ('color', 'red')
# ('age', 42)
```
Using the `keys()`, `values()`, and `items()` methods, a for loop can iterate over the keys, values, or key-value pairs in a dictionary, respectively.
```python
>>> pet = {'color': 'red', 'age': 42}
>>> for key, value in pet.items():
... print(f'Key: {key} Value: {value}')
...
# Key: color Value: red
# Key: age Value: 42
```
## get()
The `get()` method returns the value of an item with the given key. If the key doesn't exist, it returns `None`:
```python
>>> wife = {'name': 'Rose', 'age': 33}
>>> f'My wife name is {wife.get("name")}'
# 'My wife name is Rose'
>>> f'She is {wife.get("age")} years old.'
# 'She is 33 years old.'
>>> f'She is deeply in love with {wife.get("husband")}'
# 'She is deeply in love with None'
```
You can also change the default `None` value to one of your choice:
```python
>>> wife = {'name': 'Rose', 'age': 33}
>>> f'She is deeply in love with {wife.get("husband", "lover")}'
# 'She is deeply in love with lover'
```
## Adding items with setdefault()
It's possible to add an item to a dictionary in this way:
```python
>>> wife = {'name': 'Rose', 'age': 33}
>>> if 'has_hair' not in wife:
... wife['has_hair'] = True
```
Using the `setdefault` method, we can make the same code more short:
```python
>>> wife = {'name': 'Rose', 'age': 33}
>>> wife.setdefault('has_hair', True)
>>> wife
# {'name': 'Rose', 'age': 33, 'has_hair': True}
```
## Removing Items
### pop()
The `pop()` method removes and returns an item based on a given key.
```python
>>> wife = {'name': 'Rose', 'age': 33, 'hair': 'brown'}
>>> wife.pop('age')
# 33
>>> wife
# {'name': 'Rose', 'hair': 'brown'}
```
### popitem()
The `popitem()` method removes the last item in a dictionary and returns it.
```python
>>> wife = {'name': 'Rose', 'age': 33, 'hair': 'brown'}
>>> wife.popitem()
# ('hair', 'brown')
>>> wife
# {'name': 'Rose', 'age': 33}
```
### del()
The `del()` method removes an item based on a given key.
```python
>>> wife = {'name': 'Rose', 'age': 33, 'hair': 'brown'}
>>> del wife['age']
>>> wife
# {'name': 'Rose', 'hair': 'brown'}
```
### clear()
The`clear()` method removes all the items in a dictionary.
```python
>>> wife = {'name': 'Rose', 'age': 33, 'hair': 'brown'}
>>> wife.clear()
>>> wife
# {}
```
## Checking keys in a Dictionary
```python
>>> person = {'name': 'Rose', 'age': 33}
>>> 'name' in person.keys()
# True
>>> 'height' in person.keys()
# False
>>> 'skin' in person # You can omit keys()
# False
```
## Checking values in a Dictionary
```python
>>> person = {'name': 'Rose', 'age': 33}
>>> 'Rose' in person.values()
# True
>>> 33 in person.values()
# True
```
## Pretty Printing
```python
>>> import pprint
>>> wife = {'name': 'Rose', 'age': 33, 'has_hair': True, 'hair_color': 'brown', 'height': 1.6, 'eye_color': 'brown'}
>>> pprint.pprint(wife)
# {'age': 33,
# 'eye_color': 'brown',
# 'hair_color': 'brown',
# 'has_hair': True,
# 'height': 1.6,
# 'name': 'Rose'}
```
## Merge two dictionaries
For Python 3.5+:
```python
>>> dict_a = {'a': 1, 'b': 2}
>>> dict_b = {'b': 3, 'c': 4}
>>> dict_c = {**dict_a, **dict_b}
>>> dict_c
# {'a': 1, 'b': 3, 'c': 4}
```

View File

@@ -1,136 +0,0 @@
---
title: Python Exception Handling - Python Cheatsheet
description: In Python, exception handling is the process of responding to the occurrence of exceptions.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Exception Handling
</base-title>
<base-disclaimer>
<base-disclaimer-title>
<a target="_blank" href="https://en.wikipedia.org/wiki/Exception_handling">Exception handling</a>
</base-disclaimer-title>
<base-disclaimer-content>
In computing and computer programming, exception handling is the process of responding to the occurrence of exceptions anomalous or exceptional conditions requiring special processing.
</base-disclaimer-content>
</base-disclaimer>
Python has many [built-in exceptions](https://docs.python.org/3/library/exceptions.html) that are raised when a program encounters an error, and most external libraries, like the popular [Requests](https://requests.readthedocs.io/en/latest), include his own [custom exceptions](https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions) that we will need to deal to.
## Basic exception handling
You can't divide by zero, that is a mathematical true, and if you try to do it in Python, the interpreter will raise the built-in exception [ZeroDivisionError](https://docs.python.org/3/library/exceptions.html#ZeroDivisionError):
```python
>>> def divide(dividend , divisor):
... print(dividend / divisor)
...
>>> divide(dividend=10, divisor=5)
# 2
>>> divide(dividend=10, divisor=0)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ZeroDivisionError: division by zero
```
Let's say we don't want our program to stop its execution or show the user an output he will not understand. Say we want to print a useful and clear message, then we need to **_handle_** the exception with the `try` and `except` keywords:
```python
>>> def divide(dividend , divisor):
... try:
... print(dividend / divisor)
... except ZeroDivisionError:
... print('You can not divide by 0')
...
>>> divide(dividend=10, divisor=5)
# 2
>>> divide(dividend=10, divisor=0)
# You can not divide by 0
```
## Handling Multiple exceptions using one exception block
You can also handle multiple exceptions in one line like the following without the need to create multiple exception blocks.
```python
>>> def divide(dividend , divisor):
... try:
... if (dividend == 10):
... var = 'str' + 1
... else:
... print(dividend / divisor)
... except (ZeroDivisionError, TypeError) as error:
... print(error)
...
>>> divide(dividend=20, divisor=5)
# 4
>>> divide(dividend=10, divisor=5)
# `can only concatenate str (not "int") to str` Error message
>>> divide(dividend=10, divisor=0)
# `division by zero` Error message
```
## Finally code in exception handling
The code inside the `finally` section is always executed, no matter if an exception has been raised or not:
```python
>>> def divide(dividend , divisor):
... try:
... print(dividend / divisor)
... except ZeroDivisionError:
... print('You can not divide by 0')
... finally:
... print('Execution finished')
...
>>> divide(dividend=10, divisor=5)
# 5
# Execution finished
>>> divide(dividend=10, divisor=0)
# You can not divide by 0
# Execution finished
```
## Custom Exceptions
Custom exceptions initialize by creating a `class` that inherits from the base `Exception` class of Python, and are raised using the `raise` keyword:
```python
>>> class MyCustomException(Exception):
... pass
...
>>> raise MyCustomException
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# __main__.MyCustomException
```
To declare a custom exception message, you can pass it as a parameter:
```python
>>> class MyCustomException(Exception):
... pass
...
>>> raise MyCustomException('A custom message for my custom exception')
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# __main__.MyCustomException: A custom message for my custom exception
```
Handling a custom exception is the same as any other:
```python
>>> try:
... raise MyCustomException('A custom message for my custom exception')
>>> except MyCustomException:
... print('My custom exception was raised')
...
# My custom exception was raised
```

View File

@@ -1,529 +0,0 @@
---
title: File and directory Paths - Python Cheatsheet
description: There are two main modules in Python that deals with path manipulation. One is the os.path module and the other is the pathlib module.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Handling file and directory Paths
</base-title>
There are two main modules in Python that deal with path manipulation.
One is the `os.path` module and the other is the `pathlib` module.
<base-disclaimer>
<base-disclaimer-title>
os.path VS pathlib
</base-disclaimer-title>
<base-disclaimer-content>
The `pathlib` module was added in Python 3.4, offering an object-oriented way to handle file system paths.
</base-disclaimer-content>
</base-disclaimer>
## Linux and Windows Paths
On Windows, paths are written using backslashes (`\`) as the separator between
folder names. On Unix based operating system such as macOS, Linux, and BSDs,
the forward slash (`/`) is used as the path separator. Joining paths can be
a headache if your code needs to work on different platforms.
Fortunately, Python provides easy ways to handle this. We will showcase
how to deal with both, `os.path.join` and `pathlib.Path.joinpath`
Using `os.path.join` on Windows:
```python
>>> import os
>>> os.path.join('usr', 'bin', 'spam')
# 'usr\\bin\\spam'
```
And using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> print(Path('usr').joinpath('bin').joinpath('spam'))
# usr/bin/spam
```
`pathlib` also provides a shortcut to joinpath using the `/` operator:
```python
>>> from pathlib import Path
>>> print(Path('usr') / 'bin' / 'spam')
# usr/bin/spam
```
Notice the path separator is different between Windows and Unix based operating
system, that's why you want to use one of the above methods instead of
adding strings together to join paths together.
Joining paths is helpful if you need to create different file paths under
the same directory.
Using `os.path.join` on Windows:
```python
>>> my_files = ['accounts.txt', 'details.csv', 'invite.docx']
>>> for filename in my_files:
... print(os.path.join('C:\\Users\\asweigart', filename))
...
# C:\Users\asweigart\accounts.txt
# C:\Users\asweigart\details.csv
# C:\Users\asweigart\invite.docx
```
Using `pathlib` on \*nix:
```python
>>> my_files = ['accounts.txt', 'details.csv', 'invite.docx']
>>> home = Path.home()
>>> for filename in my_files:
... print(home / filename)
...
# /home/asweigart/accounts.txt
# /home/asweigart/details.csv
# /home/asweigart/invite.docx
```
## The current working directory
Using `os` on Windows:
```python
>>> import os
>>> os.getcwd()
# 'C:\\Python34'
>>> os.chdir('C:\\Windows\\System32')
>>> os.getcwd()
# 'C:\\Windows\\System32'
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> from os import chdir
>>> print(Path.cwd())
# /home/asweigart
>>> chdir('/usr/lib/python3.6')
>>> print(Path.cwd())
# /usr/lib/python3.6
```
## Creating new folders
Using `os` on Windows:
```python
>>> import os
>>> os.makedirs('C:\\delicious\\walnut\\waffles')
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> cwd = Path.cwd()
>>> (cwd / 'delicious' / 'walnut' / 'waffles').mkdir()
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "/usr/lib/python3.6/pathlib.py", line 1226, in mkdir
# self._accessor.mkdir(self, mode)
# File "/usr/lib/python3.6/pathlib.py", line 387, in wrapped
# return strfunc(str(pathobj), *args)
# FileNotFoundError: [Errno 2] No such file or directory: '/home/asweigart/delicious/walnut/waffles'
```
Oh no, we got a nasty error! The reason is that the 'delicious' directory does
not exist, so we cannot make the 'walnut' and the 'waffles' directories under
it. To fix this, do:
```python
>>> from pathlib import Path
>>> cwd = Path.cwd()
>>> (cwd / 'delicious' / 'walnut' / 'waffles').mkdir(parents=True)
```
And all is good :)
## Absolute vs. Relative paths
There are two ways to specify a file path.
- An **absolute path**, which always begins with the root folder
- A **relative path**, which is relative to the programs current working directory
There are also the dot (`.`) and dot-dot (`..`) folders. These are not real folders, but special names that can be used in a path. A single period (“dot”) for a folder name is shorthand for “this directory.” Two periods (“dot-dot”) means “the parent folder.”
### Handling Absolute paths
To see if a path is an absolute path:
Using `os.path` on \*nix:
```python
>>> import os
>>> os.path.isabs('/')
# True
>>> os.path.isabs('..')
# False
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> Path('/').is_absolute()
# True
>>> Path('..').is_absolute()
# False
```
You can extract an absolute path with both `os.path` and `pathlib`
Using `os.path` on \*nix:
```python
>>> import os
>>> os.getcwd()
'/home/asweigart'
>>> os.path.abspath('..')
'/home'
```
Using `pathlib` on \*nix:
```python
from pathlib import Path
print(Path.cwd())
# /home/asweigart
print(Path('..').resolve())
# /home
```
### Handling Relative paths
You can get a relative path from a starting path to another path.
Using `os.path` on \*nix:
```python
>>> import os
>>> os.path.relpath('/etc/passwd', '/')
# 'etc/passwd'
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> print(Path('/etc/passwd').relative_to('/'))
# etc/passwd
```
## Path and File validity
### Checking if a file/directory exists
Using `os.path` on \*nix:
```python
>>> import os
>>> os.path.exists('.')
# True
>>> os.path.exists('setup.py')
# True
>>> os.path.exists('/etc')
# True
>>> os.path.exists('nonexistentfile')
# False
```
Using `pathlib` on \*nix:
```python
from pathlib import Path
>>> Path('.').exists()
# True
>>> Path('setup.py').exists()
# True
>>> Path('/etc').exists()
# True
>>> Path('nonexistentfile').exists()
# False
```
### Checking if a path is a file
Using `os.path` on \*nix:
```python
>>> import os
>>> os.path.isfile('setup.py')
# True
>>> os.path.isfile('/home')
# False
>>> os.path.isfile('nonexistentfile')
# False
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> Path('setup.py').is_file()
# True
>>> Path('/home').is_file()
# False
>>> Path('nonexistentfile').is_file()
# False
```
### Checking if a path is a directory
Using `os.path` on \*nix:
```python
>>> import os
>>> os.path.isdir('/')
# True
>>> os.path.isdir('setup.py')
# False
>>> os.path.isdir('/spam')
# False
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> Path('/').is_dir()
# True
>>> Path('setup.py').is_dir()
# False
>>> Path('/spam').is_dir()
# False
```
## Getting a file's size in bytes
Using `os.path` on Windows:
```python
>>> import os
>>> os.path.getsize('C:\\Windows\\System32\\calc.exe')
# 776192
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> stat = Path('/bin/python3.6').stat()
>>> print(stat) # stat contains some other information about the file as well
# os.stat_result(st_mode=33261, st_ino=141087, st_dev=2051, st_nlink=2, st_uid=0,
# --snip--
# st_gid=0, st_size=10024, st_atime=1517725562, st_mtime=1515119809, st_ctime=1517261276)
>>> print(stat.st_size) # size in bytes
# 10024
```
## Listing directories
Listing directory contents using `os.listdir` on Windows:
```python
>>> import os
>>> os.listdir('C:\\Windows\\System32')
# ['0409', '12520437.cpx', '12520850.cpx', '5U877.ax', 'aaclient.dll',
# --snip--
# 'xwtpdui.dll', 'xwtpw32.dll', 'zh-CN', 'zh-HK', 'zh-TW', 'zipfldr.dll']
```
Listing directory contents using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> for f in Path('/usr/bin').iterdir():
... print(f)
...
# ...
# /usr/bin/tiff2rgba
# /usr/bin/iconv
# /usr/bin/ldd
# /usr/bin/cache_restore
# /usr/bin/udiskie
# /usr/bin/unix2dos
# /usr/bin/t1reencode
# /usr/bin/epstopdf
# /usr/bin/idle3
# ...
```
## Directory file sizes
<base-warning>
<base-warning-title>
WARNING
</base-warning-title>
<base-warning-content>
Directories themselves also have a size! So, you might want to check for whether a path is a file or directory using the methods in the methods discussed in the above section.
</base-warning-content>
</base-warning>
Using `os.path.getsize()` and `os.listdir()` together on Windows:
```python
>>> import os
>>> total_size = 0
>>> for filename in os.listdir('C:\\Windows\\System32'):
... total_size = total_size + os.path.getsize(os.path.join('C:\\Windows\\System32', filename))
...
>>> print(total_size)
# 1117846456
```
Using `pathlib` on \*nix:
```python
>>> from pathlib import Path
>>> total_size = 0
>>> for sub_path in Path('/usr/bin').iterdir():
... total_size += sub_path.stat().st_size
...
>>> print(total_size)
# 1903178911
```
## Copying files and folders
The `shutil` module provides functions for copying files, as well as entire folders.
```python
>>> import shutil, os
>>> os.chdir('C:\\')
>>> shutil.copy('C:\\spam.txt', 'C:\\delicious')
# C:\\delicious\\spam.txt'
>>> shutil.copy('eggs.txt', 'C:\\delicious\\eggs2.txt')
# 'C:\\delicious\\eggs2.txt'
```
While `shutil.copy()` will copy a single file, `shutil.copytree()` will copy an entire folder and every folder and file contained in it:
```python
>>> import shutil, os
>>> os.chdir('C:\\')
>>> shutil.copytree('C:\\bacon', 'C:\\bacon_backup')
# 'C:\\bacon_backup'
```
## Moving and Renaming
```python
>>> import shutil
>>> shutil.move('C:\\bacon.txt', 'C:\\eggs')
# 'C:\\eggs\\bacon.txt'
```
The destination path can also specify a filename. In the following example, the source file is moved and renamed:
```python
>>> shutil.move('C:\\bacon.txt', 'C:\\eggs\\new_bacon.txt')
# 'C:\\eggs\\new_bacon.txt'
```
If there is no eggs folder, then `move()` will rename bacon.txt to a file named eggs:
```python
>>> shutil.move('C:\\bacon.txt', 'C:\\eggs')
# 'C:\\eggs'
```
## Deleting files and folders
- Calling `os.unlink(path)` or `Path.unlink()` will delete the file at path.
- Calling `os.rmdir(path)` or `Path.rmdir()` will delete the folder at path. This folder must be empty of any files or folders.
- Calling `shutil.rmtree(path)` will remove the folder at path, and all files and folders it contains will also be deleted.
## Walking a Directory Tree
```python
>>> import os
>>>
>>> for folder_name, subfolders, filenames in os.walk('C:\\delicious'):
... print(f'The current folder is {folder_name}')
... for subfolder in subfolders:
... print(f'SUBFOLDER OF {folder_name}: {subfolder}')
... for filename in filenames:
... print(f'FILE INSIDE {folder_name}: {filename}')
... print('')
...
# The current folder is C:\delicious
# SUBFOLDER OF C:\delicious: cats
# SUBFOLDER OF C:\delicious: walnut
# FILE INSIDE C:\delicious: spam.txt
# The current folder is C:\delicious\cats
# FILE INSIDE C:\delicious\cats: catnames.txt
# FILE INSIDE C:\delicious\cats: zophie.jpg
# The current folder is C:\delicious\walnut
# SUBFOLDER OF C:\delicious\walnut: waffles
# The current folder is C:\delicious\walnut\waffles
# FILE INSIDE C:\delicious\walnut\waffles: butter.txt
```
<base-disclaimer>
<base-disclaimer-title>
Pathlib vs Os Module
</base-disclaimer-title>
<base-disclaimer-content>
`pathlib` provides a lot more functionality than the ones listed above, like getting file name, getting file extension, reading/writing a file without manually opening it, etc. See the <a target="_blank" href="https://docs.python.org/3/library/pathlib.html">official documentation</a> if you intend to know more.
</base-disclaimer-content>
</base-disclaimer>

View File

@@ -1,176 +0,0 @@
---
title: Python Functions - Python Cheatsheet
description: In Python, A function is a block of organized code that is used to perform a single task.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Functions
</base-title>
<base-disclaimer>
<base-disclaimer-title>
<a target="_blank" href="https://en.wikiversity.org/wiki/Programming_Fundamentals/Functions">Programming Functions</a>
</base-disclaimer-title>
<base-disclaimer-content>
A function is a block of organized code that is used to perform a single task. They provide better modularity for your application and reuse-ability.
</base-disclaimer-content>
</base-disclaimer>
## Function Arguments
A function can take `arguments` and `return values`:
In the following example, the function **say_hello** receives the argument "name" and prints a greeting:
```python
>>> def say_hello(name):
... print(f'Hello {name}')
...
>>> say_hello('Carlos')
# Hello Carlos
>>> say_hello('Wanda')
# Hello Wanda
>>> say_hello('Rose')
# Hello Rose
```
## Keyword Arguments
To improve code readability, we should be as explicit as possible. We can achieve this in our functions by using `Keyword Arguments`:
```python
>>> def say_hi(name, greeting):
... print(f"{greeting} {name}")
...
>>> # without keyword arguments
>>> say_hi('John', 'Hello')
# Hello John
>>> # with keyword arguments
>>> say_hi(name='Anna', greeting='Hi')
# Hi Anna
```
## Return Values
When creating a function using the `def` statement, you can specify what the return value should be with a `return` statement. A return statement consists of the following:
- The `return` keyword.
- The value or expression that the function should return.
```python
>>> def sum_two_numbers(number_1, number_2):
... return number_1 + number_2
...
>>> result = sum_two_numbers(7, 8)
>>> print(result)
# 15
```
## Local and Global Scope
- Code in the global scope cannot use any local variables.
- However, a local scope can access global variables.
- Code in a functions local scope cannot use variables in any other local scope.
- You can use the same name for different variables if they are in different scopes. That is, there can be a local variable named spam and a global variable also named spam.
```python
global_variable = 'I am available everywhere'
>>> def some_function():
... print(global_variable) # because is global
... local_variable = "only available within this function"
... print(local_variable)
...
>>> # the following code will throw error because
>>> # 'local_variable' only exists inside 'some_function'
>>> print(local_variable)
Traceback (most recent call last):
File "<stdin>", line 10, in <module>
NameError: name 'local_variable' is not defined
```
## The global Statement
If you need to modify a global variable from within a function, use the global statement:
```python
>>> def spam():
... global eggs
... eggs = 'spam'
...
>>> eggs = 'global'
>>> spam()
>>> print(eggs)
```
There are four rules to tell whether a variable is in a local scope or global scope:
1. If a variable is being used in the global scope (that is, outside all functions), then it is always a global variable.
1. If there is a global statement for that variable in a function, it is a global variable.
1. Otherwise, if the variable is used in an assignment statement in the function, it is a local variable.
1. But if the variable is not used in an assignment statement, it is a global variable.
## Lambda Functions
In Python, a lambda function is a single-line, anonymous function, which can have any number of arguments, but it can only have one expression.
<base-disclaimer>
<base-disclaimer-title>
From the <a target="_blank" href="https://docs.python.org/3/library/ast.html?highlight=lambda#function-and-class-definitions">Python 3 Tutorial</a>
</base-disclaimer-title>
<base-disclaimer-content>
lambda is a minimal function definition that can be used inside an expression. Unlike FunctionDef, body holds a single node.
</base-disclaimer-content>
</base-disclaimer>
<base-warning>
<base-warning-title>
Single line expression
</base-warning-title>
<base-warning-content>
Lambda functions can only evaluate an expression, like a single line of code.
</base-warning-content>
</base-warning>
This function:
```python
>>> def add(x, y):
... return x + y
...
>>> add(5, 3)
# 8
```
Is equivalent to the _lambda_ function:
```python
>>> add = lambda x, y: x + y
>>> add(5, 3)
# 8
```
Like regular nested functions, lambdas also work as lexical closures:
```python
>>> def make_adder(n):
... return lambda x: x + n
...
>>> plus_3 = make_adder(3)
>>> plus_5 = make_adder(5)
>>> plus_3(4)
# 7
>>> plus_5(4)
# 9
```

View File

@@ -1,69 +0,0 @@
---
title: Python Json and YAML - Python Cheatsheet
description: JSON stands for JavaScript Object Notation and is a lightweight format for storing and transporting data. Json is often used when data is sent from a server to a web page.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
JSON and YAML
</base-title>
## JSON
JSON stands for JavaScript Object Notation and is a lightweight format for storing and transporting data. Json is often used when data is sent from a server to a web page.
```python
>>> import json
>>> with open("filename.json", "r") as f:
... content = json.load(f)
```
Write a JSON file with:
```python
>>> import json
>>> content = {"name": "Joe", "age": 20}
>>> with open("filename.json", "w") as f:
... json.dump(content, f, indent=2)
```
## YAML
Compared to JSON, YAML allows a much better human maintainability and gives ability to add comments. It is a convenient choice for configuration files where a human will have to edit.
There are two main libraries allowing access to YAML files:
- [PyYaml](https://pypi.python.org/pypi/PyYAML)
- [Ruamel.yaml](https://pypi.python.org/pypi/ruamel.yaml)
Install them using `pip install` in your virtual environment.
The first one is easier to use but the second one, Ruamel, implements much better the YAML
specification, and allow for example to modify a YAML content without altering comments.
Open a YAML file with:
```python
>>> from ruamel.yaml import YAML
>>> with open("filename.yaml") as f:
... yaml=YAML()
... yaml.load(f)
```
## Anyconfig
[Anyconfig](https://pypi.python.org/pypi/anyconfig) is a very handy package, allowing to abstract completely the underlying configuration file format. It allows to load a Python dictionary from JSON, YAML, TOML, and so on.
Install it with:
```bash
pip install anyconfig
```
Usage:
```python
>>> import anyconfig
>>> conf1 = anyconfig.load("/path/to/foo/conf.d/a.yml")
```

View File

@@ -1,394 +0,0 @@
---
title: Python Lists and Tuples - Python Cheatsheet
description: In python, Lists are are one of the 4 data types in Python used to store collections of data.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Lists
</base-title>
Lists are one of the 4 data types in Python used to store collections of data.
```python
['John', 'Peter', 'Debora', 'Charles']
```
## Getting values with indexes
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture[0]
# 'table'
>>> furniture[1]
# 'chair'
>>> furniture[2]
# 'rack'
>>> furniture[3]
# 'shelf'
```
## Negative indexes
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture[-1]
# 'shelf'
>>> furniture[-3]
# 'chair'
>>> f'The {furniture[-1]} is bigger than the {furniture[-3]}'
# 'The shelf is bigger than the chair'
```
## Getting sublists with Slices
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture[0:4]
# ['table', 'chair', 'rack', 'shelf']
>>> furniture[1:3]
# ['chair', 'rack']
>>> furniture[0:-1]
# ['table', 'chair', 'rack']
>>> furniture[:2]
# ['table', 'chair']
>>> furniture[1:]
# ['chair', 'rack', 'shelf']
>>> furniture[:]
# ['table', 'chair', 'rack', 'shelf']
```
Slicing the complete list will perform a copy:
```python
>>> spam2 = spam[:]
# ['cat', 'bat', 'rat', 'elephant']
>>> spam.append('dog')
>>> spam
# ['cat', 'bat', 'rat', 'elephant', 'dog']
>>> spam2
# ['cat', 'bat', 'rat', 'elephant']
```
## Getting a list length with len()
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> len(furniture)
# 4
```
## Changing values with indexes
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture[0] = 'desk'
>>> furniture
# ['desk', 'chair', 'rack', 'shelf']
>>> furniture[2] = furniture[1]
>>> furniture
# ['desk', 'chair', 'chair', 'shelf']
>>> furniture[-1] = 'bed'
>>> furniture
# ['desk', 'chair', 'chair', 'bed']
```
## Concatenation and Replication
```python
>>> [1, 2, 3] + ['A', 'B', 'C']
# [1, 2, 3, 'A', 'B', 'C']
>>> ['X', 'Y', 'Z'] * 3
# ['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z']
>>> my_list = [1, 2, 3]
>>> my_list = my_list + ['A', 'B', 'C']
>>> my_list
# [1, 2, 3, 'A', 'B', 'C']
```
## Using for loops with Lists
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> for item in furniture:
... print(item)
# table
# chair
# rack
# shelf
```
## Getting the index in a loop with enumerate()
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> for index, item in enumerate(furniture):
... print(f'index: {index} - item: {item}')
# index: 0 - item: table
# index: 1 - item: chair
# index: 2 - item: rack
# index: 3 - item: shelf
```
## Loop in Multiple Lists with zip()
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> price = [100, 50, 80, 40]
>>> for item, amount in zip(furniture, price):
... print(f'The {item} costs ${amount}')
# The table costs $100
# The chair costs $50
# The rack costs $80
# The shelf costs $40
```
## The in and not in operators
```python
>>> 'rack' in ['table', 'chair', 'rack', 'shelf']
# True
>>> 'bed' in ['table', 'chair', 'rack', 'shelf']
# False
>>> 'bed' not in furniture
# True
>>> 'rack' not in furniture
# False
```
## The Multiple Assignment Trick
The multiple assignment trick is a shortcut that lets you assign multiple variables with the values in a list in one line of code. So instead of doing this:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> table = furniture[0]
>>> chair = furniture[1]
>>> rack = furniture[2]
>>> shelf = furniture[3]
```
You could type this line of code:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> table, chair, rack, shelf = furniture
>>> table
# 'table'
>>> chair
# 'chair'
>>> rack
# 'rack'
>>> shelf
# 'shelf'
```
The multiple assignment trick can also be used to swap the values in two variables:
```python
>>> a, b = 'table', 'chair'
>>> a, b = b, a
>>> print(a)
# chair
>>> print(b)
# table
```
## The index Method
The `index` method allows you to find the index of a value by passing its name:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture.index('chair')
# 1
```
## Adding Values
### append()
`append` adds an element to the end of a `list`:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture.append('bed')
>>> furniture
# ['table', 'chair', 'rack', 'shelf', 'bed']
```
### insert()
`insert` adds an element to a `list` at a given position:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture.insert(1, 'bed')
>>> furniture
# ['table', 'bed', 'chair', 'rack', 'shelf']
```
## Removing Values
### del()
`del` removes an item using the index:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> del furniture[2]
>>> furniture
# ['table', 'chair', 'shelf']
>>> del furniture[2]
>>> furniture
# ['table', 'chair']
```
### remove()
`remove` removes an item with using actual value of it:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> furniture.remove('chair')
>>> furniture
# ['table', 'rack', 'shelf']
```
<base-warning>
<base-warning-title>
Removing repeated items
</base-warning-title>
<base-warning-content>
If the value appears multiple times in the list, only the first instance of the value will be removed.
</base-warning-content>
</base-warning>
### pop()
By default, `pop` will remove and return the last item of the list. You can also pass the index of the element as an optional parameter:
```python
>>> animals = ['cat', 'bat', 'rat', 'elephant']
>>> animals.pop()
'elephant'
>>> animals
['cat', 'bat', 'rat']
>>> animals.pop(0)
'cat'
>>> animals
['bat', 'rat']
```
## Sorting values with sort()
```python
>>> numbers = [2, 5, 3.14, 1, -7]
>>> numbers.sort()
>>> numbers
# [-7, 1, 2, 3.14, 5]
furniture = ['table', 'chair', 'rack', 'shelf']
furniture.sort()
furniture
# ['chair', 'rack', 'shelf', 'table']
```
You can also pass `True` for the `reverse` keyword argument to have `sort()` sort the values in reverse order:
```python
>>> furniture.sort(reverse=True)
>>> furniture
# ['table', 'shelf', 'rack', 'chair']
```
If you need to sort the values in regular alphabetical order, pass `str.lower` for the key keyword argument in the sort() method call:
```python
>>> letters = ['a', 'z', 'A', 'Z']
>>> letters.sort(key=str.lower)
>>> letters
# ['a', 'A', 'z', 'Z']
```
You can use the built-in function `sorted` to return a new list:
```python
>>> furniture = ['table', 'chair', 'rack', 'shelf']
>>> sorted(furniture)
# ['chair', 'rack', 'shelf', 'table']
```
## The Tuple data type
<base-disclaimer>
<base-disclaimer-title>
<a target="_blank" href="https://stackoverflow.com/questions/1708510/list-vs-tuple-when-to-use-each">Tuples vs Lists</a>
</base-disclaimer-title>
<base-disclaimer-content>
The key difference between tuples and lists is that, while <code>tuples</code> are <i>immutable</i> objects, <code>lists</code> are <i>mutable</i>. This means that tuples cannot be changed while the lists can be modified. Tuples are more memory efficient than the lists.
</base-disclaimer-content>
</base-disclaimer>
```python
>>> furniture = ('table', 'chair', 'rack', 'shelf')
>>> furniture[0]
# 'table'
>>> furniture[1:3]
# ('chair', 'rack')
>>> len(furniture)
# 4
```
The main way that tuples are different from lists is that tuples, like strings, are immutable.
## Converting between list() and tuple()
```python
>>> tuple(['cat', 'dog', 5])
# ('cat', 'dog', 5)
>>> list(('cat', 'dog', 5))
# ['cat', 'dog', 5]
>>> list('hello')
# ['h', 'e', 'l', 'l', 'o']
```

View File

@@ -1,40 +0,0 @@
---
title: Python Main function - Python Cheatsheet
description: is the name of the scope in which top-level code executes. A modules name is set equal to main when read from standard input, a script, or from an interactive prompt.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Main top-level script environment
</base-title>
## What is it
`__main__` is the name of the scope in which top-level code executes.
A modules **name** is set equal to `__main__` when read from standard input, a script, or from an interactive prompt.
A module can discover whether it is running in the main scope by checking its own `__name__`, which allows a common idiom for conditionally executing code in a module. When it is run as a script or with `python -m` but not when it is imported:
```python
>>> if __name__ == "__main__":
... # execute only if run as a script
... main()
```
For a package, the same effect can be achieved by including a **main**.py module, the contents of which will be executed when the module is run with -m.
For example, we are developing a script designed to be used as a module, we should do:
```python
>>> def add(a, b):
... return a+b
...
>>> if __name__ == "__main__":
... add(3, 5)
```
## Advantages
1. Every Python module has its `__name__` defined and if this is `__main__`, it implies that the module is run standalone by the user, and we can do corresponding appropriate actions.
2. If you import this script as a module in another script, the **name** is set to the name of the script/module.
3. Python files can act as either reusable modules, or as standalone programs.
4. `if __name__ == "__main__":` is used to execute some code only if the file is run directly, and is not being imported.

View File

@@ -1,332 +0,0 @@
---
title: Manipulating strings - Python Cheatsheet
description: An escape character is created by typing a backslash \ followed by the character you want to insert.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Manipulating Strings
</base-title>
## Escape characters
An escape character is created by typing a backslash `\` followed by the character you want to insert.
| Escape character | Prints as |
| ---------------- | -------------------- |
| `\'` | Single quote |
| `\"` | Double quote |
| `\t` | Tab |
| `\n` | Newline (line break) |
| `\\` | Backslash |
| `\b` | Backspace |
| `\ooo` | Octal value |
| `\r` | Carriage Return |
```python
>>> print("Hello there!\nHow are you?\nI\'m doing fine.")
# Hello there!
# How are you?
# I'm doing fine.
```
## Raw strings
A raw string entirely ignores all escape characters and prints any backslash that appears in the string.
```python
>>> print(r"Hello there!\nHow are you?\nI\'m doing fine.")
# Hello there!\nHow are you?\nI\'m doing fine.
```
Raw strings are mostly used for <router-link to="/cheatsheet/regular-expressions">regular expression</router-link> definition.
## Multiline Strings
```python
>>> print(
... """Dear Alice,
...
... Eve's cat has been arrested for catnapping,
... cat burglary, and extortion.
...
... Sincerely,
... Bob"""
... )
# Dear Alice,
# Eve's cat has been arrested for catnapping,
# cat burglary, and extortion.
# Sincerely,
# Bob
```
## Indexing and Slicing strings
H e l l o w o r l d !
0 1 2 3 4 5 6 7 8 9 10 11
### Indexing
```python
>>> spam = 'Hello world!'
>>> spam[0]
# 'H'
>>> spam[4]
# 'o'
>>> spam[-1]
# '!'
```
### Slicing
```python
>>> spam = 'Hello world!'
>>> spam[0:5]
# 'Hello'
>>> spam[:5]
# 'Hello'
>>> spam[6:]
# 'world!'
>>> spam[6:-1]
# 'world'
>>> spam[:-1]
# 'Hello world'
>>> spam[::-1]
# '!dlrow olleH'
>>> fizz = spam[0:5]
>>> fizz
# 'Hello'
```
## The in and not in operators
```python
>>> 'Hello' in 'Hello World'
# True
>>> 'Hello' in 'Hello'
# True
>>> 'HELLO' in 'Hello World'
# False
>>> '' in 'spam'
# True
>>> 'cats' not in 'cats and dogs'
# False
```
## upper(), lower() and title()
Transforms a string to upper, lower and title case:
```python
>>> greet = 'Hello world!'
>>> greet.upper()
# 'HELLO WORLD!'
>>> greet.lower()
# 'hello world!'
>>> greet.title()
# 'Hello World!'
```
## isupper() and islower() methods
Returns `True` or `False` after evaluating if a string is in upper or lower case:
```python
>>> spam = 'Hello world!'
>>> spam.islower()
# False
>>> spam.isupper()
# False
>>> 'HELLO'.isupper()
# True
>>> 'abc12345'.islower()
# True
>>> '12345'.islower()
# False
>>> '12345'.isupper()
# False
```
## The isX string methods
| Method | Description |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------ |
| isalpha() | returns `True` if the string consists only of letters. |
| isalnum() | returns `True` if the string consists only of letters and numbers. |
| isdecimal() | returns `True` if the string consists only of numbers. |
| isspace() | returns `True` if the string consists only of spaces, tabs, and new-lines. |
| istitle() | returns `True` if the string consists only of words that begin with an uppercase letter followed by only lowercase characters. |
## startswith() and endswith()
```python
>>> 'Hello world!'.startswith('Hello')
# True
>>> 'Hello world!'.endswith('world!')
# True
>>> 'abc123'.startswith('abcdef')
# False
>>> 'abc123'.endswith('12')
# False
>>> 'Hello world!'.startswith('Hello world!')
# True
>>> 'Hello world!'.endswith('Hello world!')
# True
```
## join() and split()
### join()
The `join()` method takes all the items in an iterable, like a <router-link to="/cheatsheet/lists-and-tuples">list</router-link>, <router-link to="/cheatsheet/dictionaries">dictionary</router-link>, <router-link to="/cheatsheet/lists-and-tuples#the-tuple-data-type">tuple</router-link> or <router-link to="/cheatsheet/sets">set</router-link>, and joins them into a string. You can also specify a separator.
```python
>>> ''.join(['My', 'name', 'is', 'Simon'])
'MynameisSimon'
>>> ', '.join(['cats', 'rats', 'bats'])
# 'cats, rats, bats'
>>> ' '.join(['My', 'name', 'is', 'Simon'])
# 'My name is Simon'
>>> 'ABC'.join(['My', 'name', 'is', 'Simon'])
# 'MyABCnameABCisABCSimon'
```
### split()
The `split()` method splits a `string` into a `list`. By default, it will use whitespace to separate the items, but you can also set another character of choice:
```python
>>> 'My name is Simon'.split()
# ['My', 'name', 'is', 'Simon']
>>> 'MyABCnameABCisABCSimon'.split('ABC')
# ['My', 'name', 'is', 'Simon']
>>> 'My name is Simon'.split('m')
# ['My na', 'e is Si', 'on']
>>> ' My name is Simon'.split()
# ['My', 'name', 'is', 'Simon']
>>> ' My name is Simon'.split(' ')
# ['', 'My', '', 'name', 'is', '', 'Simon']
```
## Justifying text with rjust(), ljust() and center()
```python
>>> 'Hello'.rjust(10)
# ' Hello'
>>> 'Hello'.rjust(20)
# ' Hello'
>>> 'Hello World'.rjust(20)
# ' Hello World'
>>> 'Hello'.ljust(10)
# 'Hello '
>>> 'Hello'.center(20)
# ' Hello '
```
An optional second argument to `rjust()` and `ljust()` will specify a fill character apart from a space character:
```python
>>> 'Hello'.rjust(20, '*')
# '***************Hello'
>>> 'Hello'.ljust(20, '-')
# 'Hello---------------'
>>> 'Hello'.center(20, '=')
# '=======Hello========'
```
## Removing whitespace with strip(), rstrip(), and lstrip()
```python
>>> spam = ' Hello World '
>>> spam.strip()
# 'Hello World'
>>> spam.lstrip()
# 'Hello World '
>>> spam.rstrip()
# ' Hello World'
>>> spam = 'SpamSpamBaconSpamEggsSpamSpam'
>>> spam.strip('ampS')
# 'BaconSpamEggs'
```
## The Count Method
Counts the number of occurrences of a given character or substring in the string it is applied to. Can be optionally provided start and end index.
```python
>>> sentence = 'one sheep two sheep three sheep four'
>>> sentence.count('sheep')
# 3
>>> sentence.count('e')
# 9
>>> sentence.count('e', 6)
# 8
# returns count of e after 'one sh' i.e 6 chars since beginning of string
>>> sentence.count('e', 7)
# 7
```
## Replace Method
Replaces all occurences of a given substring with another substring. Can be optionally provided a third argument to limit the number of replacements. Returns a new string.
```python
>>> text = "Hello, world!"
>>> text.replace("world", "planet")
# 'Hello, planet!'
>>> fruits = "apple, banana, cherry, apple"
>>> fruits.replace("apple", "orange", 1)
# 'orange, banana, cherry, apple'
>>> sentence = "I like apples, Apples are my favorite fruit"
>>> sentence.replace("apples", "oranges")
# 'I like oranges, Apples are my favorite fruit'
```

View File

@@ -1,216 +0,0 @@
---
title: Python OOP Basics - Python Cheatsheet
description: Object-Oriented Programming (OOP) is a programming paradigm that revolves around the concept of objects, which are instances of classes. OOP principles are fundamental concepts that guide the design and development of software in an object-oriented way. In Python, OOP is supported by the use of classes and objects. Here are some of the basic OOP principles in Python
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python OOP Basics
</base-title>
<base-disclaimer>
<base-disclaimer-title>
<a href="https://en.wikipedia.org/wiki/Object-oriented_programming">Object-Oriented Programming</a>
</base-disclaimer-title>
<base-disclaimer-content>
Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code. The data is in the form of fields (often known as attributes or properties), and the code is in the form of procedures (often known as methods).
</base-disclaimer-content>
</base-disclaimer>
## Encapsulation
Encapsulation is one of the fundamental concepts of object-oriented programming, which helps to protect the data and methods of an object from unauthorized access and modification. It is a way to achieve data abstraction, which means that the implementation details of an object are hidden from the outside world, and only the essential information is exposed.
In Python, encapsulation can be achieved by using access modifiers. Access modifiers are keywords that define the accessibility of attributes and methods in a class. The three access modifiers available in Python are public, private, and protected. However, Python does not have an explicit way of defining access modifiers like some other programming languages such as Java and C++. Instead, it uses a convention of using underscore prefixes to indicate the access level.
In the given code example, the class MyClass has two attributes, _protected_var and __private_var. The _protected_var is marked as protected by using a single underscore prefix. This means that the attribute can be accessed within the class and its subclasses but not outside the class. The __private_var is marked as private by using two underscore prefixes. This means that the attribute can only be accessed within the class and not outside the class, not even in its subclasses.
When we create an object of the MyClass class, we can access the _protected_var attribute using the object name with a single underscore prefix. However, we cannot access the __private_var attribute using the object name, as it is hidden from the outside world. If we try to access the __private_var attribute, we will get an AttributeError as shown in the code.
In summary, encapsulation is an important concept in object-oriented programming that helps to protect the implementation details of an object. In Python, we can achieve encapsulation by using access modifiers and using underscore prefixes to indicate the access level.
```python
# Define a class named MyClass
class MyClass:
# Constructor method that initializes the class object
def __init__(self):
# Define a protected variable with an initial value of 10
# The variable name starts with a single underscore, which indicates protected access
self._protected_var = 10
# Define a private variable with an initial value of 20
# The variable name starts with two underscores, which indicates private access
self.__private_var = 20
# Create an object of MyClass class
obj = MyClass()
# Access the protected variable using the object name and print its value
# The protected variable can be accessed outside the class but
# it is intended to be used within the class or its subclasses
print(obj._protected_var) # output: 10
# Try to access the private variable using the object name and print its value
# The private variable cannot be accessed outside the class, even by its subclasses
# This will raise an AttributeError because the variable is not accessible outside the class
print(obj.__private_var) # AttributeError: 'MyClass' object has no attribute '__private_var'
```
## Inheritance
Inheritance promotes code reuse and allows you to create a hierarchy of classes that share common attributes and methods. It helps in creating clean and organized code by keeping related functionality in one place and promoting the concept of modularity. The base class from which a new class is derived is also known as a parent class, and the new class is known as the child class or subclass.
In the code, we define a class named Animal which has a constructor method that initializes the class object with a name attribute and a method named speak. The speak method is defined in the Animal class but does not have a body.
We then define two subclasses named Dog and Cat which inherit from the Animal class. These subclasses override the speak method of the Animal class.
We create a Dog object with a name attribute "Rover" and a Cat object with a name attribute "Whiskers". We call the speak method of the Dog object using dog.speak(), and it prints "Woof!" because the speak method of the Dog class overrides the speak method of the Animal class. Similarly, we call the speak method of the Cat object using cat.speak(), and it prints "Meow!" because the speak method of the Cat class overrides the speak method of the Animal class.
``` python
# Define a class named Animal
class Animal:
# Constructor method that initializes the class object with a name attribute
def __init__(self, name):
self.name = name
# Method that is defined in the Animal class but does not have a body
# This method will be overridden in the subclasses of Animal
def speak(self):
print("")
# Define a subclass named Dog that inherits from the Animal class
class Dog(Animal):
# Override the speak method of the Animal class
def speak(self):
print("Woof!")
# Define a subclass named Cat that inherits from the Animal class
class Cat(Animal):
# Override the speak method of the Animal class
def speak(self):
print("Meow!")
# Create a Dog object with a name attribute "Rover"
dog = Dog("Rover")
# Create a Cat object with a name attribute "Whiskers"
cat = Cat("Whiskers")
# Call the speak method of the Dog class and print the output
# The speak method of the Dog class overrides the speak method of the Animal class
# Therefore, when we call the speak method of the Dog object, it will print "Woof!"
dog.speak() # output: Woof!
# Call the speak method of the Cat class and print the output
# The speak method of the Cat class overrides the speak method of the Animal class
# Therefore, when we call the speak method of the Cat object, it will print "Meow!"
cat.speak() # output: Meow!
```
## Polymorphism
Polymorphism is an important concept in object-oriented programming that allows you to write code that can work with objects of different classes in a uniform way. In Python, polymorphism is achieved by using method overriding or method overloading.
Method overriding is when a subclass provides its own implementation of a method that is already defined in its parent class. This allows the subclass to modify the behavior of the method without changing its name or signature.
Method overloading is when multiple methods have the same name but different parameters. Python does not support method overloading directly, but it can be achieved using default arguments or variable-length arguments.
Polymorphism makes it easier to write flexible and reusable code. It allows you to write code that can work with different objects without needing to know their specific types.
```python
#The Shape class is defined with an abstract area method, which is intended to be overridden by subclasses.
class Shape:
def area(self):
pass
class Rectangle(Shape):
# The Rectangle class is defined with an __init__ method that initializes
# width and height instance variables.
# It also defines an area method that calculates and returns
# the area of a rectangle using the width and height instance variables.
def __init__(self, width, height):
self.width = width # Initialize width instance variable
self.height = height # Initialize height instance variable
def area(self):
return self.width * self.height # Return area of rectangle
# The Circle class is defined with an __init__ method
# that initializes a radius instance variable.
# It also defines an area method that calculates and
# returns the area of a circle using the radius instance variable.
class Circle(Shape):
def __init__(self, radius):
self.radius = radius # Initialize radius instance variable
def area(self):
return 3.14 * self.radius ** 2 # Return area of circle using pi * r^2
# The shapes list is created with one Rectangle object and one Circle object. The for
# loop iterates over each object in the list and calls the area method of each object
# The output will be the area of the rectangle (20) and the area of the circle (153.86).
shapes = [Rectangle(4, 5), Circle(7)] # Create a list of Shape objects
for shape in shapes:
print(shape.area()) # Output the area of each Shape object
```
## Abstraction
Abstraction is an important concept in object-oriented programming (OOP) because it allows you to focus on the essential features of an object or system while ignoring the details that aren't relevant to the current context. By reducing complexity and hiding unnecessary details, abstraction can make code more modular, easier to read, and easier to maintain.
In Python, abstraction can be achieved by using abstract classes or interfaces. An abstract class is a class that cannot be instantiated directly, but is meant to be subclassed by other classes. It often includes abstract methods that have no implementation, but provide a template for how the subclass should be implemented. This allows the programmer to define a common interface for a group of related classes, while still allowing each class to have its own specific behavior.
An interface, on the other hand, is a collection of method signatures that a class must implement in order to be considered "compatible" with the interface. Interfaces are often used to define a common set of methods that multiple classes can implement, allowing them to be used interchangeably in certain contexts.
Python does not have built-in support for abstract classes or interfaces, but they can be implemented using the abc (abstract base class) module. This module provides the ABC class and the abstractmethod decorator, which can be used to define abstract classes and methods.
Overall, abstraction is a powerful tool for managing complexity and improving code quality in object-oriented programming, and Python provides a range of options for achieving abstraction in your code.
```python
# Import the abc module to define abstract classes and methods
from abc import ABC, abstractmethod
# Define an abstract class called Shape that has an abstract method called area
class Shape(ABC):
@abstractmethod
def area(self):
pass
# Define a Rectangle class that inherits from Shape
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
# Implement the area method for Rectangles
def area(self):
return self.width * self.height
# Define a Circle class that also inherits from Shape
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
# Implement the area method for Circles
def area(self):
return 3.14 * self.radius ** 2
# Create a list of shapes that includes both Rectangles and Circles
shapes = [Rectangle(4, 5), Circle(7)]
# Loop through each shape in the list and print its area
for shape in shapes:
print(shape.area())
```
These are some of the basic OOP principles in Python. This page is currently in progress and more
detailed examples and explanations will be coming soon.

View File

@@ -1,71 +0,0 @@
---
title: Reading and writing files - Python Cheatsheet
description: To read/write to a file in Python, you will want to use the with statement, which will close the file for you after you are done, managing the available resources for you.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Reading and Writing Files
</base-title>
## The file Reading/Writing process
To read/write to a file in Python, you will want to use the `with`
statement, which will close the file for you after you are done, managing the available resources for you.
## Opening and reading files
The `open` function opens a file and return a corresponding file object.
```python
>>> with open('C:\\Users\\your_home_folder\\hi.txt') as hello_file:
... hello_content = hello_file.read()
...
>>> hello_content
'Hello World!'
```
Alternatively, you can use the _readlines()_ method to get a list of string values from the file, one string for each line of text:
```python
>>> with open('sonnet29.txt') as sonnet_file:
... sonnet_file.readlines()
...
# [When, in disgrace with fortune and men's eyes,\n',
# ' I all alone beweep my outcast state,\n',
# And trouble deaf heaven with my bootless cries,\n', And
# look upon myself and curse my fate,']
```
You can also iterate through the file line by line:
```python
>>> with open('sonnet29.txt') as sonnet_file:
... for line in sonnet_file:
... print(line, end='')
...
# When, in disgrace with fortune and men's eyes,
# I all alone beweep my outcast state,
# And trouble deaf heaven with my bootless cries,
# And look upon myself and curse my fate,
```
## Writing to files
```python
>>> with open('bacon.txt', 'w') as bacon_file:
... bacon_file.write('Hello world!\n')
...
# 13
>>> with open('bacon.txt', 'a') as bacon_file:
... bacon_file.write('Bacon is not a vegetable.')
...
# 25
>>> with open('bacon.txt') as bacon_file:
... content = bacon_file.read()
...
>>> print(content)
# Hello world!
# Bacon is not a vegetable.
```

View File

@@ -1,393 +0,0 @@
---
title: Python Regular Expressions - Python Cheatsheet
description: A regular expression (shortened as regex) is a sequence of characters that specifies a search pattern in text and used by string-searching algorithms.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Regular Expressions
</base-title>
<base-disclaimer>
<base-disclaimer-title>
<a target="_blank" href="https://en.wikipedia.org/wiki/Regular_expression">Regular expressions</a>
</base-disclaimer-title>
<base-disclaimer-content>
A regular expression (shortened as regex [...]) is a sequence of characters that specifies a search pattern in text. [...] used by string-searching algorithms for "find" or "find and replace" operations on strings, or for input validation.
</base-disclaimer-content>
</base-disclaimer>
1. Import the regex module with `import re`.
2. Create a Regex object with the `re.compile()` function. (Remember to use a raw string.)
3. Pass the string you want to search into the Regex objects `search()` method. This returns a `Match` object.
4. Call the Match objects `group()` method to return a string of the actual matched text.
All the regex functions in Python are in the re module:
```python
>>> import re
```
## Regex symbols
| Symbol | Matches |
| ------------------------ | ------------------------------------------------------ |
| `?` | zero or one of the preceding group. |
| `*` | zero or more of the preceding group. |
| `+` | one or more of the preceding group. |
| `{n}` | exactly n of the preceding group. |
| `{n,}` | n or more of the preceding group. |
| `{,m}` | 0 to m of the preceding group. |
| `{n,m}` | at least n and at most m of the preceding p. |
| `{n,m}?` or `*?` or `+?` | performs a non-greedy match of the preceding p. |
| `^spam` | means the string must begin with spam. |
| `spam$` | means the string must end with spam. |
| `.` | any character, except newline characters. |
| `\d`, `\w`, and `\s` | a digit, word, or space character, respectively. |
| `\D`, `\W`, and `\S` | anything except a digit, word, or space, respectively. |
| `[abc]` | any character between the brackets (such as a, b, ). |
| `[^abc]` | any character that isnt between the brackets. |
## Matching regex objects
```python
>>> phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
>>> mo = phone_num_regex.search('My number is 415-555-4242.')
>>> print(f'Phone number found: {mo.group()}')
# Phone number found: 415-555-4242
```
## Grouping with parentheses
```python
>>> phone_num_regex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)')
>>> mo = phone_num_regex.search('My number is 415-555-4242.')
>>> mo.group(1)
# '415'
>>> mo.group(2)
# '555-4242'
>>> mo.group(0)
# '415-555-4242'
>>> mo.group()
# '415-555-4242'
```
To retrieve all the groups at once use the `groups()` method:
```python
>>> mo.groups()
('415', '555-4242')
>>> area_code, main_number = mo.groups()
>>> print(area_code)
415
>>> print(main_number)
555-4242
```
## Multiple groups with Pipe
You can use the `|` character anywhere you want to match one of many expressions.
```python
>>> hero_regex = re.compile (r'Batman|Tina Fey')
>>> mo1 = hero_regex.search('Batman and Tina Fey.')
>>> mo1.group()
# 'Batman'
>>> mo2 = hero_regex.search('Tina Fey and Batman.')
>>> mo2.group()
# 'Tina Fey'
```
You can also use the pipe to match one of several patterns as part of your regex:
```python
>>> bat_regex = re.compile(r'Bat(man|mobile|copter|bat)')
>>> mo = bat_regex.search('Batmobile lost a wheel')
>>> mo.group()
# 'Batmobile'
>>> mo.group(1)
# 'mobile'
```
## Optional matching with the Question Mark
The `?` character flags the group that precedes it as an optional part of the pattern.
```python
>>> bat_regex = re.compile(r'Bat(wo)?man')
>>> mo1 = bat_regex.search('The Adventures of Batman')
>>> mo1.group()
# 'Batman'
>>> mo2 = bat_regex.search('The Adventures of Batwoman')
>>> mo2.group()
# 'Batwoman'
```
## Matching zero or more with the Star
The `*` (star or asterisk) means “match zero or more”. The group that precedes the star can occur any number of times in the text.
```python
>>> bat_regex = re.compile(r'Bat(wo)*man')
>>> mo1 = bat_regex.search('The Adventures of Batman')
>>> mo1.group()
'Batman'
>>> mo2 = bat_regex.search('The Adventures of Batwoman')
>>> mo2.group()
'Batwoman'
>>> mo3 = bat_regex.search('The Adventures of Batwowowowoman')
>>> mo3.group()
'Batwowowowoman'
```
## Matching one or more with the Plus
The `+` (or plus) _means match one or more_. The group preceding a plus must appear at least once:
```python
>>> bat_regex = re.compile(r'Bat(wo)+man')
>>> mo1 = bat_regex.search('The Adventures of Batwoman')
>>> mo1.group()
# 'Batwoman'
>>> mo2 = bat_regex.search('The Adventures of Batwowowowoman')
>>> mo2.group()
# 'Batwowowowoman'
>>> mo3 = bat_regex.search('The Adventures of Batman')
>>> mo3 is None
# True
```
## Matching specific repetitions with Curly Brackets
If you have a group that you want to repeat a specific number of times, follow the group in your regex with a number in curly brackets:
```python
>>> ha_regex = re.compile(r'(Ha){3}')
>>> mo1 = ha_regex.search('HaHaHa')
>>> mo1.group()
# 'HaHaHa'
>>> mo2 = ha_regex.search('Ha')
>>> mo2 is None
# True
```
Instead of one number, you can specify a range with minimum and a maximum in between the curly brackets. For example, the regex (Ha){3,5} will match 'HaHaHa', 'HaHaHaHa', and 'HaHaHaHaHa'.
```python
>>> ha_regex = re.compile(r'(Ha){2,3}')
>>> mo1 = ha_regex.search('HaHaHaHa')
>>> mo1.group()
# 'HaHaHa'
```
## Greedy and non-greedy matching
Pythons regular expressions are greedy by default: in ambiguous situations they will match the longest string possible. The non-greedy version of the curly brackets, which matches the shortest string possible, has the closing curly bracket followed by a question mark.
```python
>>> greedy_ha_regex = re.compile(r'(Ha){3,5}')
>>> mo1 = greedy_ha_regex.search('HaHaHaHaHa')
>>> mo1.group()
# 'HaHaHaHaHa'
>>> non_greedy_ha_regex = re.compile(r'(Ha){3,5}?')
>>> mo2 = non_greedy_ha_regex.search('HaHaHaHaHa')
>>> mo2.group()
# 'HaHaHa'
```
## The findall() method
The `findall()` method will return the strings of every match in the searched string.
```python
>>> phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') # has no groups
>>> phone_num_regex.findall('Cell: 415-555-9999 Work: 212-555-0000')
# ['415-555-9999', '212-555-0000']
```
## Making your own character classes
You can define your own character class using square brackets. For example, the character class _[aeiouAEIOU]_ will match any vowel, both lowercase and uppercase.
```python
>>> vowel_regex = re.compile(r'[aeiouAEIOU]')
>>> vowel_regex.findall('Robocop eats baby food. BABY FOOD.')
# ['o', 'o', 'o', 'e', 'a', 'a', 'o', 'o', 'A', 'O', 'O']
```
You can also include ranges of letters or numbers by using a hyphen. For example, the character class _[a-zA-Z0-9]_ will match all lowercase letters, uppercase letters, and numbers.
By placing a caret character (`^`) just after the character classs opening bracket, you can make a negative character class that will match all the characters that are not in the character class:
```python
>>> consonant_regex = re.compile(r'[^aeiouAEIOU]')
>>> consonant_regex.findall('Robocop eats baby food. BABY FOOD.')
# ['R', 'b', 'c', 'p', ' ', 't', 's', ' ', 'b', 'b', 'y', ' ', 'f', 'd', '.', '
# ', 'B', 'B', 'Y', ' ', 'F', 'D', '.']
```
## The Caret and Dollar sign characters
- You can also use the caret symbol `^` at the start of a regex to indicate that a match must occur at the beginning of the searched text.
- Likewise, you can put a dollar sign `$` at the end of the regex to indicate the string must end with this regex pattern.
- And you can use the `^` and `$` together to indicate that the entire string must match the regex.
The `r'^Hello`' regular expression string matches strings that begin with 'Hello':
```python
>>> begins_with_hello = re.compile(r'^Hello')
>>> begins_with_hello.search('Hello world!')
# <_sre.SRE_Match object; span=(0, 5), match='Hello'>
>>> begins_with_hello.search('He said hello.') is None
# True
```
The `r'\d\$'` regular expression string matches strings that end with a numeric character from 0 to 9:
```python
>>> whole_string_is_num = re.compile(r'^\d+$')
>>> whole_string_is_num.search('1234567890')
# <_sre.SRE_Match object; span=(0, 10), match='1234567890'>
>>> whole_string_is_num.search('12345xyz67890') is None
# True
>>> whole_string_is_num.search('12 34567890') is None
# True
```
## The Wildcard character
The `.` (or dot) character in a regular expression will match any character except for a newline:
```python
>>> at_regex = re.compile(r'.at')
>>> at_regex.findall('The cat in the hat sat on the flat mat.')
['cat', 'hat', 'sat', 'lat', 'mat']
```
## Matching everything with Dot-Star
```python
>>> name_regex = re.compile(r'First Name: (.*) Last Name: (.*)')
>>> mo = name_regex.search('First Name: Al Last Name: Sweigart')
>>> mo.group(1)
# 'Al'
>>> mo.group(2)
'Sweigart'
```
The `.*` uses greedy mode: It will always try to match as much text as possible. To match any and all text in a non-greedy fashion, use the dot, star, and question mark (`.*?`). The question mark tells Python to match in a non-greedy way:
```python
>>> non_greedy_regex = re.compile(r'<.*?>')
>>> mo = non_greedy_regex.search('<To serve man> for dinner.>')
>>> mo.group()
# '<To serve man>'
>>> greedy_regex = re.compile(r'<.*>')
>>> mo = greedy_regex.search('<To serve man> for dinner.>')
>>> mo.group()
# '<To serve man> for dinner.>'
```
## Matching newlines with the Dot character
The dot-star will match everything except a newline. By passing `re.DOTALL` as the second argument to `re.compile()`, you can make the dot character match all characters, including the newline character:
```python
>>> no_newline_regex = re.compile('.*')
>>> no_newline_regex.search('Serve the public trust.\nProtect the innocent.\nUphold the law.').group()
# 'Serve the public trust.'
>>> newline_regex = re.compile('.*', re.DOTALL)
>>> newline_regex.search('Serve the public trust.\nProtect the innocent.\nUphold the law.').group()
# 'Serve the public trust.\nProtect the innocent.\nUphold the law.'
```
## Case-Insensitive matching
To make your regex case-insensitive, you can pass `re.IGNORECASE` or `re.I` as a second argument to `re.compile()`:
```python
>>> robocop = re.compile(r'robocop', re.I)
>>> robocop.search('Robocop is part man, part machine, all cop.').group()
# 'Robocop'
>>> robocop.search('ROBOCOP protects the innocent.').group()
# 'ROBOCOP'
>>> robocop.search('Al, why does your programming book talk about robocop so much?').group()
# 'robocop'
```
## Substituting strings with the sub() method
The `sub()` method for Regex objects is passed two arguments:
1. The first argument is a string to replace any matches.
1. The second is the string for the regular expression.
The `sub()` method returns a string with the substitutions applied:
```python
>>> names_regex = re.compile(r'Agent \w+')
>>> names_regex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.')
# 'CENSORED gave the secret documents to CENSORED.'
```
## Managing complex Regexes
To tell the `re.compile()` function to ignore whitespace and comments inside the regular expression string, “verbose mode” can be enabled by passing the variable `re.VERBOSE` as the second argument to `re.compile()`.
Now instead of a hard-to-read regular expression like this:
```python
phone_regex = re.compile(r'((\d{3}|\(\d{3}\))?(\s|-|\.)?\d{3}(\s|-|\.)\d{4}(\s*(ext|x|ext.)\s*\d{2,5})?)')
```
you can spread the regular expression over multiple lines with comments like this:
```python
phone_regex = re.compile(r'''(
(\d{3}|\(\d{3}\))? # area code
(\s|-|\.)? # separator
\d{3} # first 3 digits
(\s|-|\.) # separator
\d{4} # last 4 digits
(\s*(ext|x|ext.)\s*\d{2,5})? # extension
)''', re.VERBOSE)
```

View File

@@ -1,158 +0,0 @@
---
title: Python Sets - Python Cheatsheet
description: Python comes equipped with several built-in data types to help us organize our data. These structures include lists, dictionaries, tuples and sets.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python Sets
</base-title>
Python comes equipped with several built-in data types to help us organize our data. These structures include lists, dictionaries, tuples and **sets**.
<base-disclaimer>
<base-disclaimer-title>
From the Python 3 <a target="_blank" href="https://docs.python.org/3/tutorial/datastructures.html#sets">documentation</a>
</base-disclaimer-title>
<base-disclaimer-content>
A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries.
</base-disclaimer-content>
</base-disclaimer>
Read <router-link to="/blog/python-sets-what-why-how">Python Sets: What, Why and How</router-link> for a more in-deep reference.
## Initializing a set
There are two ways to create sets: using curly braces `{}` and the built-in function `set()`
<base-warning>
<base-warning-title>
Empty Sets
</base-warning-title>
<base-warning-content>
When creating set, be sure to not use empty curly braces <code>{}</code> or you will get an empty dictionary instead.
</base-warning-content>
</base-warning>
```python
>>> s = {1, 2, 3}
>>> s = set([1, 2, 3])
>>> s = {} # this will create a dictionary instead of a set
>>> type(s)
# <class 'dict'>
```
## Unordered collections of unique elements
A set automatically removes all the duplicate values.
```python
>>> s = {1, 2, 3, 2, 3, 4}
>>> s
# {1, 2, 3, 4}
```
And as an unordered data type, they can't be indexed.
```python
>>> s = {1, 2, 3}
>>> s[0]
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'set' object does not support indexing
```
## set add and update
Using the `add()` method we can add a single element to the set.
```python
>>> s = {1, 2, 3}
>>> s.add(4)
>>> s
# {1, 2, 3, 4}
```
And with `update()`, multiple ones:
```python
>>> s = {1, 2, 3}
>>> s.update([2, 3, 4, 5, 6])
>>> s
# {1, 2, 3, 4, 5, 6}
```
## set remove and discard
Both methods will remove an element from the set, but `remove()` will raise a `key error` if the value doesn't exist.
```python
>>> s = {1, 2, 3}
>>> s.remove(3)
>>> s
# {1, 2}
>>> s.remove(3)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# KeyError: 3
```
`discard()` won't raise any errors.
```python
>>> s = {1, 2, 3}
>>> s.discard(3)
>>> s
# {1, 2}
>>> s.discard(3)
```
## set union
`union()` or `|` will create a new set with all the elements from the sets provided.
```python
>>> s1 = {1, 2, 3}
>>> s2 = {3, 4, 5}
>>> s1.union(s2) # or 's1 | s2'
# {1, 2, 3, 4, 5}
```
## set intersection
`intersection()` or `&` will return a set with only the elements that are common to all of them.
```python
>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s3 = {3, 4, 5}
>>> s1.intersection(s2, s3) # or 's1 & s2 & s3'
# {3}
```
## set difference
`difference()` or `-` will return only the elements that are unique to the first set (invoked set).
```python
>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1.difference(s2) # or 's1 - s2'
# {1}
>>> s2.difference(s1) # or 's2 - s1'
# {4}
```
## set symmetric_difference
`symmetric_difference()` or `^` will return all the elements that are not common between them.
```python
>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1.symmetric_difference(s2) # or 's1 ^ s2'
# {1, 4}
```

View File

@@ -1,54 +0,0 @@
---
title: Python Setup.py - Python Cheatsheet
description: The setup script is the centre of all activity in building, distributing, and installing modules using the Distutils. The main purpose of the setup script is to describe your module distribution to the Distutils, so that the various commands that operate on your modules do the right thing.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python setup.py
</base-title>
<base-warning>
<base-warning-title>
A 'controversial' opinion
</base-warning-title>
<base-warning-content>
Using `setup.py` to pack and distribute your python packages can be quite challenging every so often. Tools like <a target="_blank" href="https://python-poetry.org/">Poetry</a> make not only the packaging a <b>lot easier</b>, but also help you to manage your dependencies in a very convenient way.
</base-warning-content>
</base-warning>
If you want more information about Poetry you can read the following articles:
- <router-link to="/blog/python-projects-with-poetry-and-vscode-part-1">Python projects with Poetry and VSCode. Part 1</router-link>
- <router-link to="/blog/python-projects-with-poetry-and-vscode-part-2">Python projects with Poetry and VSCode. Part 2</router-link>
- <router-link to="/blog/python-projects-with-poetry-and-vscode-part-3">Python projects with Poetry and VSCode. Part 3</router-link>
## Introduction
The setup script is the center of all activity in building, distributing, and installing modules using the Distutils. The main purpose of the setup script is to describe your module distribution to the Distutils, so that the various commands that operate on your modules do the right thing.
The `setup.py` file is at the heart of a Python project. It describes all the metadata about your project. There are quite a few fields you can add to a project to give it a rich set of metadata describing the project. However, there are only three required fields: name, version, and packages. The name field must be unique if you wish to publish your package on the Python Package Index (PyPI). The version field keeps track of different releases of the project. The package's field describes where youve put the Python source code within your project.
This allows you to easily install Python packages. Often it's enough to write:
```bash
python setup.py install
```
and module will install itself.
## Example
Our initial setup.py will also include information about the license and will re-use the README.txt file for the long_description field. This will look like:
```python
from distutils.core import setup
setup(
name='pythonCheatsheet',
version='0.1',
packages=['pipenv',],
license='MIT',
long_description=open('README.txt').read(),
)
```
Find more information visit the [official documentation](http://docs.python.org/3.11/install/index.html).

View File

@@ -1,177 +0,0 @@
---
title: Python String Formatting - Python Cheatsheet
description: If your are using Python 3.6+, string f-strings are the recommended way to format strings.
---
<base-title :title="frontmatter.title" :description="frontmatter.description">
Python String Formatting
</base-title>
<base-disclaimer>
<base-disclaimer-title>
From the <a href="https://docs.python.org/3/library/stdtypes.html?highlight=sprintf#printf-style-string-formatting">Python 3 documentation</a>
</base-disclaimer-title>
<base-disclaimer-content>
The formatting operations described here (<b>% operator</b>) exhibit a variety of quirks that lead to a number of common errors [...]. Using the newer <a href="#formatted-string-literals-or-f-strings">formatted string literals</a> [...] helps avoid these errors. These alternatives also provide more powerful, flexible and extensible approaches to formatting text.
</base-disclaimer-content>
</base-disclaimer>
## % operator
<base-warning>
<base-warning-title>
Prefer String Literals
</base-warning-title>
<base-warning-content>
For new code, using <a href="#strformat">str.format</a>, or <a href="#formatted-string-literals-or-f-strings">formatted string literals</a> (Python 3.6+) over the <code>%</code> operator is strongly recommended.
</base-warning-content>
</base-warning>
```python
>>> name = 'Pete'
>>> 'Hello %s' % name
# "Hello Pete"
```
We can use the `%d` format specifier to convert an int value to a string:
```python
>>> num = 5
>>> 'I have %d apples' % num
# "I have 5 apples"
```
## str.format
Python 3 introduced a new way to do string formatting that was later back-ported to Python 2.7. This makes the syntax for string formatting more regular.
```python
>>> name = 'John'
>>> age = 20
>>> "Hello I'm {}, my age is {}".format(name, age)
# "Hello I'm John, my age is 20"
>>> "Hello I'm {0}, my age is {1}".format(name, age)
# "Hello I'm John, my age is 20"
```
## Formatted String Literals or f-Strings
If your are using Python 3.6+, string `f-Strings` are the recommended way to format strings.
<base-disclaimer>
<base-disclaimer-title>
From the <a href="https://docs.python.org/3/reference/lexical_analysis.html#f-strings">Python 3 documentation</a>
</base-disclaimer-title>
<base-disclaimer-content>
A formatted string literal or f-string is a string literal that is prefixed with `f` or `F`. These strings may contain replacement fields, which are expressions delimited by curly braces {}. While other string literals always have a constant value, formatted strings are really expressions evaluated at run time.
</base-disclaimer-content>
</base-disclaimer>
```python
>>> name = 'Elizabeth'
>>> f'Hello {name}!'
# 'Hello Elizabeth!'
```
It is even possible to do inline arithmetic with it:
```python
>>> a = 5
>>> b = 10
>>> f'Five plus ten is {a + b} and not {2 * (a + b)}.'
# 'Five plus ten is 15 and not 30.'
```
### Multiline f-Strings
```python
>>> name = 'Robert'
>>> messages = 12
>>> (
... f'Hi, {name}. '
... f'You have {messages} unread messages'
... )
# 'Hi, Robert. You have 12 unread messages'
```
### The `=` specifier
This will print the expression and its value:
```python
>>> from datetime import datetime
>>> now = datetime.now().strftime("%b/%d/%Y - %H:%M:%S")
>>> f'date and time: {now=}'
# "date and time: now='Nov/14/2022 - 20:50:01'"
```
### Adding spaces or characters
```python
>>> f"{name.upper() = :-^20}"
# 'name.upper() = -------ROBERT-------'
>>>
>>> f"{name.upper() = :^20}"
# 'name.upper() = ROBERT '
>>>
>>> f"{name.upper() = :20}"
# 'name.upper() = ROBERT '
```
## Formatting Digits
Adding thousands separator
```python
>>> a = 10000000
>>> f"{a:,}"
# '10,000,000'
```
Rounding
```python
>>> a = 3.1415926
>>> f"{a:.2f}"
# '3.14'
```
Showing as Percentage
```python
>>> a = 0.816562
>>> f"{a:.2%}"
# '81.66%'
```
### Number formatting table
| Number | Format | Output | description |
| ---------- | ------- | --------- | --------------------------------------------- |
| 3.1415926 | {:.2f} | 3.14 | Format float 2 decimal places |
| 3.1415926 | {:+.2f} | +3.14 | Format float 2 decimal places with sign |
| -1 | {:+.2f} | -1.00 | Format float 2 decimal places with sign |
| 2.71828 | {:.0f} | 3 | Format float with no decimal places |
| 4 | {:0>2d} | 04 | Pad number with zeros (left padding, width 2) |
| 4 | {:x<4d} | 4xxx | Pad number with xs (right padding, width 4) |
| 10 | {:x<4d} | 10xx | Pad number with xs (right padding, width 4) |
| 1000000 | {:,} | 1,000,000 | Number format with comma separator |
| 0.35 | {:.2%} | 35.00% | Format percentage |
| 1000000000 | {:.2e} | 1.00e+09 | Exponent notation |
| 11 | {:11d} | 11 | Right-aligned (default, width 10) |
| 11 | {:<11d} | 11 | Left-aligned (width 10) |
| 11 | {:^11d} | 11 | Center aligned (width 10) |
## Template Strings
A simpler and less powerful mechanism, but it is recommended when handling strings generated by users. Due to their reduced complexity, template strings are a safer choice.
```python
>>> from string import Template
>>> name = 'Elizabeth'
>>> t = Template('Hey $name!')
>>> t.substitute(name=name)
# 'Hey Elizabeth!'
```

Some files were not shown because too many files have changed in this diff Show More