Major Terminogy unification and refactor [Needs thorough testing]

This commit is contained in:
2025-05-06 18:26:26 +02:00
parent 0a3100d448
commit ff548e902e
20 changed files with 689 additions and 356 deletions

View File

@@ -242,7 +242,6 @@ class ConfigEditorDialog(QDialog):
# Clear potentially lingering widget references for this tab
self.widgets.pop("TARGET_FILENAME_PATTERN", None)
self.widgets.pop("STANDARD_MAP_TYPES_LIST", None)
self.widgets.pop("RESPECT_VARIANT_MAP_TYPES", None)
self.widgets.pop("ASPECT_RATIO_DECIMALS", None)
@@ -276,24 +275,7 @@ class ConfigEditorDialog(QDialog):
self.widgets["ASPECT_RATIO_DECIMALS"] = aspect_ratio_spinbox
main_tab_layout.addLayout(form_layout)
# 4. STANDARD_MAP_TYPES: QListWidget + Add/Remove Buttons
standard_maps_layout = QVBoxLayout()
standard_maps_label = QLabel("Standard Map Types:")
standard_maps_layout.addWidget(standard_maps_label)
standard_maps_button_layout = QHBoxLayout()
add_button = QPushButton("Add")
remove_button = QPushButton("Remove")
# TODO: Connect add/remove buttons signals
standard_maps_button_layout.addWidget(add_button)
standard_maps_button_layout.addWidget(remove_button)
standard_maps_button_layout.addStretch() # Push buttons left
standard_maps_layout.addLayout(standard_maps_button_layout)
main_tab_layout.addLayout(standard_maps_layout)
# Add the main layout to the tab's provided layout
layout.addLayout(main_tab_layout)
layout.addStretch() # Keep stretch at the end of the tab's main layout
@@ -900,7 +882,7 @@ class ConfigEditorDialog(QDialog):
# This is the final key, update the value
if isinstance(widget, QLineEdit):
# Handle simple lists displayed as comma-separated strings
if key in ["STANDARD_MAP_TYPES", "RESPECT_VARIANT_MAP_TYPES"]:
if key == "RESPECT_VARIANT_MAP_TYPES":
current_dict[k] = [item.strip() for item in widget.text().split(',') if item.strip()]
else:
current_dict[k] = widget.text()
@@ -1039,7 +1021,7 @@ class ConfigEditorDialog(QDialog):
widget = self.widgets[key]
if isinstance(widget, QLineEdit):
# Handle simple lists displayed as comma-separated strings
if key in ["STANDARD_MAP_TYPES", "RESPECT_VARIANT_MAP_TYPES"] and isinstance(value, list):
if key == "RESPECT_VARIANT_MAP_TYPES" and isinstance(value, list):
widget.setText(", ".join(map(str, value)))
elif isinstance(value, (str, int, float, bool)): # Also handle cases where simple types might be in QLineEdit
widget.setText(str(value))

View File

@@ -214,29 +214,18 @@ class SupplierSearchDelegate(QStyledItemDelegate):
class ItemTypeSearchDelegate(QStyledItemDelegate):
"""
Delegate for editing item types using a QLineEdit with auto-completion.
Loads known item types from the UnifiedViewModel's cached keys.
Loads known item types from the provided list.
"""
def __init__(self, parent=None):
def __init__(self, item_type_keys: list[str] | None = None, parent=None):
super().__init__(parent)
# No persistent list needed here, suggestions come from the model
self.item_type_keys = item_type_keys if item_type_keys else []
log.debug(f"ItemTypeSearchDelegate initialized with {len(self.item_type_keys)} keys: {self.item_type_keys}")
def createEditor(self, parent, option, index: QModelIndex):
"""Creates the QLineEdit editor with a QCompleter."""
editor = QLineEdit(parent)
model = index.model()
item_keys = []
# Get keys directly from the UnifiedViewModel
if hasattr(model, '_file_type_keys'):
try:
item_keys = model._file_type_keys # Use cached keys
except Exception as e:
log.error(f"Error getting _file_type_keys from model in ItemTypeSearchDelegate: {e}")
item_keys = []
else:
log.warning("ItemTypeSearchDelegate: Model is missing _file_type_keys attribute. Suggestions will be empty.")
completer = QCompleter(item_keys, editor)
# Use the keys passed during initialization
completer = QCompleter(self.item_type_keys, editor)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains)
completer.setCompletionMode(QCompleter.PopupCompletion)

View File

@@ -71,16 +71,18 @@ class MainPanelWidget(QWidget):
# Notify when Blender settings change
blender_settings_changed = Signal(bool, str, str) # enabled, ng_path, mat_path
def __init__(self, unified_model: UnifiedViewModel, parent=None):
def __init__(self, unified_model: UnifiedViewModel, parent=None, file_type_keys: list[str] | None = None):
"""
Initializes the MainPanelWidget.
Args:
unified_model: The shared UnifiedViewModel instance.
parent: The parent widget.
file_type_keys: A list of available file type names (keys from FILE_TYPE_DEFINITIONS).
"""
super().__init__(parent)
self.unified_model = unified_model
self.file_type_keys = file_type_keys if file_type_keys else []
self.llm_processing_active = False # Track if LLM is running (set by MainWindow)
# Get project root for resolving default paths if needed here
@@ -137,7 +139,8 @@ class MainPanelWidget(QWidget):
# TODO: Revisit ComboBoxDelegate dependency
comboBoxDelegate = ComboBoxDelegate(self) # Pass only parent (self)
supplierSearchDelegate = SupplierSearchDelegate(self) # Pass parent
itemTypeSearchDelegate = ItemTypeSearchDelegate(self) # Instantiate new delegate
# Pass file_type_keys to ItemTypeSearchDelegate
itemTypeSearchDelegate = ItemTypeSearchDelegate(self.file_type_keys, self)
# Set Delegates for Columns
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, supplierSearchDelegate)

View File

@@ -161,8 +161,22 @@ class MainWindow(QMainWindow):
# --- Create Panels ---
self.preset_editor_widget = PresetEditorWidget()
self.llm_editor_widget = LLMEditorWidget() # Instantiate the LLM editor
# Instantiate MainPanelWidget, passing the model and self (MainWindow) for context
self.main_panel_widget = MainPanelWidget(self.unified_model, self)
# --- Load File Type Definitions for Rule Editor ---
file_type_keys = []
try:
# Attempt to load from base config first
base_cfg_data = load_base_config()
if base_cfg_data and "FILE_TYPE_DEFINITIONS" in base_cfg_data:
file_type_keys = list(base_cfg_data["FILE_TYPE_DEFINITIONS"].keys())
log.info(f"Loaded {len(file_type_keys)} FILE_TYPE_DEFINITIONS keys for RuleEditor.")
else:
log.warning("FILE_TYPE_DEFINITIONS not found in base_config. RuleEditor item_type dropdown might be empty.")
except Exception as e:
log.exception(f"Error loading FILE_TYPE_DEFINITIONS for RuleEditor: {e}")
# Instantiate MainPanelWidget, passing the model, self (MainWindow) for context, and file_type_keys
self.main_panel_widget = MainPanelWidget(self.unified_model, self, file_type_keys=file_type_keys)
self.log_console = LogConsoleWidget(self)
# --- Create Left Pane with Static Selector and Stacked Editor ---

View File

@@ -407,24 +407,95 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
preset_name=preset_name
)
asset_rules = []
asset_type_definitions = config._core_settings.get('ASSET_TYPE_DEFINITIONS', {})
# asset_type_definitions = config._core_settings.get('ASSET_TYPE_DEFINITIONS', {}) # Use accessor
file_type_definitions = config._core_settings.get('FILE_TYPE_DEFINITIONS', {})
for asset_name, files_info in classified_assets.items():
if self._is_cancelled: raise RuntimeError("Prediction cancelled during hierarchy building (assets).")
if not files_info: continue
item_types_in_asset = {f_info['item_type'] for f_info in files_info}
predicted_asset_type = "Surface"
material_indicators = {"MAP_COL", "MAP_NRM", "MAP_ROUGH", "MAP_METAL", "MAP_AO", "MAP_DISP", "COL", "NRM", "ROUGH", "METAL", "AO", "DISP"}
if any(it in material_indicators for it in item_types_in_asset if it not in ["EXTRA", "FILE_IGNORE"]):
predicted_asset_type = "Surface"
asset_category_rules = config.asset_category_rules
asset_type_definitions = config.get_asset_type_definitions() # Use new accessor
asset_type_keys = list(asset_type_definitions.keys())
if asset_type_definitions and predicted_asset_type not in asset_type_definitions:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. Falling back.")
default_type = config.default_asset_category
if default_type in asset_type_definitions: predicted_asset_type = default_type
elif asset_type_definitions: predicted_asset_type = list(asset_type_definitions.keys())[0]
# Initialize predicted_asset_type using the validated default
predicted_asset_type = config.default_asset_category
log.debug(f"Asset '{asset_name}': Initial predicted_asset_type set to default: '{predicted_asset_type}'.")
# 1. Check asset_category_rules from preset
determined_by_rule = False
# Check for Model type based on file patterns
if "Model" in asset_type_keys:
model_patterns_regex = config.compiled_model_regex # Already compiled
for f_info in files_info:
# Only consider files not marked as EXTRA or FILE_IGNORE for model classification
if f_info['item_type'] in ["EXTRA", "FILE_IGNORE"]:
continue
file_path_obj = Path(f_info['file_path'])
for pattern_re in model_patterns_regex:
if pattern_re.search(file_path_obj.name):
predicted_asset_type = "Model"
determined_by_rule = True
log.debug(f"Asset '{asset_name}' classified as 'Model' due to file '{file_path_obj.name}' matching pattern '{pattern_re.pattern}'.")
break
if determined_by_rule:
break
# Check for Decal type based on keywords in asset name (if not already Model)
if not determined_by_rule and "Decal" in asset_type_keys:
decal_keywords = asset_category_rules.get('decal_keywords', [])
for keyword in decal_keywords:
# Ensure keyword is a string before trying to escape it
if isinstance(keyword, str) and keyword: # Added check for non-empty string
try:
if re.search(r'\b' + re.escape(keyword) + r'\b', asset_name, re.IGNORECASE): # Match whole word
predicted_asset_type = "Decal"
determined_by_rule = True
log.debug(f"Asset '{asset_name}' classified as 'Decal' due to keyword '{keyword}'.")
break
except re.error as e_re:
log.warning(f"Regex error with decal_keyword '{keyword}': {e_re}")
if determined_by_rule:
pass # Already logged if Decal
# 2. If not determined by specific rules, check for Surface (if not Model/Decal by rule)
if not determined_by_rule and predicted_asset_type == config.default_asset_category and "Surface" in asset_type_keys:
item_types_in_asset = {f_info['item_type'] for f_info in files_info}
# Ensure we are checking against standard map types from FILE_TYPE_DEFINITIONS
# This check is primarily for PBR texture sets.
material_indicators = {
ft_key for ft_key, ft_def in config.get_file_type_definitions_with_examples().items()
if ft_def.get('standard_type') and ft_def.get('standard_type') not in ["", "EXTRA", "FILE_IGNORE", "MODEL"]
}
# Add common direct standard types as well for robustness
material_indicators.update({"COL", "NRM", "ROUGH", "METAL", "AO", "DISP"})
has_material_map = False
for item_type in item_types_in_asset:
# Check if the item_type itself is a material indicator or its standard_type is
if item_type in material_indicators:
has_material_map = True
break
# Check standard type if item_type is a key in FILE_TYPE_DEFINITIONS
item_def = config.get_file_type_definitions_with_examples().get(item_type)
if item_def and item_def.get('standard_type') in material_indicators:
has_material_map = True
break
if has_material_map:
predicted_asset_type = "Surface"
log.debug(f"Asset '{asset_name}' classified as 'Surface' due to material indicators.")
# 3. Final validation: Ensure predicted_asset_type is a valid key.
# config.default_asset_category is already validated to be a key.
if predicted_asset_type not in asset_type_keys:
log.warning(f"Derived AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. "
f"Falling back to default: '{config.default_asset_category}'.")
predicted_asset_type = config.default_asset_category
# This case should ideally not be hit if logic above correctly uses asset_type_keys
# and default_asset_category is valid.
asset_rule = AssetRule(asset_name=asset_name, asset_type=predicted_asset_type)
file_rules = []

View File

@@ -20,6 +20,8 @@ script_dir = Path(__file__).parent
project_root = script_dir.parent
PRESETS_DIR = project_root / "Presets" # Corrected path
TEMPLATE_PATH = PRESETS_DIR / "_template.json"
APP_SETTINGS_PATH_LOCAL = project_root / "config" / "app_settings.json"
log = logging.getLogger(__name__)
@@ -49,6 +51,7 @@ class PresetEditorWidget(QWidget):
self._init_ui()
# --- Initial State ---
self._ftd_keys = self._get_file_type_definition_keys() # Load FTD keys
self._clear_editor() # Clear/disable editor fields initially
self._set_editor_enabled(False) # Disable editor initially
self.populate_presets() # Populate preset list
@@ -56,6 +59,24 @@ class PresetEditorWidget(QWidget):
# --- Connect Editor Signals ---
self._connect_editor_change_signals()
def _get_file_type_definition_keys(self) -> list[str]:
"""Loads FILE_TYPE_DEFINITIONS keys from app_settings.json."""
keys = []
try:
if APP_SETTINGS_PATH_LOCAL.is_file():
with open(APP_SETTINGS_PATH_LOCAL, 'r', encoding='utf-8') as f:
settings = json.load(f)
ftd = settings.get("FILE_TYPE_DEFINITIONS", {})
keys = list(ftd.keys())
log.debug(f"Successfully loaded {len(keys)} FILE_TYPE_DEFINITIONS keys.")
else:
log.error(f"app_settings.json not found at {APP_SETTINGS_PATH_LOCAL} for PresetEditorWidget.")
except json.JSONDecodeError as e:
log.error(f"Failed to parse app_settings.json in PresetEditorWidget: {e}")
except Exception as e:
log.error(f"Error loading FILE_TYPE_DEFINITIONS keys in PresetEditorWidget: {e}")
return keys
def _init_ui(self):
"""Initializes the UI elements for the preset editor."""
main_layout = QVBoxLayout(self)
@@ -306,7 +327,21 @@ class PresetEditorWidget(QWidget):
"""Adds an empty row to the specified table widget in the editor."""
row_count = table_widget.rowCount()
table_widget.insertRow(row_count)
for col in range(table_widget.columnCount()): table_widget.setItem(row_count, col, QTableWidgetItem(""))
if table_widget == self.editor_table_map_type_mapping:
# Column 0: Standard Type (QComboBox)
combo_box = QComboBox()
if self._ftd_keys:
combo_box.addItems(self._ftd_keys)
else:
log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping.")
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved) # Mark unsaved on change
table_widget.setCellWidget(row_count, 0, combo_box)
# Column 1: Input Keywords (QTableWidgetItem)
table_widget.setItem(row_count, 1, QTableWidgetItem(""))
else: # For other tables
for col in range(table_widget.columnCount()):
table_widget.setItem(row_count, col, QTableWidgetItem(""))
self._mark_editor_unsaved()
def _editor_remove_table_row(self, table_widget: QTableWidget):
@@ -409,18 +444,36 @@ class PresetEditorWidget(QWidget):
self.editor_table_bit_depth_variants.setItem(i, 1, QTableWidgetItem(pattern))
self.editor_list_extra_patterns.clear()
self.editor_list_extra_patterns.addItems(preset_data.get("move_to_extra_patterns", []))
self.editor_table_map_type_mapping.setRowCount(0)
self.editor_table_map_type_mapping.setRowCount(0) # Clear before populating
map_mappings = preset_data.get("map_type_mapping", [])
for i, mapping_dict in enumerate(map_mappings):
if isinstance(mapping_dict, dict) and "target_type" in mapping_dict and "keywords" in mapping_dict:
std_type = mapping_dict["target_type"]
keywords = mapping_dict["keywords"]
self.editor_table_map_type_mapping.insertRow(i)
self.editor_table_map_type_mapping.setItem(i, 0, QTableWidgetItem(std_type))
# Column 0: Standard Type (QComboBox)
combo_box = QComboBox()
if self._ftd_keys:
combo_box.addItems(self._ftd_keys)
if std_type in self._ftd_keys:
combo_box.setCurrentText(std_type)
else:
log.warning(f"Preset '{preset_data.get('preset_name', 'Unknown')}': target_type '{std_type}' not found in FILE_TYPE_DEFINITIONS. Selecting first available.")
if self._ftd_keys: combo_box.setCurrentIndex(0)
else:
log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping during population.")
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved) # Connect signal
self.editor_table_map_type_mapping.setCellWidget(i, 0, combo_box)
# Column 1: Input Keywords (QTableWidgetItem)
keywords_str = [str(k) for k in keywords if isinstance(k, str)]
self.editor_table_map_type_mapping.setItem(i, 1, QTableWidgetItem(", ".join(keywords_str)))
else:
log.warning(f"Skipping invalid map_type_mapping item during editor population: {mapping_dict}")
category_rules = preset_data.get("asset_category_rules", {})
self.editor_list_model_patterns.clear()
self.editor_list_model_patterns.addItems(category_rules.get("model_patterns", []))
@@ -543,18 +596,34 @@ class PresetEditorWidget(QWidget):
for r in range(self.editor_table_bit_depth_variants.rowCount()) if self.editor_table_bit_depth_variants.item(r, 0) and self.editor_table_bit_depth_variants.item(r, 1)}
preset_data["source_naming"] = naming_data
preset_data["move_to_extra_patterns"] = [self.editor_list_extra_patterns.item(i).text() for i in range(self.editor_list_extra_patterns.count())]
map_mappings = []
for r in range(self.editor_table_map_type_mapping.rowCount()):
type_item = self.editor_table_map_type_mapping.item(r, 0)
target_type_widget = self.editor_table_map_type_mapping.cellWidget(r, 0)
keywords_item = self.editor_table_map_type_mapping.item(r, 1)
if type_item and type_item.text() and keywords_item and keywords_item.text():
target_type = type_item.text().strip()
target_type = ""
if isinstance(target_type_widget, QComboBox):
target_type = target_type_widget.currentText()
elif self.editor_table_map_type_mapping.item(r, 0): # Fallback if item is not a widget
target_type_item = self.editor_table_map_type_mapping.item(r, 0)
if target_type_item:
target_type = target_type_item.text().strip()
if target_type and keywords_item and keywords_item.text():
keywords = [k.strip() for k in keywords_item.text().split(',') if k.strip()]
if target_type and keywords:
if keywords: # Ensure keywords list is not empty after stripping
map_mappings.append({"target_type": target_type, "keywords": keywords})
else: log.warning(f"Skipping row {r} in map type mapping table due to empty target type or keywords.")
else: log.warning(f"Skipping row {r} in map type mapping table due to missing items.")
else:
log.warning(f"Skipping row {r} in map type mapping table due to empty keywords after processing for target_type '{target_type}'.")
else:
# Log if target_type is empty or keywords_item is problematic
if not target_type:
log.warning(f"Skipping row {r} in map type mapping table due to empty target_type.")
if not (keywords_item and keywords_item.text()):
log.warning(f"Skipping row {r} in map type mapping table for target_type '{target_type}' due to missing or empty keywords item.")
preset_data["map_type_mapping"] = map_mappings
category_rules = {}
category_rules["model_patterns"] = [self.editor_list_model_patterns.item(i).text() for i in range(self.editor_list_model_patterns.count())]
category_rules["decal_keywords"] = [self.editor_list_decal_keywords.item(i).text() for i in range(self.editor_list_decal_keywords.count())]

View File

@@ -14,16 +14,18 @@ class RuleEditorWidget(QWidget):
"""
rule_updated = Signal(object) # Signal emitted when a rule is updated
def __init__(self, asset_types: list[str] | None = None, parent=None):
def __init__(self, asset_types: list[str] | None = None, file_types: list[str] | None = None, parent=None):
"""
Initializes the RuleEditorWidget.
Args:
asset_types (list[str] | None): A list of available asset type names. Defaults to None.
file_types (list[str] | None): A list of available file type names (keys from FILE_TYPE_DEFINITIONS). Defaults to None.
parent: The parent widget.
"""
super().__init__(parent)
self.asset_types = asset_types if asset_types else [] # Store asset types
self.file_types = file_types if file_types else [] # Store file types
self.current_rule_type = None
self.current_rule_object = None
@@ -71,12 +73,26 @@ class RuleEditorWidget(QWidget):
Creates an appropriate editor widget based on the attribute type.
"""
# --- Special Handling for Asset Type Dropdown ---
if self.current_rule_type == 'AssetRule' and attr_name == 'asset_type' and self.asset_types:
if self.current_rule_type == 'AssetRule' and attr_name in ('asset_type', 'asset_type_override') and self.asset_types:
widget = QComboBox()
widget.addItems(self.asset_types)
if attr_value in self.asset_types:
# Handle None case for override: if None, don't select anything or select a placeholder
if attr_value is None and attr_name == 'asset_type_override':
# Optionally add a placeholder like "<None>" or "<Default>"
# widget.insertItem(0, "<Default>") # Example placeholder
widget.setCurrentIndex(-1) # No selection or placeholder
elif attr_value in self.asset_types:
widget.setCurrentText(attr_value)
elif self.asset_types: # Select first item if current value is invalid
elif self.asset_types: # Select first item if current value is invalid (and not None override)
widget.setCurrentIndex(0)
return widget
# --- Special Handling for FileRule item_type and item_type_override ---
elif self.current_rule_type == 'FileRule' and attr_name in ('item_type', 'item_type_override') and self.file_types:
widget = QComboBox()
widget.addItems(self.file_types)
if attr_value in self.file_types:
widget.setCurrentText(attr_value)
elif self.file_types: # Select first item if current value is invalid
widget.setCurrentIndex(0)
return widget
# --- Standard Type Handling ---
@@ -187,7 +203,8 @@ if __name__ == '__main__':
# Example usage: Provide asset types during instantiation
asset_types_from_config = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"] # Example list
editor = RuleEditorWidget(asset_types=asset_types_from_config)
file_types_from_config = ["MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP", "MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK", "MAP_IMPERFECTION", "MODEL", "EXTRA", "FILE_IGNORE"] # Example list
editor = RuleEditorWidget(asset_types=asset_types_from_config, file_types=file_types_from_config)
# Test loading different rule types
source_rule = SourceRule()