{
    "sourceFile": "blenderscripts/create_nodegroups.py",
    "activeCommit": 0,
    "commits": [
        {
            "activePatchIndex": 28,
            "patches": [
                {
                    "date": 1745229222683,
                    "content": "Index: \n===================================================================\n--- \n+++ \n"
                },
                {
                    "date": 1745229590924,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,672 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.0\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = True\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n+\r\n+def calculate_factor_from_string(aspect_string):\r\n+    \"\"\"\r\n+    Parses the aspect_ratio_change_string from metadata and returns the\r\n+    appropriate UV X-scaling factor needed to correct distortion.\r\n+    Assumes the string format documented in Asset Processor Tool readme.md:\r\n+    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n+    Returns 1.0 if the string is invalid or \"EVEN\".\r\n+    \"\"\"\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        return 1.0\r\n+\r\n+    x_factor = 1.0\r\n+    y_factor = 1.0\r\n+\r\n+    # Regex to find X and Y scaling parts\r\n+    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n+\r\n+    try:\r\n+        if match_x:\r\n+            amount_x = int(match_x.group(1))\r\n+            if amount_x > 0:\r\n+                x_factor = amount_x / 100.0\r\n+            else:\r\n+                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+\r\n+        if match_y:\r\n+            amount_y = int(match_y.group(1))\r\n+            if amount_y > 0:\r\n+                y_factor = amount_y / 100.0\r\n+            else:\r\n+                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+\r\n+        # The correction factor for the U (X) coordinate is Y/X\r\n+        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n+        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n+        if x_factor == 0: # Avoid division by zero\r\n+             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n+             return 1.0\r\n+\r\n+        correction_factor = y_factor / x_factor\r\n+        return correction_factor\r\n+\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n+        return 1.0\r\n+    except Exception as e:\r\n+        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # For now, we process map-by-map. An asset is considered processed\r\n+    # if its entry exists, but we rely on map checks.\r\n+    # This could be enhanced later if needed.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context):\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        global ENABLE_MANIFEST # Allow modification of global\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Using rglob to find all metadata.json files recursively\r\n+    metadata_paths = list(root_path.rglob('metadata.json'))\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            maps_data = metadata.get(\"maps\")\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            if not asset_name or not maps_data:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'maps' data. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Manifest Check (Asset Level) ---\r\n+            # We primarily check maps, but can skip the whole asset if needed later\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n+                all_maps_done = True\r\n+                for map_type, res_data in maps_data.items():\r\n+                    for resolution in res_data.keys():\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            all_maps_done = False\r\n+                            break\r\n+                    if not all_maps_done:\r\n+                        break\r\n+                if all_maps_done:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = calculate_factor_from_string(aspect_string)\r\n+                # Check if update is needed (avoids unnecessary console spam)\r\n+                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Stats\r\n+            for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                if map_type_to_stat in maps_data:\r\n+                    # Find the stats node in the parent group\r\n+                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                    if stats_nodes:\r\n+                        stats_node = stats_nodes[0]\r\n+                        # Find the stats data in the metadata (usually from a specific resolution)\r\n+                        # Let's assume the stats are stored directly under the map_type entry\r\n+                        map_metadata = maps_data[map_type_to_stat]\r\n+                        stats = map_metadata.get(\"stats\") # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n+                        if stats and isinstance(stats, dict):\r\n+                            min_val = stats.get(\"min\")\r\n+                            max_val = stats.get(\"max\")\r\n+                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n+\r\n+                            updated_stat = False\r\n+                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n+                                stats_node.inputs[0].default_value = min_val\r\n+                                updated_stat = True\r\n+                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n+                                stats_node.inputs[1].default_value = max_val\r\n+                                updated_stat = True\r\n+                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n+                                stats_node.inputs[2].default_value = mean_val\r\n+                                updated_stat = True\r\n+\r\n+                            if updated_stat:\r\n+                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type_to_stat}' in metadata.\") # Optional\r\n+                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in metadata for stats application.\") # Optional\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n+            for map_type, type_data in maps_data.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Find placeholder node in parent\r\n+                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+\r\n+                # Determine child group name\r\n+                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n+                child_group = bpy.data.node_groups.get(target_child_name)\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                # 'type_data' should be the dictionary containing resolutions and paths for this map_type\r\n+                if not isinstance(type_data, dict):\r\n+                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' data in metadata. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution, map_info in type_data.items():\r\n+                    # 'map_info' should be the dictionary containing 'path', 'stats', etc. for this resolution\r\n+                    if not isinstance(map_info, dict) or \"path\" not in map_info:\r\n+                        print(f\"        !!! WARNING: Invalid or missing path data for {map_type}/{resolution}. Skipping.\")\r\n+                        continue\r\n+\r\n+                    image_path_str = map_info[\"path\"]\r\n+\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str)\r\n+                        if not image_path.is_file():\r\n+                            print(f\"          !!! ERROR: Image file not found: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Use check_existing=True to reuse existing datablocks if path matches\r\n+                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                            if not img:\r\n+                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                                image_load_failed = True\r\n+                            else:\r\n+                                images_loaded += 1 # Count successful loads\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+                        processed_asset_flag = False # Mark that something was processed for this asset\r\n+\r\n+                    else:\r\n+                        errors_encountered += 1\r\n+                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            # --- Update Manifest (Asset Level - if fully processed) ---\r\n+            # This logic might be redundant if we always check map level, but can be added\r\n+            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n+            # For now, we rely on map-level updates.\r\n+            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n+            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n+            #     manifest_needs_saving = True\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        process_library(bpy.context)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745229718025,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,671 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.0\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = True\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n+\r\n+def calculate_factor_from_string(aspect_string):\r\n+    \"\"\"\r\n+    Parses the aspect_ratio_change_string from metadata and returns the\r\n+    appropriate UV X-scaling factor needed to correct distortion.\r\n+    Assumes the string format documented in Asset Processor Tool readme.md:\r\n+    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n+    Returns 1.0 if the string is invalid or \"EVEN\".\r\n+    \"\"\"\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        return 1.0\r\n+\r\n+    x_factor = 1.0\r\n+    y_factor = 1.0\r\n+\r\n+    # Regex to find X and Y scaling parts\r\n+    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n+\r\n+    try:\r\n+        if match_x:\r\n+            amount_x = int(match_x.group(1))\r\n+            if amount_x > 0:\r\n+                x_factor = amount_x / 100.0\r\n+            else:\r\n+                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+\r\n+        if match_y:\r\n+            amount_y = int(match_y.group(1))\r\n+            if amount_y > 0:\r\n+                y_factor = amount_y / 100.0\r\n+            else:\r\n+                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+\r\n+        # The correction factor for the U (X) coordinate is Y/X\r\n+        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n+        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n+        if x_factor == 0: # Avoid division by zero\r\n+             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n+             return 1.0\r\n+\r\n+        correction_factor = y_factor / x_factor\r\n+        return correction_factor\r\n+\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n+        return 1.0\r\n+    except Exception as e:\r\n+        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # For now, we process map-by-map. An asset is considered processed\r\n+    # if its entry exists, but we rely on map checks.\r\n+    # This could be enhanced later if needed.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context):\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Using rglob to find all metadata.json files recursively\r\n+    metadata_paths = list(root_path.rglob('metadata.json'))\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            maps_data = metadata.get(\"maps\")\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            if not asset_name or not maps_data:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'maps' data. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Manifest Check (Asset Level) ---\r\n+            # We primarily check maps, but can skip the whole asset if needed later\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n+                all_maps_done = True\r\n+                for map_type, res_data in maps_data.items():\r\n+                    for resolution in res_data.keys():\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            all_maps_done = False\r\n+                            break\r\n+                    if not all_maps_done:\r\n+                        break\r\n+                if all_maps_done:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = calculate_factor_from_string(aspect_string)\r\n+                # Check if update is needed (avoids unnecessary console spam)\r\n+                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Stats\r\n+            for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                if map_type_to_stat in maps_data:\r\n+                    # Find the stats node in the parent group\r\n+                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                    if stats_nodes:\r\n+                        stats_node = stats_nodes[0]\r\n+                        # Find the stats data in the metadata (usually from a specific resolution)\r\n+                        # Let's assume the stats are stored directly under the map_type entry\r\n+                        map_metadata = maps_data[map_type_to_stat]\r\n+                        stats = map_metadata.get(\"stats\") # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n+                        if stats and isinstance(stats, dict):\r\n+                            min_val = stats.get(\"min\")\r\n+                            max_val = stats.get(\"max\")\r\n+                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n+\r\n+                            updated_stat = False\r\n+                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n+                                stats_node.inputs[0].default_value = min_val\r\n+                                updated_stat = True\r\n+                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n+                                stats_node.inputs[1].default_value = max_val\r\n+                                updated_stat = True\r\n+                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n+                                stats_node.inputs[2].default_value = mean_val\r\n+                                updated_stat = True\r\n+\r\n+                            if updated_stat:\r\n+                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type_to_stat}' in metadata.\") # Optional\r\n+                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in metadata for stats application.\") # Optional\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n+            for map_type, type_data in maps_data.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Find placeholder node in parent\r\n+                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+\r\n+                # Determine child group name\r\n+                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n+                child_group = bpy.data.node_groups.get(target_child_name)\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                # 'type_data' should be the dictionary containing resolutions and paths for this map_type\r\n+                if not isinstance(type_data, dict):\r\n+                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' data in metadata. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution, map_info in type_data.items():\r\n+                    # 'map_info' should be the dictionary containing 'path', 'stats', etc. for this resolution\r\n+                    if not isinstance(map_info, dict) or \"path\" not in map_info:\r\n+                        print(f\"        !!! WARNING: Invalid or missing path data for {map_type}/{resolution}. Skipping.\")\r\n+                        continue\r\n+\r\n+                    image_path_str = map_info[\"path\"]\r\n+\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str)\r\n+                        if not image_path.is_file():\r\n+                            print(f\"          !!! ERROR: Image file not found: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Use check_existing=True to reuse existing datablocks if path matches\r\n+                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                            if not img:\r\n+                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                                image_load_failed = True\r\n+                            else:\r\n+                                images_loaded += 1 # Count successful loads\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+                        processed_asset_flag = False # Mark that something was processed for this asset\r\n+\r\n+                    else:\r\n+                        errors_encountered += 1\r\n+                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            # --- Update Manifest (Asset Level - if fully processed) ---\r\n+            # This logic might be redundant if we always check map level, but can be added\r\n+            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n+            # For now, we rely on map-level updates.\r\n+            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n+            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n+            #     manifest_needs_saving = True\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        process_library(bpy.context)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745229767336,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -667,1348 +667,5 @@\n         import bpy\r\n     except ImportError:\r\n         print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n     else:\r\n-        process_library(bpy.context)\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.0\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n-\r\n-def calculate_factor_from_string(aspect_string):\r\n-    \"\"\"\r\n-    Parses the aspect_ratio_change_string from metadata and returns the\r\n-    appropriate UV X-scaling factor needed to correct distortion.\r\n-    Assumes the string format documented in Asset Processor Tool readme.md:\r\n-    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n-    Returns 1.0 if the string is invalid or \"EVEN\".\r\n-    \"\"\"\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        return 1.0\r\n-\r\n-    x_factor = 1.0\r\n-    y_factor = 1.0\r\n-\r\n-    # Regex to find X and Y scaling parts\r\n-    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n-\r\n-    try:\r\n-        if match_x:\r\n-            amount_x = int(match_x.group(1))\r\n-            if amount_x > 0:\r\n-                x_factor = amount_x / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n-\r\n-        if match_y:\r\n-            amount_y = int(match_y.group(1))\r\n-            if amount_y > 0:\r\n-                y_factor = amount_y / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n-\r\n-        # The correction factor for the U (X) coordinate is Y/X\r\n-        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n-        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n-        if x_factor == 0: # Avoid division by zero\r\n-             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n-             return 1.0\r\n-\r\n-        correction_factor = y_factor / x_factor\r\n-        return correction_factor\r\n-\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n-        return 1.0\r\n-    except Exception as e:\r\n-        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # For now, we process map-by-map. An asset is considered processed\r\n-    # if its entry exists, but we rely on map checks.\r\n-    # This could be enhanced later if needed.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context):\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        global ENABLE_MANIFEST # Allow modification of global\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Using rglob to find all metadata.json files recursively\r\n-    metadata_paths = list(root_path.rglob('metadata.json'))\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            maps_data = metadata.get(\"maps\")\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            if not asset_name or not maps_data:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'maps' data. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Manifest Check (Asset Level) ---\r\n-            # We primarily check maps, but can skip the whole asset if needed later\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n-                all_maps_done = True\r\n-                for map_type, res_data in maps_data.items():\r\n-                    for resolution in res_data.keys():\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            all_maps_done = False\r\n-                            break\r\n-                    if not all_maps_done:\r\n-                        break\r\n-                if all_maps_done:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = calculate_factor_from_string(aspect_string)\r\n-                # Check if update is needed (avoids unnecessary console spam)\r\n-                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Stats\r\n-            for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                if map_type_to_stat in maps_data:\r\n-                    # Find the stats node in the parent group\r\n-                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                    if stats_nodes:\r\n-                        stats_node = stats_nodes[0]\r\n-                        # Find the stats data in the metadata (usually from a specific resolution)\r\n-                        # Let's assume the stats are stored directly under the map_type entry\r\n-                        map_metadata = maps_data[map_type_to_stat]\r\n-                        stats = map_metadata.get(\"stats\") # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n-                        if stats and isinstance(stats, dict):\r\n-                            min_val = stats.get(\"min\")\r\n-                            max_val = stats.get(\"max\")\r\n-                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n-\r\n-                            updated_stat = False\r\n-                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n-                                stats_node.inputs[0].default_value = min_val\r\n-                                updated_stat = True\r\n-                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n-                                stats_node.inputs[1].default_value = max_val\r\n-                                updated_stat = True\r\n-                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n-                                stats_node.inputs[2].default_value = mean_val\r\n-                                updated_stat = True\r\n-\r\n-                            if updated_stat:\r\n-                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type_to_stat}' in metadata.\") # Optional\r\n-                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in metadata for stats application.\") # Optional\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n-            for map_type, type_data in maps_data.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Find placeholder node in parent\r\n-                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-\r\n-                # Determine child group name\r\n-                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n-                child_group = bpy.data.node_groups.get(target_child_name)\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                # 'type_data' should be the dictionary containing resolutions and paths for this map_type\r\n-                if not isinstance(type_data, dict):\r\n-                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' data in metadata. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution, map_info in type_data.items():\r\n-                    # 'map_info' should be the dictionary containing 'path', 'stats', etc. for this resolution\r\n-                    if not isinstance(map_info, dict) or \"path\" not in map_info:\r\n-                        print(f\"        !!! WARNING: Invalid or missing path data for {map_type}/{resolution}. Skipping.\")\r\n-                        continue\r\n-\r\n-                    image_path_str = map_info[\"path\"]\r\n-\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str)\r\n-                        if not image_path.is_file():\r\n-                            print(f\"          !!! ERROR: Image file not found: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Use check_existing=True to reuse existing datablocks if path matches\r\n-                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                            if not img:\r\n-                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                                image_load_failed = True\r\n-                            else:\r\n-                                images_loaded += 1 # Count successful loads\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-                        processed_asset_flag = False # Mark that something was processed for this asset\r\n-\r\n-                    else:\r\n-                        errors_encountered += 1\r\n-                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            # --- Update Manifest (Asset Level - if fully processed) ---\r\n-            # This logic might be redundant if we always check map level, but can be added\r\n-            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n-            # For now, we rely on map-level updates.\r\n-            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n-            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n-            #     manifest_needs_saving = True\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        process_library(bpy.context)\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.0\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n-\r\n-def calculate_factor_from_string(aspect_string):\r\n-    \"\"\"\r\n-    Parses the aspect_ratio_change_string from metadata and returns the\r\n-    appropriate UV X-scaling factor needed to correct distortion.\r\n-    Assumes the string format documented in Asset Processor Tool readme.md:\r\n-    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n-    Returns 1.0 if the string is invalid or \"EVEN\".\r\n-    \"\"\"\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        return 1.0\r\n-\r\n-    x_factor = 1.0\r\n-    y_factor = 1.0\r\n-\r\n-    # Regex to find X and Y scaling parts\r\n-    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n-\r\n-    try:\r\n-        if match_x:\r\n-            amount_x = int(match_x.group(1))\r\n-            if amount_x > 0:\r\n-                x_factor = amount_x / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n-\r\n-        if match_y:\r\n-            amount_y = int(match_y.group(1))\r\n-            if amount_y > 0:\r\n-                y_factor = amount_y / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n-\r\n-        # The correction factor for the U (X) coordinate is Y/X\r\n-        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n-        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n-        if x_factor == 0: # Avoid division by zero\r\n-             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n-             return 1.0\r\n-\r\n-        correction_factor = y_factor / x_factor\r\n-        return correction_factor\r\n-\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n-        return 1.0\r\n-    except Exception as e:\r\n-        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # For now, we process map-by-map. An asset is considered processed\r\n-    # if its entry exists, but we rely on map checks.\r\n-    # This could be enhanced later if needed.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context):\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        global ENABLE_MANIFEST # Allow modification of global\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Using rglob to find all metadata.json files recursively\r\n-    metadata_paths = list(root_path.rglob('metadata.json'))\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            maps_data = metadata.get(\"maps\")\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            if not asset_name or not maps_data:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'maps' data. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Manifest Check (Asset Level) ---\r\n-            # We primarily check maps, but can skip the whole asset if needed later\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n-                all_maps_done = True\r\n-                for map_type, res_data in maps_data.items():\r\n-                    for resolution in res_data.keys():\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            all_maps_done = False\r\n-                            break\r\n-                    if not all_maps_done:\r\n-                        break\r\n-                if all_maps_done:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = calculate_factor_from_string(aspect_string)\r\n-                # Check if update is needed (avoids unnecessary console spam)\r\n-                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Stats\r\n-            for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                if map_type_to_stat in maps_data:\r\n-                    # Find the stats node in the parent group\r\n-                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                    if stats_nodes:\r\n-                        stats_node = stats_nodes[0]\r\n-                        # Find the stats data in the metadata (usually from a specific resolution)\r\n-                        # Let's assume the stats are stored directly under the map_type entry\r\n-                        map_metadata = maps_data[map_type_to_stat]\r\n-                        stats = map_metadata.get(\"stats\") # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n-                        if stats and isinstance(stats, dict):\r\n-                            min_val = stats.get(\"min\")\r\n-                            max_val = stats.get(\"max\")\r\n-                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n-\r\n-                            updated_stat = False\r\n-                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n-                                stats_node.inputs[0].default_value = min_val\r\n-                                updated_stat = True\r\n-                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n-                                stats_node.inputs[1].default_value = max_val\r\n-                                updated_stat = True\r\n-                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n-                                stats_node.inputs[2].default_value = mean_val\r\n-                                updated_stat = True\r\n-\r\n-                            if updated_stat:\r\n-                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type_to_stat}' in metadata.\") # Optional\r\n-                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in metadata for stats application.\") # Optional\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n-            for map_type, type_data in maps_data.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Find placeholder node in parent\r\n-                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-\r\n-                # Determine child group name\r\n-                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n-                child_group = bpy.data.node_groups.get(target_child_name)\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                # 'type_data' should be the dictionary containing resolutions and paths for this map_type\r\n-                if not isinstance(type_data, dict):\r\n-                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' data in metadata. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution, map_info in type_data.items():\r\n-                    # 'map_info' should be the dictionary containing 'path', 'stats', etc. for this resolution\r\n-                    if not isinstance(map_info, dict) or \"path\" not in map_info:\r\n-                        print(f\"        !!! WARNING: Invalid or missing path data for {map_type}/{resolution}. Skipping.\")\r\n-                        continue\r\n-\r\n-                    image_path_str = map_info[\"path\"]\r\n-\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str)\r\n-                        if not image_path.is_file():\r\n-                            print(f\"          !!! ERROR: Image file not found: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Use check_existing=True to reuse existing datablocks if path matches\r\n-                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                            if not img:\r\n-                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                                image_load_failed = True\r\n-                            else:\r\n-                                images_loaded += 1 # Count successful loads\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-                        processed_asset_flag = False # Mark that something was processed for this asset\r\n-\r\n-                    else:\r\n-                        errors_encountered += 1\r\n-                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            # --- Update Manifest (Asset Level - if fully processed) ---\r\n-            # This logic might be redundant if we always check map level, but can be added\r\n-            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n-            # For now, we rely on map-level updates.\r\n-            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n-            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n-            #     manifest_needs_saving = True\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n         process_library(bpy.context)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745229942459,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,685 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.0\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = True\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n+\r\n+def calculate_factor_from_string(aspect_string):\r\n+    \"\"\"\r\n+    Parses the aspect_ratio_change_string from metadata and returns the\r\n+    appropriate UV X-scaling factor needed to correct distortion.\r\n+    Assumes the string format documented in Asset Processor Tool readme.md:\r\n+    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n+    Returns 1.0 if the string is invalid or \"EVEN\".\r\n+    \"\"\"\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        return 1.0\r\n+\r\n+    x_factor = 1.0\r\n+    y_factor = 1.0\r\n+\r\n+    # Regex to find X and Y scaling parts\r\n+    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n+\r\n+    try:\r\n+        if match_x:\r\n+            amount_x = int(match_x.group(1))\r\n+            if amount_x > 0:\r\n+                x_factor = amount_x / 100.0\r\n+            else:\r\n+                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+\r\n+        if match_y:\r\n+            amount_y = int(match_y.group(1))\r\n+            if amount_y > 0:\r\n+                y_factor = amount_y / 100.0\r\n+            else:\r\n+                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+\r\n+        # The correction factor for the U (X) coordinate is Y/X\r\n+        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n+        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n+        if x_factor == 0: # Avoid division by zero\r\n+             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n+             return 1.0\r\n+\r\n+        correction_factor = y_factor / x_factor\r\n+        return correction_factor\r\n+\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n+        return 1.0\r\n+    except Exception as e:\r\n+        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # For now, we process map-by-map. An asset is considered processed\r\n+    # if its entry exists, but we rely on map checks.\r\n+    # This could be enhanced later if needed.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context):\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Using rglob to find all metadata.json files recursively\r\n+    metadata_paths = list(root_path.rglob('metadata.json'))\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+:start_line:325\r\n+-------\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            # maps_data = metadata.get(\"maps\") # Old structure\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+            processed_map_resolutions = metadata.get(\"processed_map_resolutions\", {})\r\n+            map_details = metadata.get(\"map_details\", {})\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\", {}) # Get 1k stats here\r\n+\r\n+            # Check for essential data\r\n+            if not asset_name or not processed_map_resolutions:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'processed_map_resolutions' data. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Manifest Check (Asset Level) ---\r\n+            # We primarily check maps, but can skip the whole asset if needed later\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n+                all_maps_done = True\r\n+                for map_type, resolutions_list in processed_map_resolutions.items():\r\n+                    if not isinstance(resolutions_list, list): continue # Skip invalid entries\r\n+                    for resolution in resolutions_list:\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            all_maps_done = False\r\n+                            break\r\n+                    if not all_maps_done:\r\n+                        break\r\n+                if all_maps_done:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = calculate_factor_from_string(aspect_string)\r\n+                # Check if update is needed (avoids unnecessary console spam)\r\n+                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n+            # Iterate through map types and their resolutions from processed_map_resolutions\r\n+            for map_type, resolutions_list in processed_map_resolutions.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Apply Stats (moved here to be inside the map_type loop)\r\n+                if map_type in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    # Find the stats node in the parent group\r\n+                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type}\"\r\n+                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                    if stats_nodes:\r\n+                        stats_node = stats_nodes[0]\r\n+                        # Find the stats data in the metadata (from image_stats_1k)\r\n+                        stats = image_stats_1k.get(map_type) # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n+                        if stats and isinstance(stats, dict):\r\n+                            min_val = stats.get(\"min\")\r\n+                            max_val = stats.get(\"max\")\r\n+                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n+\r\n+                            updated_stat = False\r\n+                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n+                                stats_node.inputs[0].default_value = min_val\r\n+                                updated_stat = True\r\n+                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n+                                stats_node.inputs[1].default_value = max_val\r\n+                                updated_stat = True\r\n+                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n+                                stats_node.inputs[2].default_value = mean_val\r\n+                                updated_stat = True\r\n+\r\n+                            if updated_stat:\r\n+                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type}' in image_stats_1k.\") # Optional\r\n+                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                # else: print(f\"    Info: Map type '{map_type}' not in APPLY_STATS_FOR_MAP_TYPES.\") # Optional\r\n+\r\n+\r\n+                # Find placeholder node in parent\r\n+                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+\r\n+                # Determine child group name\r\n+                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n+                child_group = bpy.data.node_groups.get(target_child_name)\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                # Iterate through resolutions listed for this map type\r\n+                if not isinstance(resolutions_list, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' resolutions list in metadata. Skipping.\")\r\n+                     continue\r\n+\r\n+                # Get the output format for this map type from map_details\r\n+                map_output_format = map_details.get(map_type, {}).get(\"output_format\", \"png\") # Default to png if not found\r\n+\r\n+                for resolution in resolutions_list:\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+\r\n+                    # --- Reconstruct Image Path ---\r\n+                    # Expected path: <PROCESSED_ASSET_LIBRARY_ROOT>/<supplier_name>/<asset_name>/<AssetName>_<MapType>_<Resolution>.<output_format>\r\n+                    # Need to handle potential case sensitivity or variations in naming\r\n+                    # Let's assume the standard naming convention from the readme for now.\r\n+                    image_filename = f\"{asset_name}_{map_type}_{resolution}.{map_output_format}\"\r\n+                    image_path_str = str(root_path / supplier_name / asset_name / image_filename)\r\n+                    print(f\"          Reconstructed Image Path: {image_path_str}\") # Add log to verify path\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str)\r\n+                        if not image_path.is_file():\r\n+                            print(f\"          !!! ERROR: Image file not found at reconstructed path: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Use check_existing=True to reuse existing datablocks if path matches\r\n+                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                            if not img:\r\n+                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                                image_load_failed = True\r\n+                            else:\r\n+                                images_loaded += 1 # Count successful loads\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+                        processed_asset_flag = False # Mark that something was processed for this asset\r\n+\r\n+                    else:\r\n+                        errors_encountered += 1\r\n+                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            # --- Update Manifest (Asset Level - if fully processed) ---\r\n+            # This logic might be redundant if we always check map level, but can be added\r\n+            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n+            # For now, we rely on map-level updates.\r\n+            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n+            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n+            #     manifest_needs_saving = True\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        process_library(bpy.context)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745230160934,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -321,693 +321,8 @@\n             # --- Extract Key Info ---\r\n             asset_name = metadata.get(\"asset_name\")\r\n             supplier_name = metadata.get(\"supplier_name\")\r\n             archetype = metadata.get(\"archetype\")\r\n-:start_line:325\r\n--------\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            # maps_data = metadata.get(\"maps\") # Old structure\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-            processed_map_resolutions = metadata.get(\"processed_map_resolutions\", {})\r\n-            map_details = metadata.get(\"map_details\", {})\r\n-            image_stats_1k = metadata.get(\"image_stats_1k\", {}) # Get 1k stats here\r\n-\r\n-            # Check for essential data\r\n-            if not asset_name or not processed_map_resolutions:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'processed_map_resolutions' data. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Manifest Check (Asset Level) ---\r\n-            # We primarily check maps, but can skip the whole asset if needed later\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n-                all_maps_done = True\r\n-                for map_type, resolutions_list in processed_map_resolutions.items():\r\n-                    if not isinstance(resolutions_list, list): continue # Skip invalid entries\r\n-                    for resolution in resolutions_list:\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            all_maps_done = False\r\n-                            break\r\n-                    if not all_maps_done:\r\n-                        break\r\n-                if all_maps_done:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = calculate_factor_from_string(aspect_string)\r\n-                # Check if update is needed (avoids unnecessary console spam)\r\n-                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n-            # Iterate through map types and their resolutions from processed_map_resolutions\r\n-            for map_type, resolutions_list in processed_map_resolutions.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Apply Stats (moved here to be inside the map_type loop)\r\n-                if map_type in APPLY_STATS_FOR_MAP_TYPES:\r\n-                    # Find the stats node in the parent group\r\n-                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type}\"\r\n-                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                    if stats_nodes:\r\n-                        stats_node = stats_nodes[0]\r\n-                        # Find the stats data in the metadata (from image_stats_1k)\r\n-                        stats = image_stats_1k.get(map_type) # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n-                        if stats and isinstance(stats, dict):\r\n-                            min_val = stats.get(\"min\")\r\n-                            max_val = stats.get(\"max\")\r\n-                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n-\r\n-                            updated_stat = False\r\n-                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n-                                stats_node.inputs[0].default_value = min_val\r\n-                                updated_stat = True\r\n-                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n-                                stats_node.inputs[1].default_value = max_val\r\n-                                updated_stat = True\r\n-                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n-                                stats_node.inputs[2].default_value = mean_val\r\n-                                updated_stat = True\r\n-\r\n-                            if updated_stat:\r\n-                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type}' in image_stats_1k.\") # Optional\r\n-                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                # else: print(f\"    Info: Map type '{map_type}' not in APPLY_STATS_FOR_MAP_TYPES.\") # Optional\r\n-\r\n-\r\n-                # Find placeholder node in parent\r\n-                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-\r\n-                # Determine child group name\r\n-                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n-                child_group = bpy.data.node_groups.get(target_child_name)\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                # Iterate through resolutions listed for this map type\r\n-                if not isinstance(resolutions_list, list):\r\n-                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' resolutions list in metadata. Skipping.\")\r\n-                     continue\r\n-\r\n-                # Get the output format for this map type from map_details\r\n-                map_output_format = map_details.get(map_type, {}).get(\"output_format\", \"png\") # Default to png if not found\r\n-\r\n-                for resolution in resolutions_list:\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-\r\n-                    # --- Reconstruct Image Path ---\r\n-                    # Expected path: <PROCESSED_ASSET_LIBRARY_ROOT>/<supplier_name>/<asset_name>/<AssetName>_<MapType>_<Resolution>.<output_format>\r\n-                    # Need to handle potential case sensitivity or variations in naming\r\n-                    # Let's assume the standard naming convention from the readme for now.\r\n-                    image_filename = f\"{asset_name}_{map_type}_{resolution}.{map_output_format}\"\r\n-                    image_path_str = str(root_path / supplier_name / asset_name / image_filename)\r\n-                    print(f\"          Reconstructed Image Path: {image_path_str}\") # Add log to verify path\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str)\r\n-                        if not image_path.is_file():\r\n-                            print(f\"          !!! ERROR: Image file not found at reconstructed path: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Use check_existing=True to reuse existing datablocks if path matches\r\n-                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                            if not img:\r\n-                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                                image_load_failed = True\r\n-                            else:\r\n-                                images_loaded += 1 # Count successful loads\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-                        processed_asset_flag = False # Mark that something was processed for this asset\r\n-\r\n-                    else:\r\n-                        errors_encountered += 1\r\n-                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            # --- Update Manifest (Asset Level - if fully processed) ---\r\n-            # This logic might be redundant if we always check map level, but can be added\r\n-            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n-            # For now, we rely on map-level updates.\r\n-            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n-            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n-            #     manifest_needs_saving = True\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        process_library(bpy.context)\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.0\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n-\r\n-def calculate_factor_from_string(aspect_string):\r\n-    \"\"\"\r\n-    Parses the aspect_ratio_change_string from metadata and returns the\r\n-    appropriate UV X-scaling factor needed to correct distortion.\r\n-    Assumes the string format documented in Asset Processor Tool readme.md:\r\n-    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n-    Returns 1.0 if the string is invalid or \"EVEN\".\r\n-    \"\"\"\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        return 1.0\r\n-\r\n-    x_factor = 1.0\r\n-    y_factor = 1.0\r\n-\r\n-    # Regex to find X and Y scaling parts\r\n-    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n-\r\n-    try:\r\n-        if match_x:\r\n-            amount_x = int(match_x.group(1))\r\n-            if amount_x > 0:\r\n-                x_factor = amount_x / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n-\r\n-        if match_y:\r\n-            amount_y = int(match_y.group(1))\r\n-            if amount_y > 0:\r\n-                y_factor = amount_y / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n-\r\n-        # The correction factor for the U (X) coordinate is Y/X\r\n-        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n-        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n-        if x_factor == 0: # Avoid division by zero\r\n-             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n-             return 1.0\r\n-\r\n-        correction_factor = y_factor / x_factor\r\n-        return correction_factor\r\n-\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n-        return 1.0\r\n-    except Exception as e:\r\n-        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # For now, we process map-by-map. An asset is considered processed\r\n-    # if its entry exists, but we rely on map checks.\r\n-    # This could be enhanced later if needed.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context):\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Using rglob to find all metadata.json files recursively\r\n-    metadata_paths = list(root_path.rglob('metadata.json'))\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n             maps_data = metadata.get(\"maps\")\r\n             aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n \r\n             if not asset_name or not maps_data:\r\n"
                },
                {
                    "date": 1745230243556,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,9 +1,12 @@\n # Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.0\r\n+# Version: 1.1\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n #              reads metadata.json files, and creates/updates corresponding\r\n #              PBR node groups in the active Blender file.\r\n+# Changes v1.1:\r\n+#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n+#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n \r\n import bpy\r\n import os\r\n import json\r\n@@ -14,9 +17,10 @@\n # --- USER CONFIGURATION ---\r\n \r\n # Path to the root output directory of the Asset Processor Tool\r\n # Example: r\"G:\\Assets\\Processed\"\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n \r\n # Names of the required node group templates in the Blender file\r\n PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n@@ -28,13 +32,20 @@\n # Enable/disable the manifest system to track processed assets/maps\r\n # If enabled, requires the blend file to be saved.\r\n ENABLE_MANIFEST = True\r\n \r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n # Map PBR type strings (from metadata) to Blender color spaces\r\n # Add more mappings as needed based on your metadata types\r\n PBR_COLOR_SPACE_MAP = {\r\n     \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n     \"COL\": \"sRGB\",\r\n+    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n+    \"COL-2\": \"sRGB\",\r\n     \"DISP\": \"Non-Color\",\r\n     \"NRM\": \"Non-Color\",\r\n     \"REFL\": \"Non-Color\", # Reflection/Specular\r\n     \"ROUGH\": \"Non-Color\",\r\n@@ -47,9 +58,10 @@\n }\r\n DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n \r\n # Map types for which stats should be applied (if found in metadata and node exists)\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n+# Reads stats from the 'image_stats_1k' section of metadata.json\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n \r\n # --- END USER CONFIGURATION ---\r\n \r\n \r\n@@ -88,9 +100,11 @@\n     return False # Tag already existed\r\n \r\n def get_color_space(map_type):\r\n     \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n+    # Handle potential numbered variants like COL-1, COL-2\r\n+    base_map_type = map_type.split('-')[0]\r\n+    return PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)\r\n \r\n def calculate_factor_from_string(aspect_string):\r\n     \"\"\"\r\n     Parses the aspect_ratio_change_string from metadata and returns the\r\n@@ -140,9 +154,34 @@\n     except Exception as e:\r\n         print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n         return 1.0\r\n \r\n+def reconstruct_image_path(asset_dir_path, asset_name, map_type, resolution, file_format):\r\n+    \"\"\"\r\n+    Constructs the expected image file path based on the provided components\r\n+    and the IMAGE_FILENAME_PATTERN.\r\n+    \"\"\"\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution, file_format]):\r\n+        return None\r\n \r\n+    try:\r\n+        filename = IMAGE_FILENAME_PATTERN.format(\r\n+            asset_name=asset_name,\r\n+            map_type=map_type,\r\n+            resolution=resolution,\r\n+            format=file_format\r\n+        )\r\n+        # The image file should be directly inside the asset directory\r\n+        full_path = asset_dir_path / filename\r\n+        return str(full_path)\r\n+    except KeyError as e:\r\n+        print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+        return None\r\n+    except Exception as e:\r\n+        print(f\"    !!! ERROR reconstructing image path: {e}\")\r\n+        return None\r\n+\r\n+\r\n # --- Manifest Functions ---\r\n \r\n def get_manifest_path(context):\r\n     \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n@@ -205,11 +244,9 @@\n \r\n def is_asset_processed(manifest_data, asset_name):\r\n     \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n     if not ENABLE_MANIFEST: return False\r\n-    # For now, we process map-by-map. An asset is considered processed\r\n-    # if its entry exists, but we rely on map checks.\r\n-    # This could be enhanced later if needed.\r\n+    # Basic check if asset entry exists. Detailed check happens at map level.\r\n     return asset_name in manifest_data\r\n \r\n def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n     \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n@@ -300,10 +337,20 @@\n \r\n     print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n \r\n     # --- Scan for metadata.json ---\r\n-    # Using rglob to find all metadata.json files recursively\r\n-    metadata_paths = list(root_path.rglob('metadata.json'))\r\n+    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n+    # Then scan within each supplier for asset folders containing metadata.json\r\n+    metadata_paths = []\r\n+    for supplier_dir in root_path.iterdir():\r\n+        if supplier_dir.is_dir():\r\n+            # Now look for asset folders inside the supplier directory\r\n+            for asset_dir in supplier_dir.iterdir():\r\n+                if asset_dir.is_dir():\r\n+                    metadata_file = asset_dir / 'metadata.json'\r\n+                    if metadata_file.is_file():\r\n+                        metadata_paths.append(metadata_file)\r\n+\r\n     metadata_files_found = len(metadata_paths)\r\n     print(f\"Found {metadata_files_found} metadata.json files.\")\r\n \r\n     if metadata_files_found == 0:\r\n@@ -312,8 +359,9 @@\n         return True # No work needed is considered success\r\n \r\n     # --- Process Each Metadata File ---\r\n     for metadata_path in metadata_paths:\r\n+        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n         print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n         try:\r\n             with open(metadata_path, 'r', encoding='utf-8') as f:\r\n                 metadata = json.load(f)\r\n@@ -321,31 +369,41 @@\n             # --- Extract Key Info ---\r\n             asset_name = metadata.get(\"asset_name\")\r\n             supplier_name = metadata.get(\"supplier_name\")\r\n             archetype = metadata.get(\"archetype\")\r\n-            maps_data = metadata.get(\"maps\")\r\n+            # Get map info from the correct keys\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\") # Dict: {map_type: [res1, res2]}\r\n+            map_details = metadata.get(\"map_details\") # Dict: {map_type: {details}}\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n             aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n \r\n-            if not asset_name or not maps_data:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'maps' data. Skipping.\")\r\n+            # Validate essential data\r\n+            if not asset_name:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n                 errors_encountered += 1\r\n                 continue\r\n+            if not processed_resolutions or not isinstance(processed_resolutions, dict):\r\n+                 print(f\"    !!! ERROR: Metadata file is missing or has invalid 'processed_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+                 errors_encountered += 1\r\n+                 continue\r\n+            if not map_details or not isinstance(map_details, dict):\r\n+                 print(f\"    !!! WARNING: Metadata file is missing or has invalid 'map_details'. Path reconstruction might fail for asset '{asset_name}'.\")\r\n+                 # Continue processing, but path reconstruction might fail later\r\n \r\n             print(f\"  Asset Name: {asset_name}\")\r\n \r\n-            # --- Manifest Check (Asset Level) ---\r\n-            # We primarily check maps, but can skip the whole asset if needed later\r\n+            # --- Manifest Check (Asset Level - Basic) ---\r\n             if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n-                all_maps_done = True\r\n-                for map_type, res_data in maps_data.items():\r\n-                    for resolution in res_data.keys():\r\n+                # Perform a quick check if *any* map needs processing for this asset\r\n+                needs_processing = False\r\n+                for map_type, resolutions in processed_resolutions.items():\r\n+                    for resolution in resolutions:\r\n                         if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            all_maps_done = False\r\n+                            needs_processing = True\r\n                             break\r\n-                    if not all_maps_done:\r\n+                    if needs_processing:\r\n                         break\r\n-                if all_maps_done:\r\n+                if not needs_processing:\r\n                     print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n                     assets_skipped_manifest += 1\r\n                     continue # Skip to next metadata file\r\n \r\n@@ -396,50 +454,62 @@\n                     aspect_node.outputs[0].default_value = correction_factor\r\n                     print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n             # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n \r\n-            # Apply Stats\r\n-            for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                if map_type_to_stat in maps_data:\r\n-                    # Find the stats node in the parent group\r\n-                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                    if stats_nodes:\r\n-                        stats_node = stats_nodes[0]\r\n-                        # Find the stats data in the metadata (usually from a specific resolution)\r\n-                        # Let's assume the stats are stored directly under the map_type entry\r\n-                        map_metadata = maps_data[map_type_to_stat]\r\n-                        stats = map_metadata.get(\"stats\") # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n-                        if stats and isinstance(stats, dict):\r\n-                            min_val = stats.get(\"min\")\r\n-                            max_val = stats.get(\"max\")\r\n-                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n+            # Apply Stats (using image_stats_1k)\r\n+            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    if map_type_to_stat in image_stats_1k:\r\n+                        # Find the stats node in the parent group\r\n+                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                        if stats_nodes:\r\n+                            stats_node = stats_nodes[0]\r\n+                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n \r\n-                            updated_stat = False\r\n-                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n-                                stats_node.inputs[0].default_value = min_val\r\n-                                updated_stat = True\r\n-                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n-                                stats_node.inputs[1].default_value = max_val\r\n-                                updated_stat = True\r\n-                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n-                                stats_node.inputs[2].default_value = mean_val\r\n-                                updated_stat = True\r\n+                            if stats and isinstance(stats, dict):\r\n+                                # Handle potential list format for RGB stats (use first value) or direct float\r\n+                                def get_stat_value(stat_val):\r\n+                                    if isinstance(stat_val, list):\r\n+                                        return stat_val[0] if stat_val else None\r\n+                                    return stat_val\r\n \r\n-                            if updated_stat:\r\n-                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type_to_stat}' in metadata.\") # Optional\r\n-                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in metadata for stats application.\") # Optional\r\n+                                min_val = get_stat_value(stats.get(\"min\"))\r\n+                                max_val = get_stat_value(stats.get(\"max\"))\r\n+                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n \r\n+                                updated_stat = False\r\n+                                if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n+                                    stats_node.inputs[0].default_value = min_val\r\n+                                    updated_stat = True\r\n+                                if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n+                                    stats_node.inputs[1].default_value = max_val\r\n+                                    updated_stat = True\r\n+                                if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n+                                    stats_node.inputs[2].default_value = mean_val\r\n+                                    updated_stat = True\r\n \r\n+                                if updated_stat:\r\n+                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n+                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n+            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n+\r\n+\r\n             # --- Child Group Handling ---\r\n-            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n-            for map_type, type_data in maps_data.items():\r\n+            # Iterate through the map types listed in processed_resolutions\r\n+            for map_type, resolutions in processed_resolutions.items():\r\n                 print(f\"    Processing Map Type: {map_type}\")\r\n \r\n+                # Get details for this map type (needed for format)\r\n+                current_map_details = map_details.get(map_type, {})\r\n+                output_format = current_map_details.get(\"output_format\")\r\n+                if not output_format:\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for '{map_type}'. Cannot reconstruct path. Skipping map type.\")\r\n+                    continue\r\n+\r\n                 # Find placeholder node in parent\r\n-                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n                 holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n                 if not holder_nodes:\r\n                     print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n                     continue\r\n@@ -511,29 +581,35 @@\n                     print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n \r\n \r\n                 # --- Image Node Handling (Inside Child Group) ---\r\n-                # 'type_data' should be the dictionary containing resolutions and paths for this map_type\r\n-                if not isinstance(type_data, dict):\r\n-                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' data in metadata. Skipping.\")\r\n+                if not isinstance(resolutions, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n                      continue\r\n \r\n-                for resolution, map_info in type_data.items():\r\n-                    # 'map_info' should be the dictionary containing 'path', 'stats', etc. for this resolution\r\n-                    if not isinstance(map_info, dict) or \"path\" not in map_info:\r\n-                        print(f\"        !!! WARNING: Invalid or missing path data for {map_type}/{resolution}. Skipping.\")\r\n-                        continue\r\n-\r\n-                    image_path_str = map_info[\"path\"]\r\n-\r\n+                for resolution in resolutions:\r\n                     # --- Manifest Check (Map/Resolution Level) ---\r\n                     if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n                         # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n                         maps_skipped_manifest += 1\r\n                         continue\r\n \r\n                     print(f\"        Processing Resolution: {resolution}\")\r\n \r\n+                    # Reconstruct the image path\r\n+                    image_path_str = reconstruct_image_path(\r\n+                        asset_dir_path=asset_dir_path,\r\n+                        asset_name=asset_name,\r\n+                        map_type=map_type,\r\n+                        resolution=resolution,\r\n+                        file_format=output_format\r\n+                    )\r\n+\r\n+                    if not image_path_str:\r\n+                        print(f\"          !!! ERROR: Failed to reconstruct image path for {map_type}/{resolution}. Skipping.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+\r\n                     # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n                     image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n                     if not image_nodes:\r\n                         print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n@@ -544,9 +620,9 @@\n                     image_load_failed = False\r\n                     try:\r\n                         image_path = Path(image_path_str)\r\n                         if not image_path.is_file():\r\n-                            print(f\"          !!! ERROR: Image file not found: {image_path_str}\")\r\n+                            print(f\"          !!! ERROR: Reconstructed image file not found: {image_path_str}\")\r\n                             image_load_failed = True\r\n                         else:\r\n                             # Use check_existing=True to reuse existing datablocks if path matches\r\n                             img = bpy.data.images.load(str(image_path), check_existing=True)\r\n@@ -592,25 +668,16 @@\n                         if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n                             manifest_needs_saving = True\r\n                             # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n                         maps_processed += 1\r\n-                        processed_asset_flag = False # Mark that something was processed for this asset\r\n \r\n                     else:\r\n-                        errors_encountered += 1\r\n-                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n+                        # Increment error count if loading failed\r\n+                        if image_load_failed: errors_encountered += 1\r\n \r\n                 # --- End Resolution Loop ---\r\n             # --- End Map Type Loop ---\r\n \r\n-            # --- Update Manifest (Asset Level - if fully processed) ---\r\n-            # This logic might be redundant if we always check map level, but can be added\r\n-            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n-            # For now, we rely on map-level updates.\r\n-            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n-            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n-            #     manifest_needs_saving = True\r\n-\r\n             assets_processed += 1\r\n \r\n         except FileNotFoundError:\r\n             print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n"
                },
                {
                    "date": 1745231081386,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,9 +1,13 @@\n # Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.1\r\n+# Version: 1.2\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n #              reads metadata.json files, and creates/updates corresponding\r\n #              PBR node groups in the active Blender file.\r\n+# Changes v1.2:\r\n+#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n+#   - Added fallback logic for reconstructing image paths with different extensions.\r\n+#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n # Changes v1.1:\r\n #   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n #   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n \r\n@@ -12,8 +16,9 @@\n import json\r\n from pathlib import Path\r\n import time\r\n import re # For parsing aspect ratio string\r\n+import base64 # For encoding node group names\r\n \r\n # --- USER CONFIGURATION ---\r\n \r\n # Path to the root output directory of the Asset Processor Tool\r\n@@ -37,8 +42,18 @@\n # {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n # Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n \r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map type(s) to use for generating the asset preview (e.g., COL, COL-1)\r\n+# The script will look for these in order and use the first one found.\r\n+PREVIEW_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n+# Preferred resolution order for preview (lowest first is often faster)\r\n+PREVIEW_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n # Map PBR type strings (from metadata) to Blender color spaces\r\n # Add more mappings as needed based on your metadata types\r\n PBR_COLOR_SPACE_MAP = {\r\n     \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n@@ -66,8 +81,18 @@\n \r\n \r\n # --- Helper Functions ---\r\n \r\n+def encode_name_b64(name_str):\r\n+    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n+    try:\r\n+        # Ensure the input is a string\r\n+        name_str = str(name_str)\r\n+        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n+    except Exception as e:\r\n+        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n+        return name_str # Fallback to original name on error\r\n+\r\n def find_nodes_by_label(node_tree, label, node_type=None):\r\n     \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n     if not node_tree:\r\n         return []\r\n@@ -154,34 +179,68 @@\n     except Exception as e:\r\n         print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n         return 1.0\r\n \r\n-def reconstruct_image_path(asset_dir_path, asset_name, map_type, resolution, file_format):\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format):\r\n     \"\"\"\r\n-    Constructs the expected image file path based on the provided components\r\n-    and the IMAGE_FILENAME_PATTERN.\r\n+    Constructs the expected image file path, trying the primary format first,\r\n+    then falling back to common extensions if the primary path doesn't exist.\r\n+    Returns the found path as a string, or None if not found.\r\n     \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution, file_format]):\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution, primary_format]):\r\n+        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n         return None\r\n \r\n+    # 1. Try the primary format from metadata\r\n+    primary_path_str = None\r\n     try:\r\n         filename = IMAGE_FILENAME_PATTERN.format(\r\n             asset_name=asset_name,\r\n             map_type=map_type,\r\n             resolution=resolution,\r\n-            format=file_format\r\n+            format=primary_format.lower() # Ensure format is lowercase\r\n         )\r\n-        # The image file should be directly inside the asset directory\r\n-        full_path = asset_dir_path / filename\r\n-        return str(full_path)\r\n+        primary_path = asset_dir_path / filename\r\n+        primary_path_str = str(primary_path)\r\n+        if primary_path.is_file():\r\n+            # print(f\"          Found primary path: {primary_path_str}\") # Verbose\r\n+            return primary_path_str\r\n+        # else: print(f\"          Primary path not found: {primary_path_str}\") # Verbose\r\n     except KeyError as e:\r\n         print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-        return None\r\n+        return None # Cannot proceed without valid pattern\r\n     except Exception as e:\r\n-        print(f\"    !!! ERROR reconstructing image path: {e}\")\r\n-        return None\r\n+        print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+        # Continue to fallback even if primary reconstruction had issues\r\n \r\n+    # 2. Try fallback extensions if primary failed\r\n+    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+        if ext.lower() == primary_format.lower(): # Don't retry the primary format\r\n+            continue\r\n+        try:\r\n+            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=ext.lower()\r\n+            )\r\n+            fallback_path = asset_dir_path / fallback_filename\r\n+            if fallback_path.is_file():\r\n+                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n+                return str(fallback_path)\r\n+        except KeyError:\r\n+             # Should not happen if primary format worked, but handle defensively\r\n+             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+             return None\r\n+        except Exception as e_fallback:\r\n+            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+            continue # Try next extension\r\n \r\n+    print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    return None # Not found after all checks\r\n+\r\n+\r\n # --- Manifest Functions ---\r\n \r\n def get_manifest_path(context):\r\n     \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n@@ -332,8 +391,9 @@\n     images_assigned = 0\r\n     maps_processed = 0\r\n     maps_skipped_manifest = 0\r\n     errors_encountered = 0\r\n+    previews_set = 0\r\n     # --- End Counters ---\r\n \r\n     print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n \r\n@@ -477,16 +537,17 @@\n                                 max_val = get_stat_value(stats.get(\"max\"))\r\n                                 mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n \r\n                                 updated_stat = False\r\n-                                if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n-                                    stats_node.inputs[0].default_value = min_val\r\n+                                # Check inputs exist before assigning\r\n+                                if stats_node.inputs.get(\"X\") and min_val is not None and abs(stats_node.inputs[\"X\"].default_value - min_val) > 0.0001:\r\n+                                    stats_node.inputs[\"X\"].default_value = min_val\r\n                                     updated_stat = True\r\n-                                if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n-                                    stats_node.inputs[1].default_value = max_val\r\n+                                if stats_node.inputs.get(\"Y\") and max_val is not None and abs(stats_node.inputs[\"Y\"].default_value - max_val) > 0.0001:\r\n+                                    stats_node.inputs[\"Y\"].default_value = max_val\r\n                                     updated_stat = True\r\n-                                if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n-                                    stats_node.inputs[2].default_value = mean_val\r\n+                                if stats_node.inputs.get(\"Z\") and mean_val is not None and abs(stats_node.inputs[\"Z\"].default_value - mean_val) > 0.0001:\r\n+                                    stats_node.inputs[\"Z\"].default_value = mean_val\r\n                                     updated_stat = True\r\n \r\n                                 if updated_stat:\r\n                                     print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n@@ -494,9 +555,49 @@\n                         # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n                     # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n             # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n \r\n+            # --- Set Asset Preview (only for new parent groups) ---\r\n+            if is_new_parent and parent_group.asset_data:\r\n+                preview_path = None\r\n+                # Find the lowest resolution color map path\r\n+                for preview_map_type in PREVIEW_MAP_TYPES:\r\n+                    if preview_map_type in processed_resolutions:\r\n+                        available_resolutions = processed_resolutions[preview_map_type]\r\n+                        # Find the lowest resolution available based on preferred order\r\n+                        lowest_res = None\r\n+                        for res_pref in PREVIEW_RESOLUTION_ORDER:\r\n+                            if res_pref in available_resolutions:\r\n+                                lowest_res = res_pref\r\n+                                break\r\n+                        if lowest_res:\r\n+                            preview_map_details = map_details.get(preview_map_type, {})\r\n+                            preview_format = preview_map_details.get(\"output_format\")\r\n+                            if preview_format:\r\n+                                preview_path = reconstruct_image_path_with_fallback(\r\n+                                    asset_dir_path=asset_dir_path,\r\n+                                    asset_name=asset_name,\r\n+                                    map_type=preview_map_type,\r\n+                                    resolution=lowest_res,\r\n+                                    primary_format=preview_format\r\n+                                )\r\n+                                if preview_path:\r\n+                                    break # Found a suitable preview image path\r\n+                # Load the preview if a path was found\r\n+                if preview_path:\r\n+                    print(f\"    Attempting to set preview from: {Path(preview_path).name}\")\r\n+                    try:\r\n+                        # Ensure the ID (node group) is the active one for the operator context\r\n+                        with context.temp_override(id=parent_group):\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=preview_path)\r\n+                        print(f\"      Successfully set custom preview.\")\r\n+                        previews_set += 1\r\n+                    except Exception as e_preview:\r\n+                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n+                        errors_encountered += 1\r\n+                # else: print(f\"    Info: Could not find suitable preview image ({PREVIEW_MAP_TYPES} at {PREVIEW_RESOLUTION_ORDER}) for '{asset_name}'.\")\r\n \r\n+\r\n             # --- Child Group Handling ---\r\n             # Iterate through the map types listed in processed_resolutions\r\n             for map_type, resolutions in processed_resolutions.items():\r\n                 print(f\"    Processing Map Type: {map_type}\")\r\n@@ -514,25 +615,27 @@\n                     print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n                     continue\r\n                 holder_node = holder_nodes[0] # Assume first is correct\r\n \r\n-                # Determine child group name\r\n-                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n-                child_group = bpy.data.node_groups.get(target_child_name)\r\n+                # Determine child group name (LOGICAL and ENCODED)\r\n+                logical_child_name = f\"{asset_name}_{map_type}\"\r\n+                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n+\r\n+                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n                 is_new_child = False\r\n \r\n                 if child_group is None:\r\n-                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n+                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n                     child_group = template_child.copy()\r\n                     if not child_group:\r\n                         print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n                         errors_encountered += 1\r\n                         continue\r\n-                    child_group.name = target_child_name\r\n+                    child_group.name = target_child_name_b64 # Set encoded name\r\n                     child_groups_created += 1\r\n                     is_new_child = True\r\n                 else:\r\n-                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n+                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n                     child_groups_updated += 1\r\n \r\n                 # Assign child group to placeholder if needed\r\n                 if holder_node.node_tree != child_group:\r\n@@ -594,21 +697,21 @@\n                         continue\r\n \r\n                     print(f\"        Processing Resolution: {resolution}\")\r\n \r\n-                    # Reconstruct the image path\r\n-                    image_path_str = reconstruct_image_path(\r\n+                    # Reconstruct the image path using fallback logic\r\n+                    image_path_str = reconstruct_image_path_with_fallback(\r\n                         asset_dir_path=asset_dir_path,\r\n                         asset_name=asset_name,\r\n                         map_type=map_type,\r\n                         resolution=resolution,\r\n-                        file_format=output_format\r\n+                        primary_format=output_format # Pass the format from metadata\r\n                     )\r\n \r\n                     if not image_path_str:\r\n-                        print(f\"          !!! ERROR: Failed to reconstruct image path for {map_type}/{resolution}. Skipping.\")\r\n+                        # Error already printed by reconstruct function\r\n                         errors_encountered += 1\r\n-                        continue\r\n+                        continue # Skip this resolution if path not found\r\n \r\n                     # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n                     image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n                     if not image_nodes:\r\n@@ -618,20 +721,16 @@\n                     # --- Load Image ---\r\n                     img = None\r\n                     image_load_failed = False\r\n                     try:\r\n-                        image_path = Path(image_path_str)\r\n-                        if not image_path.is_file():\r\n-                            print(f\"          !!! ERROR: Reconstructed image file not found: {image_path_str}\")\r\n+                        image_path = Path(image_path_str) # Path object created from already found path string\r\n+                        # Use check_existing=True to reuse existing datablocks if path matches\r\n+                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                        if not img:\r\n+                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n                             image_load_failed = True\r\n                         else:\r\n-                            # Use check_existing=True to reuse existing datablocks if path matches\r\n-                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                            if not img:\r\n-                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                                image_load_failed = True\r\n-                            else:\r\n-                                images_loaded += 1 # Count successful loads\r\n+                            images_loaded += 1 # Count successful loads\r\n                     except RuntimeError as e_runtime_load:\r\n                         # Catch specific Blender runtime errors (e.g., unsupported format)\r\n                         print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n                         image_load_failed = True\r\n@@ -718,8 +817,9 @@\n     print(f\"Child Groups Updated: {child_groups_updated}\")\r\n     print(f\"Images Loaded: {images_loaded}\")\r\n     print(f\"Image Nodes Assigned: {images_assigned}\")\r\n     print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n     if errors_encountered > 0:\r\n         print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n     print(\"---------------------------\")\r\n \r\n@@ -731,8 +831,9 @@\n if __name__ == \"__main__\":\r\n     # Ensure we are running within Blender\r\n     try:\r\n         import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n     except ImportError:\r\n         print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n     else:\r\n         process_library(bpy.context)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745232352430,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,9 +1,13 @@\n # Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.2\r\n+# Version: 1.3\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n #              reads metadata.json files, and creates/updates corresponding\r\n #              PBR node groups in the active Blender file.\r\n+# Changes v1.3:\r\n+#   - Added logic to find the highest resolution present for an asset.\r\n+#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n+#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n # Changes v1.2:\r\n #   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n #   - Added fallback logic for reconstructing image paths with different extensions.\r\n #   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n@@ -32,8 +36,9 @@\n \r\n # Labels of specific nodes within the PARENT template\r\n ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n \r\n # Enable/disable the manifest system to track processed assets/maps\r\n # If enabled, requires the blend file to be saved.\r\n ENABLE_MANIFEST = True\r\n@@ -52,8 +57,13 @@\n PREVIEW_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n # Preferred resolution order for preview (lowest first is often faster)\r\n PREVIEW_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n \r\n+# Mapping from resolution string to numerical value for the HighestResolution node\r\n+RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n+# Order to check resolutions to find the highest present (highest value first)\r\n+RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n+\r\n # Map PBR type strings (from metadata) to Blender color spaces\r\n # Add more mappings as needed based on your metadata types\r\n PBR_COLOR_SPACE_MAP = {\r\n     \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n@@ -214,9 +224,10 @@\n \r\n     # 2. Try fallback extensions if primary failed\r\n     # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n     for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        if ext.lower() == primary_format.lower(): # Don't retry the primary format\r\n+        # Ensure ext is treated as string and handle potential None values defensively\r\n+        if not isinstance(ext, str) or not isinstance(primary_format, str) or ext.lower() == primary_format.lower():\r\n             continue\r\n         try:\r\n             fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n                 asset_name=asset_name,\r\n@@ -392,8 +403,9 @@\n     maps_processed = 0\r\n     maps_skipped_manifest = 0\r\n     errors_encountered = 0\r\n     previews_set = 0\r\n+    highest_res_set = 0\r\n     # --- End Counters ---\r\n \r\n     print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n \r\n@@ -450,8 +462,27 @@\n                  # Continue processing, but path reconstruction might fail later\r\n \r\n             print(f\"  Asset Name: {asset_name}\")\r\n \r\n+            # --- Determine Highest Resolution ---\r\n+            highest_resolution_value = 0.0\r\n+            highest_resolution_str = \"Unknown\"\r\n+            all_resolutions_present = set()\r\n+            if processed_resolutions: # Check if the dict exists and is not empty\r\n+                for res_list in processed_resolutions.values():\r\n+                    if isinstance(res_list, list):\r\n+                         all_resolutions_present.update(res_list)\r\n+\r\n+            if all_resolutions_present:\r\n+                for res_str in RESOLUTION_ORDER_DESC:\r\n+                    if res_str in all_resolutions_present:\r\n+                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n+                        highest_resolution_str = res_str\r\n+                        if highest_resolution_value > 0.0:\r\n+                            break # Found the highest valid resolution\r\n+\r\n+            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n+\r\n             # --- Manifest Check (Asset Level - Basic) ---\r\n             if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n                 # Perform a quick check if *any* map needs processing for this asset\r\n                 needs_processing = False\r\n@@ -514,8 +545,19 @@\n                     aspect_node.outputs[0].default_value = correction_factor\r\n                     print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n             # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n \r\n+            # Apply Highest Resolution Value\r\n+            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n+            if hr_nodes:\r\n+                hr_node = hr_nodes[0]\r\n+                if highest_resolution_value > 0.0 and abs(hr_node.outputs[0].default_value - highest_resolution_value) > 0.001:\r\n+                    hr_node.outputs[0].default_value = highest_resolution_value\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str})\")\r\n+                    highest_res_set += 1 # Count successful sets\r\n+            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n             # Apply Stats (using image_stats_1k)\r\n             if image_stats_1k and isinstance(image_stats_1k, dict):\r\n                 for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n                     if map_type_to_stat in image_stats_1k:\r\n@@ -818,8 +860,9 @@\n     print(f\"Images Loaded: {images_loaded}\")\r\n     print(f\"Image Nodes Assigned: {images_assigned}\")\r\n     print(f\"Individual Maps Processed: {maps_processed}\")\r\n     print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\") # Added counter\r\n     if errors_encountered > 0:\r\n         print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n     print(\"---------------------------\")\r\n \r\n"
                },
                {
                    "date": 1745233342179,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,9 +1,12 @@\n # Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.3\r\n+# Version: 1.4\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n #              reads metadata.json files, and creates/updates corresponding\r\n #              PBR node groups in the active Blender file.\r\n+# Changes v1.4:\r\n+#   - Corrected aspect ratio calculation to use actual image dimensions\r\n+#     and the aspect_ratio_change_string, mirroring original script logic.\r\n # Changes v1.3:\r\n #   - Added logic to find the highest resolution present for an asset.\r\n #   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n #     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n@@ -51,13 +54,13 @@\n # Fallback extensions to try if the primary format from metadata is not found\r\n # Order matters - first found will be used.\r\n FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n \r\n-# Map type(s) to use for generating the asset preview (e.g., COL, COL-1)\r\n+# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n # The script will look for these in order and use the first one found.\r\n-PREVIEW_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"]\r\n-# Preferred resolution order for preview (lowest first is often faster)\r\n-PREVIEW_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n \r\n # Mapping from resolution string to numerical value for the HighestResolution node\r\n RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n # Order to check resolutions to find the highest present (highest value first)\r\n@@ -139,58 +142,69 @@\n     # Handle potential numbered variants like COL-1, COL-2\r\n     base_map_type = map_type.split('-')[0]\r\n     return PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)\r\n \r\n-def calculate_factor_from_string(aspect_string):\r\n+def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n     \"\"\"\r\n-    Parses the aspect_ratio_change_string from metadata and returns the\r\n-    appropriate UV X-scaling factor needed to correct distortion.\r\n-    Assumes the string format documented in Asset Processor Tool readme.md:\r\n-    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n-    Returns 1.0 if the string is invalid or \"EVEN\".\r\n+    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n+    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n+    Mirrors the logic from the original POC script.\r\n+    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n     \"\"\"\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+    if image_height <= 0 or image_width <= 0:\r\n+        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n         return 1.0\r\n \r\n-    x_factor = 1.0\r\n-    y_factor = 1.0\r\n+    # Calculate the actual aspect ratio of the image file\r\n+    current_aspect_ratio = image_width / image_height\r\n \r\n-    # Regex to find X and Y scaling parts\r\n-    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        # If scaling was even, the correction factor is just the image's aspect ratio\r\n+        # to make UVs match the image proportions.\r\n+        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n+        return current_aspect_ratio\r\n \r\n+    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n+    match = re.match(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not match:\r\n+        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n+        return current_aspect_ratio # Fallback to the image's own ratio\r\n+\r\n+    axis = match.group(1).upper()\r\n     try:\r\n-        if match_x:\r\n-            amount_x = int(match_x.group(1))\r\n-            if amount_x > 0:\r\n-                x_factor = amount_x / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+        amount = int(match.group(2))\r\n+        if amount <= 0:\r\n+            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+            return current_aspect_ratio\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n \r\n-        if match_y:\r\n-            amount_y = int(match_y.group(1))\r\n-            if amount_y > 0:\r\n-                y_factor = amount_y / 100.0\r\n-            else:\r\n-                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n+    # Apply the non-uniform correction formula based on original script logic\r\n+    scaling_factor_percent = amount / 100.0\r\n+    correction_factor = current_aspect_ratio # Default\r\n \r\n-        # The correction factor for the U (X) coordinate is Y/X\r\n-        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n-        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n-        if x_factor == 0: # Avoid division by zero\r\n-             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n-             return 1.0\r\n+    try:\r\n+        if axis == 'X':\r\n+            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n+            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n+            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n+        elif axis == 'Y':\r\n+            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n+            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n+        # No 'else' needed due to regex structure\r\n \r\n-        correction_factor = y_factor / x_factor\r\n-        return correction_factor\r\n-\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n-        return 1.0\r\n+    except ZeroDivisionError as e:\r\n+        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n     except Exception as e:\r\n-        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n-        return 1.0\r\n+         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+         return current_aspect_ratio\r\n \r\n+    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n+    return correction_factor\r\n+\r\n+\r\n def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format):\r\n     \"\"\"\r\n     Constructs the expected image file path, trying the primary format first,\r\n     then falling back to common extensions if the primary path doesn't exist.\r\n@@ -404,8 +418,9 @@\n     maps_skipped_manifest = 0\r\n     errors_encountered = 0\r\n     previews_set = 0\r\n     highest_res_set = 0\r\n+    aspect_ratio_set = 0\r\n     # --- End Counters ---\r\n \r\n     print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n \r\n@@ -481,8 +496,54 @@\n                             break # Found the highest valid resolution\r\n \r\n             print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n \r\n+            # --- Load Reference Image for Aspect Ratio ---\r\n+            ref_image_path = None\r\n+            ref_image_width = 0\r\n+            ref_image_height = 0\r\n+            ref_image_loaded = False\r\n+            for ref_map_type in REFERENCE_MAP_TYPES:\r\n+                if ref_map_type in processed_resolutions:\r\n+                    available_resolutions = processed_resolutions[ref_map_type]\r\n+                    lowest_res = None\r\n+                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+                        if res_pref in available_resolutions:\r\n+                            lowest_res = res_pref\r\n+                            break\r\n+                    if lowest_res:\r\n+                        ref_map_details = map_details.get(ref_map_type, {})\r\n+                        ref_format = ref_map_details.get(\"output_format\")\r\n+                        if ref_format:\r\n+                            ref_image_path = reconstruct_image_path_with_fallback(\r\n+                                asset_dir_path=asset_dir_path,\r\n+                                asset_name=asset_name,\r\n+                                map_type=ref_map_type,\r\n+                                resolution=lowest_res,\r\n+                                primary_format=ref_format\r\n+                            )\r\n+                            if ref_image_path:\r\n+                                break # Found a suitable reference image path\r\n+\r\n+            if ref_image_path:\r\n+                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n+                try:\r\n+                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n+                    if ref_img:\r\n+                        ref_image_width = ref_img.size[0]\r\n+                        ref_image_height = ref_img.size[1]\r\n+                        ref_image_loaded = True\r\n+                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n+                        # We don't need to keep the image data block loaded after getting dimensions\r\n+                        bpy.data.images.remove(ref_img)\r\n+                    else:\r\n+                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n+                except Exception as e_ref_load:\r\n+                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n+            else:\r\n+                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n+\r\n+\r\n             # --- Manifest Check (Asset Level - Basic) ---\r\n             if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n                 # Perform a quick check if *any* map needs processing for this asset\r\n                 needs_processing = False\r\n@@ -538,13 +599,20 @@\n             # Apply Aspect Ratio Correction\r\n             aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n             if aspect_nodes:\r\n                 aspect_node = aspect_nodes[0]\r\n-                correction_factor = calculate_factor_from_string(aspect_string)\r\n-                # Check if update is needed (avoids unnecessary console spam)\r\n+                correction_factor = 1.0 # Default if ref image fails\r\n+                if ref_image_loaded:\r\n+                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n+                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n+                else:\r\n+                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+\r\n+                # Check if update is needed\r\n                 if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n                     aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f}\")\r\n+                    aspect_ratio_set += 1\r\n             # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n \r\n             # Apply Highest Resolution Value\r\n             hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n@@ -598,46 +666,23 @@\n                     # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n             # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n \r\n             # --- Set Asset Preview (only for new parent groups) ---\r\n+            # Use the reference image path found earlier if available\r\n             if is_new_parent and parent_group.asset_data:\r\n-                preview_path = None\r\n-                # Find the lowest resolution color map path\r\n-                for preview_map_type in PREVIEW_MAP_TYPES:\r\n-                    if preview_map_type in processed_resolutions:\r\n-                        available_resolutions = processed_resolutions[preview_map_type]\r\n-                        # Find the lowest resolution available based on preferred order\r\n-                        lowest_res = None\r\n-                        for res_pref in PREVIEW_RESOLUTION_ORDER:\r\n-                            if res_pref in available_resolutions:\r\n-                                lowest_res = res_pref\r\n-                                break\r\n-                        if lowest_res:\r\n-                            preview_map_details = map_details.get(preview_map_type, {})\r\n-                            preview_format = preview_map_details.get(\"output_format\")\r\n-                            if preview_format:\r\n-                                preview_path = reconstruct_image_path_with_fallback(\r\n-                                    asset_dir_path=asset_dir_path,\r\n-                                    asset_name=asset_name,\r\n-                                    map_type=preview_map_type,\r\n-                                    resolution=lowest_res,\r\n-                                    primary_format=preview_format\r\n-                                )\r\n-                                if preview_path:\r\n-                                    break # Found a suitable preview image path\r\n-                # Load the preview if a path was found\r\n-                if preview_path:\r\n-                    print(f\"    Attempting to set preview from: {Path(preview_path).name}\")\r\n+                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n+                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n                     try:\r\n                         # Ensure the ID (node group) is the active one for the operator context\r\n                         with context.temp_override(id=parent_group):\r\n-                             bpy.ops.ed.lib_id_load_custom_preview(filepath=preview_path)\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n                         print(f\"      Successfully set custom preview.\")\r\n                         previews_set += 1\r\n                     except Exception as e_preview:\r\n                         print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n                         errors_encountered += 1\r\n-                # else: print(f\"    Info: Could not find suitable preview image ({PREVIEW_MAP_TYPES} at {PREVIEW_RESOLUTION_ORDER}) for '{asset_name}'.\")\r\n+                else:\r\n+                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n \r\n \r\n             # --- Child Group Handling ---\r\n             # Iterate through the map types listed in processed_resolutions\r\n@@ -770,9 +815,13 @@\n                         if not img:\r\n                             print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n                             image_load_failed = True\r\n                         else:\r\n-                            images_loaded += 1 # Count successful loads\r\n+                            # Only count as loaded if bpy.data.images.load succeeded\r\n+                            # Check if it's newly loaded or reused\r\n+                            is_newly_loaded = img.filepath == str(image_path) # Simple check, might not be perfect if paths are identical but data differs\r\n+                            if is_newly_loaded: images_loaded += 1\r\n+\r\n                     except RuntimeError as e_runtime_load:\r\n                         # Catch specific Blender runtime errors (e.g., unsupported format)\r\n                         print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n                         image_load_failed = True\r\n@@ -860,9 +909,10 @@\n     print(f\"Images Loaded: {images_loaded}\")\r\n     print(f\"Image Nodes Assigned: {images_assigned}\")\r\n     print(f\"Individual Maps Processed: {maps_processed}\")\r\n     print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\") # Added counter\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n     if errors_encountered > 0:\r\n         print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n     print(\"---------------------------\")\r\n \r\n"
                },
                {
                    "date": 1745234213954,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -43,9 +43,9 @@\n HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n \r\n # Enable/disable the manifest system to track processed assets/maps\r\n # If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True\r\n+ENABLE_MANIFEST = False\r\n \r\n # Assumed filename pattern for processed images.\r\n # {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n # Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n@@ -69,20 +69,19 @@\n # Map PBR type strings (from metadata) to Blender color spaces\r\n # Add more mappings as needed based on your metadata types\r\n PBR_COLOR_SPACE_MAP = {\r\n     \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n     \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n     \"COL-2\": \"sRGB\",\r\n+    \"COL-3\": \"sRGB\",\r\n     \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n     \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n     \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"MASK\": \"Non-Color\", # Opacity/Alpha\r\n     \"SSS\": \"sRGB\",      # Subsurface Color\r\n     \"EMISS\": \"sRGB\",    # Emission Color\r\n+    \"NRMRGH\": \"Non-Color\",\r\n+    \"FUZZ\": \"Non-Color\",\r\n     # Add other types like GLOSS, HEIGHT, etc. if needed\r\n }\r\n DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n \r\n"
                },
                {
                    "date": 1745234305963,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,874 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.4\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+# Changes v1.4:\r\n+#   - Corrected aspect ratio calculation to use actual image dimensions\r\n+#     and the aspect_ratio_change_string, mirroring original script logic.\r\n+# Changes v1.3:\r\n+#   - Added logic to find the highest resolution present for an asset.\r\n+#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n+#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n+# Changes v1.2:\r\n+#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n+#   - Added fallback logic for reconstructing image paths with different extensions.\r\n+#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n+# Changes v1.1:\r\n+#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n+#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+import base64 # For encoding node group names\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = True\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Mapping from resolution string to numerical value for the HighestResolution node\r\n+RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n+# Order to check resolutions to find the highest present (highest value first)\r\n+RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n+    \"COL-2\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+# Reads stats from the 'image_stats_1k' section of metadata.json\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def encode_name_b64(name_str):\r\n+    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n+    try:\r\n+        # Ensure the input is a string\r\n+        name_str = str(name_str)\r\n+        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n+    except Exception as e:\r\n+        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n+    match = re.match(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not match:\r\n+        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n+        return current_aspect_ratio # Fallback to the image's own ratio\r\n+\r\n+    axis = match.group(1).upper()\r\n+    try:\r\n+        amount = int(match.group(2))\r\n+        if amount <= 0:\r\n+            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+            return current_aspect_ratio\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Apply the non-uniform correction formula based on original script logic\r\n+    scaling_factor_percent = amount / 100.0\r\n+    correction_factor = current_aspect_ratio # Default\r\n+\r\n+    try:\r\n+        if axis == 'X':\r\n+            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n+            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n+            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n+        elif axis == 'Y':\r\n+            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n+            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n+        # No 'else' needed due to regex structure\r\n+\r\n+    except ZeroDivisionError as e:\r\n+        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+    except Exception as e:\r\n+         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+         return current_aspect_ratio\r\n+\r\n+    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n+    return correction_factor\r\n+\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format):\r\n+    \"\"\"\r\n+    Constructs the expected image file path, trying the primary format first,\r\n+    then falling back to common extensions if the primary path doesn't exist.\r\n+    Returns the found path as a string, or None if not found.\r\n+    \"\"\"\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution, primary_format]):\r\n+        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+        return None\r\n+\r\n+    # 1. Try the primary format from metadata\r\n+    primary_path_str = None\r\n+    try:\r\n+        filename = IMAGE_FILENAME_PATTERN.format(\r\n+            asset_name=asset_name,\r\n+            map_type=map_type,\r\n+            resolution=resolution,\r\n+            format=primary_format.lower() # Ensure format is lowercase\r\n+        )\r\n+        primary_path = asset_dir_path / filename\r\n+        primary_path_str = str(primary_path)\r\n+        if primary_path.is_file():\r\n+            # print(f\"          Found primary path: {primary_path_str}\") # Verbose\r\n+            return primary_path_str\r\n+        # else: print(f\"          Primary path not found: {primary_path_str}\") # Verbose\r\n+    except KeyError as e:\r\n+        print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+        return None # Cannot proceed without valid pattern\r\n+    except Exception as e:\r\n+        print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+        # Continue to fallback even if primary reconstruction had issues\r\n+\r\n+    # 2. Try fallback extensions if primary failed\r\n+    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+        # Ensure ext is treated as string and handle potential None values defensively\r\n+        if not isinstance(ext, str) or not isinstance(primary_format, str) or ext.lower() == primary_format.lower():\r\n+            continue\r\n+        try:\r\n+            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=ext.lower()\r\n+            )\r\n+            fallback_path = asset_dir_path / fallback_filename\r\n+            if fallback_path.is_file():\r\n+                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n+                return str(fallback_path)\r\n+        except KeyError:\r\n+             # Should not happen if primary format worked, but handle defensively\r\n+             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+             return None\r\n+        except Exception as e_fallback:\r\n+            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+            continue # Try next extension\r\n+\r\n+    print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    return None # Not found after all checks\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # Basic check if asset entry exists. Detailed check happens at map level.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context):\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    previews_set = 0\r\n+    highest_res_set = 0\r\n+    aspect_ratio_set = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n+    # Then scan within each supplier for asset folders containing metadata.json\r\n+    metadata_paths = []\r\n+    for supplier_dir in root_path.iterdir():\r\n+        if supplier_dir.is_dir():\r\n+            # Now look for asset folders inside the supplier directory\r\n+            for asset_dir in supplier_dir.iterdir():\r\n+                if asset_dir.is_dir():\r\n+                    metadata_file = asset_dir / 'metadata.json'\r\n+                    if metadata_file.is_file():\r\n+                        metadata_paths.append(metadata_file)\r\n+\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            # Get map info from the correct keys\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\") # Dict: {map_type: [res1, res2]}\r\n+            map_details = metadata.get(\"map_details\") # Dict: {map_type: {details}}\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            # Validate essential data\r\n+            if not asset_name:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+            if not processed_resolutions or not isinstance(processed_resolutions, dict):\r\n+                 print(f\"    !!! ERROR: Metadata file is missing or has invalid 'processed_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+                 errors_encountered += 1\r\n+                 continue\r\n+            if not map_details or not isinstance(map_details, dict):\r\n+                 print(f\"    !!! WARNING: Metadata file is missing or has invalid 'map_details'. Path reconstruction might fail for asset '{asset_name}'.\")\r\n+                 # Continue processing, but path reconstruction might fail later\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Determine Highest Resolution ---\r\n+            highest_resolution_value = 0.0\r\n+            highest_resolution_str = \"Unknown\"\r\n+            all_resolutions_present = set()\r\n+            if processed_resolutions: # Check if the dict exists and is not empty\r\n+                for res_list in processed_resolutions.values():\r\n+                    if isinstance(res_list, list):\r\n+                         all_resolutions_present.update(res_list)\r\n+\r\n+            if all_resolutions_present:\r\n+                for res_str in RESOLUTION_ORDER_DESC:\r\n+                    if res_str in all_resolutions_present:\r\n+                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n+                        highest_resolution_str = res_str\r\n+                        if highest_resolution_value > 0.0:\r\n+                            break # Found the highest valid resolution\r\n+\r\n+            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n+\r\n+            # --- Load Reference Image for Aspect Ratio ---\r\n+            ref_image_path = None\r\n+            ref_image_width = 0\r\n+            ref_image_height = 0\r\n+            ref_image_loaded = False\r\n+            for ref_map_type in REFERENCE_MAP_TYPES:\r\n+                if ref_map_type in processed_resolutions:\r\n+                    available_resolutions = processed_resolutions[ref_map_type]\r\n+                    lowest_res = None\r\n+                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+                        if res_pref in available_resolutions:\r\n+                            lowest_res = res_pref\r\n+                            break\r\n+                    if lowest_res:\r\n+                        ref_map_details = map_details.get(ref_map_type, {})\r\n+                        ref_format = ref_map_details.get(\"output_format\")\r\n+                        if ref_format:\r\n+                            ref_image_path = reconstruct_image_path_with_fallback(\r\n+                                asset_dir_path=asset_dir_path,\r\n+                                asset_name=asset_name,\r\n+                                map_type=ref_map_type,\r\n+                                resolution=lowest_res,\r\n+                                primary_format=ref_format\r\n+                            )\r\n+                            if ref_image_path:\r\n+                                break # Found a suitable reference image path\r\n+\r\n+            if ref_image_path:\r\n+                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n+                try:\r\n+                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n+                    if ref_img:\r\n+                        ref_image_width = ref_img.size[0]\r\n+                        ref_image_height = ref_img.size[1]\r\n+                        ref_image_loaded = True\r\n+                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n+                        # We don't need to keep the image data block loaded after getting dimensions\r\n+                        bpy.data.images.remove(ref_img)\r\n+                    else:\r\n+                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n+                except Exception as e_ref_load:\r\n+                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n+            else:\r\n+                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n+\r\n+\r\n+            # --- Manifest Check (Asset Level - Basic) ---\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Perform a quick check if *any* map needs processing for this asset\r\n+                needs_processing = False\r\n+                for map_type, resolutions in processed_resolutions.items():\r\n+                    for resolution in resolutions:\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            needs_processing = True\r\n+                            break\r\n+                    if needs_processing:\r\n+                        break\r\n+                if not needs_processing:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = 1.0 # Default if ref image fails\r\n+                if ref_image_loaded:\r\n+                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n+                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n+                else:\r\n+                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+\r\n+                # Check if update is needed\r\n+                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f}\")\r\n+                    aspect_ratio_set += 1\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Highest Resolution Value\r\n+            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n+            if hr_nodes:\r\n+                hr_node = hr_nodes[0]\r\n+                if highest_resolution_value > 0.0 and abs(hr_node.outputs[0].default_value - highest_resolution_value) > 0.001:\r\n+                    hr_node.outputs[0].default_value = highest_resolution_value\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str})\")\r\n+                    highest_res_set += 1 # Count successful sets\r\n+            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n+            # Apply Stats (using image_stats_1k)\r\n+            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    if map_type_to_stat in image_stats_1k:\r\n+                        # Find the stats node in the parent group\r\n+                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                        if stats_nodes:\r\n+                            stats_node = stats_nodes[0]\r\n+                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n+\r\n+                            if stats and isinstance(stats, dict):\r\n+                                # Handle potential list format for RGB stats (use first value) or direct float\r\n+                                def get_stat_value(stat_val):\r\n+                                    if isinstance(stat_val, list):\r\n+                                        return stat_val[0] if stat_val else None\r\n+                                    return stat_val\r\n+\r\n+                                min_val = get_stat_value(stats.get(\"min\"))\r\n+                                max_val = get_stat_value(stats.get(\"max\"))\r\n+                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n+\r\n+                                updated_stat = False\r\n+                                # Check inputs exist before assigning\r\n+                                if stats_node.inputs.get(\"X\") and min_val is not None and abs(stats_node.inputs[\"X\"].default_value - min_val) > 0.0001:\r\n+                                    stats_node.inputs[\"X\"].default_value = min_val\r\n+                                    updated_stat = True\r\n+                                if stats_node.inputs.get(\"Y\") and max_val is not None and abs(stats_node.inputs[\"Y\"].default_value - max_val) > 0.0001:\r\n+                                    stats_node.inputs[\"Y\"].default_value = max_val\r\n+                                    updated_stat = True\r\n+                                if stats_node.inputs.get(\"Z\") and mean_val is not None and abs(stats_node.inputs[\"Z\"].default_value - mean_val) > 0.0001:\r\n+                                    stats_node.inputs[\"Z\"].default_value = mean_val\r\n+                                    updated_stat = True\r\n+\r\n+                                if updated_stat:\r\n+                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n+                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n+            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n+\r\n+            # --- Set Asset Preview (only for new parent groups) ---\r\n+            # Use the reference image path found earlier if available\r\n+            if is_new_parent and parent_group.asset_data:\r\n+                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n+                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n+                    try:\r\n+                        # Ensure the ID (node group) is the active one for the operator context\r\n+                        with context.temp_override(id=parent_group):\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+                        print(f\"      Successfully set custom preview.\")\r\n+                        previews_set += 1\r\n+                    except Exception as e_preview:\r\n+                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n+                        errors_encountered += 1\r\n+                else:\r\n+                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            # Iterate through the map types listed in processed_resolutions\r\n+            for map_type, resolutions in processed_resolutions.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Get details for this map type (needed for format)\r\n+                current_map_details = map_details.get(map_type, {})\r\n+                output_format = current_map_details.get(\"output_format\")\r\n+                if not output_format:\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for '{map_type}'. Cannot reconstruct path. Skipping map type.\")\r\n+                    continue\r\n+\r\n+                # Find placeholder node in parent\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+\r\n+                # Determine child group name (LOGICAL and ENCODED)\r\n+                logical_child_name = f\"{asset_name}_{map_type}\"\r\n+                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n+\r\n+                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name_b64 # Set encoded name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                if not isinstance(resolutions, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution in resolutions:\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Reconstruct the image path using fallback logic\r\n+                    image_path_str = reconstruct_image_path_with_fallback(\r\n+                        asset_dir_path=asset_dir_path,\r\n+                        asset_name=asset_name,\r\n+                        map_type=map_type,\r\n+                        resolution=resolution,\r\n+                        primary_format=output_format # Pass the format from metadata\r\n+                    )\r\n+\r\n+                    if not image_path_str:\r\n+                        # Error already printed by reconstruct function\r\n+                        errors_encountered += 1\r\n+                        continue # Skip this resolution if path not found\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str) # Path object created from already found path string\r\n+                        # Use check_existing=True to reuse existing datablocks if path matches\r\n+                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                        if not img:\r\n+                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Only count as loaded if bpy.data.images.load succeeded\r\n+                            # Check if it's newly loaded or reused\r\n+                            is_newly_loaded = img.filepath == str(image_path) # Simple check, might not be perfect if paths are identical but data differs\r\n+                            if is_newly_loaded: images_loaded += 1\r\n+\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+\r\n+                    else:\r\n+                        # Increment error count if loading failed\r\n+                        if image_load_failed: errors_encountered += 1\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        process_library(bpy.context)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745234414799,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,12 +1,15 @@\n # Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.4\r\n+# Version: 1.5\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n #              reads metadata.json files, and creates/updates corresponding\r\n #              PBR node groups in the active Blender file.\r\n-# Changes v1.4:\r\n-#   - Corrected aspect ratio calculation to use actual image dimensions\r\n-#     and the aspect_ratio_change_string, mirroring original script logic.\r\n+# Changes v1.5:\r\n+#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n+#     to use actual image dimensions from a loaded reference image and the\r\n+#     `aspect_ratio_change_string`, mirroring original script logic for\r\n+#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n+#   - Added logic in main loop to load reference image for dimensions.\r\n # Changes v1.3:\r\n #   - Added logic to find the highest resolution present for an asset.\r\n #   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n #     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n@@ -43,9 +46,9 @@\n HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n \r\n # Enable/disable the manifest system to track processed assets/maps\r\n # If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True\r\n+ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n \r\n # Assumed filename pattern for processed images.\r\n # {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n # Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n@@ -72,8 +75,9 @@\n     \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n     \"COL\": \"sRGB\",\r\n     \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n     \"COL-2\": \"sRGB\",\r\n+    \"COL-3\": \"sRGB\",\r\n     \"DISP\": \"Non-Color\",\r\n     \"NRM\": \"Non-Color\",\r\n     \"REFL\": \"Non-Color\", # Reflection/Specular\r\n     \"ROUGH\": \"Non-Color\",\r\n@@ -81,880 +85,9 @@\n     \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n     \"TRN\": \"Non-Color\", # Transmission\r\n     \"SSS\": \"sRGB\",      # Subsurface Color\r\n     \"EMISS\": \"sRGB\",    # Emission Color\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-# Reads stats from the 'image_stats_1k' section of metadata.json\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def encode_name_b64(name_str):\r\n-    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n-    try:\r\n-        # Ensure the input is a string\r\n-        name_str = str(name_str)\r\n-        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n-    except Exception as e:\r\n-        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n-    match = re.match(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    if not match:\r\n-        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n-        return current_aspect_ratio # Fallback to the image's own ratio\r\n-\r\n-    axis = match.group(1).upper()\r\n-    try:\r\n-        amount = int(match.group(2))\r\n-        if amount <= 0:\r\n-            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-            return current_aspect_ratio\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Apply the non-uniform correction formula based on original script logic\r\n-    scaling_factor_percent = amount / 100.0\r\n-    correction_factor = current_aspect_ratio # Default\r\n-\r\n-    try:\r\n-        if axis == 'X':\r\n-            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n-            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n-            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n-        elif axis == 'Y':\r\n-            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n-            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n-        # No 'else' needed due to regex structure\r\n-\r\n-    except ZeroDivisionError as e:\r\n-        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-    except Exception as e:\r\n-         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-         return current_aspect_ratio\r\n-\r\n-    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n-    return correction_factor\r\n-\r\n-\r\n-def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format):\r\n-    \"\"\"\r\n-    Constructs the expected image file path, trying the primary format first,\r\n-    then falling back to common extensions if the primary path doesn't exist.\r\n-    Returns the found path as a string, or None if not found.\r\n-    \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution, primary_format]):\r\n-        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n-        return None\r\n-\r\n-    # 1. Try the primary format from metadata\r\n-    primary_path_str = None\r\n-    try:\r\n-        filename = IMAGE_FILENAME_PATTERN.format(\r\n-            asset_name=asset_name,\r\n-            map_type=map_type,\r\n-            resolution=resolution,\r\n-            format=primary_format.lower() # Ensure format is lowercase\r\n-        )\r\n-        primary_path = asset_dir_path / filename\r\n-        primary_path_str = str(primary_path)\r\n-        if primary_path.is_file():\r\n-            # print(f\"          Found primary path: {primary_path_str}\") # Verbose\r\n-            return primary_path_str\r\n-        # else: print(f\"          Primary path not found: {primary_path_str}\") # Verbose\r\n-    except KeyError as e:\r\n-        print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-        return None # Cannot proceed without valid pattern\r\n-    except Exception as e:\r\n-        print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n-        # Continue to fallback even if primary reconstruction had issues\r\n-\r\n-    # 2. Try fallback extensions if primary failed\r\n-    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n-    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        # Ensure ext is treated as string and handle potential None values defensively\r\n-        if not isinstance(ext, str) or not isinstance(primary_format, str) or ext.lower() == primary_format.lower():\r\n-            continue\r\n-        try:\r\n-            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=ext.lower()\r\n-            )\r\n-            fallback_path = asset_dir_path / fallback_filename\r\n-            if fallback_path.is_file():\r\n-                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n-                return str(fallback_path)\r\n-        except KeyError:\r\n-             # Should not happen if primary format worked, but handle defensively\r\n-             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n-             return None\r\n-        except Exception as e_fallback:\r\n-            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n-            continue # Try next extension\r\n-\r\n-    print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    return None # Not found after all checks\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # Basic check if asset entry exists. Detailed check happens at map level.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context):\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    previews_set = 0\r\n-    highest_res_set = 0\r\n-    aspect_ratio_set = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n-    # Then scan within each supplier for asset folders containing metadata.json\r\n-    metadata_paths = []\r\n-    for supplier_dir in root_path.iterdir():\r\n-        if supplier_dir.is_dir():\r\n-            # Now look for asset folders inside the supplier directory\r\n-            for asset_dir in supplier_dir.iterdir():\r\n-                if asset_dir.is_dir():\r\n-                    metadata_file = asset_dir / 'metadata.json'\r\n-                    if metadata_file.is_file():\r\n-                        metadata_paths.append(metadata_file)\r\n-\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            # Get map info from the correct keys\r\n-            processed_resolutions = metadata.get(\"processed_map_resolutions\") # Dict: {map_type: [res1, res2]}\r\n-            map_details = metadata.get(\"map_details\") # Dict: {map_type: {details}}\r\n-            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            # Validate essential data\r\n-            if not asset_name:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-            if not processed_resolutions or not isinstance(processed_resolutions, dict):\r\n-                 print(f\"    !!! ERROR: Metadata file is missing or has invalid 'processed_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n-                 errors_encountered += 1\r\n-                 continue\r\n-            if not map_details or not isinstance(map_details, dict):\r\n-                 print(f\"    !!! WARNING: Metadata file is missing or has invalid 'map_details'. Path reconstruction might fail for asset '{asset_name}'.\")\r\n-                 # Continue processing, but path reconstruction might fail later\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Determine Highest Resolution ---\r\n-            highest_resolution_value = 0.0\r\n-            highest_resolution_str = \"Unknown\"\r\n-            all_resolutions_present = set()\r\n-            if processed_resolutions: # Check if the dict exists and is not empty\r\n-                for res_list in processed_resolutions.values():\r\n-                    if isinstance(res_list, list):\r\n-                         all_resolutions_present.update(res_list)\r\n-\r\n-            if all_resolutions_present:\r\n-                for res_str in RESOLUTION_ORDER_DESC:\r\n-                    if res_str in all_resolutions_present:\r\n-                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n-                        highest_resolution_str = res_str\r\n-                        if highest_resolution_value > 0.0:\r\n-                            break # Found the highest valid resolution\r\n-\r\n-            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n-\r\n-            # --- Load Reference Image for Aspect Ratio ---\r\n-            ref_image_path = None\r\n-            ref_image_width = 0\r\n-            ref_image_height = 0\r\n-            ref_image_loaded = False\r\n-            for ref_map_type in REFERENCE_MAP_TYPES:\r\n-                if ref_map_type in processed_resolutions:\r\n-                    available_resolutions = processed_resolutions[ref_map_type]\r\n-                    lowest_res = None\r\n-                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n-                        if res_pref in available_resolutions:\r\n-                            lowest_res = res_pref\r\n-                            break\r\n-                    if lowest_res:\r\n-                        ref_map_details = map_details.get(ref_map_type, {})\r\n-                        ref_format = ref_map_details.get(\"output_format\")\r\n-                        if ref_format:\r\n-                            ref_image_path = reconstruct_image_path_with_fallback(\r\n-                                asset_dir_path=asset_dir_path,\r\n-                                asset_name=asset_name,\r\n-                                map_type=ref_map_type,\r\n-                                resolution=lowest_res,\r\n-                                primary_format=ref_format\r\n-                            )\r\n-                            if ref_image_path:\r\n-                                break # Found a suitable reference image path\r\n-\r\n-            if ref_image_path:\r\n-                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n-                try:\r\n-                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n-                    if ref_img:\r\n-                        ref_image_width = ref_img.size[0]\r\n-                        ref_image_height = ref_img.size[1]\r\n-                        ref_image_loaded = True\r\n-                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n-                        # We don't need to keep the image data block loaded after getting dimensions\r\n-                        bpy.data.images.remove(ref_img)\r\n-                    else:\r\n-                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n-                except Exception as e_ref_load:\r\n-                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n-            else:\r\n-                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n-\r\n-\r\n-            # --- Manifest Check (Asset Level - Basic) ---\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Perform a quick check if *any* map needs processing for this asset\r\n-                needs_processing = False\r\n-                for map_type, resolutions in processed_resolutions.items():\r\n-                    for resolution in resolutions:\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            needs_processing = True\r\n-                            break\r\n-                    if needs_processing:\r\n-                        break\r\n-                if not needs_processing:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = 1.0 # Default if ref image fails\r\n-                if ref_image_loaded:\r\n-                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n-                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n-                else:\r\n-                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n-\r\n-                # Check if update is needed\r\n-                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f}\")\r\n-                    aspect_ratio_set += 1\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Highest Resolution Value\r\n-            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n-            if hr_nodes:\r\n-                hr_node = hr_nodes[0]\r\n-                if highest_resolution_value > 0.0 and abs(hr_node.outputs[0].default_value - highest_resolution_value) > 0.001:\r\n-                    hr_node.outputs[0].default_value = highest_resolution_value\r\n-                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str})\")\r\n-                    highest_res_set += 1 # Count successful sets\r\n-            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-\r\n-            # Apply Stats (using image_stats_1k)\r\n-            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n-                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                    if map_type_to_stat in image_stats_1k:\r\n-                        # Find the stats node in the parent group\r\n-                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                        if stats_nodes:\r\n-                            stats_node = stats_nodes[0]\r\n-                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n-\r\n-                            if stats and isinstance(stats, dict):\r\n-                                # Handle potential list format for RGB stats (use first value) or direct float\r\n-                                def get_stat_value(stat_val):\r\n-                                    if isinstance(stat_val, list):\r\n-                                        return stat_val[0] if stat_val else None\r\n-                                    return stat_val\r\n-\r\n-                                min_val = get_stat_value(stats.get(\"min\"))\r\n-                                max_val = get_stat_value(stats.get(\"max\"))\r\n-                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n-\r\n-                                updated_stat = False\r\n-                                # Check inputs exist before assigning\r\n-                                if stats_node.inputs.get(\"X\") and min_val is not None and abs(stats_node.inputs[\"X\"].default_value - min_val) > 0.0001:\r\n-                                    stats_node.inputs[\"X\"].default_value = min_val\r\n-                                    updated_stat = True\r\n-                                if stats_node.inputs.get(\"Y\") and max_val is not None and abs(stats_node.inputs[\"Y\"].default_value - max_val) > 0.0001:\r\n-                                    stats_node.inputs[\"Y\"].default_value = max_val\r\n-                                    updated_stat = True\r\n-                                if stats_node.inputs.get(\"Z\") and mean_val is not None and abs(stats_node.inputs[\"Z\"].default_value - mean_val) > 0.0001:\r\n-                                    stats_node.inputs[\"Z\"].default_value = mean_val\r\n-                                    updated_stat = True\r\n-\r\n-                                if updated_stat:\r\n-                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n-                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n-            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n-\r\n-            # --- Set Asset Preview (only for new parent groups) ---\r\n-            # Use the reference image path found earlier if available\r\n-            if is_new_parent and parent_group.asset_data:\r\n-                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n-                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n-                    try:\r\n-                        # Ensure the ID (node group) is the active one for the operator context\r\n-                        with context.temp_override(id=parent_group):\r\n-                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n-                        print(f\"      Successfully set custom preview.\")\r\n-                        previews_set += 1\r\n-                    except Exception as e_preview:\r\n-                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n-                        errors_encountered += 1\r\n-                else:\r\n-                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            # Iterate through the map types listed in processed_resolutions\r\n-            for map_type, resolutions in processed_resolutions.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Get details for this map type (needed for format)\r\n-                current_map_details = map_details.get(map_type, {})\r\n-                output_format = current_map_details.get(\"output_format\")\r\n-                if not output_format:\r\n-                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for '{map_type}'. Cannot reconstruct path. Skipping map type.\")\r\n-                    continue\r\n-\r\n-                # Find placeholder node in parent\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-\r\n-                # Determine child group name (LOGICAL and ENCODED)\r\n-                logical_child_name = f\"{asset_name}_{map_type}\"\r\n-                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n-\r\n-                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name_b64 # Set encoded name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                if not isinstance(resolutions, list):\r\n-                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution in resolutions:\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Reconstruct the image path using fallback logic\r\n-                    image_path_str = reconstruct_image_path_with_fallback(\r\n-                        asset_dir_path=asset_dir_path,\r\n-                        asset_name=asset_name,\r\n-                        map_type=map_type,\r\n-                        resolution=resolution,\r\n-                        primary_format=output_format # Pass the format from metadata\r\n-                    )\r\n-\r\n-                    if not image_path_str:\r\n-                        # Error already printed by reconstruct function\r\n-                        errors_encountered += 1\r\n-                        continue # Skip this resolution if path not found\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str) # Path object created from already found path string\r\n-                        # Use check_existing=True to reuse existing datablocks if path matches\r\n-                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                        if not img:\r\n-                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Only count as loaded if bpy.data.images.load succeeded\r\n-                            # Check if it's newly loaded or reused\r\n-                            is_newly_loaded = img.filepath == str(image_path) # Simple check, might not be perfect if paths are identical but data differs\r\n-                            if is_newly_loaded: images_loaded += 1\r\n-\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-\r\n-                    else:\r\n-                        # Increment error count if loading failed\r\n-                        if image_load_failed: errors_encountered += 1\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n-    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-        import base64 # Ensure base64 is imported here too if needed globally\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        process_library(bpy.context)\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.4\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-# Changes v1.4:\r\n-#   - Corrected aspect ratio calculation to use actual image dimensions\r\n-#     and the aspect_ratio_change_string, mirroring original script logic.\r\n-# Changes v1.3:\r\n-#   - Added logic to find the highest resolution present for an asset.\r\n-#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n-#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n-# Changes v1.2:\r\n-#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n-#   - Added fallback logic for reconstructing image paths with different extensions.\r\n-#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n-# Changes v1.1:\r\n-#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n-#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-import base64 # For encoding node group names\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = False\r\n-\r\n-# Assumed filename pattern for processed images.\r\n-# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n-# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n-IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n-\r\n-# Fallback extensions to try if the primary format from metadata is not found\r\n-# Order matters - first found will be used.\r\n-FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n-\r\n-# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n-# The script will look for these in order and use the first one found.\r\n-REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n-# Preferred resolution order for reference image (lowest first is often faster)\r\n-REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n-\r\n-# Mapping from resolution string to numerical value for the HighestResolution node\r\n-RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n-# Order to check resolutions to find the highest present (highest value first)\r\n-RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n-    \"COL-2\": \"sRGB\",\r\n-    \"COL-3\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"MASK\": \"Non-Color\", # Opacity/Alpha\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    \"NRMRGH\": \"Non-Color\",\r\n+    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n     \"FUZZ\": \"Non-Color\",\r\n     # Add other types like GLOSS, HEIGHT, etc. if needed\r\n }\r\n DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n@@ -1013,9 +146,10 @@\n def get_color_space(map_type):\r\n     \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n     # Handle potential numbered variants like COL-1, COL-2\r\n     base_map_type = map_type.split('-')[0]\r\n-    return PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n+               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n \r\n def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n     \"\"\"\r\n     Calculates the UV X-axis scaling factor needed to correct distortion,\r\n@@ -1036,9 +170,10 @@\n         # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n         return current_aspect_ratio\r\n \r\n     # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n-    match = re.match(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n+    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n     if not match:\r\n         print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n         return current_aspect_ratio # Fallback to the image's own ratio\r\n \r\n@@ -1063,9 +198,9 @@\n             correction_factor = current_aspect_ratio / scaling_factor_percent\r\n         elif axis == 'Y':\r\n             # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n             correction_factor = current_aspect_ratio * scaling_factor_percent\r\n-        # No 'else' needed due to regex structure\r\n+        # No 'else' needed as regex ensures X or Y\r\n \r\n     except ZeroDivisionError as e:\r\n         print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n         return current_aspect_ratio\r\n@@ -1076,45 +211,47 @@\n     # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n     return correction_factor\r\n \r\n \r\n-def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format):\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n     \"\"\"\r\n-    Constructs the expected image file path, trying the primary format first,\r\n-    then falling back to common extensions if the primary path doesn't exist.\r\n+    Constructs the expected image file path.\r\n+    If primary_format is provided, tries that first.\r\n+    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n     Returns the found path as a string, or None if not found.\r\n     \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution, primary_format]):\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n         print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n         return None\r\n \r\n-    # 1. Try the primary format from metadata\r\n-    primary_path_str = None\r\n-    try:\r\n-        filename = IMAGE_FILENAME_PATTERN.format(\r\n-            asset_name=asset_name,\r\n-            map_type=map_type,\r\n-            resolution=resolution,\r\n-            format=primary_format.lower() # Ensure format is lowercase\r\n-        )\r\n-        primary_path = asset_dir_path / filename\r\n-        primary_path_str = str(primary_path)\r\n-        if primary_path.is_file():\r\n-            # print(f\"          Found primary path: {primary_path_str}\") # Verbose\r\n-            return primary_path_str\r\n-        # else: print(f\"          Primary path not found: {primary_path_str}\") # Verbose\r\n-    except KeyError as e:\r\n-        print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-        return None # Cannot proceed without valid pattern\r\n-    except Exception as e:\r\n-        print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n-        # Continue to fallback even if primary reconstruction had issues\r\n+    found_path = None\r\n \r\n-    # 2. Try fallback extensions if primary failed\r\n+    # 1. Try the primary format if provided\r\n+    if primary_format:\r\n+        try:\r\n+            filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=primary_format.lower() # Ensure format is lowercase\r\n+            )\r\n+            primary_path = asset_dir_path / filename\r\n+            if primary_path.is_file():\r\n+                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n+                return str(primary_path)\r\n+            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n+        except KeyError as e:\r\n+            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+            return None # Cannot proceed without valid pattern\r\n+        except Exception as e:\r\n+            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+            # Continue to fallback\r\n+\r\n+    # 2. Try fallback extensions\r\n     # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n     for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        # Ensure ext is treated as string and handle potential None values defensively\r\n-        if not isinstance(ext, str) or not isinstance(primary_format, str) or ext.lower() == primary_format.lower():\r\n+        # Skip if we already tried this extension as primary (and it failed)\r\n+        if primary_format and ext.lower() == primary_format.lower():\r\n             continue\r\n         try:\r\n             fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n                 asset_name=asset_name,\r\n@@ -1124,18 +261,22 @@\n             )\r\n             fallback_path = asset_dir_path / fallback_filename\r\n             if fallback_path.is_file():\r\n                 print(f\"          Found fallback path: {str(fallback_path)}\")\r\n-                return str(fallback_path)\r\n+                return str(fallback_path) # Found it!\r\n         except KeyError:\r\n              # Should not happen if primary format worked, but handle defensively\r\n              print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n              return None\r\n         except Exception as e_fallback:\r\n             print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n             continue # Try next extension\r\n \r\n-    print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    # If we get here, neither primary nor fallbacks worked\r\n+    if primary_format:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    else:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n     return None # Not found after all checks\r\n \r\n \r\n # --- Manifest Functions ---\r\n@@ -1330,34 +471,36 @@\n             asset_name = metadata.get(\"asset_name\")\r\n             supplier_name = metadata.get(\"supplier_name\")\r\n             archetype = metadata.get(\"archetype\")\r\n             # Get map info from the correct keys\r\n-            processed_resolutions = metadata.get(\"processed_map_resolutions\") # Dict: {map_type: [res1, res2]}\r\n-            map_details = metadata.get(\"map_details\") # Dict: {map_type: {details}}\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n+            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n+            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n             image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n             aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n \r\n+            # Combine processed and merged maps for iteration\r\n+            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n             # Validate essential data\r\n             if not asset_name:\r\n                 print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n                 errors_encountered += 1\r\n                 continue\r\n-            if not processed_resolutions or not isinstance(processed_resolutions, dict):\r\n-                 print(f\"    !!! ERROR: Metadata file is missing or has invalid 'processed_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+            if not all_map_resolutions:\r\n+                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n                  errors_encountered += 1\r\n                  continue\r\n-            if not map_details or not isinstance(map_details, dict):\r\n-                 print(f\"    !!! WARNING: Metadata file is missing or has invalid 'map_details'. Path reconstruction might fail for asset '{asset_name}'.\")\r\n-                 # Continue processing, but path reconstruction might fail later\r\n+            # map_details check remains a warning as merged maps won't be in it\r\n \r\n             print(f\"  Asset Name: {asset_name}\")\r\n \r\n             # --- Determine Highest Resolution ---\r\n             highest_resolution_value = 0.0\r\n             highest_resolution_str = \"Unknown\"\r\n             all_resolutions_present = set()\r\n-            if processed_resolutions: # Check if the dict exists and is not empty\r\n-                for res_list in processed_resolutions.values():\r\n+            if all_map_resolutions: # Check combined dict\r\n+                for res_list in all_map_resolutions.values():\r\n                     if isinstance(res_list, list):\r\n                          all_resolutions_present.update(res_list)\r\n \r\n             if all_resolutions_present:\r\n@@ -1374,40 +517,42 @@\n             ref_image_path = None\r\n             ref_image_width = 0\r\n             ref_image_height = 0\r\n             ref_image_loaded = False\r\n+            # Use combined resolutions dict to find reference map\r\n             for ref_map_type in REFERENCE_MAP_TYPES:\r\n-                if ref_map_type in processed_resolutions:\r\n-                    available_resolutions = processed_resolutions[ref_map_type]\r\n+                if ref_map_type in all_map_resolutions:\r\n+                    available_resolutions = all_map_resolutions[ref_map_type]\r\n                     lowest_res = None\r\n                     for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n                         if res_pref in available_resolutions:\r\n                             lowest_res = res_pref\r\n                             break\r\n                     if lowest_res:\r\n+                        # Get format from map_details if available, otherwise None\r\n                         ref_map_details = map_details.get(ref_map_type, {})\r\n                         ref_format = ref_map_details.get(\"output_format\")\r\n-                        if ref_format:\r\n-                            ref_image_path = reconstruct_image_path_with_fallback(\r\n-                                asset_dir_path=asset_dir_path,\r\n-                                asset_name=asset_name,\r\n-                                map_type=ref_map_type,\r\n-                                resolution=lowest_res,\r\n-                                primary_format=ref_format\r\n-                            )\r\n-                            if ref_image_path:\r\n-                                break # Found a suitable reference image path\r\n+                        ref_image_path = reconstruct_image_path_with_fallback(\r\n+                            asset_dir_path=asset_dir_path,\r\n+                            asset_name=asset_name,\r\n+                            map_type=ref_map_type,\r\n+                            resolution=lowest_res,\r\n+                            primary_format=ref_format # Pass None if not in map_details\r\n+                        )\r\n+                        if ref_image_path:\r\n+                            break # Found a suitable reference image path\r\n \r\n             if ref_image_path:\r\n                 print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n                 try:\r\n+                    # Load image temporarily\r\n                     ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n                     if ref_img:\r\n                         ref_image_width = ref_img.size[0]\r\n                         ref_image_height = ref_img.size[1]\r\n                         ref_image_loaded = True\r\n                         print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n-                        # We don't need to keep the image data block loaded after getting dimensions\r\n+                        # Remove the temporary image datablock to save memory\r\n                         bpy.data.images.remove(ref_img)\r\n                     else:\r\n                         print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n                 except Exception as e_ref_load:\r\n@@ -1419,9 +564,9 @@\n             # --- Manifest Check (Asset Level - Basic) ---\r\n             if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n                 # Perform a quick check if *any* map needs processing for this asset\r\n                 needs_processing = False\r\n-                for map_type, resolutions in processed_resolutions.items():\r\n+                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n                     for resolution in resolutions:\r\n                         if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n                             needs_processing = True\r\n                             break\r\n@@ -1480,21 +625,23 @@\n                 else:\r\n                     print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n \r\n                 # Check if update is needed\r\n-                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n+                current_val = aspect_node.outputs[0].default_value\r\n+                if abs(current_val - correction_factor) > 0.0001:\r\n                     aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f}\")\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n                     aspect_ratio_set += 1\r\n             # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n \r\n             # Apply Highest Resolution Value\r\n             hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n             if hr_nodes:\r\n                 hr_node = hr_nodes[0]\r\n-                if highest_resolution_value > 0.0 and abs(hr_node.outputs[0].default_value - highest_resolution_value) > 0.001:\r\n+                current_hr_val = hr_node.outputs[0].default_value\r\n+                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n                     hr_node.outputs[0].default_value = highest_resolution_value\r\n-                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str})\")\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n                     highest_res_set += 1 # Count successful sets\r\n             # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n \r\n \r\n@@ -1521,16 +668,20 @@\n                                 mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n \r\n                                 updated_stat = False\r\n                                 # Check inputs exist before assigning\r\n-                                if stats_node.inputs.get(\"X\") and min_val is not None and abs(stats_node.inputs[\"X\"].default_value - min_val) > 0.0001:\r\n-                                    stats_node.inputs[\"X\"].default_value = min_val\r\n+                                input_x = stats_node.inputs.get(\"X\")\r\n+                                input_y = stats_node.inputs.get(\"Y\")\r\n+                                input_z = stats_node.inputs.get(\"Z\")\r\n+\r\n+                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n+                                    input_x.default_value = min_val\r\n                                     updated_stat = True\r\n-                                if stats_node.inputs.get(\"Y\") and max_val is not None and abs(stats_node.inputs[\"Y\"].default_value - max_val) > 0.0001:\r\n-                                    stats_node.inputs[\"Y\"].default_value = max_val\r\n+                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n+                                    input_y.default_value = max_val\r\n                                     updated_stat = True\r\n-                                if stats_node.inputs.get(\"Z\") and mean_val is not None and abs(stats_node.inputs[\"Z\"].default_value - mean_val) > 0.0001:\r\n-                                    stats_node.inputs[\"Z\"].default_value = mean_val\r\n+                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n+                                    input_z.default_value = mean_val\r\n                                     updated_stat = True\r\n \r\n                                 if updated_stat:\r\n                                     print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n@@ -1557,19 +708,26 @@\n                      print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n \r\n \r\n             # --- Child Group Handling ---\r\n-            # Iterate through the map types listed in processed_resolutions\r\n-            for map_type, resolutions in processed_resolutions.items():\r\n+            # Iterate through the COMBINED map types\r\n+            for map_type, resolutions in all_map_resolutions.items():\r\n                 print(f\"    Processing Map Type: {map_type}\")\r\n \r\n-                # Get details for this map type (needed for format)\r\n+                # Determine if this is a merged map (not in map_details)\r\n+                is_merged_map = map_type not in map_details\r\n+\r\n+                # Get details for this map type if available\r\n                 current_map_details = map_details.get(map_type, {})\r\n+                # For merged maps, primary_format will be None\r\n                 output_format = current_map_details.get(\"output_format\")\r\n-                if not output_format:\r\n-                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for '{map_type}'. Cannot reconstruct path. Skipping map type.\")\r\n-                    continue\r\n \r\n+                if not output_format and not is_merged_map:\r\n+                    # This case should ideally not happen if metadata is well-formed\r\n+                    # but handle defensively for processed maps.\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n+                    # We will rely solely on fallback for this map type\r\n+\r\n                 # Find placeholder node in parent\r\n                 holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n                 if not holder_nodes:\r\n                     print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n@@ -1658,14 +816,15 @@\n \r\n                     print(f\"        Processing Resolution: {resolution}\")\r\n \r\n                     # Reconstruct the image path using fallback logic\r\n+                    # Pass output_format (which might be None for merged maps)\r\n                     image_path_str = reconstruct_image_path_with_fallback(\r\n                         asset_dir_path=asset_dir_path,\r\n                         asset_name=asset_name,\r\n                         map_type=map_type,\r\n                         resolution=resolution,\r\n-                        primary_format=output_format # Pass the format from metadata\r\n+                        primary_format=output_format\r\n                     )\r\n \r\n                     if not image_path_str:\r\n                         # Error already printed by reconstruct function\r\n@@ -1690,9 +849,9 @@\n                             image_load_failed = True\r\n                         else:\r\n                             # Only count as loaded if bpy.data.images.load succeeded\r\n                             # Check if it's newly loaded or reused\r\n-                            is_newly_loaded = img.filepath == str(image_path) # Simple check, might not be perfect if paths are identical but data differs\r\n+                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n                             if is_newly_loaded: images_loaded += 1\r\n \r\n                     except RuntimeError as e_runtime_load:\r\n                         # Catch specific Blender runtime errors (e.g., unsupported format)\r\n"
                },
                {
                    "date": 1745261945437,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -372,14 +372,24 @@\n \r\n \r\n # --- Core Logic ---\r\n \r\n-def process_library(context):\r\n+def process_library(context, asset_library_root_override=None): # Add override parameter\r\n     global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n     \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n     start_time = time.time()\r\n     print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n \r\n+    # --- Determine Asset Library Root ---\r\n+    if asset_library_root_override:\r\n+        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+        print(\"--- Script aborted. ---\")\r\n+        return False\r\n+\r\n     # --- Pre-run Checks ---\r\n     print(\"Performing pre-run checks...\")\r\n     valid_setup = True\r\n     # 1. Check Library Root Path\r\n@@ -957,8 +967,25 @@\n     # Ensure we are running within Blender\r\n     try:\r\n         import bpy\r\n         import base64 # Ensure base64 is imported here too if needed globally\r\n+        import sys\r\n     except ImportError:\r\n         print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n     else:\r\n\\ No newline at end of file\n-        process_library(bpy.context)\n+        # --- Argument Parsing for Asset Library Root ---\r\n+        asset_root_arg = None\r\n+        try:\r\n+            # Blender arguments passed after '--' appear in sys.argv\r\n+            if \"--\" in sys.argv:\r\n+                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+                if len(args_after_dash) >= 1:\r\n+                    asset_root_arg = args_after_dash[0]\r\n+                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+                else:\r\n+                    print(\"Info: '--' found but no arguments after it.\")\r\n+            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+        except Exception as e:\r\n+            print(f\"Error parsing command line arguments: {e}\")\r\n+        # --- End Argument Parsing ---\r\n+\r\n+        process_library(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745261958348,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -33,9 +33,10 @@\n \r\n # Path to the root output directory of the Asset Processor Tool\r\n # Example: r\"G:\\Assets\\Processed\"\r\n # IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-PROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\Asset_Processor_Output\" # <<< CHANGE THIS PATH!\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n \r\n # Names of the required node group templates in the Blender file\r\n PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n"
                },
                {
                    "date": 1745263537952,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -958,8 +958,16 @@\n     if errors_encountered > 0:\r\n         print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n     print(\"---------------------------\")\r\n \r\n+    # --- Explicit Save ---\r\n+    try:\r\n+        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+    except Exception as e_save:\r\n+        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+        errors_encountered += 1 # Count save errors\r\n+\r\n     return True\r\n \r\n \r\n # --- Execution Block ---\r\n"
                },
                {
                    "date": 1745263806905,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -379,8 +379,9 @@\n     global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n     \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n     start_time = time.time()\r\n     print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+    print(f\"DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG\r\n \r\n     # --- Determine Asset Library Root ---\r\n     if asset_library_root_override:\r\n         PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n@@ -388,8 +389,9 @@\n     elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n         print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n         print(\"--- Script aborted. ---\")\r\n         return False\r\n+    print(f\"DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG\r\n \r\n     # --- Pre-run Checks ---\r\n     print(\"Performing pre-run checks...\")\r\n     valid_setup = True\r\n@@ -400,8 +402,9 @@\n         print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n         valid_setup = False\r\n     else:\r\n         print(f\"   Asset Library Root: '{root_path}'\")\r\n+    print(f\"DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG\r\n \r\n     # 2. Check Templates\r\n     template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n     template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n@@ -412,10 +415,12 @@\n         print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n         valid_setup = False\r\n     if template_parent and template_child:\r\n          print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+   print(f\"DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG\r\n+   print(f\"DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG\r\n \r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n+   # 3. Check Blend File Saved (if manifest enabled)\r\n     if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n         print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n         print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n         ENABLE_MANIFEST = False # Disable manifest for this run\r\n@@ -463,8 +468,9 @@\n                         metadata_paths.append(metadata_file)\r\n \r\n     metadata_files_found = len(metadata_paths)\r\n     print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+    print(f\"DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG\r\n \r\n     if metadata_files_found == 0:\r\n         print(\"No metadata files found. Nothing to process.\")\r\n         print(\"--- Script Finished ---\")\r\n@@ -473,8 +479,9 @@\n     # --- Process Each Metadata File ---\r\n     for metadata_path in metadata_paths:\r\n         asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n         print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        print(f\"DEBUG: Processing file: {metadata_path}\") # DEBUG LOG\r\n         try:\r\n             with open(metadata_path, 'r', encoding='utf-8') as f:\r\n                 metadata = json.load(f)\r\n \r\n@@ -594,8 +601,9 @@\n             is_new_parent = False\r\n \r\n             if parent_group is None:\r\n                 print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                print(f\"DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG\r\n                 parent_group = template_parent.copy()\r\n                 if not parent_group:\r\n                     print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n                     errors_encountered += 1\r\n@@ -604,8 +612,9 @@\n                 parent_groups_created += 1\r\n                 is_new_parent = True\r\n             else:\r\n                 print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                print(f\"DEBUG: Found existing parent group.\") # DEBUG LOG\r\n                 parent_groups_updated += 1\r\n \r\n             # Ensure marked as asset\r\n             if not parent_group.asset_data:\r\n@@ -720,8 +729,9 @@\n \r\n \r\n             # --- Child Group Handling ---\r\n             # Iterate through the COMBINED map types\r\n+            print(f\"DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG\r\n             for map_type, resolutions in all_map_resolutions.items():\r\n                 print(f\"    Processing Map Type: {map_type}\")\r\n \r\n                 # Determine if this is a merged map (not in map_details)\r\n@@ -752,8 +762,9 @@\n                 child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n                 is_new_child = False\r\n \r\n                 if child_group is None:\r\n+                    print(f\"DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG\r\n                     # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n                     child_group = template_child.copy()\r\n                     if not child_group:\r\n                         print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n@@ -762,8 +773,9 @@\n                     child_group.name = target_child_name_b64 # Set encoded name\r\n                     child_groups_created += 1\r\n                     is_new_child = True\r\n                 else:\r\n+                    print(f\"DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG\r\n                     # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n                     child_groups_updated += 1\r\n \r\n                 # Assign child group to placeholder if needed\r\n@@ -823,8 +835,9 @@\n                     if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n                         # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n                         maps_skipped_manifest += 1\r\n                         continue\r\n+                    print(f\"DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG\r\n \r\n                     print(f\"        Processing Resolution: {resolution}\")\r\n \r\n                     # Reconstruct the image path using fallback logic\r\n@@ -959,8 +972,9 @@\n         print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n     print(\"---------------------------\")\r\n \r\n     # --- Explicit Save ---\r\n+    print(f\"DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG\r\n     try:\r\n         bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n         print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n     except Exception as e_save:\r\n"
                },
                {
                    "date": 1745263848166,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -379,9 +379,9 @@\n     global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n     \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n     start_time = time.time()\r\n     print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-    print(f\"DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG\r\n+    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n \r\n     # --- Determine Asset Library Root ---\r\n     if asset_library_root_override:\r\n         PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n@@ -389,9 +389,9 @@\n     elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n         print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n         print(\"--- Script aborted. ---\")\r\n         return False\r\n-    print(f\"DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG\r\n+    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n \r\n     # --- Pre-run Checks ---\r\n     print(\"Performing pre-run checks...\")\r\n     valid_setup = True\r\n@@ -402,9 +402,9 @@\n         print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n         valid_setup = False\r\n     else:\r\n         print(f\"   Asset Library Root: '{root_path}'\")\r\n-    print(f\"DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG\r\n+    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n \r\n     # 2. Check Templates\r\n     template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n     template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n@@ -415,13 +415,13 @@\n         print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n         valid_setup = False\r\n     if template_parent and template_child:\r\n          print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-   print(f\"DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG\r\n-   print(f\"DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG\r\n+   print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n+   print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n \r\n    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+   if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n         print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n         print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n         ENABLE_MANIFEST = False # Disable manifest for this run\r\n \r\n@@ -468,9 +468,9 @@\n                         metadata_paths.append(metadata_file)\r\n \r\n     metadata_files_found = len(metadata_paths)\r\n     print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-    print(f\"DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG\r\n+    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n \r\n     if metadata_files_found == 0:\r\n         print(\"No metadata files found. Nothing to process.\")\r\n         print(\"--- Script Finished ---\")\r\n@@ -479,9 +479,9 @@\n     # --- Process Each Metadata File ---\r\n     for metadata_path in metadata_paths:\r\n         asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n         print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        print(f\"DEBUG: Processing file: {metadata_path}\") # DEBUG LOG\r\n+        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n         try:\r\n             with open(metadata_path, 'r', encoding='utf-8') as f:\r\n                 metadata = json.load(f)\r\n \r\n@@ -601,9 +601,9 @@\n             is_new_parent = False\r\n \r\n             if parent_group is None:\r\n                 print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                print(f\"DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG\r\n+                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n                 parent_group = template_parent.copy()\r\n                 if not parent_group:\r\n                     print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n                     errors_encountered += 1\r\n@@ -612,9 +612,9 @@\n                 parent_groups_created += 1\r\n                 is_new_parent = True\r\n             else:\r\n                 print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                print(f\"DEBUG: Found existing parent group.\") # DEBUG LOG\r\n+                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n                 parent_groups_updated += 1\r\n \r\n             # Ensure marked as asset\r\n             if not parent_group.asset_data:\r\n@@ -729,9 +729,9 @@\n \r\n \r\n             # --- Child Group Handling ---\r\n             # Iterate through the COMBINED map types\r\n-            print(f\"DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG\r\n+            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n             for map_type, resolutions in all_map_resolutions.items():\r\n                 print(f\"    Processing Map Type: {map_type}\")\r\n \r\n                 # Determine if this is a merged map (not in map_details)\r\n@@ -762,9 +762,9 @@\n                 child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n                 is_new_child = False\r\n \r\n                 if child_group is None:\r\n-                    print(f\"DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG\r\n+                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n                     # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n                     child_group = template_child.copy()\r\n                     if not child_group:\r\n                         print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n@@ -773,9 +773,9 @@\n                     child_group.name = target_child_name_b64 # Set encoded name\r\n                     child_groups_created += 1\r\n                     is_new_child = True\r\n                 else:\r\n-                    print(f\"DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG\r\n+                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n                     # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n                     child_groups_updated += 1\r\n \r\n                 # Assign child group to placeholder if needed\r\n@@ -835,9 +835,9 @@\n                     if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n                         # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n                         maps_skipped_manifest += 1\r\n                         continue\r\n-                    print(f\"DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG\r\n+                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n \r\n                     print(f\"        Processing Resolution: {resolution}\")\r\n \r\n                     # Reconstruct the image path using fallback logic\r\n@@ -972,9 +972,9 @@\n         print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n     print(\"---------------------------\")\r\n \r\n     # --- Explicit Save ---\r\n-    print(f\"DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG\r\n+    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n     try:\r\n         bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n         print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n     except Exception as e_save:\r\n"
                },
                {
                    "date": 1745263876730,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,51 @@\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    # --- Explicit Save ---\r\n+    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+    try:\r\n+        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+    except Exception as e_save:\r\n+        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+        errors_encountered += 1 # Count save errors\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n+        import sys\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        # --- Argument Parsing for Asset Library Root ---\r\n+        asset_root_arg = None\r\n+        try:\r\n+            # Blender arguments passed after '--' appear in sys.argv\r\n+            if \"--\" in sys.argv:\r\n+                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+                if len(args_after_dash) >= 1:\r\n+                    asset_root_arg = args_after_dash[0]\r\n+                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+                else:\r\n+                    print(\"Info: '--' found but no arguments after it.\")\r\n+            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+        except Exception as e:\r\n+            print(f\"Error parsing command line arguments: {e}\")\r\n+        # --- End Argument Parsing ---\r\n+\r\n+        process_library(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745264002733,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,55 +1,4 @@\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n-    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    # --- Explicit Save ---\r\n-    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n-    try:\r\n-        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n-        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n-    except Exception as e_save:\r\n-        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n-        errors_encountered += 1 # Count save errors\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-        import base64 # Ensure base64 is imported here too if needed globally\r\n-        import sys\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        # --- Argument Parsing for Asset Library Root ---\r\n-        asset_root_arg = None\r\n-        try:\r\n-            # Blender arguments passed after '--' appear in sys.argv\r\n-            if \"--\" in sys.argv:\r\n-                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n-                if len(args_after_dash) >= 1:\r\n-                    asset_root_arg = args_after_dash[0]\r\n-                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n-                else:\r\n-                    print(\"Info: '--' found but no arguments after it.\")\r\n-            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n-        except Exception as e:\r\n-            print(f\"Error parsing command line arguments: {e}\")\r\n-        # --- End Argument Parsing ---\r\n-\r\n-        process_library(bpy.context, asset_library_root_override=asset_root_arg)\n # Blender Script: Create/Update Node Groups from Asset Processor Output\r\n # Version: 1.5\r\n # Description: Scans a library processed by the Asset Processor Tool,\r\n #              reads metadata.json files, and creates/updates corresponding\r\n@@ -466,13 +415,13 @@\n         print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n         valid_setup = False\r\n     if template_parent and template_child:\r\n          print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-   print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n-   print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n+    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n+    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n \r\n-   # 3. Check Blend File Saved (if manifest enabled)\r\n-   if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n         print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n         print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n         ENABLE_MANIFEST = False # Disable manifest for this run\r\n \r\n@@ -559,8 +508,9 @@\n                  print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n                  errors_encountered += 1\r\n                  continue\r\n             # map_details check remains a warning as merged maps won't be in it\r\n+            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n \r\n             print(f\"  Asset Name: {asset_name}\")\r\n \r\n             # --- Determine Highest Resolution ---\r\n@@ -804,8 +754,9 @@\n                 if not holder_nodes:\r\n                     print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n                     continue\r\n                 holder_node = holder_nodes[0] # Assume first is correct\r\n+                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n \r\n                 # Determine child group name (LOGICAL and ENCODED)\r\n                 logical_child_name = f\"{asset_name}_{map_type}\"\r\n                 target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n@@ -899,8 +850,9 @@\n                         map_type=map_type,\r\n                         resolution=resolution,\r\n                         primary_format=output_format\r\n                     )\r\n+                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n \r\n                     if not image_path_str:\r\n                         # Error already printed by reconstruct function\r\n                         errors_encountered += 1\r\n@@ -910,8 +862,9 @@\n                     image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n                     if not image_nodes:\r\n                         print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n                         continue # Skip this resolution if node not found\r\n+                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n \r\n                     # --- Load Image ---\r\n                     img = None\r\n                     image_load_failed = False\r\n@@ -1061,5 +1014,4 @@\n         except Exception as e:\r\n             print(f\"Error parsing command line arguments: {e}\")\r\n         # --- End Argument Parsing ---\r\n \r\n-        process_library(bpy.context, asset_library_root_override=asset_root_arg)\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745264122725,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,1019 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.5\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+# Changes v1.5:\r\n+#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n+#     to use actual image dimensions from a loaded reference image and the\r\n+#     `aspect_ratio_change_string`, mirroring original script logic for\r\n+#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n+#   - Added logic in main loop to load reference image for dimensions.\r\n+# Changes v1.3:\r\n+#   - Added logic to find the highest resolution present for an asset.\r\n+#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n+#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n+# Changes v1.2:\r\n+#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n+#   - Added fallback logic for reconstructing image paths with different extensions.\r\n+#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n+# Changes v1.1:\r\n+#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n+#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+import base64 # For encoding node group names\r\n+import sys # <<< ADDED IMPORT\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Mapping from resolution string to numerical value for the HighestResolution node\r\n+RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n+# Order to check resolutions to find the highest present (highest value first)\r\n+RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n+    \"COL-2\": \"sRGB\",\r\n+    \"COL-3\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n+    \"FUZZ\": \"Non-Color\",\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+# Reads stats from the 'image_stats_1k' section of metadata.json\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def encode_name_b64(name_str):\r\n+    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n+    try:\r\n+        # Ensure the input is a string\r\n+        name_str = str(name_str)\r\n+        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n+    except Exception as e:\r\n+        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n+        return name_str # Fallback to original name on error\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    # Handle potential numbered variants like COL-1, COL-2\r\n+    base_map_type = map_type.split('-')[0]\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n+               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n+\r\n+def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n+    \"\"\"\r\n+    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n+    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n+    Mirrors the logic from the original POC script.\r\n+    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n+    \"\"\"\r\n+    if image_height <= 0 or image_width <= 0:\r\n+        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+    # Calculate the actual aspect ratio of the image file\r\n+    current_aspect_ratio = image_width / image_height\r\n+\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        # If scaling was even, the correction factor is just the image's aspect ratio\r\n+        # to make UVs match the image proportions.\r\n+        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n+    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n+    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not match:\r\n+        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n+        return current_aspect_ratio # Fallback to the image's own ratio\r\n+\r\n+    axis = match.group(1).upper()\r\n+    try:\r\n+        amount = int(match.group(2))\r\n+        if amount <= 0:\r\n+            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+            return current_aspect_ratio\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Apply the non-uniform correction formula based on original script logic\r\n+    scaling_factor_percent = amount / 100.0\r\n+    correction_factor = current_aspect_ratio # Default\r\n+\r\n+    try:\r\n+        if axis == 'X':\r\n+            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n+            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n+            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n+        elif axis == 'Y':\r\n+            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n+            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n+        # No 'else' needed as regex ensures X or Y\r\n+\r\n+    except ZeroDivisionError as e:\r\n+        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+    except Exception as e:\r\n+         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+         return current_aspect_ratio\r\n+\r\n+    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n+    return correction_factor\r\n+\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n+    \"\"\"\r\n+    Constructs the expected image file path.\r\n+    If primary_format is provided, tries that first.\r\n+    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n+    Returns the found path as a string, or None if not found.\r\n+    \"\"\"\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n+        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+        return None\r\n+\r\n+    found_path = None\r\n+\r\n+    # 1. Try the primary format if provided\r\n+    if primary_format:\r\n+        try:\r\n+            filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=primary_format.lower() # Ensure format is lowercase\r\n+            )\r\n+            primary_path = asset_dir_path / filename\r\n+            if primary_path.is_file():\r\n+                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n+                return str(primary_path)\r\n+            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n+        except KeyError as e:\r\n+            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+            return None # Cannot proceed without valid pattern\r\n+        except Exception as e:\r\n+            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+            # Continue to fallback\r\n+\r\n+    # 2. Try fallback extensions\r\n+    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+        # Skip if we already tried this extension as primary (and it failed)\r\n+        if primary_format and ext.lower() == primary_format.lower():\r\n+            continue\r\n+        try:\r\n+            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=ext.lower()\r\n+            )\r\n+            fallback_path = asset_dir_path / fallback_filename\r\n+            if fallback_path.is_file():\r\n+                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n+                return str(fallback_path) # Found it!\r\n+        except KeyError:\r\n+             # Should not happen if primary format worked, but handle defensively\r\n+             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+             return None\r\n+        except Exception as e_fallback:\r\n+            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+            continue # Try next extension\r\n+\r\n+    # If we get here, neither primary nor fallbacks worked\r\n+    if primary_format:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    else:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    return None # Not found after all checks\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # Basic check if asset entry exists. Detailed check happens at map level.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context, asset_library_root_override=None): # Add override parameter\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Determine Asset Library Root ---\r\n+    if asset_library_root_override:\r\n+        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+        print(\"--- Script aborted. ---\")\r\n+        return False\r\n+    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n+    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    previews_set = 0\r\n+    highest_res_set = 0\r\n+    aspect_ratio_set = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n+    # Then scan within each supplier for asset folders containing metadata.json\r\n+    metadata_paths = []\r\n+    for supplier_dir in root_path.iterdir():\r\n+        if supplier_dir.is_dir():\r\n+            # Now look for asset folders inside the supplier directory\r\n+            for asset_dir in supplier_dir.iterdir():\r\n+                if asset_dir.is_dir():\r\n+                    metadata_file = asset_dir / 'metadata.json'\r\n+                    if metadata_file.is_file():\r\n+                        metadata_paths.append(metadata_file)\r\n+\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            # Get map info from the correct keys\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n+            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n+            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            # Combine processed and merged maps for iteration\r\n+            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n+            # Validate essential data\r\n+            if not asset_name:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+            if not all_map_resolutions:\r\n+                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+                 errors_encountered += 1\r\n+                 continue\r\n+            # map_details check remains a warning as merged maps won't be in it\r\n+            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Determine Highest Resolution ---\r\n+            highest_resolution_value = 0.0\r\n+            highest_resolution_str = \"Unknown\"\r\n+            all_resolutions_present = set()\r\n+            if all_map_resolutions: # Check combined dict\r\n+                for res_list in all_map_resolutions.values():\r\n+                    if isinstance(res_list, list):\r\n+                         all_resolutions_present.update(res_list)\r\n+\r\n+            if all_resolutions_present:\r\n+                for res_str in RESOLUTION_ORDER_DESC:\r\n+                    if res_str in all_resolutions_present:\r\n+                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n+                        highest_resolution_str = res_str\r\n+                        if highest_resolution_value > 0.0:\r\n+                            break # Found the highest valid resolution\r\n+\r\n+            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n+\r\n+            # --- Load Reference Image for Aspect Ratio ---\r\n+            ref_image_path = None\r\n+            ref_image_width = 0\r\n+            ref_image_height = 0\r\n+            ref_image_loaded = False\r\n+            # Use combined resolutions dict to find reference map\r\n+            for ref_map_type in REFERENCE_MAP_TYPES:\r\n+                if ref_map_type in all_map_resolutions:\r\n+                    available_resolutions = all_map_resolutions[ref_map_type]\r\n+                    lowest_res = None\r\n+                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+                        if res_pref in available_resolutions:\r\n+                            lowest_res = res_pref\r\n+                            break\r\n+                    if lowest_res:\r\n+                        # Get format from map_details if available, otherwise None\r\n+                        ref_map_details = map_details.get(ref_map_type, {})\r\n+                        ref_format = ref_map_details.get(\"output_format\")\r\n+                        ref_image_path = reconstruct_image_path_with_fallback(\r\n+                            asset_dir_path=asset_dir_path,\r\n+                            asset_name=asset_name,\r\n+                            map_type=ref_map_type,\r\n+                            resolution=lowest_res,\r\n+                            primary_format=ref_format # Pass None if not in map_details\r\n+                        )\r\n+                        if ref_image_path:\r\n+                            break # Found a suitable reference image path\r\n+\r\n+            if ref_image_path:\r\n+                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n+                try:\r\n+                    # Load image temporarily\r\n+                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n+                    if ref_img:\r\n+                        ref_image_width = ref_img.size[0]\r\n+                        ref_image_height = ref_img.size[1]\r\n+                        ref_image_loaded = True\r\n+                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n+                        # Remove the temporary image datablock to save memory\r\n+                        bpy.data.images.remove(ref_img)\r\n+                    else:\r\n+                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n+                except Exception as e_ref_load:\r\n+                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n+            else:\r\n+                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n+\r\n+\r\n+            # --- Manifest Check (Asset Level - Basic) ---\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Perform a quick check if *any* map needs processing for this asset\r\n+                needs_processing = False\r\n+                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n+                    for resolution in resolutions:\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            needs_processing = True\r\n+                            break\r\n+                    if needs_processing:\r\n+                        break\r\n+                if not needs_processing:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = 1.0 # Default if ref image fails\r\n+                if ref_image_loaded:\r\n+                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n+                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n+                else:\r\n+                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+\r\n+                # Check if update is needed\r\n+                current_val = aspect_node.outputs[0].default_value\r\n+                if abs(current_val - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n+                    aspect_ratio_set += 1\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Highest Resolution Value\r\n+            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n+            if hr_nodes:\r\n+                hr_node = hr_nodes[0]\r\n+                current_hr_val = hr_node.outputs[0].default_value\r\n+                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n+                    hr_node.outputs[0].default_value = highest_resolution_value\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n+                    highest_res_set += 1 # Count successful sets\r\n+            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n+            # Apply Stats (using image_stats_1k)\r\n+            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    if map_type_to_stat in image_stats_1k:\r\n+                        # Find the stats node in the parent group\r\n+                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                        if stats_nodes:\r\n+                            stats_node = stats_nodes[0]\r\n+                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n+\r\n+                            if stats and isinstance(stats, dict):\r\n+                                # Handle potential list format for RGB stats (use first value) or direct float\r\n+                                def get_stat_value(stat_val):\r\n+                                    if isinstance(stat_val, list):\r\n+                                        return stat_val[0] if stat_val else None\r\n+                                    return stat_val\r\n+\r\n+                                min_val = get_stat_value(stats.get(\"min\"))\r\n+                                max_val = get_stat_value(stats.get(\"max\"))\r\n+                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n+\r\n+                                updated_stat = False\r\n+                                # Check inputs exist before assigning\r\n+                                input_x = stats_node.inputs.get(\"X\")\r\n+                                input_y = stats_node.inputs.get(\"Y\")\r\n+                                input_z = stats_node.inputs.get(\"Z\")\r\n+\r\n+                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n+                                    input_x.default_value = min_val\r\n+                                    updated_stat = True\r\n+                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n+                                    input_y.default_value = max_val\r\n+                                    updated_stat = True\r\n+                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n+                                    input_z.default_value = mean_val\r\n+                                    updated_stat = True\r\n+\r\n+                                if updated_stat:\r\n+                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n+                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n+            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n+\r\n+            # --- Set Asset Preview (only for new parent groups) ---\r\n+            # Use the reference image path found earlier if available\r\n+            if is_new_parent and parent_group.asset_data:\r\n+                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n+                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n+                    try:\r\n+                        # Ensure the ID (node group) is the active one for the operator context\r\n+                        with context.temp_override(id=parent_group):\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+                        print(f\"      Successfully set custom preview.\")\r\n+                        previews_set += 1\r\n+                    except Exception as e_preview:\r\n+                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n+                        errors_encountered += 1\r\n+                else:\r\n+                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            # Iterate through the COMBINED map types\r\n+            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n+            for map_type, resolutions in all_map_resolutions.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Determine if this is a merged map (not in map_details)\r\n+                is_merged_map = map_type not in map_details\r\n+\r\n+                # Get details for this map type if available\r\n+                current_map_details = map_details.get(map_type, {})\r\n+                # For merged maps, primary_format will be None\r\n+                output_format = current_map_details.get(\"output_format\")\r\n+\r\n+                if not output_format and not is_merged_map:\r\n+                    # This case should ideally not happen if metadata is well-formed\r\n+                    # but handle defensively for processed maps.\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n+                    # We will rely solely on fallback for this map type\r\n+\r\n+                # Find placeholder node in parent\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                # Determine child group name (LOGICAL and ENCODED)\r\n+                logical_child_name = f\"{asset_name}_{map_type}\"\r\n+                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n+\r\n+                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name_b64 # Set encoded name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                if not isinstance(resolutions, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution in resolutions:\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Reconstruct the image path using fallback logic\r\n+                    # Pass output_format (which might be None for merged maps)\r\n+                    image_path_str = reconstruct_image_path_with_fallback(\r\n+                        asset_dir_path=asset_dir_path,\r\n+                        asset_name=asset_name,\r\n+                        map_type=map_type,\r\n+                        resolution=resolution,\r\n+                        primary_format=output_format\r\n+                    )\r\n+                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n+\r\n+                    if not image_path_str:\r\n+                        # Error already printed by reconstruct function\r\n+                        errors_encountered += 1\r\n+                        continue # Skip this resolution if path not found\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str) # Path object created from already found path string\r\n+                        # Use check_existing=True to reuse existing datablocks if path matches\r\n+                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                        if not img:\r\n+                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Only count as loaded if bpy.data.images.load succeeded\r\n+                            # Check if it's newly loaded or reused\r\n+                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n+                            if is_newly_loaded: images_loaded += 1\r\n+\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+\r\n+                    else:\r\n+                        # Increment error count if loading failed\r\n+                        if image_load_failed: errors_encountered += 1\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    # --- Explicit Save ---\r\n+    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+    try:\r\n+        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+    except Exception as e_save:\r\n+        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+        errors_encountered += 1 # Count save errors\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n+        import sys\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        # --- Argument Parsing for Asset Library Root ---\r\n+        asset_root_arg = None\r\n+        try:\r\n+            # Blender arguments passed after '--' appear in sys.argv\r\n+            if \"--\" in sys.argv:\r\n+                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+                if len(args_after_dash) >= 1:\r\n+                    asset_root_arg = args_after_dash[0]\r\n+                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+                else:\r\n+                    print(\"Info: '--' found but no arguments after it.\")\r\n+            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+        except Exception as e:\r\n+            print(f\"Error parsing command line arguments: {e}\")\r\n+        # --- End Argument Parsing ---\r\n+\r\n+        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n"
                },
                {
                    "date": 1745266802108,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -48,9 +48,9 @@\n HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n \r\n # Enable/disable the manifest system to track processed assets/maps\r\n # If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n+ENABLE_MANIFEST = True # Disabled based on user feedback in previous run\r\n \r\n # Assumed filename pattern for processed images.\r\n # {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n # Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n@@ -1016,1021 +1016,4 @@\n             print(f\"Error parsing command line arguments: {e}\")\r\n         # --- End Argument Parsing ---\r\n \r\n         process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.5\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-# Changes v1.5:\r\n-#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n-#     to use actual image dimensions from a loaded reference image and the\r\n-#     `aspect_ratio_change_string`, mirroring original script logic for\r\n-#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n-#   - Added logic in main loop to load reference image for dimensions.\r\n-# Changes v1.3:\r\n-#   - Added logic to find the highest resolution present for an asset.\r\n-#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n-#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n-# Changes v1.2:\r\n-#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n-#   - Added fallback logic for reconstructing image paths with different extensions.\r\n-#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n-# Changes v1.1:\r\n-#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n-#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-import base64 # For encoding node group names\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-# This will be overridden by command-line arguments if provided.\r\n-PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n-\r\n-# Assumed filename pattern for processed images.\r\n-# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n-# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n-IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n-\r\n-# Fallback extensions to try if the primary format from metadata is not found\r\n-# Order matters - first found will be used.\r\n-FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n-\r\n-# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n-# The script will look for these in order and use the first one found.\r\n-REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n-# Preferred resolution order for reference image (lowest first is often faster)\r\n-REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n-\r\n-# Mapping from resolution string to numerical value for the HighestResolution node\r\n-RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n-# Order to check resolutions to find the highest present (highest value first)\r\n-RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n-    \"COL-2\": \"sRGB\",\r\n-    \"COL-3\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n-    \"FUZZ\": \"Non-Color\",\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-# Reads stats from the 'image_stats_1k' section of metadata.json\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def encode_name_b64(name_str):\r\n-    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n-    try:\r\n-        # Ensure the input is a string\r\n-        name_str = str(name_str)\r\n-        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n-    except Exception as e:\r\n-        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n-        return name_str # Fallback to original name on error\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    # Handle potential numbered variants like COL-1, COL-2\r\n-    base_map_type = map_type.split('-')[0]\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n-               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n-\r\n-def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n-    \"\"\"\r\n-    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n-    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n-    Mirrors the logic from the original POC script.\r\n-    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n-    \"\"\"\r\n-    if image_height <= 0 or image_width <= 0:\r\n-        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-    # Calculate the actual aspect ratio of the image file\r\n-    current_aspect_ratio = image_width / image_height\r\n-\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        # If scaling was even, the correction factor is just the image's aspect ratio\r\n-        # to make UVs match the image proportions.\r\n-        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n-    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n-    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    if not match:\r\n-        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n-        return current_aspect_ratio # Fallback to the image's own ratio\r\n-\r\n-    axis = match.group(1).upper()\r\n-    try:\r\n-        amount = int(match.group(2))\r\n-        if amount <= 0:\r\n-            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-            return current_aspect_ratio\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Apply the non-uniform correction formula based on original script logic\r\n-    scaling_factor_percent = amount / 100.0\r\n-    correction_factor = current_aspect_ratio # Default\r\n-\r\n-    try:\r\n-        if axis == 'X':\r\n-            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n-            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n-            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n-        elif axis == 'Y':\r\n-            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n-            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n-        # No 'else' needed as regex ensures X or Y\r\n-\r\n-    except ZeroDivisionError as e:\r\n-        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-    except Exception as e:\r\n-         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-         return current_aspect_ratio\r\n-\r\n-    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n-    return correction_factor\r\n-\r\n-\r\n-def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n-    \"\"\"\r\n-    Constructs the expected image file path.\r\n-    If primary_format is provided, tries that first.\r\n-    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n-    Returns the found path as a string, or None if not found.\r\n-    \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n-        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n-        return None\r\n-\r\n-    found_path = None\r\n-\r\n-    # 1. Try the primary format if provided\r\n-    if primary_format:\r\n-        try:\r\n-            filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=primary_format.lower() # Ensure format is lowercase\r\n-            )\r\n-            primary_path = asset_dir_path / filename\r\n-            if primary_path.is_file():\r\n-                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n-                return str(primary_path)\r\n-            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n-        except KeyError as e:\r\n-            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-            return None # Cannot proceed without valid pattern\r\n-        except Exception as e:\r\n-            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n-            # Continue to fallback\r\n-\r\n-    # 2. Try fallback extensions\r\n-    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n-    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        # Skip if we already tried this extension as primary (and it failed)\r\n-        if primary_format and ext.lower() == primary_format.lower():\r\n-            continue\r\n-        try:\r\n-            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=ext.lower()\r\n-            )\r\n-            fallback_path = asset_dir_path / fallback_filename\r\n-            if fallback_path.is_file():\r\n-                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n-                return str(fallback_path) # Found it!\r\n-        except KeyError:\r\n-             # Should not happen if primary format worked, but handle defensively\r\n-             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n-             return None\r\n-        except Exception as e_fallback:\r\n-            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n-            continue # Try next extension\r\n-\r\n-    # If we get here, neither primary nor fallbacks worked\r\n-    if primary_format:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    else:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    return None # Not found after all checks\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # Basic check if asset entry exists. Detailed check happens at map level.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context, asset_library_root_override=None): # Add override parameter\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Determine Asset Library Root ---\r\n-    if asset_library_root_override:\r\n-        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n-        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n-        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n-        print(\"--- Script aborted. ---\")\r\n-        return False\r\n-    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n-    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    previews_set = 0\r\n-    highest_res_set = 0\r\n-    aspect_ratio_set = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n-    # Then scan within each supplier for asset folders containing metadata.json\r\n-    metadata_paths = []\r\n-    for supplier_dir in root_path.iterdir():\r\n-        if supplier_dir.is_dir():\r\n-            # Now look for asset folders inside the supplier directory\r\n-            for asset_dir in supplier_dir.iterdir():\r\n-                if asset_dir.is_dir():\r\n-                    metadata_file = asset_dir / 'metadata.json'\r\n-                    if metadata_file.is_file():\r\n-                        metadata_paths.append(metadata_file)\r\n-\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            # Get map info from the correct keys\r\n-            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n-            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n-            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n-            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            # Combine processed and merged maps for iteration\r\n-            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n-\r\n-            # Validate essential data\r\n-            if not asset_name:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-            if not all_map_resolutions:\r\n-                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n-                 errors_encountered += 1\r\n-                 continue\r\n-            # map_details check remains a warning as merged maps won't be in it\r\n-            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Determine Highest Resolution ---\r\n-            highest_resolution_value = 0.0\r\n-            highest_resolution_str = \"Unknown\"\r\n-            all_resolutions_present = set()\r\n-            if all_map_resolutions: # Check combined dict\r\n-                for res_list in all_map_resolutions.values():\r\n-                    if isinstance(res_list, list):\r\n-                         all_resolutions_present.update(res_list)\r\n-\r\n-            if all_resolutions_present:\r\n-                for res_str in RESOLUTION_ORDER_DESC:\r\n-                    if res_str in all_resolutions_present:\r\n-                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n-                        highest_resolution_str = res_str\r\n-                        if highest_resolution_value > 0.0:\r\n-                            break # Found the highest valid resolution\r\n-\r\n-            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n-\r\n-            # --- Load Reference Image for Aspect Ratio ---\r\n-            ref_image_path = None\r\n-            ref_image_width = 0\r\n-            ref_image_height = 0\r\n-            ref_image_loaded = False\r\n-            # Use combined resolutions dict to find reference map\r\n-            for ref_map_type in REFERENCE_MAP_TYPES:\r\n-                if ref_map_type in all_map_resolutions:\r\n-                    available_resolutions = all_map_resolutions[ref_map_type]\r\n-                    lowest_res = None\r\n-                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n-                        if res_pref in available_resolutions:\r\n-                            lowest_res = res_pref\r\n-                            break\r\n-                    if lowest_res:\r\n-                        # Get format from map_details if available, otherwise None\r\n-                        ref_map_details = map_details.get(ref_map_type, {})\r\n-                        ref_format = ref_map_details.get(\"output_format\")\r\n-                        ref_image_path = reconstruct_image_path_with_fallback(\r\n-                            asset_dir_path=asset_dir_path,\r\n-                            asset_name=asset_name,\r\n-                            map_type=ref_map_type,\r\n-                            resolution=lowest_res,\r\n-                            primary_format=ref_format # Pass None if not in map_details\r\n-                        )\r\n-                        if ref_image_path:\r\n-                            break # Found a suitable reference image path\r\n-\r\n-            if ref_image_path:\r\n-                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n-                try:\r\n-                    # Load image temporarily\r\n-                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n-                    if ref_img:\r\n-                        ref_image_width = ref_img.size[0]\r\n-                        ref_image_height = ref_img.size[1]\r\n-                        ref_image_loaded = True\r\n-                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n-                        # Remove the temporary image datablock to save memory\r\n-                        bpy.data.images.remove(ref_img)\r\n-                    else:\r\n-                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n-                except Exception as e_ref_load:\r\n-                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n-            else:\r\n-                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n-\r\n-\r\n-            # --- Manifest Check (Asset Level - Basic) ---\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Perform a quick check if *any* map needs processing for this asset\r\n-                needs_processing = False\r\n-                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n-                    for resolution in resolutions:\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            needs_processing = True\r\n-                            break\r\n-                    if needs_processing:\r\n-                        break\r\n-                if not needs_processing:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = 1.0 # Default if ref image fails\r\n-                if ref_image_loaded:\r\n-                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n-                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n-                else:\r\n-                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n-\r\n-                # Check if update is needed\r\n-                current_val = aspect_node.outputs[0].default_value\r\n-                if abs(current_val - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n-                    aspect_ratio_set += 1\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Highest Resolution Value\r\n-            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n-            if hr_nodes:\r\n-                hr_node = hr_nodes[0]\r\n-                current_hr_val = hr_node.outputs[0].default_value\r\n-                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n-                    hr_node.outputs[0].default_value = highest_resolution_value\r\n-                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n-                    highest_res_set += 1 # Count successful sets\r\n-            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-\r\n-            # Apply Stats (using image_stats_1k)\r\n-            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n-                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                    if map_type_to_stat in image_stats_1k:\r\n-                        # Find the stats node in the parent group\r\n-                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                        if stats_nodes:\r\n-                            stats_node = stats_nodes[0]\r\n-                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n-\r\n-                            if stats and isinstance(stats, dict):\r\n-                                # Handle potential list format for RGB stats (use first value) or direct float\r\n-                                def get_stat_value(stat_val):\r\n-                                    if isinstance(stat_val, list):\r\n-                                        return stat_val[0] if stat_val else None\r\n-                                    return stat_val\r\n-\r\n-                                min_val = get_stat_value(stats.get(\"min\"))\r\n-                                max_val = get_stat_value(stats.get(\"max\"))\r\n-                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n-\r\n-                                updated_stat = False\r\n-                                # Check inputs exist before assigning\r\n-                                input_x = stats_node.inputs.get(\"X\")\r\n-                                input_y = stats_node.inputs.get(\"Y\")\r\n-                                input_z = stats_node.inputs.get(\"Z\")\r\n-\r\n-                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n-                                    input_x.default_value = min_val\r\n-                                    updated_stat = True\r\n-                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n-                                    input_y.default_value = max_val\r\n-                                    updated_stat = True\r\n-                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n-                                    input_z.default_value = mean_val\r\n-                                    updated_stat = True\r\n-\r\n-                                if updated_stat:\r\n-                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n-                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n-            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n-\r\n-            # --- Set Asset Preview (only for new parent groups) ---\r\n-            # Use the reference image path found earlier if available\r\n-            if is_new_parent and parent_group.asset_data:\r\n-                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n-                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n-                    try:\r\n-                        # Ensure the ID (node group) is the active one for the operator context\r\n-                        with context.temp_override(id=parent_group):\r\n-                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n-                        print(f\"      Successfully set custom preview.\")\r\n-                        previews_set += 1\r\n-                    except Exception as e_preview:\r\n-                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n-                        errors_encountered += 1\r\n-                else:\r\n-                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            # Iterate through the COMBINED map types\r\n-            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n-            for map_type, resolutions in all_map_resolutions.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Determine if this is a merged map (not in map_details)\r\n-                is_merged_map = map_type not in map_details\r\n-\r\n-                # Get details for this map type if available\r\n-                current_map_details = map_details.get(map_type, {})\r\n-                # For merged maps, primary_format will be None\r\n-                output_format = current_map_details.get(\"output_format\")\r\n-\r\n-                if not output_format and not is_merged_map:\r\n-                    # This case should ideally not happen if metadata is well-formed\r\n-                    # but handle defensively for processed maps.\r\n-                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n-                    # We will rely solely on fallback for this map type\r\n-\r\n-                # Find placeholder node in parent\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                # Determine child group name (LOGICAL and ENCODED)\r\n-                logical_child_name = f\"{asset_name}_{map_type}\"\r\n-                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n-\r\n-                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name_b64 # Set encoded name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                if not isinstance(resolutions, list):\r\n-                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution in resolutions:\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Reconstruct the image path using fallback logic\r\n-                    # Pass output_format (which might be None for merged maps)\r\n-                    image_path_str = reconstruct_image_path_with_fallback(\r\n-                        asset_dir_path=asset_dir_path,\r\n-                        asset_name=asset_name,\r\n-                        map_type=map_type,\r\n-                        resolution=resolution,\r\n-                        primary_format=output_format\r\n-                    )\r\n-                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n-\r\n-                    if not image_path_str:\r\n-                        # Error already printed by reconstruct function\r\n-                        errors_encountered += 1\r\n-                        continue # Skip this resolution if path not found\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str) # Path object created from already found path string\r\n-                        # Use check_existing=True to reuse existing datablocks if path matches\r\n-                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                        if not img:\r\n-                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Only count as loaded if bpy.data.images.load succeeded\r\n-                            # Check if it's newly loaded or reused\r\n-                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n-                            if is_newly_loaded: images_loaded += 1\r\n-\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-\r\n-                    else:\r\n-                        # Increment error count if loading failed\r\n-                        if image_load_failed: errors_encountered += 1\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n-    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    # --- Explicit Save ---\r\n-    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n-    try:\r\n-        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n-        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n-    except Exception as e_save:\r\n-        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n-        errors_encountered += 1 # Count save errors\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-        import base64 # Ensure base64 is imported here too if needed globally\r\n-        import sys\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        # --- Argument Parsing for Asset Library Root ---\r\n-        asset_root_arg = None\r\n-        try:\r\n-            # Blender arguments passed after '--' appear in sys.argv\r\n-            if \"--\" in sys.argv:\r\n-                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n-                if len(args_after_dash) >= 1:\r\n-                    asset_root_arg = args_after_dash[0]\r\n-                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n-                else:\r\n-                    print(\"Info: '--' found but no arguments after it.\")\r\n-            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n-        except Exception as e:\r\n-            print(f\"Error parsing command line arguments: {e}\")\r\n-        # --- End Argument Parsing ---\r\n-\r\n"
                },
                {
                    "date": 1745266814187,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,1019 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.5\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+# Changes v1.5:\r\n+#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n+#     to use actual image dimensions from a loaded reference image and the\r\n+#     `aspect_ratio_change_string`, mirroring original script logic for\r\n+#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n+#   - Added logic in main loop to load reference image for dimensions.\r\n+# Changes v1.3:\r\n+#   - Added logic to find the highest resolution present for an asset.\r\n+#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n+#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n+# Changes v1.2:\r\n+#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n+#   - Added fallback logic for reconstructing image paths with different extensions.\r\n+#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n+# Changes v1.1:\r\n+#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n+#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+import base64 # For encoding node group names\r\n+import sys # <<< ADDED IMPORT\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = True # Disabled based on user feedback in previous run\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Mapping from resolution string to numerical value for the HighestResolution node\r\n+RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n+# Order to check resolutions to find the highest present (highest value first)\r\n+RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n+    \"COL-2\": \"sRGB\",\r\n+    \"COL-3\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n+    \"FUZZ\": \"Non-Color\",\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+# Reads stats from the 'image_stats_1k' section of metadata.json\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def encode_name_b64(name_str):\r\n+    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n+    try:\r\n+        # Ensure the input is a string\r\n+        name_str = str(name_str)\r\n+        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n+    except Exception as e:\r\n+        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n+        return name_str # Fallback to original name on error\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    # Handle potential numbered variants like COL-1, COL-2\r\n+    base_map_type = map_type.split('-')[0]\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n+               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n+\r\n+def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n+    \"\"\"\r\n+    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n+    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n+    Mirrors the logic from the original POC script.\r\n+    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n+    \"\"\"\r\n+    if image_height <= 0 or image_width <= 0:\r\n+        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+    # Calculate the actual aspect ratio of the image file\r\n+    current_aspect_ratio = image_width / image_height\r\n+\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        # If scaling was even, the correction factor is just the image's aspect ratio\r\n+        # to make UVs match the image proportions.\r\n+        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n+    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n+    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not match:\r\n+        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n+        return current_aspect_ratio # Fallback to the image's own ratio\r\n+\r\n+    axis = match.group(1).upper()\r\n+    try:\r\n+        amount = int(match.group(2))\r\n+        if amount <= 0:\r\n+            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+            return current_aspect_ratio\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Apply the non-uniform correction formula based on original script logic\r\n+    scaling_factor_percent = amount / 100.0\r\n+    correction_factor = current_aspect_ratio # Default\r\n+\r\n+    try:\r\n+        if axis == 'X':\r\n+            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n+            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n+            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n+        elif axis == 'Y':\r\n+            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n+            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n+        # No 'else' needed as regex ensures X or Y\r\n+\r\n+    except ZeroDivisionError as e:\r\n+        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+    except Exception as e:\r\n+         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+         return current_aspect_ratio\r\n+\r\n+    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n+    return correction_factor\r\n+\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n+    \"\"\"\r\n+    Constructs the expected image file path.\r\n+    If primary_format is provided, tries that first.\r\n+    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n+    Returns the found path as a string, or None if not found.\r\n+    \"\"\"\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n+        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+        return None\r\n+\r\n+    found_path = None\r\n+\r\n+    # 1. Try the primary format if provided\r\n+    if primary_format:\r\n+        try:\r\n+            filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=primary_format.lower() # Ensure format is lowercase\r\n+            )\r\n+            primary_path = asset_dir_path / filename\r\n+            if primary_path.is_file():\r\n+                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n+                return str(primary_path)\r\n+            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n+        except KeyError as e:\r\n+            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+            return None # Cannot proceed without valid pattern\r\n+        except Exception as e:\r\n+            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+            # Continue to fallback\r\n+\r\n+    # 2. Try fallback extensions\r\n+    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+        # Skip if we already tried this extension as primary (and it failed)\r\n+        if primary_format and ext.lower() == primary_format.lower():\r\n+            continue\r\n+        try:\r\n+            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=ext.lower()\r\n+            )\r\n+            fallback_path = asset_dir_path / fallback_filename\r\n+            if fallback_path.is_file():\r\n+                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n+                return str(fallback_path) # Found it!\r\n+        except KeyError:\r\n+             # Should not happen if primary format worked, but handle defensively\r\n+             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+             return None\r\n+        except Exception as e_fallback:\r\n+            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+            continue # Try next extension\r\n+\r\n+    # If we get here, neither primary nor fallbacks worked\r\n+    if primary_format:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    else:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    return None # Not found after all checks\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # Basic check if asset entry exists. Detailed check happens at map level.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context, asset_library_root_override=None): # Add override parameter\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Determine Asset Library Root ---\r\n+    if asset_library_root_override:\r\n+        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+        print(\"--- Script aborted. ---\")\r\n+        return False\r\n+    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n+    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    previews_set = 0\r\n+    highest_res_set = 0\r\n+    aspect_ratio_set = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n+    # Then scan within each supplier for asset folders containing metadata.json\r\n+    metadata_paths = []\r\n+    for supplier_dir in root_path.iterdir():\r\n+        if supplier_dir.is_dir():\r\n+            # Now look for asset folders inside the supplier directory\r\n+            for asset_dir in supplier_dir.iterdir():\r\n+                if asset_dir.is_dir():\r\n+                    metadata_file = asset_dir / 'metadata.json'\r\n+                    if metadata_file.is_file():\r\n+                        metadata_paths.append(metadata_file)\r\n+\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            # Get map info from the correct keys\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n+            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n+            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            # Combine processed and merged maps for iteration\r\n+            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n+            # Validate essential data\r\n+            if not asset_name:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+            if not all_map_resolutions:\r\n+                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+                 errors_encountered += 1\r\n+                 continue\r\n+            # map_details check remains a warning as merged maps won't be in it\r\n+            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Determine Highest Resolution ---\r\n+            highest_resolution_value = 0.0\r\n+            highest_resolution_str = \"Unknown\"\r\n+            all_resolutions_present = set()\r\n+            if all_map_resolutions: # Check combined dict\r\n+                for res_list in all_map_resolutions.values():\r\n+                    if isinstance(res_list, list):\r\n+                         all_resolutions_present.update(res_list)\r\n+\r\n+            if all_resolutions_present:\r\n+                for res_str in RESOLUTION_ORDER_DESC:\r\n+                    if res_str in all_resolutions_present:\r\n+                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n+                        highest_resolution_str = res_str\r\n+                        if highest_resolution_value > 0.0:\r\n+                            break # Found the highest valid resolution\r\n+\r\n+            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n+\r\n+            # --- Load Reference Image for Aspect Ratio ---\r\n+            ref_image_path = None\r\n+            ref_image_width = 0\r\n+            ref_image_height = 0\r\n+            ref_image_loaded = False\r\n+            # Use combined resolutions dict to find reference map\r\n+            for ref_map_type in REFERENCE_MAP_TYPES:\r\n+                if ref_map_type in all_map_resolutions:\r\n+                    available_resolutions = all_map_resolutions[ref_map_type]\r\n+                    lowest_res = None\r\n+                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+                        if res_pref in available_resolutions:\r\n+                            lowest_res = res_pref\r\n+                            break\r\n+                    if lowest_res:\r\n+                        # Get format from map_details if available, otherwise None\r\n+                        ref_map_details = map_details.get(ref_map_type, {})\r\n+                        ref_format = ref_map_details.get(\"output_format\")\r\n+                        ref_image_path = reconstruct_image_path_with_fallback(\r\n+                            asset_dir_path=asset_dir_path,\r\n+                            asset_name=asset_name,\r\n+                            map_type=ref_map_type,\r\n+                            resolution=lowest_res,\r\n+                            primary_format=ref_format # Pass None if not in map_details\r\n+                        )\r\n+                        if ref_image_path:\r\n+                            break # Found a suitable reference image path\r\n+\r\n+            if ref_image_path:\r\n+                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n+                try:\r\n+                    # Load image temporarily\r\n+                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n+                    if ref_img:\r\n+                        ref_image_width = ref_img.size[0]\r\n+                        ref_image_height = ref_img.size[1]\r\n+                        ref_image_loaded = True\r\n+                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n+                        # Remove the temporary image datablock to save memory\r\n+                        bpy.data.images.remove(ref_img)\r\n+                    else:\r\n+                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n+                except Exception as e_ref_load:\r\n+                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n+            else:\r\n+                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n+\r\n+\r\n+            # --- Manifest Check (Asset Level - Basic) ---\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Perform a quick check if *any* map needs processing for this asset\r\n+                needs_processing = False\r\n+                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n+                    for resolution in resolutions:\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            needs_processing = True\r\n+                            break\r\n+                    if needs_processing:\r\n+                        break\r\n+                if not needs_processing:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = 1.0 # Default if ref image fails\r\n+                if ref_image_loaded:\r\n+                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n+                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n+                else:\r\n+                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+\r\n+                # Check if update is needed\r\n+                current_val = aspect_node.outputs[0].default_value\r\n+                if abs(current_val - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n+                    aspect_ratio_set += 1\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Highest Resolution Value\r\n+            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n+            if hr_nodes:\r\n+                hr_node = hr_nodes[0]\r\n+                current_hr_val = hr_node.outputs[0].default_value\r\n+                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n+                    hr_node.outputs[0].default_value = highest_resolution_value\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n+                    highest_res_set += 1 # Count successful sets\r\n+            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n+            # Apply Stats (using image_stats_1k)\r\n+            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    if map_type_to_stat in image_stats_1k:\r\n+                        # Find the stats node in the parent group\r\n+                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                        if stats_nodes:\r\n+                            stats_node = stats_nodes[0]\r\n+                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n+\r\n+                            if stats and isinstance(stats, dict):\r\n+                                # Handle potential list format for RGB stats (use first value) or direct float\r\n+                                def get_stat_value(stat_val):\r\n+                                    if isinstance(stat_val, list):\r\n+                                        return stat_val[0] if stat_val else None\r\n+                                    return stat_val\r\n+\r\n+                                min_val = get_stat_value(stats.get(\"min\"))\r\n+                                max_val = get_stat_value(stats.get(\"max\"))\r\n+                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n+\r\n+                                updated_stat = False\r\n+                                # Check inputs exist before assigning\r\n+                                input_x = stats_node.inputs.get(\"X\")\r\n+                                input_y = stats_node.inputs.get(\"Y\")\r\n+                                input_z = stats_node.inputs.get(\"Z\")\r\n+\r\n+                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n+                                    input_x.default_value = min_val\r\n+                                    updated_stat = True\r\n+                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n+                                    input_y.default_value = max_val\r\n+                                    updated_stat = True\r\n+                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n+                                    input_z.default_value = mean_val\r\n+                                    updated_stat = True\r\n+\r\n+                                if updated_stat:\r\n+                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n+                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n+            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n+\r\n+            # --- Set Asset Preview (only for new parent groups) ---\r\n+            # Use the reference image path found earlier if available\r\n+            if is_new_parent and parent_group.asset_data:\r\n+                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n+                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n+                    try:\r\n+                        # Ensure the ID (node group) is the active one for the operator context\r\n+                        with context.temp_override(id=parent_group):\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+                        print(f\"      Successfully set custom preview.\")\r\n+                        previews_set += 1\r\n+                    except Exception as e_preview:\r\n+                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n+                        errors_encountered += 1\r\n+                else:\r\n+                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            # Iterate through the COMBINED map types\r\n+            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n+            for map_type, resolutions in all_map_resolutions.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Determine if this is a merged map (not in map_details)\r\n+                is_merged_map = map_type not in map_details\r\n+\r\n+                # Get details for this map type if available\r\n+                current_map_details = map_details.get(map_type, {})\r\n+                # For merged maps, primary_format will be None\r\n+                output_format = current_map_details.get(\"output_format\")\r\n+\r\n+                if not output_format and not is_merged_map:\r\n+                    # This case should ideally not happen if metadata is well-formed\r\n+                    # but handle defensively for processed maps.\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n+                    # We will rely solely on fallback for this map type\r\n+\r\n+                # Find placeholder node in parent\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                # Determine child group name (LOGICAL and ENCODED)\r\n+                logical_child_name = f\"{asset_name}_{map_type}\"\r\n+                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n+\r\n+                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name_b64 # Set encoded name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                if not isinstance(resolutions, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution in resolutions:\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Reconstruct the image path using fallback logic\r\n+                    # Pass output_format (which might be None for merged maps)\r\n+                    image_path_str = reconstruct_image_path_with_fallback(\r\n+                        asset_dir_path=asset_dir_path,\r\n+                        asset_name=asset_name,\r\n+                        map_type=map_type,\r\n+                        resolution=resolution,\r\n+                        primary_format=output_format\r\n+                    )\r\n+                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n+\r\n+                    if not image_path_str:\r\n+                        # Error already printed by reconstruct function\r\n+                        errors_encountered += 1\r\n+                        continue # Skip this resolution if path not found\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str) # Path object created from already found path string\r\n+                        # Use check_existing=True to reuse existing datablocks if path matches\r\n+                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                        if not img:\r\n+                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Only count as loaded if bpy.data.images.load succeeded\r\n+                            # Check if it's newly loaded or reused\r\n+                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n+                            if is_newly_loaded: images_loaded += 1\r\n+\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+\r\n+                    else:\r\n+                        # Increment error count if loading failed\r\n+                        if image_load_failed: errors_encountered += 1\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    # --- Explicit Save ---\r\n+    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+    try:\r\n+        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+    except Exception as e_save:\r\n+        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+        errors_encountered += 1 # Count save errors\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n+        import sys\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        # --- Argument Parsing for Asset Library Root ---\r\n+        asset_root_arg = None\r\n+        try:\r\n+            # Blender arguments passed after '--' appear in sys.argv\r\n+            if \"--\" in sys.argv:\r\n+                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+                if len(args_after_dash) >= 1:\r\n+                    asset_root_arg = args_after_dash[0]\r\n+                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+                else:\r\n+                    print(\"Info: '--' found but no arguments after it.\")\r\n+            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+        except Exception as e:\r\n+            print(f\"Error parsing command line arguments: {e}\")\r\n+        # --- End Argument Parsing ---\r\n+\r\n+        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n"
                },
                {
                    "date": 1745267735035,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,1019 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.5\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+# Changes v1.5:\r\n+#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n+#     to use actual image dimensions from a loaded reference image and the\r\n+#     `aspect_ratio_change_string`, mirroring original script logic for\r\n+#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n+#   - Added logic in main loop to load reference image for dimensions.\r\n+# Changes v1.3:\r\n+#   - Added logic to find the highest resolution present for an asset.\r\n+#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n+#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n+# Changes v1.2:\r\n+#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n+#   - Added fallback logic for reconstructing image paths with different extensions.\r\n+#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n+# Changes v1.1:\r\n+#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n+#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+import base64 # For encoding node group names\r\n+import sys # <<< ADDED IMPORT\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Mapping from resolution string to numerical value for the HighestResolution node\r\n+RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n+# Order to check resolutions to find the highest present (highest value first)\r\n+RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n+    \"COL-2\": \"sRGB\",\r\n+    \"COL-3\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n+    \"FUZZ\": \"Non-Color\",\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+# Reads stats from the 'image_stats_1k' section of metadata.json\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def encode_name_b64(name_str):\r\n+    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n+    try:\r\n+        # Ensure the input is a string\r\n+        name_str = str(name_str)\r\n+        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n+    except Exception as e:\r\n+        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n+        return name_str # Fallback to original name on error\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    # Handle potential numbered variants like COL-1, COL-2\r\n+    base_map_type = map_type.split('-')[0]\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n+               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n+\r\n+def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n+    \"\"\"\r\n+    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n+    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n+    Mirrors the logic from the original POC script.\r\n+    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n+    \"\"\"\r\n+    if image_height <= 0 or image_width <= 0:\r\n+        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+    # Calculate the actual aspect ratio of the image file\r\n+    current_aspect_ratio = image_width / image_height\r\n+\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        # If scaling was even, the correction factor is just the image's aspect ratio\r\n+        # to make UVs match the image proportions.\r\n+        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n+    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n+    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not match:\r\n+        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n+        return current_aspect_ratio # Fallback to the image's own ratio\r\n+\r\n+    axis = match.group(1).upper()\r\n+    try:\r\n+        amount = int(match.group(2))\r\n+        if amount <= 0:\r\n+            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+            return current_aspect_ratio\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Apply the non-uniform correction formula based on original script logic\r\n+    scaling_factor_percent = amount / 100.0\r\n+    correction_factor = current_aspect_ratio # Default\r\n+\r\n+    try:\r\n+        if axis == 'X':\r\n+            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n+            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n+            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n+        elif axis == 'Y':\r\n+            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n+            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n+        # No 'else' needed as regex ensures X or Y\r\n+\r\n+    except ZeroDivisionError as e:\r\n+        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+    except Exception as e:\r\n+         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+         return current_aspect_ratio\r\n+\r\n+    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n+    return correction_factor\r\n+\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n+    \"\"\"\r\n+    Constructs the expected image file path.\r\n+    If primary_format is provided, tries that first.\r\n+    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n+    Returns the found path as a string, or None if not found.\r\n+    \"\"\"\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n+        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+        return None\r\n+\r\n+    found_path = None\r\n+\r\n+    # 1. Try the primary format if provided\r\n+    if primary_format:\r\n+        try:\r\n+            filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=primary_format.lower() # Ensure format is lowercase\r\n+            )\r\n+            primary_path = asset_dir_path / filename\r\n+            if primary_path.is_file():\r\n+                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n+                return str(primary_path)\r\n+            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n+        except KeyError as e:\r\n+            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+            return None # Cannot proceed without valid pattern\r\n+        except Exception as e:\r\n+            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+            # Continue to fallback\r\n+\r\n+    # 2. Try fallback extensions\r\n+    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+        # Skip if we already tried this extension as primary (and it failed)\r\n+        if primary_format and ext.lower() == primary_format.lower():\r\n+            continue\r\n+        try:\r\n+            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=ext.lower()\r\n+            )\r\n+            fallback_path = asset_dir_path / fallback_filename\r\n+            if fallback_path.is_file():\r\n+                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n+                return str(fallback_path) # Found it!\r\n+        except KeyError:\r\n+             # Should not happen if primary format worked, but handle defensively\r\n+             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+             return None\r\n+        except Exception as e_fallback:\r\n+            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+            continue # Try next extension\r\n+\r\n+    # If we get here, neither primary nor fallbacks worked\r\n+    if primary_format:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    else:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    return None # Not found after all checks\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # Basic check if asset entry exists. Detailed check happens at map level.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context, asset_library_root_override=None): # Add override parameter\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Determine Asset Library Root ---\r\n+    if asset_library_root_override:\r\n+        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+        print(\"--- Script aborted. ---\")\r\n+        return False\r\n+    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n+    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    previews_set = 0\r\n+    highest_res_set = 0\r\n+    aspect_ratio_set = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n+    # Then scan within each supplier for asset folders containing metadata.json\r\n+    metadata_paths = []\r\n+    for supplier_dir in root_path.iterdir():\r\n+        if supplier_dir.is_dir():\r\n+            # Now look for asset folders inside the supplier directory\r\n+            for asset_dir in supplier_dir.iterdir():\r\n+                if asset_dir.is_dir():\r\n+                    metadata_file = asset_dir / 'metadata.json'\r\n+                    if metadata_file.is_file():\r\n+                        metadata_paths.append(metadata_file)\r\n+\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            # Get map info from the correct keys\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n+            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n+            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            # Combine processed and merged maps for iteration\r\n+            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n+            # Validate essential data\r\n+            if not asset_name:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+            if not all_map_resolutions:\r\n+                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+                 errors_encountered += 1\r\n+                 continue\r\n+            # map_details check remains a warning as merged maps won't be in it\r\n+            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Determine Highest Resolution ---\r\n+            highest_resolution_value = 0.0\r\n+            highest_resolution_str = \"Unknown\"\r\n+            all_resolutions_present = set()\r\n+            if all_map_resolutions: # Check combined dict\r\n+                for res_list in all_map_resolutions.values():\r\n+                    if isinstance(res_list, list):\r\n+                         all_resolutions_present.update(res_list)\r\n+\r\n+            if all_resolutions_present:\r\n+                for res_str in RESOLUTION_ORDER_DESC:\r\n+                    if res_str in all_resolutions_present:\r\n+                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n+                        highest_resolution_str = res_str\r\n+                        if highest_resolution_value > 0.0:\r\n+                            break # Found the highest valid resolution\r\n+\r\n+            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n+\r\n+            # --- Load Reference Image for Aspect Ratio ---\r\n+            ref_image_path = None\r\n+            ref_image_width = 0\r\n+            ref_image_height = 0\r\n+            ref_image_loaded = False\r\n+            # Use combined resolutions dict to find reference map\r\n+            for ref_map_type in REFERENCE_MAP_TYPES:\r\n+                if ref_map_type in all_map_resolutions:\r\n+                    available_resolutions = all_map_resolutions[ref_map_type]\r\n+                    lowest_res = None\r\n+                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+                        if res_pref in available_resolutions:\r\n+                            lowest_res = res_pref\r\n+                            break\r\n+                    if lowest_res:\r\n+                        # Get format from map_details if available, otherwise None\r\n+                        ref_map_details = map_details.get(ref_map_type, {})\r\n+                        ref_format = ref_map_details.get(\"output_format\")\r\n+                        ref_image_path = reconstruct_image_path_with_fallback(\r\n+                            asset_dir_path=asset_dir_path,\r\n+                            asset_name=asset_name,\r\n+                            map_type=ref_map_type,\r\n+                            resolution=lowest_res,\r\n+                            primary_format=ref_format # Pass None if not in map_details\r\n+                        )\r\n+                        if ref_image_path:\r\n+                            break # Found a suitable reference image path\r\n+\r\n+            if ref_image_path:\r\n+                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n+                try:\r\n+                    # Load image temporarily\r\n+                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n+                    if ref_img:\r\n+                        ref_image_width = ref_img.size[0]\r\n+                        ref_image_height = ref_img.size[1]\r\n+                        ref_image_loaded = True\r\n+                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n+                        # Remove the temporary image datablock to save memory\r\n+                        bpy.data.images.remove(ref_img)\r\n+                    else:\r\n+                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n+                except Exception as e_ref_load:\r\n+                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n+            else:\r\n+                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n+\r\n+\r\n+            # --- Manifest Check (Asset Level - Basic) ---\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Perform a quick check if *any* map needs processing for this asset\r\n+                needs_processing = False\r\n+                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n+                    for resolution in resolutions:\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            needs_processing = True\r\n+                            break\r\n+                    if needs_processing:\r\n+                        break\r\n+                if not needs_processing:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = 1.0 # Default if ref image fails\r\n+                if ref_image_loaded:\r\n+                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n+                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n+                else:\r\n+                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+\r\n+                # Check if update is needed\r\n+                current_val = aspect_node.outputs[0].default_value\r\n+                if abs(current_val - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n+                    aspect_ratio_set += 1\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Highest Resolution Value\r\n+            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n+            if hr_nodes:\r\n+                hr_node = hr_nodes[0]\r\n+                current_hr_val = hr_node.outputs[0].default_value\r\n+                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n+                    hr_node.outputs[0].default_value = highest_resolution_value\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n+                    highest_res_set += 1 # Count successful sets\r\n+            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n+            # Apply Stats (using image_stats_1k)\r\n+            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    if map_type_to_stat in image_stats_1k:\r\n+                        # Find the stats node in the parent group\r\n+                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                        if stats_nodes:\r\n+                            stats_node = stats_nodes[0]\r\n+                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n+\r\n+                            if stats and isinstance(stats, dict):\r\n+                                # Handle potential list format for RGB stats (use first value) or direct float\r\n+                                def get_stat_value(stat_val):\r\n+                                    if isinstance(stat_val, list):\r\n+                                        return stat_val[0] if stat_val else None\r\n+                                    return stat_val\r\n+\r\n+                                min_val = get_stat_value(stats.get(\"min\"))\r\n+                                max_val = get_stat_value(stats.get(\"max\"))\r\n+                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n+\r\n+                                updated_stat = False\r\n+                                # Check inputs exist before assigning\r\n+                                input_x = stats_node.inputs.get(\"X\")\r\n+                                input_y = stats_node.inputs.get(\"Y\")\r\n+                                input_z = stats_node.inputs.get(\"Z\")\r\n+\r\n+                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n+                                    input_x.default_value = min_val\r\n+                                    updated_stat = True\r\n+                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n+                                    input_y.default_value = max_val\r\n+                                    updated_stat = True\r\n+                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n+                                    input_z.default_value = mean_val\r\n+                                    updated_stat = True\r\n+\r\n+                                if updated_stat:\r\n+                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n+                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n+            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n+\r\n+            # --- Set Asset Preview (only for new parent groups) ---\r\n+            # Use the reference image path found earlier if available\r\n+            if is_new_parent and parent_group.asset_data:\r\n+                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n+                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n+                    try:\r\n+                        # Ensure the ID (node group) is the active one for the operator context\r\n+                        with context.temp_override(id=parent_group):\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+                        print(f\"      Successfully set custom preview.\")\r\n+                        previews_set += 1\r\n+                    except Exception as e_preview:\r\n+                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n+                        errors_encountered += 1\r\n+                else:\r\n+                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            # Iterate through the COMBINED map types\r\n+            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n+            for map_type, resolutions in all_map_resolutions.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Determine if this is a merged map (not in map_details)\r\n+                is_merged_map = map_type not in map_details\r\n+\r\n+                # Get details for this map type if available\r\n+                current_map_details = map_details.get(map_type, {})\r\n+                # For merged maps, primary_format will be None\r\n+                output_format = current_map_details.get(\"output_format\")\r\n+\r\n+                if not output_format and not is_merged_map:\r\n+                    # This case should ideally not happen if metadata is well-formed\r\n+                    # but handle defensively for processed maps.\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n+                    # We will rely solely on fallback for this map type\r\n+\r\n+                # Find placeholder node in parent\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                # Determine child group name (LOGICAL and ENCODED)\r\n+                logical_child_name = f\"{asset_name}_{map_type}\"\r\n+                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n+\r\n+                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name_b64 # Set encoded name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                if not isinstance(resolutions, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution in resolutions:\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Reconstruct the image path using fallback logic\r\n+                    # Pass output_format (which might be None for merged maps)\r\n+                    image_path_str = reconstruct_image_path_with_fallback(\r\n+                        asset_dir_path=asset_dir_path,\r\n+                        asset_name=asset_name,\r\n+                        map_type=map_type,\r\n+                        resolution=resolution,\r\n+                        primary_format=output_format\r\n+                    )\r\n+                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n+\r\n+                    if not image_path_str:\r\n+                        # Error already printed by reconstruct function\r\n+                        errors_encountered += 1\r\n+                        continue # Skip this resolution if path not found\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str) # Path object created from already found path string\r\n+                        # Use check_existing=True to reuse existing datablocks if path matches\r\n+                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                        if not img:\r\n+                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Only count as loaded if bpy.data.images.load succeeded\r\n+                            # Check if it's newly loaded or reused\r\n+                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n+                            if is_newly_loaded: images_loaded += 1\r\n+\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+\r\n+                    else:\r\n+                        # Increment error count if loading failed\r\n+                        if image_load_failed: errors_encountered += 1\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    # --- Explicit Save ---\r\n+    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+    try:\r\n+        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+    except Exception as e_save:\r\n+        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+        errors_encountered += 1 # Count save errors\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n+        import sys\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        # --- Argument Parsing for Asset Library Root ---\r\n+        asset_root_arg = None\r\n+        try:\r\n+            # Blender arguments passed after '--' appear in sys.argv\r\n+            if \"--\" in sys.argv:\r\n+                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+                if len(args_after_dash) >= 1:\r\n+                    asset_root_arg = args_after_dash[0]\r\n+                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+                else:\r\n+                    print(\"Info: '--' found but no arguments after it.\")\r\n+            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+        except Exception as e:\r\n+            print(f\"Error parsing command line arguments: {e}\")\r\n+        # --- End Argument Parsing ---\r\n+\r\n+        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n"
                },
                {
                    "date": 1745309402905,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -489,8 +489,9 @@\n             # --- Extract Key Info ---\r\n             asset_name = metadata.get(\"asset_name\")\r\n             supplier_name = metadata.get(\"supplier_name\")\r\n             archetype = metadata.get(\"archetype\")\r\n+            category = metadata.get(\"category\", \"Unknown\") # <<< ADDED: Extract category, default to \"Unknown\"\r\n             # Get map info from the correct keys\r\n             processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n             merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n             map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n@@ -632,1030 +633,19 @@\n                 if supplier_name:\r\n                     add_tag_if_new(parent_group.asset_data, supplier_name)\r\n                 if archetype:\r\n                     add_tag_if_new(parent_group.asset_data, archetype)\r\n+                if category: # <<< ADDED: Add category tag\r\n+                    add_tag_if_new(parent_group.asset_data, category)\r\n                 # Add other tags if needed\r\n             # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n \r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = 1.0 # Default if ref image fails\r\n-                if ref_image_loaded:\r\n-                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n-                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n-                else:\r\n-                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+            # <<< ADDED: Conditional skip based on category >>>\r\n+            if category not in [\"Surface\", \"Decal\"]:\r\n+                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{category}'). Tag added.\")\r\n+                assets_processed += 1 # Still count as processed for summary, even if skipped\r\n+                continue # Skip the rest of the processing for this asset\r\n \r\n-                # Check if update is needed\r\n-                current_val = aspect_node.outputs[0].default_value\r\n-                if abs(current_val - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n-                    aspect_ratio_set += 1\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Highest Resolution Value\r\n-            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n-            if hr_nodes:\r\n-                hr_node = hr_nodes[0]\r\n-                current_hr_val = hr_node.outputs[0].default_value\r\n-                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n-                    hr_node.outputs[0].default_value = highest_resolution_value\r\n-                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n-                    highest_res_set += 1 # Count successful sets\r\n-            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-\r\n-            # Apply Stats (using image_stats_1k)\r\n-            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n-                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                    if map_type_to_stat in image_stats_1k:\r\n-                        # Find the stats node in the parent group\r\n-                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                        if stats_nodes:\r\n-                            stats_node = stats_nodes[0]\r\n-                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n-\r\n-                            if stats and isinstance(stats, dict):\r\n-                                # Handle potential list format for RGB stats (use first value) or direct float\r\n-                                def get_stat_value(stat_val):\r\n-                                    if isinstance(stat_val, list):\r\n-                                        return stat_val[0] if stat_val else None\r\n-                                    return stat_val\r\n-\r\n-                                min_val = get_stat_value(stats.get(\"min\"))\r\n-                                max_val = get_stat_value(stats.get(\"max\"))\r\n-                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n-\r\n-                                updated_stat = False\r\n-                                # Check inputs exist before assigning\r\n-                                input_x = stats_node.inputs.get(\"X\")\r\n-                                input_y = stats_node.inputs.get(\"Y\")\r\n-                                input_z = stats_node.inputs.get(\"Z\")\r\n-\r\n-                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n-                                    input_x.default_value = min_val\r\n-                                    updated_stat = True\r\n-                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n-                                    input_y.default_value = max_val\r\n-                                    updated_stat = True\r\n-                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n-                                    input_z.default_value = mean_val\r\n-                                    updated_stat = True\r\n-\r\n-                                if updated_stat:\r\n-                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n-                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n-            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n-\r\n-            # --- Set Asset Preview (only for new parent groups) ---\r\n-            # Use the reference image path found earlier if available\r\n-            if is_new_parent and parent_group.asset_data:\r\n-                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n-                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n-                    try:\r\n-                        # Ensure the ID (node group) is the active one for the operator context\r\n-                        with context.temp_override(id=parent_group):\r\n-                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n-                        print(f\"      Successfully set custom preview.\")\r\n-                        previews_set += 1\r\n-                    except Exception as e_preview:\r\n-                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n-                        errors_encountered += 1\r\n-                else:\r\n-                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            # Iterate through the COMBINED map types\r\n-            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n-            for map_type, resolutions in all_map_resolutions.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Determine if this is a merged map (not in map_details)\r\n-                is_merged_map = map_type not in map_details\r\n-\r\n-                # Get details for this map type if available\r\n-                current_map_details = map_details.get(map_type, {})\r\n-                # For merged maps, primary_format will be None\r\n-                output_format = current_map_details.get(\"output_format\")\r\n-\r\n-                if not output_format and not is_merged_map:\r\n-                    # This case should ideally not happen if metadata is well-formed\r\n-                    # but handle defensively for processed maps.\r\n-                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n-                    # We will rely solely on fallback for this map type\r\n-\r\n-                # Find placeholder node in parent\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                # Determine child group name (LOGICAL and ENCODED)\r\n-                logical_child_name = f\"{asset_name}_{map_type}\"\r\n-                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n-\r\n-                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name_b64 # Set encoded name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                if not isinstance(resolutions, list):\r\n-                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution in resolutions:\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Reconstruct the image path using fallback logic\r\n-                    # Pass output_format (which might be None for merged maps)\r\n-                    image_path_str = reconstruct_image_path_with_fallback(\r\n-                        asset_dir_path=asset_dir_path,\r\n-                        asset_name=asset_name,\r\n-                        map_type=map_type,\r\n-                        resolution=resolution,\r\n-                        primary_format=output_format\r\n-                    )\r\n-                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n-\r\n-                    if not image_path_str:\r\n-                        # Error already printed by reconstruct function\r\n-                        errors_encountered += 1\r\n-                        continue # Skip this resolution if path not found\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str) # Path object created from already found path string\r\n-                        # Use check_existing=True to reuse existing datablocks if path matches\r\n-                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                        if not img:\r\n-                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Only count as loaded if bpy.data.images.load succeeded\r\n-                            # Check if it's newly loaded or reused\r\n-                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n-                            if is_newly_loaded: images_loaded += 1\r\n-\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-\r\n-                    else:\r\n-                        # Increment error count if loading failed\r\n-                        if image_load_failed: errors_encountered += 1\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n-    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    # --- Explicit Save ---\r\n-    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n-    try:\r\n-        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n-        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n-    except Exception as e_save:\r\n-        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n-        errors_encountered += 1 # Count save errors\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-        import base64 # Ensure base64 is imported here too if needed globally\r\n-        import sys\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        # --- Argument Parsing for Asset Library Root ---\r\n-        asset_root_arg = None\r\n-        try:\r\n-            # Blender arguments passed after '--' appear in sys.argv\r\n-            if \"--\" in sys.argv:\r\n-                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n-                if len(args_after_dash) >= 1:\r\n-                    asset_root_arg = args_after_dash[0]\r\n-                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n-                else:\r\n-                    print(\"Info: '--' found but no arguments after it.\")\r\n-            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n-        except Exception as e:\r\n-            print(f\"Error parsing command line arguments: {e}\")\r\n-        # --- End Argument Parsing ---\r\n-\r\n-        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.5\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-# Changes v1.5:\r\n-#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n-#     to use actual image dimensions from a loaded reference image and the\r\n-#     `aspect_ratio_change_string`, mirroring original script logic for\r\n-#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n-#   - Added logic in main loop to load reference image for dimensions.\r\n-# Changes v1.3:\r\n-#   - Added logic to find the highest resolution present for an asset.\r\n-#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n-#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n-# Changes v1.2:\r\n-#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n-#   - Added fallback logic for reconstructing image paths with different extensions.\r\n-#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n-# Changes v1.1:\r\n-#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n-#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-import base64 # For encoding node group names\r\n-import sys # <<< ADDED IMPORT\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-# This will be overridden by command-line arguments if provided.\r\n-PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True # Disabled based on user feedback in previous run\r\n-\r\n-# Assumed filename pattern for processed images.\r\n-# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n-# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n-IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n-\r\n-# Fallback extensions to try if the primary format from metadata is not found\r\n-# Order matters - first found will be used.\r\n-FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n-\r\n-# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n-# The script will look for these in order and use the first one found.\r\n-REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n-# Preferred resolution order for reference image (lowest first is often faster)\r\n-REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n-\r\n-# Mapping from resolution string to numerical value for the HighestResolution node\r\n-RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n-# Order to check resolutions to find the highest present (highest value first)\r\n-RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n-    \"COL-2\": \"sRGB\",\r\n-    \"COL-3\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n-    \"FUZZ\": \"Non-Color\",\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-# Reads stats from the 'image_stats_1k' section of metadata.json\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def encode_name_b64(name_str):\r\n-    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n-    try:\r\n-        # Ensure the input is a string\r\n-        name_str = str(name_str)\r\n-        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n-    except Exception as e:\r\n-        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n-        return name_str # Fallback to original name on error\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    # Handle potential numbered variants like COL-1, COL-2\r\n-    base_map_type = map_type.split('-')[0]\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n-               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n-\r\n-def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n-    \"\"\"\r\n-    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n-    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n-    Mirrors the logic from the original POC script.\r\n-    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n-    \"\"\"\r\n-    if image_height <= 0 or image_width <= 0:\r\n-        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-    # Calculate the actual aspect ratio of the image file\r\n-    current_aspect_ratio = image_width / image_height\r\n-\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        # If scaling was even, the correction factor is just the image's aspect ratio\r\n-        # to make UVs match the image proportions.\r\n-        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n-    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n-    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    if not match:\r\n-        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n-        return current_aspect_ratio # Fallback to the image's own ratio\r\n-\r\n-    axis = match.group(1).upper()\r\n-    try:\r\n-        amount = int(match.group(2))\r\n-        if amount <= 0:\r\n-            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-            return current_aspect_ratio\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Apply the non-uniform correction formula based on original script logic\r\n-    scaling_factor_percent = amount / 100.0\r\n-    correction_factor = current_aspect_ratio # Default\r\n-\r\n-    try:\r\n-        if axis == 'X':\r\n-            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n-            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n-            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n-        elif axis == 'Y':\r\n-            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n-            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n-        # No 'else' needed as regex ensures X or Y\r\n-\r\n-    except ZeroDivisionError as e:\r\n-        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-    except Exception as e:\r\n-         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-         return current_aspect_ratio\r\n-\r\n-    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n-    return correction_factor\r\n-\r\n-\r\n-def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n-    \"\"\"\r\n-    Constructs the expected image file path.\r\n-    If primary_format is provided, tries that first.\r\n-    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n-    Returns the found path as a string, or None if not found.\r\n-    \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n-        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n-        return None\r\n-\r\n-    found_path = None\r\n-\r\n-    # 1. Try the primary format if provided\r\n-    if primary_format:\r\n-        try:\r\n-            filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=primary_format.lower() # Ensure format is lowercase\r\n-            )\r\n-            primary_path = asset_dir_path / filename\r\n-            if primary_path.is_file():\r\n-                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n-                return str(primary_path)\r\n-            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n-        except KeyError as e:\r\n-            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-            return None # Cannot proceed without valid pattern\r\n-        except Exception as e:\r\n-            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n-            # Continue to fallback\r\n-\r\n-    # 2. Try fallback extensions\r\n-    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n-    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        # Skip if we already tried this extension as primary (and it failed)\r\n-        if primary_format and ext.lower() == primary_format.lower():\r\n-            continue\r\n-        try:\r\n-            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=ext.lower()\r\n-            )\r\n-            fallback_path = asset_dir_path / fallback_filename\r\n-            if fallback_path.is_file():\r\n-                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n-                return str(fallback_path) # Found it!\r\n-        except KeyError:\r\n-             # Should not happen if primary format worked, but handle defensively\r\n-             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n-             return None\r\n-        except Exception as e_fallback:\r\n-            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n-            continue # Try next extension\r\n-\r\n-    # If we get here, neither primary nor fallbacks worked\r\n-    if primary_format:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    else:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    return None # Not found after all checks\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # Basic check if asset entry exists. Detailed check happens at map level.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context, asset_library_root_override=None): # Add override parameter\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Determine Asset Library Root ---\r\n-    if asset_library_root_override:\r\n-        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n-        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n-        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n-        print(\"--- Script aborted. ---\")\r\n-        return False\r\n-    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n-    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    previews_set = 0\r\n-    highest_res_set = 0\r\n-    aspect_ratio_set = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n-    # Then scan within each supplier for asset folders containing metadata.json\r\n-    metadata_paths = []\r\n-    for supplier_dir in root_path.iterdir():\r\n-        if supplier_dir.is_dir():\r\n-            # Now look for asset folders inside the supplier directory\r\n-            for asset_dir in supplier_dir.iterdir():\r\n-                if asset_dir.is_dir():\r\n-                    metadata_file = asset_dir / 'metadata.json'\r\n-                    if metadata_file.is_file():\r\n-                        metadata_paths.append(metadata_file)\r\n-\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            # Get map info from the correct keys\r\n-            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n-            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n-            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n-            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            # Combine processed and merged maps for iteration\r\n-            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n-\r\n-            # Validate essential data\r\n-            if not asset_name:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-            if not all_map_resolutions:\r\n-                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n-                 errors_encountered += 1\r\n-                 continue\r\n-            # map_details check remains a warning as merged maps won't be in it\r\n-            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Determine Highest Resolution ---\r\n-            highest_resolution_value = 0.0\r\n-            highest_resolution_str = \"Unknown\"\r\n-            all_resolutions_present = set()\r\n-            if all_map_resolutions: # Check combined dict\r\n-                for res_list in all_map_resolutions.values():\r\n-                    if isinstance(res_list, list):\r\n-                         all_resolutions_present.update(res_list)\r\n-\r\n-            if all_resolutions_present:\r\n-                for res_str in RESOLUTION_ORDER_DESC:\r\n-                    if res_str in all_resolutions_present:\r\n-                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n-                        highest_resolution_str = res_str\r\n-                        if highest_resolution_value > 0.0:\r\n-                            break # Found the highest valid resolution\r\n-\r\n-            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n-\r\n-            # --- Load Reference Image for Aspect Ratio ---\r\n-            ref_image_path = None\r\n-            ref_image_width = 0\r\n-            ref_image_height = 0\r\n-            ref_image_loaded = False\r\n-            # Use combined resolutions dict to find reference map\r\n-            for ref_map_type in REFERENCE_MAP_TYPES:\r\n-                if ref_map_type in all_map_resolutions:\r\n-                    available_resolutions = all_map_resolutions[ref_map_type]\r\n-                    lowest_res = None\r\n-                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n-                        if res_pref in available_resolutions:\r\n-                            lowest_res = res_pref\r\n-                            break\r\n-                    if lowest_res:\r\n-                        # Get format from map_details if available, otherwise None\r\n-                        ref_map_details = map_details.get(ref_map_type, {})\r\n-                        ref_format = ref_map_details.get(\"output_format\")\r\n-                        ref_image_path = reconstruct_image_path_with_fallback(\r\n-                            asset_dir_path=asset_dir_path,\r\n-                            asset_name=asset_name,\r\n-                            map_type=ref_map_type,\r\n-                            resolution=lowest_res,\r\n-                            primary_format=ref_format # Pass None if not in map_details\r\n-                        )\r\n-                        if ref_image_path:\r\n-                            break # Found a suitable reference image path\r\n-\r\n-            if ref_image_path:\r\n-                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n-                try:\r\n-                    # Load image temporarily\r\n-                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n-                    if ref_img:\r\n-                        ref_image_width = ref_img.size[0]\r\n-                        ref_image_height = ref_img.size[1]\r\n-                        ref_image_loaded = True\r\n-                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n-                        # Remove the temporary image datablock to save memory\r\n-                        bpy.data.images.remove(ref_img)\r\n-                    else:\r\n-                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n-                except Exception as e_ref_load:\r\n-                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n-            else:\r\n-                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n-\r\n-\r\n-            # --- Manifest Check (Asset Level - Basic) ---\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Perform a quick check if *any* map needs processing for this asset\r\n-                needs_processing = False\r\n-                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n-                    for resolution in resolutions:\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            needs_processing = True\r\n-                            break\r\n-                    if needs_processing:\r\n-                        break\r\n-                if not needs_processing:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n             # Apply Aspect Ratio Correction\r\n             aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n             if aspect_nodes:\r\n                 aspect_node = aspect_nodes[0]\r\n@@ -2035,1023 +1025,4 @@\n             print(f\"Error parsing command line arguments: {e}\")\r\n         # --- End Argument Parsing ---\r\n \r\n         process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.5\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-# Changes v1.5:\r\n-#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n-#     to use actual image dimensions from a loaded reference image and the\r\n-#     `aspect_ratio_change_string`, mirroring original script logic for\r\n-#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n-#   - Added logic in main loop to load reference image for dimensions.\r\n-# Changes v1.3:\r\n-#   - Added logic to find the highest resolution present for an asset.\r\n-#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n-#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n-# Changes v1.2:\r\n-#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n-#   - Added fallback logic for reconstructing image paths with different extensions.\r\n-#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n-# Changes v1.1:\r\n-#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n-#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-import base64 # For encoding node group names\r\n-import sys # <<< ADDED IMPORT\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-# This will be overridden by command-line arguments if provided.\r\n-PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = True # Disabled based on user feedback in previous run\r\n-\r\n-# Assumed filename pattern for processed images.\r\n-# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n-# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n-IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n-\r\n-# Fallback extensions to try if the primary format from metadata is not found\r\n-# Order matters - first found will be used.\r\n-FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n-\r\n-# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n-# The script will look for these in order and use the first one found.\r\n-REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n-# Preferred resolution order for reference image (lowest first is often faster)\r\n-REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n-\r\n-# Mapping from resolution string to numerical value for the HighestResolution node\r\n-RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n-# Order to check resolutions to find the highest present (highest value first)\r\n-RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n-    \"COL-2\": \"sRGB\",\r\n-    \"COL-3\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n-    \"FUZZ\": \"Non-Color\",\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-# Reads stats from the 'image_stats_1k' section of metadata.json\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def encode_name_b64(name_str):\r\n-    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n-    try:\r\n-        # Ensure the input is a string\r\n-        name_str = str(name_str)\r\n-        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n-    except Exception as e:\r\n-        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n-        return name_str # Fallback to original name on error\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    # Handle potential numbered variants like COL-1, COL-2\r\n-    base_map_type = map_type.split('-')[0]\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n-               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n-\r\n-def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n-    \"\"\"\r\n-    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n-    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n-    Mirrors the logic from the original POC script.\r\n-    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n-    \"\"\"\r\n-    if image_height <= 0 or image_width <= 0:\r\n-        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-    # Calculate the actual aspect ratio of the image file\r\n-    current_aspect_ratio = image_width / image_height\r\n-\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        # If scaling was even, the correction factor is just the image's aspect ratio\r\n-        # to make UVs match the image proportions.\r\n-        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n-    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n-    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    if not match:\r\n-        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n-        return current_aspect_ratio # Fallback to the image's own ratio\r\n-\r\n-    axis = match.group(1).upper()\r\n-    try:\r\n-        amount = int(match.group(2))\r\n-        if amount <= 0:\r\n-            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-            return current_aspect_ratio\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Apply the non-uniform correction formula based on original script logic\r\n-    scaling_factor_percent = amount / 100.0\r\n-    correction_factor = current_aspect_ratio # Default\r\n-\r\n-    try:\r\n-        if axis == 'X':\r\n-            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n-            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n-            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n-        elif axis == 'Y':\r\n-            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n-            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n-        # No 'else' needed as regex ensures X or Y\r\n-\r\n-    except ZeroDivisionError as e:\r\n-        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-    except Exception as e:\r\n-         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-         return current_aspect_ratio\r\n-\r\n-    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n-    return correction_factor\r\n-\r\n-\r\n-def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n-    \"\"\"\r\n-    Constructs the expected image file path.\r\n-    If primary_format is provided, tries that first.\r\n-    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n-    Returns the found path as a string, or None if not found.\r\n-    \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n-        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n-        return None\r\n-\r\n-    found_path = None\r\n-\r\n-    # 1. Try the primary format if provided\r\n-    if primary_format:\r\n-        try:\r\n-            filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=primary_format.lower() # Ensure format is lowercase\r\n-            )\r\n-            primary_path = asset_dir_path / filename\r\n-            if primary_path.is_file():\r\n-                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n-                return str(primary_path)\r\n-            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n-        except KeyError as e:\r\n-            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-            return None # Cannot proceed without valid pattern\r\n-        except Exception as e:\r\n-            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n-            # Continue to fallback\r\n-\r\n-    # 2. Try fallback extensions\r\n-    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n-    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        # Skip if we already tried this extension as primary (and it failed)\r\n-        if primary_format and ext.lower() == primary_format.lower():\r\n-            continue\r\n-        try:\r\n-            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=ext.lower()\r\n-            )\r\n-            fallback_path = asset_dir_path / fallback_filename\r\n-            if fallback_path.is_file():\r\n-                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n-                return str(fallback_path) # Found it!\r\n-        except KeyError:\r\n-             # Should not happen if primary format worked, but handle defensively\r\n-             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n-             return None\r\n-        except Exception as e_fallback:\r\n-            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n-            continue # Try next extension\r\n-\r\n-    # If we get here, neither primary nor fallbacks worked\r\n-    if primary_format:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    else:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    return None # Not found after all checks\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # Basic check if asset entry exists. Detailed check happens at map level.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context, asset_library_root_override=None): # Add override parameter\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Determine Asset Library Root ---\r\n-    if asset_library_root_override:\r\n-        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n-        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n-        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n-        print(\"--- Script aborted. ---\")\r\n-        return False\r\n-    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n-    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    previews_set = 0\r\n-    highest_res_set = 0\r\n-    aspect_ratio_set = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n-    # Then scan within each supplier for asset folders containing metadata.json\r\n-    metadata_paths = []\r\n-    for supplier_dir in root_path.iterdir():\r\n-        if supplier_dir.is_dir():\r\n-            # Now look for asset folders inside the supplier directory\r\n-            for asset_dir in supplier_dir.iterdir():\r\n-                if asset_dir.is_dir():\r\n-                    metadata_file = asset_dir / 'metadata.json'\r\n-                    if metadata_file.is_file():\r\n-                        metadata_paths.append(metadata_file)\r\n-\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            # Get map info from the correct keys\r\n-            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n-            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n-            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n-            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            # Combine processed and merged maps for iteration\r\n-            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n-\r\n-            # Validate essential data\r\n-            if not asset_name:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-            if not all_map_resolutions:\r\n-                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n-                 errors_encountered += 1\r\n-                 continue\r\n-            # map_details check remains a warning as merged maps won't be in it\r\n-            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Determine Highest Resolution ---\r\n-            highest_resolution_value = 0.0\r\n-            highest_resolution_str = \"Unknown\"\r\n-            all_resolutions_present = set()\r\n-            if all_map_resolutions: # Check combined dict\r\n-                for res_list in all_map_resolutions.values():\r\n-                    if isinstance(res_list, list):\r\n-                         all_resolutions_present.update(res_list)\r\n-\r\n-            if all_resolutions_present:\r\n-                for res_str in RESOLUTION_ORDER_DESC:\r\n-                    if res_str in all_resolutions_present:\r\n-                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n-                        highest_resolution_str = res_str\r\n-                        if highest_resolution_value > 0.0:\r\n-                            break # Found the highest valid resolution\r\n-\r\n-            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n-\r\n-            # --- Load Reference Image for Aspect Ratio ---\r\n-            ref_image_path = None\r\n-            ref_image_width = 0\r\n-            ref_image_height = 0\r\n-            ref_image_loaded = False\r\n-            # Use combined resolutions dict to find reference map\r\n-            for ref_map_type in REFERENCE_MAP_TYPES:\r\n-                if ref_map_type in all_map_resolutions:\r\n-                    available_resolutions = all_map_resolutions[ref_map_type]\r\n-                    lowest_res = None\r\n-                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n-                        if res_pref in available_resolutions:\r\n-                            lowest_res = res_pref\r\n-                            break\r\n-                    if lowest_res:\r\n-                        # Get format from map_details if available, otherwise None\r\n-                        ref_map_details = map_details.get(ref_map_type, {})\r\n-                        ref_format = ref_map_details.get(\"output_format\")\r\n-                        ref_image_path = reconstruct_image_path_with_fallback(\r\n-                            asset_dir_path=asset_dir_path,\r\n-                            asset_name=asset_name,\r\n-                            map_type=ref_map_type,\r\n-                            resolution=lowest_res,\r\n-                            primary_format=ref_format # Pass None if not in map_details\r\n-                        )\r\n-                        if ref_image_path:\r\n-                            break # Found a suitable reference image path\r\n-\r\n-            if ref_image_path:\r\n-                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n-                try:\r\n-                    # Load image temporarily\r\n-                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n-                    if ref_img:\r\n-                        ref_image_width = ref_img.size[0]\r\n-                        ref_image_height = ref_img.size[1]\r\n-                        ref_image_loaded = True\r\n-                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n-                        # Remove the temporary image datablock to save memory\r\n-                        bpy.data.images.remove(ref_img)\r\n-                    else:\r\n-                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n-                except Exception as e_ref_load:\r\n-                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n-            else:\r\n-                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n-\r\n-\r\n-            # --- Manifest Check (Asset Level - Basic) ---\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Perform a quick check if *any* map needs processing for this asset\r\n-                needs_processing = False\r\n-                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n-                    for resolution in resolutions:\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            needs_processing = True\r\n-                            break\r\n-                    if needs_processing:\r\n-                        break\r\n-                if not needs_processing:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = 1.0 # Default if ref image fails\r\n-                if ref_image_loaded:\r\n-                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n-                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n-                else:\r\n-                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n-\r\n-                # Check if update is needed\r\n-                current_val = aspect_node.outputs[0].default_value\r\n-                if abs(current_val - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n-                    aspect_ratio_set += 1\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Highest Resolution Value\r\n-            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n-            if hr_nodes:\r\n-                hr_node = hr_nodes[0]\r\n-                current_hr_val = hr_node.outputs[0].default_value\r\n-                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n-                    hr_node.outputs[0].default_value = highest_resolution_value\r\n-                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n-                    highest_res_set += 1 # Count successful sets\r\n-            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-\r\n-            # Apply Stats (using image_stats_1k)\r\n-            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n-                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                    if map_type_to_stat in image_stats_1k:\r\n-                        # Find the stats node in the parent group\r\n-                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                        if stats_nodes:\r\n-                            stats_node = stats_nodes[0]\r\n-                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n-\r\n-                            if stats and isinstance(stats, dict):\r\n-                                # Handle potential list format for RGB stats (use first value) or direct float\r\n-                                def get_stat_value(stat_val):\r\n-                                    if isinstance(stat_val, list):\r\n-                                        return stat_val[0] if stat_val else None\r\n-                                    return stat_val\r\n-\r\n-                                min_val = get_stat_value(stats.get(\"min\"))\r\n-                                max_val = get_stat_value(stats.get(\"max\"))\r\n-                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n-\r\n-                                updated_stat = False\r\n-                                # Check inputs exist before assigning\r\n-                                input_x = stats_node.inputs.get(\"X\")\r\n-                                input_y = stats_node.inputs.get(\"Y\")\r\n-                                input_z = stats_node.inputs.get(\"Z\")\r\n-\r\n-                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n-                                    input_x.default_value = min_val\r\n-                                    updated_stat = True\r\n-                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n-                                    input_y.default_value = max_val\r\n-                                    updated_stat = True\r\n-                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n-                                    input_z.default_value = mean_val\r\n-                                    updated_stat = True\r\n-\r\n-                                if updated_stat:\r\n-                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n-                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n-            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n-\r\n-            # --- Set Asset Preview (only for new parent groups) ---\r\n-            # Use the reference image path found earlier if available\r\n-            if is_new_parent and parent_group.asset_data:\r\n-                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n-                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n-                    try:\r\n-                        # Ensure the ID (node group) is the active one for the operator context\r\n-                        with context.temp_override(id=parent_group):\r\n-                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n-                        print(f\"      Successfully set custom preview.\")\r\n-                        previews_set += 1\r\n-                    except Exception as e_preview:\r\n-                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n-                        errors_encountered += 1\r\n-                else:\r\n-                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            # Iterate through the COMBINED map types\r\n-            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n-            for map_type, resolutions in all_map_resolutions.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Determine if this is a merged map (not in map_details)\r\n-                is_merged_map = map_type not in map_details\r\n-\r\n-                # Get details for this map type if available\r\n-                current_map_details = map_details.get(map_type, {})\r\n-                # For merged maps, primary_format will be None\r\n-                output_format = current_map_details.get(\"output_format\")\r\n-\r\n-                if not output_format and not is_merged_map:\r\n-                    # This case should ideally not happen if metadata is well-formed\r\n-                    # but handle defensively for processed maps.\r\n-                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n-                    # We will rely solely on fallback for this map type\r\n-\r\n-                # Find placeholder node in parent\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                # Determine child group name (LOGICAL and ENCODED)\r\n-                logical_child_name = f\"{asset_name}_{map_type}\"\r\n-                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n-\r\n-                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name_b64 # Set encoded name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                if not isinstance(resolutions, list):\r\n-                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution in resolutions:\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Reconstruct the image path using fallback logic\r\n-                    # Pass output_format (which might be None for merged maps)\r\n-                    image_path_str = reconstruct_image_path_with_fallback(\r\n-                        asset_dir_path=asset_dir_path,\r\n-                        asset_name=asset_name,\r\n-                        map_type=map_type,\r\n-                        resolution=resolution,\r\n-                        primary_format=output_format\r\n-                    )\r\n-                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n-\r\n-                    if not image_path_str:\r\n-                        # Error already printed by reconstruct function\r\n-                        errors_encountered += 1\r\n-                        continue # Skip this resolution if path not found\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str) # Path object created from already found path string\r\n-                        # Use check_existing=True to reuse existing datablocks if path matches\r\n-                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                        if not img:\r\n-                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Only count as loaded if bpy.data.images.load succeeded\r\n-                            # Check if it's newly loaded or reused\r\n-                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n-                            if is_newly_loaded: images_loaded += 1\r\n-\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-\r\n-                    else:\r\n-                        # Increment error count if loading failed\r\n-                        if image_load_failed: errors_encountered += 1\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n-    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    # --- Explicit Save ---\r\n-    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n-    try:\r\n-        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n-        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n-    except Exception as e_save:\r\n-        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n-        errors_encountered += 1 # Count save errors\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-        import base64 # Ensure base64 is imported here too if needed globally\r\n-        import sys\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        # --- Argument Parsing for Asset Library Root ---\r\n-        asset_root_arg = None\r\n-        try:\r\n-            # Blender arguments passed after '--' appear in sys.argv\r\n-            if \"--\" in sys.argv:\r\n-                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n-                if len(args_after_dash) >= 1:\r\n-                    asset_root_arg = args_after_dash[0]\r\n-                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n-                else:\r\n-                    print(\"Info: '--' found but no arguments after it.\")\r\n-            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n-        except Exception as e:\r\n-            print(f\"Error parsing command line arguments: {e}\")\r\n-        # --- End Argument Parsing ---\r\n-\r\n-        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n"
                },
                {
                    "date": 1745309684670,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -97,8 +97,11 @@\n # Map types for which stats should be applied (if found in metadata and node exists)\r\n # Reads stats from the 'image_stats_1k' section of metadata.json\r\n APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n \r\n+# Categories for which full nodegroup generation should occur\r\n+CATEGORIES_FOR_NODEGROUP_GENERATION = [\"Surface\", \"Decal\"]\r\n+\r\n # --- END USER CONFIGURATION ---\r\n \r\n \r\n # --- Helper Functions ---\r\n@@ -639,9 +642,9 @@\n                 # Add other tags if needed\r\n             # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n \r\n             # <<< ADDED: Conditional skip based on category >>>\r\n-            if category not in [\"Surface\", \"Decal\"]:\r\n+            if category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # <<< MODIFIED: Use config variable\r\n                 print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{category}'). Tag added.\")\r\n                 assets_processed += 1 # Still count as processed for summary, even if skipped\r\n                 continue # Skip the rest of the processing for this asset\r\n \r\n"
                },
                {
                    "date": 1745345342856,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,1031 @@\n+# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n+# Version: 1.5\r\n+# Description: Scans a library processed by the Asset Processor Tool,\r\n+#              reads metadata.json files, and creates/updates corresponding\r\n+#              PBR node groups in the active Blender file.\r\n+# Changes v1.5:\r\n+#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n+#     to use actual image dimensions from a loaded reference image and the\r\n+#     `aspect_ratio_change_string`, mirroring original script logic for\r\n+#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n+#   - Added logic in main loop to load reference image for dimensions.\r\n+# Changes v1.3:\r\n+#   - Added logic to find the highest resolution present for an asset.\r\n+#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n+#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n+# Changes v1.2:\r\n+#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n+#   - Added fallback logic for reconstructing image paths with different extensions.\r\n+#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n+# Changes v1.1:\r\n+#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n+#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n+\r\n+import bpy\r\n+import os\r\n+import json\r\n+from pathlib import Path\r\n+import time\r\n+import re # For parsing aspect ratio string\r\n+import base64 # For encoding node group names\r\n+import sys # <<< ADDED IMPORT\r\n+\r\n+# --- USER CONFIGURATION ---\r\n+\r\n+# Path to the root output directory of the Asset Processor Tool\r\n+# Example: r\"G:\\Assets\\Processed\"\r\n+# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n+# This will be overridden by command-line arguments if provided.\r\n+PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n+\r\n+# Names of the required node group templates in the Blender file\r\n+PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n+CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n+\r\n+# Labels of specific nodes within the PARENT template\r\n+ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n+STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n+HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n+\r\n+# Enable/disable the manifest system to track processed assets/maps\r\n+# If enabled, requires the blend file to be saved.\r\n+ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n+\r\n+# Assumed filename pattern for processed images.\r\n+# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n+# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n+IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n+\r\n+# Fallback extensions to try if the primary format from metadata is not found\r\n+# Order matters - first found will be used.\r\n+FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n+\r\n+# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n+# The script will look for these in order and use the first one found.\r\n+REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n+# Preferred resolution order for reference image (lowest first is often faster)\r\n+REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n+\r\n+# Mapping from resolution string to numerical value for the HighestResolution node\r\n+RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n+# Order to check resolutions to find the highest present (highest value first)\r\n+RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n+\r\n+# Map PBR type strings (from metadata) to Blender color spaces\r\n+# Add more mappings as needed based on your metadata types\r\n+PBR_COLOR_SPACE_MAP = {\r\n+    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n+    \"COL\": \"sRGB\",\r\n+    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n+    \"COL-2\": \"sRGB\",\r\n+    \"COL-3\": \"sRGB\",\r\n+    \"DISP\": \"Non-Color\",\r\n+    \"NRM\": \"Non-Color\",\r\n+    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n+    \"ROUGH\": \"Non-Color\",\r\n+    \"METAL\": \"Non-Color\",\r\n+    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n+    \"TRN\": \"Non-Color\", # Transmission\r\n+    \"SSS\": \"sRGB\",      # Subsurface Color\r\n+    \"EMISS\": \"sRGB\",    # Emission Color\r\n+    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n+    \"FUZZ\": \"Non-Color\",\r\n+    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n+}\r\n+DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n+\r\n+# Map types for which stats should be applied (if found in metadata and node exists)\r\n+# Reads stats from the 'image_stats_1k' section of metadata.json\r\n+APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n+\r\n+# Categories for which full nodegroup generation should occur\r\n+CATEGORIES_FOR_NODEGROUP_GENERATION = [\"Surface\", \"Decal\"]\r\n+\r\n+# --- END USER CONFIGURATION ---\r\n+\r\n+\r\n+# --- Helper Functions ---\r\n+\r\n+def encode_name_b64(name_str):\r\n+    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n+    try:\r\n+        # Ensure the input is a string\r\n+        name_str = str(name_str)\r\n+        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n+    except Exception as e:\r\n+        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n+        return name_str # Fallback to original name on error\r\n+\r\n+def find_nodes_by_label(node_tree, label, node_type=None):\r\n+    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n+    if not node_tree:\r\n+        return []\r\n+    matching_nodes = []\r\n+    for node in node_tree.nodes:\r\n+        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n+        node_identifier = node.label if node.label else node.name\r\n+        if node_identifier and node_identifier == label:\r\n+            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n+                matching_nodes.append(node)\r\n+    return matching_nodes\r\n+\r\n+def add_tag_if_new(asset_data, tag_name):\r\n+    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n+    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n+        return False\r\n+    cleaned_tag_name = tag_name.strip()\r\n+    if not cleaned_tag_name:\r\n+        return False\r\n+\r\n+    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n+    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n+        try:\r\n+            asset_data.tags.new(cleaned_tag_name)\r\n+            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n+            return True\r\n+        except Exception as e:\r\n+            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n+            return False\r\n+    return False # Tag already existed\r\n+\r\n+def get_color_space(map_type):\r\n+    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n+    # Handle potential numbered variants like COL-1, COL-2\r\n+    base_map_type = map_type.split('-')[0]\r\n+    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n+               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n+\r\n+def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n+    \"\"\"\r\n+    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n+    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n+    Mirrors the logic from the original POC script.\r\n+    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n+    \"\"\"\r\n+    if image_height <= 0 or image_width <= 0:\r\n+        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n+        return 1.0\r\n+\r\n+    # Calculate the actual aspect ratio of the image file\r\n+    current_aspect_ratio = image_width / image_height\r\n+\r\n+    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n+        # If scaling was even, the correction factor is just the image's aspect ratio\r\n+        # to make UVs match the image proportions.\r\n+        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n+    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n+    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n+    if not match:\r\n+        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n+        return current_aspect_ratio # Fallback to the image's own ratio\r\n+\r\n+    axis = match.group(1).upper()\r\n+    try:\r\n+        amount = int(match.group(2))\r\n+        if amount <= 0:\r\n+            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+            return current_aspect_ratio\r\n+    except ValueError:\r\n+        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+\r\n+    # Apply the non-uniform correction formula based on original script logic\r\n+    scaling_factor_percent = amount / 100.0\r\n+    correction_factor = current_aspect_ratio # Default\r\n+\r\n+    try:\r\n+        if axis == 'X':\r\n+            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n+            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n+            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n+        elif axis == 'Y':\r\n+            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n+            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n+        # No 'else' needed as regex ensures X or Y\r\n+\r\n+    except ZeroDivisionError as e:\r\n+        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+        return current_aspect_ratio\r\n+    except Exception as e:\r\n+         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n+         return current_aspect_ratio\r\n+\r\n+    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n+    return correction_factor\r\n+\r\n+\r\n+def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n+    \"\"\"\r\n+    Constructs the expected image file path.\r\n+    If primary_format is provided, tries that first.\r\n+    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n+    Returns the found path as a string, or None if not found.\r\n+    \"\"\"\r\n+    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n+        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n+        return None\r\n+\r\n+    found_path = None\r\n+\r\n+    # 1. Try the primary format if provided\r\n+    if primary_format:\r\n+        try:\r\n+            filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=primary_format.lower() # Ensure format is lowercase\r\n+            )\r\n+            primary_path = asset_dir_path / filename\r\n+            if primary_path.is_file():\r\n+                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n+                return str(primary_path)\r\n+            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n+        except KeyError as e:\r\n+            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n+            return None # Cannot proceed without valid pattern\r\n+        except Exception as e:\r\n+            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n+            # Continue to fallback\r\n+\r\n+    # 2. Try fallback extensions\r\n+    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n+    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n+        # Skip if we already tried this extension as primary (and it failed)\r\n+        if primary_format and ext.lower() == primary_format.lower():\r\n+            continue\r\n+        try:\r\n+            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n+                asset_name=asset_name,\r\n+                map_type=map_type,\r\n+                resolution=resolution,\r\n+                format=ext.lower()\r\n+            )\r\n+            fallback_path = asset_dir_path / fallback_filename\r\n+            if fallback_path.is_file():\r\n+                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n+                return str(fallback_path) # Found it!\r\n+        except KeyError:\r\n+             # Should not happen if primary format worked, but handle defensively\r\n+             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n+             return None\r\n+        except Exception as e_fallback:\r\n+            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n+            continue # Try next extension\r\n+\r\n+    # If we get here, neither primary nor fallbacks worked\r\n+    if primary_format:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    else:\r\n+        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n+    return None # Not found after all checks\r\n+\r\n+\r\n+# --- Manifest Functions ---\r\n+\r\n+def get_manifest_path(context):\r\n+    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n+    if not context or not context.blend_data or not context.blend_data.filepath:\r\n+        return None # Cannot determine path if blend file is not saved\r\n+    blend_path = Path(context.blend_data.filepath)\r\n+    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n+    return blend_path.parent / manifest_filename\r\n+\r\n+def load_manifest(context):\r\n+    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST:\r\n+        return {} # Manifest disabled\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n+        return {} # Cannot load without a path\r\n+\r\n+    if not manifest_path.exists():\r\n+        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n+        return {} # No manifest file exists yet\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n+            data = json.load(f)\r\n+        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n+        # Basic validation (check if it's a dictionary)\r\n+        if not isinstance(data, dict):\r\n+             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n+             return {}\r\n+        return data\r\n+    except json.JSONDecodeError:\r\n+        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n+        return {}\r\n+    except Exception as e:\r\n+        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n+        return {} # Treat as starting fresh on error\r\n+\r\n+def save_manifest(context, manifest_data):\r\n+    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n+    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n+        return False\r\n+\r\n+    manifest_path = get_manifest_path(context)\r\n+    if not manifest_path:\r\n+        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n+        return False\r\n+\r\n+    try:\r\n+        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n+            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n+        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n+        return True\r\n+    except Exception as e:\r\n+        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n+              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n+              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n+        return False\r\n+\r\n+def is_asset_processed(manifest_data, asset_name):\r\n+    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    # Basic check if asset entry exists. Detailed check happens at map level.\r\n+    return asset_name in manifest_data\r\n+\r\n+def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n+\r\n+def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n+    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n+    if not ENABLE_MANIFEST: return False\r\n+\r\n+    # Ensure asset entry exists\r\n+    if asset_name not in manifest_data:\r\n+        manifest_data[asset_name] = {}\r\n+\r\n+    # If map_type and resolution are provided, update the specific map entry\r\n+    if map_type and resolution:\r\n+        if map_type not in manifest_data[asset_name]:\r\n+            manifest_data[asset_name][map_type] = []\r\n+\r\n+        if resolution not in manifest_data[asset_name][map_type]:\r\n+            manifest_data[asset_name][map_type].append(resolution)\r\n+            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n+            return True # Indicate that a change was made\r\n+    return False # No change made to this specific map/res\r\n+\r\n+\r\n+# --- Core Logic ---\r\n+\r\n+def process_library(context, asset_library_root_override=None): # Add override parameter\r\n+    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n+    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n+    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n+    start_time = time.time()\r\n+    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n+    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Determine Asset Library Root ---\r\n+    if asset_library_root_override:\r\n+        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n+        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n+        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n+        print(\"--- Script aborted. ---\")\r\n+        return False\r\n+    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n+\r\n+    # --- Pre-run Checks ---\r\n+    print(\"Performing pre-run checks...\")\r\n+    valid_setup = True\r\n+    # 1. Check Library Root Path\r\n+    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n+    if not root_path.is_dir():\r\n+        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n+        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n+        valid_setup = False\r\n+    else:\r\n+        print(f\"   Asset Library Root: '{root_path}'\")\r\n+    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+\r\n+    # 2. Check Templates\r\n+    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n+    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n+    if not template_parent:\r\n+        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if not template_child:\r\n+        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n+        valid_setup = False\r\n+    if template_parent and template_child:\r\n+         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n+    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n+    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n+\r\n+    # 3. Check Blend File Saved (if manifest enabled)\r\n+    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n+        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n+        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n+        ENABLE_MANIFEST = False # Disable manifest for this run\r\n+\r\n+    if not valid_setup:\r\n+        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n+        return False\r\n+    print(\"Pre-run checks passed.\")\r\n+    # --- End Pre-run Checks ---\r\n+\r\n+    manifest_data = load_manifest(context)\r\n+    manifest_needs_saving = False\r\n+\r\n+    # --- Initialize Counters ---\r\n+    metadata_files_found = 0\r\n+    assets_processed = 0\r\n+    assets_skipped_manifest = 0\r\n+    parent_groups_created = 0\r\n+    parent_groups_updated = 0\r\n+    child_groups_created = 0\r\n+    child_groups_updated = 0\r\n+    images_loaded = 0\r\n+    images_assigned = 0\r\n+    maps_processed = 0\r\n+    maps_skipped_manifest = 0\r\n+    errors_encountered = 0\r\n+    previews_set = 0\r\n+    highest_res_set = 0\r\n+    aspect_ratio_set = 0\r\n+    # --- End Counters ---\r\n+\r\n+    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n+\r\n+    # --- Scan for metadata.json ---\r\n+    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n+    # Then scan within each supplier for asset folders containing metadata.json\r\n+    metadata_paths = []\r\n+    for supplier_dir in root_path.iterdir():\r\n+        if supplier_dir.is_dir():\r\n+            # Now look for asset folders inside the supplier directory\r\n+            for asset_dir in supplier_dir.iterdir():\r\n+                if asset_dir.is_dir():\r\n+                    metadata_file = asset_dir / 'metadata.json'\r\n+                    if metadata_file.is_file():\r\n+                        metadata_paths.append(metadata_file)\r\n+\r\n+    metadata_files_found = len(metadata_paths)\r\n+    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n+    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n+\r\n+    if metadata_files_found == 0:\r\n+        print(\"No metadata files found. Nothing to process.\")\r\n+        print(\"--- Script Finished ---\")\r\n+        return True # No work needed is considered success\r\n+\r\n+    # --- Process Each Metadata File ---\r\n+    for metadata_path in metadata_paths:\r\n+        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n+        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n+        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n+        try:\r\n+            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n+                metadata = json.load(f)\r\n+\r\n+            # --- Extract Key Info ---\r\n+            asset_name = metadata.get(\"asset_name\")\r\n+            supplier_name = metadata.get(\"supplier_name\")\r\n+            archetype = metadata.get(\"archetype\")\r\n+            category = metadata.get(\"category\", \"Unknown\") # <<< ADDED: Extract category, default to \"Unknown\"\r\n+            # Get map info from the correct keys\r\n+            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n+            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n+            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n+            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n+            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n+\r\n+            # Combine processed and merged maps for iteration\r\n+            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n+\r\n+            # Validate essential data\r\n+            if not asset_name:\r\n+                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n+                errors_encountered += 1\r\n+                continue\r\n+            if not all_map_resolutions:\r\n+                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n+                 errors_encountered += 1\r\n+                 continue\r\n+            # map_details check remains a warning as merged maps won't be in it\r\n+            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n+\r\n+            print(f\"  Asset Name: {asset_name}\")\r\n+\r\n+            # --- Determine Highest Resolution ---\r\n+            highest_resolution_value = 0.0\r\n+            highest_resolution_str = \"Unknown\"\r\n+            all_resolutions_present = set()\r\n+            if all_map_resolutions: # Check combined dict\r\n+                for res_list in all_map_resolutions.values():\r\n+                    if isinstance(res_list, list):\r\n+                         all_resolutions_present.update(res_list)\r\n+\r\n+            if all_resolutions_present:\r\n+                for res_str in RESOLUTION_ORDER_DESC:\r\n+                    if res_str in all_resolutions_present:\r\n+                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n+                        highest_resolution_str = res_str\r\n+                        if highest_resolution_value > 0.0:\r\n+                            break # Found the highest valid resolution\r\n+\r\n+            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n+\r\n+            # --- Load Reference Image for Aspect Ratio ---\r\n+            ref_image_path = None\r\n+            ref_image_width = 0\r\n+            ref_image_height = 0\r\n+            ref_image_loaded = False\r\n+            # Use combined resolutions dict to find reference map\r\n+            for ref_map_type in REFERENCE_MAP_TYPES:\r\n+                if ref_map_type in all_map_resolutions:\r\n+                    available_resolutions = all_map_resolutions[ref_map_type]\r\n+                    lowest_res = None\r\n+                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n+                        if res_pref in available_resolutions:\r\n+                            lowest_res = res_pref\r\n+                            break\r\n+                    if lowest_res:\r\n+                        # Get format from map_details if available, otherwise None\r\n+                        ref_map_details = map_details.get(ref_map_type, {})\r\n+                        ref_format = ref_map_details.get(\"output_format\")\r\n+                        ref_image_path = reconstruct_image_path_with_fallback(\r\n+                            asset_dir_path=asset_dir_path,\r\n+                            asset_name=asset_name,\r\n+                            map_type=ref_map_type,\r\n+                            resolution=lowest_res,\r\n+                            primary_format=ref_format # Pass None if not in map_details\r\n+                        )\r\n+                        if ref_image_path:\r\n+                            break # Found a suitable reference image path\r\n+\r\n+            if ref_image_path:\r\n+                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n+                try:\r\n+                    # Load image temporarily\r\n+                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n+                    if ref_img:\r\n+                        ref_image_width = ref_img.size[0]\r\n+                        ref_image_height = ref_img.size[1]\r\n+                        ref_image_loaded = True\r\n+                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n+                        # Remove the temporary image datablock to save memory\r\n+                        bpy.data.images.remove(ref_img)\r\n+                    else:\r\n+                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n+                except Exception as e_ref_load:\r\n+                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n+            else:\r\n+                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n+\r\n+\r\n+            # --- Manifest Check (Asset Level - Basic) ---\r\n+            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n+                # Perform a quick check if *any* map needs processing for this asset\r\n+                needs_processing = False\r\n+                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n+                    for resolution in resolutions:\r\n+                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                            needs_processing = True\r\n+                            break\r\n+                    if needs_processing:\r\n+                        break\r\n+                if not needs_processing:\r\n+                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n+                    assets_skipped_manifest += 1\r\n+                    continue # Skip to next metadata file\r\n+\r\n+            # --- Parent Group Handling ---\r\n+            target_parent_name = f\"PBRSET_{asset_name}\"\r\n+            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n+            is_new_parent = False\r\n+\r\n+            if parent_group is None:\r\n+                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n+                parent_group = template_parent.copy()\r\n+                if not parent_group:\r\n+                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n+                    errors_encountered += 1\r\n+                    continue\r\n+                parent_group.name = target_parent_name\r\n+                parent_groups_created += 1\r\n+                is_new_parent = True\r\n+            else:\r\n+                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n+                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n+                parent_groups_updated += 1\r\n+\r\n+            # Ensure marked as asset\r\n+            if not parent_group.asset_data:\r\n+                try:\r\n+                    parent_group.asset_mark()\r\n+                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n+                except Exception as e_mark:\r\n+                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n+                    # Continue processing other parts if possible\r\n+\r\n+            # Apply Asset Tags\r\n+            if parent_group.asset_data:\r\n+                if supplier_name:\r\n+                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n+                if archetype:\r\n+                    add_tag_if_new(parent_group.asset_data, archetype)\r\n+                if category: # <<< ADDED: Add category tag\r\n+                    add_tag_if_new(parent_group.asset_data, category)\r\n+                # Add other tags if needed\r\n+            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n+\r\n+            # <<< ADDED: Conditional skip based on category >>>\r\n+            if category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # <<< MODIFIED: Use config variable\r\n+                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{category}'). Tag added.\")\r\n+                assets_processed += 1 # Still count as processed for summary, even if skipped\r\n+                continue # Skip the rest of the processing for this asset\r\n+\r\n+            # Apply Aspect Ratio Correction\r\n+            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n+            if aspect_nodes:\r\n+                aspect_node = aspect_nodes[0]\r\n+                correction_factor = 1.0 # Default if ref image fails\r\n+                if ref_image_loaded:\r\n+                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n+                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n+                else:\r\n+                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n+\r\n+                # Check if update is needed\r\n+                current_val = aspect_node.outputs[0].default_value\r\n+                if abs(current_val - correction_factor) > 0.0001:\r\n+                    aspect_node.outputs[0].default_value = correction_factor\r\n+                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n+                    aspect_ratio_set += 1\r\n+            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+            # Apply Highest Resolution Value\r\n+            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n+            if hr_nodes:\r\n+                hr_node = hr_nodes[0]\r\n+                current_hr_val = hr_node.outputs[0].default_value\r\n+                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n+                    hr_node.outputs[0].default_value = highest_resolution_value\r\n+                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n+                    highest_res_set += 1 # Count successful sets\r\n+            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n+\r\n+\r\n+            # Apply Stats (using image_stats_1k)\r\n+            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n+                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n+                    if map_type_to_stat in image_stats_1k:\r\n+                        # Find the stats node in the parent group\r\n+                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n+                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n+                        if stats_nodes:\r\n+                            stats_node = stats_nodes[0]\r\n+                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n+\r\n+                            if stats and isinstance(stats, dict):\r\n+                                # Handle potential list format for RGB stats (use first value) or direct float\r\n+                                def get_stat_value(stat_val):\r\n+                                    if isinstance(stat_val, list):\r\n+                                        return stat_val[0] if stat_val else None\r\n+                                    return stat_val\r\n+\r\n+                                min_val = get_stat_value(stats.get(\"min\"))\r\n+                                max_val = get_stat_value(stats.get(\"max\"))\r\n+                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n+\r\n+                                updated_stat = False\r\n+                                # Check inputs exist before assigning\r\n+                                input_x = stats_node.inputs.get(\"X\")\r\n+                                input_y = stats_node.inputs.get(\"Y\")\r\n+                                input_z = stats_node.inputs.get(\"Z\")\r\n+\r\n+                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n+                                    input_x.default_value = min_val\r\n+                                    updated_stat = True\r\n+                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n+                                    input_y.default_value = max_val\r\n+                                    updated_stat = True\r\n+                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n+                                    input_z.default_value = mean_val\r\n+                                    updated_stat = True\r\n+\r\n+                                if updated_stat:\r\n+                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n+                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n+                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n+                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n+            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n+\r\n+            # --- Set Asset Preview (only for new parent groups) ---\r\n+            # Use the reference image path found earlier if available\r\n+            if is_new_parent and parent_group.asset_data:\r\n+                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n+                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n+                    try:\r\n+                        # Ensure the ID (node group) is the active one for the operator context\r\n+                        with context.temp_override(id=parent_group):\r\n+                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n+                        print(f\"      Successfully set custom preview.\")\r\n+                        previews_set += 1\r\n+                    except Exception as e_preview:\r\n+                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n+                        errors_encountered += 1\r\n+                else:\r\n+                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n+\r\n+\r\n+            # --- Child Group Handling ---\r\n+            # Iterate through the COMBINED map types\r\n+            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n+            for map_type, resolutions in all_map_resolutions.items():\r\n+                print(f\"    Processing Map Type: {map_type}\")\r\n+\r\n+                # Determine if this is a merged map (not in map_details)\r\n+                is_merged_map = map_type not in map_details\r\n+\r\n+                # Get details for this map type if available\r\n+                current_map_details = map_details.get(map_type, {})\r\n+                # For merged maps, primary_format will be None\r\n+                output_format = current_map_details.get(\"output_format\")\r\n+\r\n+                if not output_format and not is_merged_map:\r\n+                    # This case should ideally not happen if metadata is well-formed\r\n+                    # but handle defensively for processed maps.\r\n+                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n+                    # We will rely solely on fallback for this map type\r\n+\r\n+                # Find placeholder node in parent\r\n+                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n+                if not holder_nodes:\r\n+                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n+                    continue\r\n+                holder_node = holder_nodes[0] # Assume first is correct\r\n+                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                # Determine child group name (LOGICAL and ENCODED)\r\n+                logical_child_name = f\"{asset_name}_{map_type}\"\r\n+                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n+\r\n+                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n+                is_new_child = False\r\n+\r\n+                if child_group is None:\r\n+                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n+                    child_group = template_child.copy()\r\n+                    if not child_group:\r\n+                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n+                        errors_encountered += 1\r\n+                        continue\r\n+                    child_group.name = target_child_name_b64 # Set encoded name\r\n+                    child_groups_created += 1\r\n+                    is_new_child = True\r\n+                else:\r\n+                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n+                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n+                    child_groups_updated += 1\r\n+\r\n+                # Assign child group to placeholder if needed\r\n+                if holder_node.node_tree != child_group:\r\n+                    try:\r\n+                        holder_node.node_tree = child_group\r\n+                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n+                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n+                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n+                         continue # Skip this map type if assignment fails\r\n+\r\n+                # Link placeholder output to parent output socket\r\n+                try:\r\n+                    # Find parent's output node\r\n+                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n+                    if group_output_node:\r\n+                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n+                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n+                        # Get the specific input socket on the parent output node (matching map_type)\r\n+                        target_socket = group_output_node.inputs.get(map_type)\r\n+\r\n+                        if source_socket and target_socket:\r\n+                            # Check if link already exists\r\n+                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n+                            if not link_exists:\r\n+                                parent_group.links.new(source_socket, target_socket)\r\n+                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n+                        # else: # Optional warnings\r\n+                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n+                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n+                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n+\r\n+                except Exception as e_link:\r\n+                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n+\r\n+                # Ensure parent output socket type is Color (if it exists)\r\n+                try:\r\n+                    # Use the interface API for modern Blender versions\r\n+                    item = parent_group.interface.items_tree.get(map_type)\r\n+                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n+                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n+                         # Defaulting to Color seems reasonable for most PBR outputs\r\n+                        if item.socket_type != 'NodeSocketColor':\r\n+                            item.socket_type = 'NodeSocketColor'\r\n+                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n+                except Exception as e_sock_type:\r\n+                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n+\r\n+\r\n+                # --- Image Node Handling (Inside Child Group) ---\r\n+                if not isinstance(resolutions, list):\r\n+                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n+                     continue\r\n+\r\n+                for resolution in resolutions:\r\n+                    # --- Manifest Check (Map/Resolution Level) ---\r\n+                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n+                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n+                        maps_skipped_manifest += 1\r\n+                        continue\r\n+                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n+\r\n+                    print(f\"        Processing Resolution: {resolution}\")\r\n+\r\n+                    # Reconstruct the image path using fallback logic\r\n+                    # Pass output_format (which might be None for merged maps)\r\n+                    image_path_str = reconstruct_image_path_with_fallback(\r\n+                        asset_dir_path=asset_dir_path,\r\n+                        asset_name=asset_name,\r\n+                        map_type=map_type,\r\n+                        resolution=resolution,\r\n+                        primary_format=output_format\r\n+                    )\r\n+                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n+\r\n+                    if not image_path_str:\r\n+                        # Error already printed by reconstruct function\r\n+                        errors_encountered += 1\r\n+                        continue # Skip this resolution if path not found\r\n+\r\n+                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n+                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n+                    if not image_nodes:\r\n+                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n+                        continue # Skip this resolution if node not found\r\n+                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n+\r\n+                    # --- Load Image ---\r\n+                    img = None\r\n+                    image_load_failed = False\r\n+                    try:\r\n+                        image_path = Path(image_path_str) # Path object created from already found path string\r\n+                        # Use check_existing=True to reuse existing datablocks if path matches\r\n+                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n+                        if not img:\r\n+                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n+                            image_load_failed = True\r\n+                        else:\r\n+                            # Only count as loaded if bpy.data.images.load succeeded\r\n+                            # Check if it's newly loaded or reused\r\n+                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n+                            if is_newly_loaded: images_loaded += 1\r\n+\r\n+                    except RuntimeError as e_runtime_load:\r\n+                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n+                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n+                        image_load_failed = True\r\n+                    except Exception as e_gen_load:\r\n+                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n+                        image_load_failed = True\r\n+                        errors_encountered += 1\r\n+\r\n+                    # --- Assign Image & Set Color Space ---\r\n+                    if not image_load_failed and img:\r\n+                        assigned_count_this_res = 0\r\n+                        for image_node in image_nodes:\r\n+                            if image_node.image != img:\r\n+                                image_node.image = img\r\n+                                assigned_count_this_res += 1\r\n+\r\n+                        if assigned_count_this_res > 0:\r\n+                             images_assigned += assigned_count_this_res\r\n+                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n+\r\n+                        # Set Color Space\r\n+                        correct_color_space = get_color_space(map_type)\r\n+                        try:\r\n+                            if img.colorspace_settings.name != correct_color_space:\r\n+                                img.colorspace_settings.name = correct_color_space\r\n+                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n+                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n+                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n+                        except Exception as e_cs_gen:\r\n+                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n+\r\n+\r\n+                        # --- Update Manifest (Map/Resolution Level) ---\r\n+                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n+                            manifest_needs_saving = True\r\n+                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n+                        maps_processed += 1\r\n+\r\n+                    else:\r\n+                        # Increment error count if loading failed\r\n+                        if image_load_failed: errors_encountered += 1\r\n+\r\n+                # --- End Resolution Loop ---\r\n+            # --- End Map Type Loop ---\r\n+\r\n+            assets_processed += 1\r\n+\r\n+        except FileNotFoundError:\r\n+            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except json.JSONDecodeError:\r\n+            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n+            errors_encountered += 1\r\n+        except Exception as e_main_loop:\r\n+            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n+            import traceback\r\n+            traceback.print_exc() # Print detailed traceback for debugging\r\n+            errors_encountered += 1\r\n+            # Continue to the next asset\r\n+\r\n+    # --- End Metadata File Loop ---\r\n+\r\n+    # --- Final Manifest Save ---\r\n+    if ENABLE_MANIFEST and manifest_needs_saving:\r\n+        print(\"\\nAttempting final manifest save...\")\r\n+        save_manifest(context, manifest_data)\r\n+    elif ENABLE_MANIFEST:\r\n+        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n+    # --- End Final Manifest Save ---\r\n+\r\n+    # --- Final Summary ---\r\n+    end_time = time.time()\r\n+    duration = end_time - start_time\r\n+    print(\"\\n--- Script Run Finished ---\")\r\n+    print(f\"Duration: {duration:.2f} seconds\")\r\n+    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n+    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n+    if ENABLE_MANIFEST:\r\n+        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n+        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n+    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n+    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n+    print(f\"Child Groups Created: {child_groups_created}\")\r\n+    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n+    print(f\"Images Loaded: {images_loaded}\")\r\n+    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n+    print(f\"Individual Maps Processed: {maps_processed}\")\r\n+    print(f\"Asset Previews Set: {previews_set}\")\r\n+    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n+    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n+    if errors_encountered > 0:\r\n+        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n+    print(\"---------------------------\")\r\n+\r\n+    # --- Explicit Save ---\r\n+    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n+    try:\r\n+        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n+        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n+    except Exception as e_save:\r\n+        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n+        errors_encountered += 1 # Count save errors\r\n+\r\n+    return True\r\n+\r\n+\r\n+# --- Execution Block ---\r\n+\r\n+if __name__ == \"__main__\":\r\n+    # Ensure we are running within Blender\r\n+    try:\r\n+        import bpy\r\n+        import base64 # Ensure base64 is imported here too if needed globally\r\n+        import sys\r\n+    except ImportError:\r\n+        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n+    else:\r\n+        # --- Argument Parsing for Asset Library Root ---\r\n+        asset_root_arg = None\r\n+        try:\r\n+            # Blender arguments passed after '--' appear in sys.argv\r\n+            if \"--\" in sys.argv:\r\n+                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n+                if len(args_after_dash) >= 1:\r\n+                    asset_root_arg = args_after_dash[0]\r\n+                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n+                else:\r\n+                    print(\"Info: '--' found but no arguments after it.\")\r\n+            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n+        except Exception as e:\r\n+            print(f\"Error parsing command line arguments: {e}\")\r\n+        # --- End Argument Parsing ---\r\n+\r\n+        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n"
                },
                {
                    "date": 1745346278448,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -492,9 +492,9 @@\n             # --- Extract Key Info ---\r\n             asset_name = metadata.get(\"asset_name\")\r\n             supplier_name = metadata.get(\"supplier_name\")\r\n             archetype = metadata.get(\"archetype\")\r\n-            category = metadata.get(\"category\", \"Unknown\") # <<< ADDED: Extract category, default to \"Unknown\"\r\n+            asset_category = metadata.get(\"asset_category\", \"Unknown\") # Read asset_category instead of category\r\n             # Get map info from the correct keys\r\n             processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n             merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n             map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n@@ -636,16 +636,16 @@\n                 if supplier_name:\r\n                     add_tag_if_new(parent_group.asset_data, supplier_name)\r\n                 if archetype:\r\n                     add_tag_if_new(parent_group.asset_data, archetype)\r\n-                if category: # <<< ADDED: Add category tag\r\n-                    add_tag_if_new(parent_group.asset_data, category)\r\n+                if asset_category: # Use asset_category for tagging\r\n+                    add_tag_if_new(parent_group.asset_data, asset_category)\r\n                 # Add other tags if needed\r\n             # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n \r\n-            # <<< ADDED: Conditional skip based on category >>>\r\n-            if category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # <<< MODIFIED: Use config variable\r\n-                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{category}'). Tag added.\")\r\n+            # Conditional skip based on asset_category\r\n+            if asset_category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # Check asset_category\r\n+                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{asset_category}'). Tag added.\") # Use asset_category in log\r\n                 assets_processed += 1 # Still count as processed for summary, even if skipped\r\n                 continue # Skip the rest of the processing for this asset\r\n \r\n             # Apply Aspect Ratio Correction\r\n@@ -1028,1035 +1028,4 @@\n             print(f\"Error parsing command line arguments: {e}\")\r\n         # --- End Argument Parsing ---\r\n \r\n         process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n-# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n-# Version: 1.5\r\n-# Description: Scans a library processed by the Asset Processor Tool,\r\n-#              reads metadata.json files, and creates/updates corresponding\r\n-#              PBR node groups in the active Blender file.\r\n-# Changes v1.5:\r\n-#   - Corrected aspect ratio calculation (`calculate_aspect_correction_factor`)\r\n-#     to use actual image dimensions from a loaded reference image and the\r\n-#     `aspect_ratio_change_string`, mirroring original script logic for\r\n-#     \"EVEN\", \"Xnnn\", \"Ynnn\" formats.\r\n-#   - Added logic in main loop to load reference image for dimensions.\r\n-# Changes v1.3:\r\n-#   - Added logic to find the highest resolution present for an asset.\r\n-#   - Added logic to set a \"HighestResolution\" Value node in the parent group\r\n-#     (maps 1K->1.0, 2K->2.0, 4K->3.0, 8K->4.0).\r\n-# Changes v1.2:\r\n-#   - Added Base64 encoding for child node group names (PBRTYPE_...).\r\n-#   - Added fallback logic for reconstructing image paths with different extensions.\r\n-#   - Added logic to set custom asset preview for new parent groups (using lowest res COL map).\r\n-# Changes v1.1:\r\n-#   - Updated metadata parsing to match actual structure (using processed_map_resolutions, image_stats_1k, map_details).\r\n-#   - Added logic to reconstruct image file paths based on metadata and assumed naming convention.\r\n-\r\n-import bpy\r\n-import os\r\n-import json\r\n-from pathlib import Path\r\n-import time\r\n-import re # For parsing aspect ratio string\r\n-import base64 # For encoding node group names\r\n-import sys # <<< ADDED IMPORT\r\n-\r\n-# --- USER CONFIGURATION ---\r\n-\r\n-# Path to the root output directory of the Asset Processor Tool\r\n-# Example: r\"G:\\Assets\\Processed\"\r\n-# IMPORTANT: This should point to the base directory containing supplier folders (e.g., Poliigon)\r\n-# This will be overridden by command-line arguments if provided.\r\n-PROCESSED_ASSET_LIBRARY_ROOT = None # Set to None initially\r\n-\r\n-# Names of the required node group templates in the Blender file\r\n-PARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\n-CHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n-\r\n-# Labels of specific nodes within the PARENT template\r\n-ASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\n-STATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n-HIGHEST_RESOLUTION_NODE_LABEL = \"HighestResolution\" # Value node to store highest res index\r\n-\r\n-# Enable/disable the manifest system to track processed assets/maps\r\n-# If enabled, requires the blend file to be saved.\r\n-ENABLE_MANIFEST = False # Disabled based on user feedback in previous run\r\n-\r\n-# Assumed filename pattern for processed images.\r\n-# {asset_name}, {map_type}, {resolution}, {format} will be replaced.\r\n-# Check Asset Processor Tool's config.py (TARGET_FILENAME_PATTERN) if this is wrong.\r\n-IMAGE_FILENAME_PATTERN = \"{asset_name}_{map_type}_{resolution}.{format}\"\r\n-\r\n-# Fallback extensions to try if the primary format from metadata is not found\r\n-# Order matters - first found will be used.\r\n-FALLBACK_IMAGE_EXTENSIONS = ['png', 'jpg', 'exr', 'tif']\r\n-\r\n-# Map type(s) to use for generating the asset preview AND for aspect ratio calculation reference\r\n-# The script will look for these in order and use the first one found.\r\n-REFERENCE_MAP_TYPES = [\"COL\", \"COL-1\", \"COL-2\"] # Used for preview and aspect calc\r\n-# Preferred resolution order for reference image (lowest first is often faster)\r\n-REFERENCE_RESOLUTION_ORDER = [\"1K\", \"512\", \"2K\", \"4K\"] # Adjust as needed\r\n-\r\n-# Mapping from resolution string to numerical value for the HighestResolution node\r\n-RESOLUTION_VALUE_MAP = {\"1K\": 1.0, \"2K\": 2.0, \"4K\": 3.0, \"8K\": 4.0}\r\n-# Order to check resolutions to find the highest present (highest value first)\r\n-RESOLUTION_ORDER_DESC = [\"8K\", \"4K\", \"2K\", \"1K\"] # Add others like \"512\" if needed and map them in RESOLUTION_VALUE_MAP\r\n-\r\n-# Map PBR type strings (from metadata) to Blender color spaces\r\n-# Add more mappings as needed based on your metadata types\r\n-PBR_COLOR_SPACE_MAP = {\r\n-    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n-    \"COL\": \"sRGB\",\r\n-    \"COL-1\": \"sRGB\", # Handle variants if present in metadata\r\n-    \"COL-2\": \"sRGB\",\r\n-    \"COL-3\": \"sRGB\",\r\n-    \"DISP\": \"Non-Color\",\r\n-    \"NRM\": \"Non-Color\",\r\n-    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n-    \"ROUGH\": \"Non-Color\",\r\n-    \"METAL\": \"Non-Color\",\r\n-    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n-    \"TRN\": \"Non-Color\", # Transmission\r\n-    \"SSS\": \"sRGB\",      # Subsurface Color\r\n-    \"EMISS\": \"sRGB\",    # Emission Color\r\n-    \"NRMRGH\": \"Non-Color\", # Added for merged map\r\n-    \"FUZZ\": \"Non-Color\",\r\n-    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n-}\r\n-DEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n-\r\n-# Map types for which stats should be applied (if found in metadata and node exists)\r\n-# Reads stats from the 'image_stats_1k' section of metadata.json\r\n-APPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\", \"AO\", \"REFL\"] # Add others if needed\r\n-\r\n-# Categories for which full nodegroup generation should occur\r\n-CATEGORIES_FOR_NODEGROUP_GENERATION = [\"Surface\", \"Decal\"]\r\n-\r\n-# --- END USER CONFIGURATION ---\r\n-\r\n-\r\n-# --- Helper Functions ---\r\n-\r\n-def encode_name_b64(name_str):\r\n-    \"\"\"Encodes a string using URL-safe Base64 for node group names.\"\"\"\r\n-    try:\r\n-        # Ensure the input is a string\r\n-        name_str = str(name_str)\r\n-        return base64.urlsafe_b64encode(name_str.encode('utf-8')).decode('ascii')\r\n-    except Exception as e:\r\n-        print(f\"        Error base64 encoding '{name_str}': {e}\")\r\n-        return name_str # Fallback to original name on error\r\n-\r\n-def find_nodes_by_label(node_tree, label, node_type=None):\r\n-    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n-    if not node_tree:\r\n-        return []\r\n-    matching_nodes = []\r\n-    for node in node_tree.nodes:\r\n-        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n-        node_identifier = node.label if node.label else node.name\r\n-        if node_identifier and node_identifier == label:\r\n-            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n-                matching_nodes.append(node)\r\n-    return matching_nodes\r\n-\r\n-def add_tag_if_new(asset_data, tag_name):\r\n-    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n-    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n-        return False\r\n-    cleaned_tag_name = tag_name.strip()\r\n-    if not cleaned_tag_name:\r\n-        return False\r\n-\r\n-    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n-    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n-        try:\r\n-            asset_data.tags.new(cleaned_tag_name)\r\n-            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n-            return True\r\n-        except Exception as e:\r\n-            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n-            return False\r\n-    return False # Tag already existed\r\n-\r\n-def get_color_space(map_type):\r\n-    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n-    # Handle potential numbered variants like COL-1, COL-2\r\n-    base_map_type = map_type.split('-')[0]\r\n-    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), # Check full name first (e.g., NRMRGH)\r\n-               PBR_COLOR_SPACE_MAP.get(base_map_type.upper(), DEFAULT_COLOR_SPACE)) # Fallback to base type\r\n-\r\n-def calculate_aspect_correction_factor(image_width, image_height, aspect_string):\r\n-    \"\"\"\r\n-    Calculates the UV X-axis scaling factor needed to correct distortion,\r\n-    based on image dimensions and the aspect_ratio_change_string (\"EVEN\", \"Xnnn\", \"Ynnn\").\r\n-    Mirrors the logic from the original POC script.\r\n-    Returns 1.0 if dimensions are invalid or string is \"EVEN\" or invalid.\r\n-    \"\"\"\r\n-    if image_height <= 0 or image_width <= 0:\r\n-        print(\"        Warn: Invalid image dimensions for aspect ratio calculation. Returning 1.0.\")\r\n-        return 1.0\r\n-\r\n-    # Calculate the actual aspect ratio of the image file\r\n-    current_aspect_ratio = image_width / image_height\r\n-\r\n-    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n-        # If scaling was even, the correction factor is just the image's aspect ratio\r\n-        # to make UVs match the image proportions.\r\n-        # print(f\"        Aspect string is EVEN. Correction factor = current aspect ratio: {current_aspect_ratio:.4f}\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Handle non-uniform scaling cases (\"Xnnn\", \"Ynnn\")\r\n-    # Use search instead of match to find anywhere in string (though unlikely needed based on format)\r\n-    match = re.search(r\"([XY])(\\d+)\", aspect_string, re.IGNORECASE)\r\n-    if not match:\r\n-        print(f\"        Warn: Invalid Scaling string format '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f} as fallback.\")\r\n-        return current_aspect_ratio # Fallback to the image's own ratio\r\n-\r\n-    axis = match.group(1).upper()\r\n-    try:\r\n-        amount = int(match.group(2))\r\n-        if amount <= 0:\r\n-            print(f\"        Warn: Zero or negative Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-            return current_aspect_ratio\r\n-    except ValueError:\r\n-        print(f\"        Warn: Invalid Amount in Scaling string '{aspect_string}'. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-\r\n-    # Apply the non-uniform correction formula based on original script logic\r\n-    scaling_factor_percent = amount / 100.0\r\n-    correction_factor = current_aspect_ratio # Default\r\n-\r\n-    try:\r\n-        if axis == 'X':\r\n-            if scaling_factor_percent == 0: raise ZeroDivisionError(\"X scaling factor is zero\")\r\n-            # If image was stretched horizontally (X > 1), divide UV.x by factor\r\n-            correction_factor = current_aspect_ratio / scaling_factor_percent\r\n-        elif axis == 'Y':\r\n-            # If image was stretched vertically (Y > 1), multiply UV.x by factor\r\n-            correction_factor = current_aspect_ratio * scaling_factor_percent\r\n-        # No 'else' needed as regex ensures X or Y\r\n-\r\n-    except ZeroDivisionError as e:\r\n-        print(f\"        Warn: Division by zero during aspect factor calculation ({e}). Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-        return current_aspect_ratio\r\n-    except Exception as e:\r\n-         print(f\"        Error calculating aspect correction factor: {e}. Returning current ratio {current_aspect_ratio:.4f}.\")\r\n-         return current_aspect_ratio\r\n-\r\n-    # print(f\"        Calculated aspect correction factor: {correction_factor:.4f} (from {image_width}x{image_height}, Scaling='{aspect_string}')\")\r\n-    return correction_factor\r\n-\r\n-\r\n-def reconstruct_image_path_with_fallback(asset_dir_path, asset_name, map_type, resolution, primary_format=None):\r\n-    \"\"\"\r\n-    Constructs the expected image file path.\r\n-    If primary_format is provided, tries that first.\r\n-    Then falls back to common extensions if the path doesn't exist or primary_format was None.\r\n-    Returns the found path as a string, or None if not found.\r\n-    \"\"\"\r\n-    if not all([asset_dir_path, asset_name, map_type, resolution]):\r\n-        print(f\"    !!! ERROR: Missing data for path reconstruction ({asset_name}/{map_type}/{resolution}).\")\r\n-        return None\r\n-\r\n-    found_path = None\r\n-\r\n-    # 1. Try the primary format if provided\r\n-    if primary_format:\r\n-        try:\r\n-            filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=primary_format.lower() # Ensure format is lowercase\r\n-            )\r\n-            primary_path = asset_dir_path / filename\r\n-            if primary_path.is_file():\r\n-                # print(f\"          Found primary path: {str(primary_path)}\") # Verbose\r\n-                return str(primary_path)\r\n-            # else: print(f\"          Primary path not found: {str(primary_path)}\") # Verbose\r\n-        except KeyError as e:\r\n-            print(f\"    !!! ERROR: Missing key '{e}' in IMAGE_FILENAME_PATTERN. Cannot reconstruct path.\")\r\n-            return None # Cannot proceed without valid pattern\r\n-        except Exception as e:\r\n-            print(f\"    !!! ERROR reconstructing primary image path: {e}\")\r\n-            # Continue to fallback\r\n-\r\n-    # 2. Try fallback extensions\r\n-    # print(f\"          Trying fallback extensions for {map_type}/{resolution}...\") # Verbose\r\n-    for ext in FALLBACK_IMAGE_EXTENSIONS:\r\n-        # Skip if we already tried this extension as primary (and it failed)\r\n-        if primary_format and ext.lower() == primary_format.lower():\r\n-            continue\r\n-        try:\r\n-            fallback_filename = IMAGE_FILENAME_PATTERN.format(\r\n-                asset_name=asset_name,\r\n-                map_type=map_type,\r\n-                resolution=resolution,\r\n-                format=ext.lower()\r\n-            )\r\n-            fallback_path = asset_dir_path / fallback_filename\r\n-            if fallback_path.is_file():\r\n-                print(f\"          Found fallback path: {str(fallback_path)}\")\r\n-                return str(fallback_path) # Found it!\r\n-        except KeyError:\r\n-             # Should not happen if primary format worked, but handle defensively\r\n-             print(f\"    !!! ERROR: Missing key in IMAGE_FILENAME_PATTERN during fallback. Cannot reconstruct path.\")\r\n-             return None\r\n-        except Exception as e_fallback:\r\n-            print(f\"    !!! ERROR reconstructing fallback image path ({ext}): {e_fallback}\")\r\n-            continue # Try next extension\r\n-\r\n-    # If we get here, neither primary nor fallbacks worked\r\n-    if primary_format:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using primary format '{primary_format}' or fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    else:\r\n-        print(f\"    !!! ERROR: Could not find image file for {map_type}/{resolution} using fallbacks {FALLBACK_IMAGE_EXTENSIONS}.\")\r\n-    return None # Not found after all checks\r\n-\r\n-\r\n-# --- Manifest Functions ---\r\n-\r\n-def get_manifest_path(context):\r\n-    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n-    if not context or not context.blend_data or not context.blend_data.filepath:\r\n-        return None # Cannot determine path if blend file is not saved\r\n-    blend_path = Path(context.blend_data.filepath)\r\n-    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n-    return blend_path.parent / manifest_filename\r\n-\r\n-def load_manifest(context):\r\n-    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST:\r\n-        return {} # Manifest disabled\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n-        return {} # Cannot load without a path\r\n-\r\n-    if not manifest_path.exists():\r\n-        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n-        return {} # No manifest file exists yet\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n-            data = json.load(f)\r\n-        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n-        # Basic validation (check if it's a dictionary)\r\n-        if not isinstance(data, dict):\r\n-             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n-             return {}\r\n-        return data\r\n-    except json.JSONDecodeError:\r\n-        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n-        return {}\r\n-    except Exception as e:\r\n-        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n-        return {} # Treat as starting fresh on error\r\n-\r\n-def save_manifest(context, manifest_data):\r\n-    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n-    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n-        return False\r\n-\r\n-    manifest_path = get_manifest_path(context)\r\n-    if not manifest_path:\r\n-        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n-        return False\r\n-\r\n-    try:\r\n-        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n-            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n-        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n-        return True\r\n-    except Exception as e:\r\n-        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n-              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n-              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n-        return False\r\n-\r\n-def is_asset_processed(manifest_data, asset_name):\r\n-    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    # Basic check if asset entry exists. Detailed check happens at map level.\r\n-    return asset_name in manifest_data\r\n-\r\n-def is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n-\r\n-def update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n-    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n-    if not ENABLE_MANIFEST: return False\r\n-\r\n-    # Ensure asset entry exists\r\n-    if asset_name not in manifest_data:\r\n-        manifest_data[asset_name] = {}\r\n-\r\n-    # If map_type and resolution are provided, update the specific map entry\r\n-    if map_type and resolution:\r\n-        if map_type not in manifest_data[asset_name]:\r\n-            manifest_data[asset_name][map_type] = []\r\n-\r\n-        if resolution not in manifest_data[asset_name][map_type]:\r\n-            manifest_data[asset_name][map_type].append(resolution)\r\n-            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n-            return True # Indicate that a change was made\r\n-    return False # No change made to this specific map/res\r\n-\r\n-\r\n-# --- Core Logic ---\r\n-\r\n-def process_library(context, asset_library_root_override=None): # Add override parameter\r\n-    global ENABLE_MANIFEST # Declare intent to modify global if needed\r\n-    global PROCESSED_ASSET_LIBRARY_ROOT # Allow modification of global\r\n-    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n-    start_time = time.time()\r\n-    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n-    print(f\"  DEBUG: Received asset_library_root_override: {asset_library_root_override}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Determine Asset Library Root ---\r\n-    if asset_library_root_override:\r\n-        PROCESSED_ASSET_LIBRARY_ROOT = asset_library_root_override\r\n-        print(f\"Using asset library root from argument: '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-    elif not PROCESSED_ASSET_LIBRARY_ROOT:\r\n-        print(\"!!! ERROR: Processed asset library root not set in script and not provided via argument.\")\r\n-        print(\"--- Script aborted. ---\")\r\n-        return False\r\n-    print(f\"  DEBUG: Using final PROCESSED_ASSET_LIBRARY_ROOT: {PROCESSED_ASSET_LIBRARY_ROOT}\") # DEBUG LOG (Indented)\r\n-\r\n-    # --- Pre-run Checks ---\r\n-    print(\"Performing pre-run checks...\")\r\n-    valid_setup = True\r\n-    # 1. Check Library Root Path\r\n-    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n-    if not root_path.is_dir():\r\n-        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n-        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n-        valid_setup = False\r\n-    else:\r\n-        print(f\"   Asset Library Root: '{root_path}'\")\r\n-    print(f\"  DEBUG: Checking for templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-\r\n-    # 2. Check Templates\r\n-    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n-    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n-    if not template_parent:\r\n-        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if not template_child:\r\n-        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n-        valid_setup = False\r\n-    if template_parent and template_child:\r\n-         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n-    print(f\"  DEBUG: Template Parent Found: {template_parent is not None}\") # DEBUG LOG (Indented)\r\n-    print(f\"  DEBUG: Template Child Found: {template_child is not None}\") # DEBUG LOG (Indented)\r\n-\r\n-    # 3. Check Blend File Saved (if manifest enabled)\r\n-    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n-        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n-        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n-        ENABLE_MANIFEST = False # Disable manifest for this run\r\n-\r\n-    if not valid_setup:\r\n-        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n-        return False\r\n-    print(\"Pre-run checks passed.\")\r\n-    # --- End Pre-run Checks ---\r\n-\r\n-    manifest_data = load_manifest(context)\r\n-    manifest_needs_saving = False\r\n-\r\n-    # --- Initialize Counters ---\r\n-    metadata_files_found = 0\r\n-    assets_processed = 0\r\n-    assets_skipped_manifest = 0\r\n-    parent_groups_created = 0\r\n-    parent_groups_updated = 0\r\n-    child_groups_created = 0\r\n-    child_groups_updated = 0\r\n-    images_loaded = 0\r\n-    images_assigned = 0\r\n-    maps_processed = 0\r\n-    maps_skipped_manifest = 0\r\n-    errors_encountered = 0\r\n-    previews_set = 0\r\n-    highest_res_set = 0\r\n-    aspect_ratio_set = 0\r\n-    # --- End Counters ---\r\n-\r\n-    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n-\r\n-    # --- Scan for metadata.json ---\r\n-    # Scan one level deeper for supplier folders (e.g., Poliigon)\r\n-    # Then scan within each supplier for asset folders containing metadata.json\r\n-    metadata_paths = []\r\n-    for supplier_dir in root_path.iterdir():\r\n-        if supplier_dir.is_dir():\r\n-            # Now look for asset folders inside the supplier directory\r\n-            for asset_dir in supplier_dir.iterdir():\r\n-                if asset_dir.is_dir():\r\n-                    metadata_file = asset_dir / 'metadata.json'\r\n-                    if metadata_file.is_file():\r\n-                        metadata_paths.append(metadata_file)\r\n-\r\n-    metadata_files_found = len(metadata_paths)\r\n-    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n-    print(f\"  DEBUG: Metadata paths found: {metadata_paths}\") # DEBUG LOG (Indented)\r\n-\r\n-    if metadata_files_found == 0:\r\n-        print(\"No metadata files found. Nothing to process.\")\r\n-        print(\"--- Script Finished ---\")\r\n-        return True # No work needed is considered success\r\n-\r\n-    # --- Process Each Metadata File ---\r\n-    for metadata_path in metadata_paths:\r\n-        asset_dir_path = metadata_path.parent # Get the directory containing the metadata file\r\n-        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n-        print(f\"  DEBUG: Processing file: {metadata_path}\") # DEBUG LOG (Indented)\r\n-        try:\r\n-            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n-                metadata = json.load(f)\r\n-\r\n-            # --- Extract Key Info ---\r\n-            asset_name = metadata.get(\"asset_name\")\r\n-            supplier_name = metadata.get(\"supplier_name\")\r\n-            archetype = metadata.get(\"archetype\")\r\n-            category = metadata.get(\"category\", \"Unknown\") # <<< ADDED: Extract category, default to \"Unknown\"\r\n-            # Get map info from the correct keys\r\n-            processed_resolutions = metadata.get(\"processed_map_resolutions\", {}) # Default to empty dict\r\n-            merged_resolutions = metadata.get(\"merged_map_resolutions\", {}) # Get merged maps too\r\n-            map_details = metadata.get(\"map_details\", {}) # Default to empty dict\r\n-            image_stats_1k = metadata.get(\"image_stats_1k\") # Dict: {map_type: {stats}}\r\n-            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n-\r\n-            # Combine processed and merged maps for iteration\r\n-            all_map_resolutions = {**processed_resolutions, **merged_resolutions}\r\n-\r\n-            # Validate essential data\r\n-            if not asset_name:\r\n-                print(f\"    !!! ERROR: Metadata file is missing 'asset_name'. Skipping.\")\r\n-                errors_encountered += 1\r\n-                continue\r\n-            if not all_map_resolutions:\r\n-                 print(f\"    !!! ERROR: Metadata file has no 'processed_map_resolutions' or 'merged_map_resolutions'. Skipping asset '{asset_name}'.\")\r\n-                 errors_encountered += 1\r\n-                 continue\r\n-            # map_details check remains a warning as merged maps won't be in it\r\n-            print(f\"    DEBUG: Valid metadata loaded for asset: {asset_name}\") # DEBUG LOG (Indented)\r\n-\r\n-            print(f\"  Asset Name: {asset_name}\")\r\n-\r\n-            # --- Determine Highest Resolution ---\r\n-            highest_resolution_value = 0.0\r\n-            highest_resolution_str = \"Unknown\"\r\n-            all_resolutions_present = set()\r\n-            if all_map_resolutions: # Check combined dict\r\n-                for res_list in all_map_resolutions.values():\r\n-                    if isinstance(res_list, list):\r\n-                         all_resolutions_present.update(res_list)\r\n-\r\n-            if all_resolutions_present:\r\n-                for res_str in RESOLUTION_ORDER_DESC:\r\n-                    if res_str in all_resolutions_present:\r\n-                        highest_resolution_value = RESOLUTION_VALUE_MAP.get(res_str, 0.0)\r\n-                        highest_resolution_str = res_str\r\n-                        if highest_resolution_value > 0.0:\r\n-                            break # Found the highest valid resolution\r\n-\r\n-            print(f\"    Highest resolution found: {highest_resolution_str} (Value: {highest_resolution_value})\")\r\n-\r\n-            # --- Load Reference Image for Aspect Ratio ---\r\n-            ref_image_path = None\r\n-            ref_image_width = 0\r\n-            ref_image_height = 0\r\n-            ref_image_loaded = False\r\n-            # Use combined resolutions dict to find reference map\r\n-            for ref_map_type in REFERENCE_MAP_TYPES:\r\n-                if ref_map_type in all_map_resolutions:\r\n-                    available_resolutions = all_map_resolutions[ref_map_type]\r\n-                    lowest_res = None\r\n-                    for res_pref in REFERENCE_RESOLUTION_ORDER:\r\n-                        if res_pref in available_resolutions:\r\n-                            lowest_res = res_pref\r\n-                            break\r\n-                    if lowest_res:\r\n-                        # Get format from map_details if available, otherwise None\r\n-                        ref_map_details = map_details.get(ref_map_type, {})\r\n-                        ref_format = ref_map_details.get(\"output_format\")\r\n-                        ref_image_path = reconstruct_image_path_with_fallback(\r\n-                            asset_dir_path=asset_dir_path,\r\n-                            asset_name=asset_name,\r\n-                            map_type=ref_map_type,\r\n-                            resolution=lowest_res,\r\n-                            primary_format=ref_format # Pass None if not in map_details\r\n-                        )\r\n-                        if ref_image_path:\r\n-                            break # Found a suitable reference image path\r\n-\r\n-            if ref_image_path:\r\n-                print(f\"    Loading reference image for aspect ratio: {Path(ref_image_path).name}\")\r\n-                try:\r\n-                    # Load image temporarily\r\n-                    ref_img = bpy.data.images.load(ref_image_path, check_existing=True)\r\n-                    if ref_img:\r\n-                        ref_image_width = ref_img.size[0]\r\n-                        ref_image_height = ref_img.size[1]\r\n-                        ref_image_loaded = True\r\n-                        print(f\"      Reference image dimensions: {ref_image_width}x{ref_image_height}\")\r\n-                        # Remove the temporary image datablock to save memory\r\n-                        bpy.data.images.remove(ref_img)\r\n-                    else:\r\n-                        print(f\"      !!! ERROR: Failed loading reference image via bpy.data.images.load: {ref_image_path}\")\r\n-                except Exception as e_ref_load:\r\n-                    print(f\"      !!! ERROR loading reference image '{ref_image_path}': {e_ref_load}\")\r\n-            else:\r\n-                print(f\"    !!! WARNING: Could not find suitable reference image ({REFERENCE_MAP_TYPES} at {REFERENCE_RESOLUTION_ORDER}) for aspect ratio calculation.\")\r\n-\r\n-\r\n-            # --- Manifest Check (Asset Level - Basic) ---\r\n-            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n-                # Perform a quick check if *any* map needs processing for this asset\r\n-                needs_processing = False\r\n-                for map_type, resolutions in all_map_resolutions.items(): # Check combined maps\r\n-                    for resolution in resolutions:\r\n-                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                            needs_processing = True\r\n-                            break\r\n-                    if needs_processing:\r\n-                        break\r\n-                if not needs_processing:\r\n-                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n-                    assets_skipped_manifest += 1\r\n-                    continue # Skip to next metadata file\r\n-\r\n-            # --- Parent Group Handling ---\r\n-            target_parent_name = f\"PBRSET_{asset_name}\"\r\n-            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n-            is_new_parent = False\r\n-\r\n-            if parent_group is None:\r\n-                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Copying parent template '{PARENT_TEMPLATE_NAME}'\") # DEBUG LOG (Indented)\r\n-                parent_group = template_parent.copy()\r\n-                if not parent_group:\r\n-                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n-                    errors_encountered += 1\r\n-                    continue\r\n-                parent_group.name = target_parent_name\r\n-                parent_groups_created += 1\r\n-                is_new_parent = True\r\n-            else:\r\n-                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n-                print(f\"    DEBUG: Found existing parent group.\") # DEBUG LOG (Indented)\r\n-                parent_groups_updated += 1\r\n-\r\n-            # Ensure marked as asset\r\n-            if not parent_group.asset_data:\r\n-                try:\r\n-                    parent_group.asset_mark()\r\n-                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n-                except Exception as e_mark:\r\n-                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n-                    # Continue processing other parts if possible\r\n-\r\n-            # Apply Asset Tags\r\n-            if parent_group.asset_data:\r\n-                if supplier_name:\r\n-                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n-                if archetype:\r\n-                    add_tag_if_new(parent_group.asset_data, archetype)\r\n-                if category: # <<< ADDED: Add category tag\r\n-                    add_tag_if_new(parent_group.asset_data, category)\r\n-                # Add other tags if needed\r\n-            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n-\r\n-            # <<< ADDED: Conditional skip based on category >>>\r\n-            if category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # <<< MODIFIED: Use config variable\r\n-                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{category}'). Tag added.\")\r\n-                assets_processed += 1 # Still count as processed for summary, even if skipped\r\n-                continue # Skip the rest of the processing for this asset\r\n-\r\n-            # Apply Aspect Ratio Correction\r\n-            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n-            if aspect_nodes:\r\n-                aspect_node = aspect_nodes[0]\r\n-                correction_factor = 1.0 # Default if ref image fails\r\n-                if ref_image_loaded:\r\n-                    correction_factor = calculate_aspect_correction_factor(ref_image_width, ref_image_height, aspect_string)\r\n-                    print(f\"    Calculated aspect correction factor: {correction_factor:.4f}\")\r\n-                else:\r\n-                    print(f\"    !!! WARNING: Using default aspect ratio correction (1.0) due to missing reference image.\")\r\n-\r\n-                # Check if update is needed\r\n-                current_val = aspect_node.outputs[0].default_value\r\n-                if abs(current_val - correction_factor) > 0.0001:\r\n-                    aspect_node.outputs[0].default_value = correction_factor\r\n-                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (was {current_val:.4f})\")\r\n-                    aspect_ratio_set += 1\r\n-            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-            # Apply Highest Resolution Value\r\n-            hr_nodes = find_nodes_by_label(parent_group, HIGHEST_RESOLUTION_NODE_LABEL, 'ShaderNodeValue')\r\n-            if hr_nodes:\r\n-                hr_node = hr_nodes[0]\r\n-                current_hr_val = hr_node.outputs[0].default_value\r\n-                if highest_resolution_value > 0.0 and abs(current_hr_val - highest_resolution_value) > 0.001:\r\n-                    hr_node.outputs[0].default_value = highest_resolution_value\r\n-                    print(f\"    Set '{HIGHEST_RESOLUTION_NODE_LABEL}' value to {highest_resolution_value} ({highest_resolution_str}) (was {current_hr_val:.1f})\")\r\n-                    highest_res_set += 1 # Count successful sets\r\n-            # else: print(f\"    Warn: Highest resolution node '{HIGHEST_RESOLUTION_NODE_LABEL}' not found in parent group.\") # Optional\r\n-\r\n-\r\n-            # Apply Stats (using image_stats_1k)\r\n-            if image_stats_1k and isinstance(image_stats_1k, dict):\r\n-                for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n-                    if map_type_to_stat in image_stats_1k:\r\n-                        # Find the stats node in the parent group\r\n-                        stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n-                        stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n-                        if stats_nodes:\r\n-                            stats_node = stats_nodes[0]\r\n-                            stats = image_stats_1k[map_type_to_stat] # Get stats dict for this map type\r\n-\r\n-                            if stats and isinstance(stats, dict):\r\n-                                # Handle potential list format for RGB stats (use first value) or direct float\r\n-                                def get_stat_value(stat_val):\r\n-                                    if isinstance(stat_val, list):\r\n-                                        return stat_val[0] if stat_val else None\r\n-                                    return stat_val\r\n-\r\n-                                min_val = get_stat_value(stats.get(\"min\"))\r\n-                                max_val = get_stat_value(stats.get(\"max\"))\r\n-                                mean_val = get_stat_value(stats.get(\"mean\")) # Often stored as 'mean' or 'avg'\r\n-\r\n-                                updated_stat = False\r\n-                                # Check inputs exist before assigning\r\n-                                input_x = stats_node.inputs.get(\"X\")\r\n-                                input_y = stats_node.inputs.get(\"Y\")\r\n-                                input_z = stats_node.inputs.get(\"Z\")\r\n-\r\n-                                if input_x and min_val is not None and abs(input_x.default_value - min_val) > 0.0001:\r\n-                                    input_x.default_value = min_val\r\n-                                    updated_stat = True\r\n-                                if input_y and max_val is not None and abs(input_y.default_value - max_val) > 0.0001:\r\n-                                    input_y.default_value = max_val\r\n-                                    updated_stat = True\r\n-                                if input_z and mean_val is not None and abs(input_z.default_value - mean_val) > 0.0001:\r\n-                                    input_z.default_value = mean_val\r\n-                                    updated_stat = True\r\n-\r\n-                                if updated_stat:\r\n-                                    print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n-                            # else: print(f\"    Info: No valid 'stats' dictionary found for map type '{map_type_to_stat}' in image_stats_1k.\") # Optional\r\n-                        # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n-                    # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in image_stats_1k for stats application.\") # Optional\r\n-            # else: print(f\"    Warn: 'image_stats_1k' missing or invalid in metadata.\") # Optional\r\n-\r\n-            # --- Set Asset Preview (only for new parent groups) ---\r\n-            # Use the reference image path found earlier if available\r\n-            if is_new_parent and parent_group.asset_data:\r\n-                if ref_image_loaded and ref_image_path: # Check if ref image was successfully loaded earlier\r\n-                    print(f\"    Attempting to set preview from reference image: {Path(ref_image_path).name}\")\r\n-                    try:\r\n-                        # Ensure the ID (node group) is the active one for the operator context\r\n-                        with context.temp_override(id=parent_group):\r\n-                             bpy.ops.ed.lib_id_load_custom_preview(filepath=ref_image_path)\r\n-                        print(f\"      Successfully set custom preview.\")\r\n-                        previews_set += 1\r\n-                    except Exception as e_preview:\r\n-                        print(f\"      !!! ERROR setting custom preview: {e_preview}\")\r\n-                        errors_encountered += 1\r\n-                else:\r\n-                     print(f\"    Info: Could not set preview for '{asset_name}' as reference image was not found or loaded.\")\r\n-\r\n-\r\n-            # --- Child Group Handling ---\r\n-            # Iterate through the COMBINED map types\r\n-            print(f\"  DEBUG: Starting child group loop for asset '{asset_name}'. Map types: {list(all_map_resolutions.keys())}\") # DEBUG LOG (Indented)\r\n-            for map_type, resolutions in all_map_resolutions.items():\r\n-                print(f\"    Processing Map Type: {map_type}\")\r\n-\r\n-                # Determine if this is a merged map (not in map_details)\r\n-                is_merged_map = map_type not in map_details\r\n-\r\n-                # Get details for this map type if available\r\n-                current_map_details = map_details.get(map_type, {})\r\n-                # For merged maps, primary_format will be None\r\n-                output_format = current_map_details.get(\"output_format\")\r\n-\r\n-                if not output_format and not is_merged_map:\r\n-                    # This case should ideally not happen if metadata is well-formed\r\n-                    # but handle defensively for processed maps.\r\n-                    print(f\"      !!! WARNING: Missing 'output_format' in map_details for processed map '{map_type}'. Path reconstruction might fail.\")\r\n-                    # We will rely solely on fallback for this map type\r\n-\r\n-                # Find placeholder node in parent\r\n-                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n-                if not holder_nodes:\r\n-                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n-                    continue\r\n-                holder_node = holder_nodes[0] # Assume first is correct\r\n-                print(f\"      DEBUG: Found placeholder node '{holder_node.label}' for map type '{map_type}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                # Determine child group name (LOGICAL and ENCODED)\r\n-                logical_child_name = f\"{asset_name}_{map_type}\"\r\n-                target_child_name_b64 = encode_name_b64(logical_child_name) # Use Base64 name\r\n-\r\n-                child_group = bpy.data.node_groups.get(target_child_name_b64) # Find using encoded name\r\n-                is_new_child = False\r\n-\r\n-                if child_group is None:\r\n-                    print(f\"      DEBUG: Child group '{target_child_name_b64}' not found. Creating new one.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Creating new child group: '{target_child_name_b64}' (logical: '{logical_child_name}')\") # Verbose\r\n-                    child_group = template_child.copy()\r\n-                    if not child_group:\r\n-                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n-                        errors_encountered += 1\r\n-                        continue\r\n-                    child_group.name = target_child_name_b64 # Set encoded name\r\n-                    child_groups_created += 1\r\n-                    is_new_child = True\r\n-                else:\r\n-                    print(f\"      DEBUG: Found existing child group '{target_child_name_b64}'.\") # DEBUG LOG (Indented)\r\n-                    # print(f\"      Updating existing child group: '{target_child_name_b64}'\") # Verbose\r\n-                    child_groups_updated += 1\r\n-\r\n-                # Assign child group to placeholder if needed\r\n-                if holder_node.node_tree != child_group:\r\n-                    try:\r\n-                        holder_node.node_tree = child_group\r\n-                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n-                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n-                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n-                         continue # Skip this map type if assignment fails\r\n-\r\n-                # Link placeholder output to parent output socket\r\n-                try:\r\n-                    # Find parent's output node\r\n-                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n-                    if group_output_node:\r\n-                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n-                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n-                        # Get the specific input socket on the parent output node (matching map_type)\r\n-                        target_socket = group_output_node.inputs.get(map_type)\r\n-\r\n-                        if source_socket and target_socket:\r\n-                            # Check if link already exists\r\n-                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n-                            if not link_exists:\r\n-                                parent_group.links.new(source_socket, target_socket)\r\n-                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n-                        # else: # Optional warnings\r\n-                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n-                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n-                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n-\r\n-                except Exception as e_link:\r\n-                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n-\r\n-                # Ensure parent output socket type is Color (if it exists)\r\n-                try:\r\n-                    # Use the interface API for modern Blender versions\r\n-                    item = parent_group.interface.items_tree.get(map_type)\r\n-                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n-                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n-                         # Defaulting to Color seems reasonable for most PBR outputs\r\n-                        if item.socket_type != 'NodeSocketColor':\r\n-                            item.socket_type = 'NodeSocketColor'\r\n-                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n-                except Exception as e_sock_type:\r\n-                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n-\r\n-\r\n-                # --- Image Node Handling (Inside Child Group) ---\r\n-                if not isinstance(resolutions, list):\r\n-                     print(f\"      !!! ERROR: Invalid format for resolutions list for map type '{map_type}'. Skipping.\")\r\n-                     continue\r\n-\r\n-                for resolution in resolutions:\r\n-                    # --- Manifest Check (Map/Resolution Level) ---\r\n-                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n-                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n-                        maps_skipped_manifest += 1\r\n-                        continue\r\n-                    print(f\"        DEBUG: Processing map '{map_type}' resolution '{resolution}'. Manifest skip check passed.\") # DEBUG LOG (Indented)\r\n-\r\n-                    print(f\"        Processing Resolution: {resolution}\")\r\n-\r\n-                    # Reconstruct the image path using fallback logic\r\n-                    # Pass output_format (which might be None for merged maps)\r\n-                    image_path_str = reconstruct_image_path_with_fallback(\r\n-                        asset_dir_path=asset_dir_path,\r\n-                        asset_name=asset_name,\r\n-                        map_type=map_type,\r\n-                        resolution=resolution,\r\n-                        primary_format=output_format\r\n-                    )\r\n-                    print(f\"          DEBUG: Reconstructed image path for {map_type}/{resolution}: {image_path_str}\") # DEBUG LOG (Indented)\r\n-\r\n-                    if not image_path_str:\r\n-                        # Error already printed by reconstruct function\r\n-                        errors_encountered += 1\r\n-                        continue # Skip this resolution if path not found\r\n-\r\n-                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n-                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n-                    if not image_nodes:\r\n-                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n-                        continue # Skip this resolution if node not found\r\n-                    print(f\"          DEBUG: Found {len(image_nodes)} image node(s) labeled '{resolution}' in child group '{child_group.name}'.\") # DEBUG LOG (Indented)\r\n-\r\n-                    # --- Load Image ---\r\n-                    img = None\r\n-                    image_load_failed = False\r\n-                    try:\r\n-                        image_path = Path(image_path_str) # Path object created from already found path string\r\n-                        # Use check_existing=True to reuse existing datablocks if path matches\r\n-                        img = bpy.data.images.load(str(image_path), check_existing=True)\r\n-                        if not img:\r\n-                            print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n-                            image_load_failed = True\r\n-                        else:\r\n-                            # Only count as loaded if bpy.data.images.load succeeded\r\n-                            # Check if it's newly loaded or reused\r\n-                            is_newly_loaded = img.library is None # Newly loaded images don't have a library initially\r\n-                            if is_newly_loaded: images_loaded += 1\r\n-\r\n-                    except RuntimeError as e_runtime_load:\r\n-                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n-                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n-                        image_load_failed = True\r\n-                    except Exception as e_gen_load:\r\n-                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n-                        image_load_failed = True\r\n-                        errors_encountered += 1\r\n-\r\n-                    # --- Assign Image & Set Color Space ---\r\n-                    if not image_load_failed and img:\r\n-                        assigned_count_this_res = 0\r\n-                        for image_node in image_nodes:\r\n-                            if image_node.image != img:\r\n-                                image_node.image = img\r\n-                                assigned_count_this_res += 1\r\n-\r\n-                        if assigned_count_this_res > 0:\r\n-                             images_assigned += assigned_count_this_res\r\n-                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n-\r\n-                        # Set Color Space\r\n-                        correct_color_space = get_color_space(map_type)\r\n-                        try:\r\n-                            if img.colorspace_settings.name != correct_color_space:\r\n-                                img.colorspace_settings.name = correct_color_space\r\n-                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n-                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n-                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n-                        except Exception as e_cs_gen:\r\n-                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n-\r\n-\r\n-                        # --- Update Manifest (Map/Resolution Level) ---\r\n-                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n-                            manifest_needs_saving = True\r\n-                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n-                        maps_processed += 1\r\n-\r\n-                    else:\r\n-                        # Increment error count if loading failed\r\n-                        if image_load_failed: errors_encountered += 1\r\n-\r\n-                # --- End Resolution Loop ---\r\n-            # --- End Map Type Loop ---\r\n-\r\n-            assets_processed += 1\r\n-\r\n-        except FileNotFoundError:\r\n-            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except json.JSONDecodeError:\r\n-            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n-            errors_encountered += 1\r\n-        except Exception as e_main_loop:\r\n-            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n-            import traceback\r\n-            traceback.print_exc() # Print detailed traceback for debugging\r\n-            errors_encountered += 1\r\n-            # Continue to the next asset\r\n-\r\n-    # --- End Metadata File Loop ---\r\n-\r\n-    # --- Final Manifest Save ---\r\n-    if ENABLE_MANIFEST and manifest_needs_saving:\r\n-        print(\"\\nAttempting final manifest save...\")\r\n-        save_manifest(context, manifest_data)\r\n-    elif ENABLE_MANIFEST:\r\n-        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n-    # --- End Final Manifest Save ---\r\n-\r\n-    # --- Final Summary ---\r\n-    end_time = time.time()\r\n-    duration = end_time - start_time\r\n-    print(\"\\n--- Script Run Finished ---\")\r\n-    print(f\"Duration: {duration:.2f} seconds\")\r\n-    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n-    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n-    if ENABLE_MANIFEST:\r\n-        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n-        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n-    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n-    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n-    print(f\"Child Groups Created: {child_groups_created}\")\r\n-    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n-    print(f\"Images Loaded: {images_loaded}\")\r\n-    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n-    print(f\"Individual Maps Processed: {maps_processed}\")\r\n-    print(f\"Asset Previews Set: {previews_set}\")\r\n-    print(f\"Highest Resolution Nodes Set: {highest_res_set}\")\r\n-    print(f\"Aspect Ratio Nodes Set: {aspect_ratio_set}\") # Added counter\r\n-    if errors_encountered > 0:\r\n-        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n-    print(\"---------------------------\")\r\n-\r\n-    # --- Explicit Save ---\r\n-    print(f\"  DEBUG: Attempting explicit save for file: {bpy.data.filepath}\") # DEBUG LOG (Indented)\r\n-    try:\r\n-        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)\r\n-        print(\"\\n--- Explicitly saved the .blend file. ---\")\r\n-    except Exception as e_save:\r\n-        print(f\"\\n!!! ERROR explicitly saving .blend file: {e_save} !!!\")\r\n-        errors_encountered += 1 # Count save errors\r\n-\r\n-    return True\r\n-\r\n-\r\n-# --- Execution Block ---\r\n-\r\n-if __name__ == \"__main__\":\r\n-    # Ensure we are running within Blender\r\n-    try:\r\n-        import bpy\r\n-        import base64 # Ensure base64 is imported here too if needed globally\r\n-        import sys\r\n-    except ImportError:\r\n-        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n-    else:\r\n-        # --- Argument Parsing for Asset Library Root ---\r\n-        asset_root_arg = None\r\n-        try:\r\n-            # Blender arguments passed after '--' appear in sys.argv\r\n-            if \"--\" in sys.argv:\r\n-                args_after_dash = sys.argv[sys.argv.index(\"--\") + 1:]\r\n-                if len(args_after_dash) >= 1:\r\n-                    asset_root_arg = args_after_dash[0]\r\n-                    print(f\"Found asset library root argument: {asset_root_arg}\")\r\n-                else:\r\n-                    print(\"Info: '--' found but no arguments after it.\")\r\n-            # else: print(\"Info: No '--' found in arguments.\") # Optional debug\r\n-        except Exception as e:\r\n-            print(f\"Error parsing command line arguments: {e}\")\r\n-        # --- End Argument Parsing ---\r\n-\r\n-        process_library(bpy.context, asset_library_root_override=asset_root_arg)\r\n"
                },
                {
                    "date": 1745348312679,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -600,8 +600,15 @@\n                     print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n                     assets_skipped_manifest += 1\r\n                     continue # Skip to next metadata file\r\n \r\n+\r\n+            # Conditional skip based on asset_category\r\n+            if asset_category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # Check asset_category\r\n+                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{asset_category}'). Tag added.\") # Use asset_category in log\r\n+                assets_processed += 1 # Still count as processed for summary, even if skipped\r\n+                continue # Skip the rest of the processing for this asset\r\n+\r\n             # --- Parent Group Handling ---\r\n             target_parent_name = f\"PBRSET_{asset_name}\"\r\n             parent_group = bpy.data.node_groups.get(target_parent_name)\r\n             is_new_parent = False\r\n@@ -641,13 +648,8 @@\n                     add_tag_if_new(parent_group.asset_data, asset_category)\r\n                 # Add other tags if needed\r\n             # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n \r\n-            # Conditional skip based on asset_category\r\n-            if asset_category not in CATEGORIES_FOR_NODEGROUP_GENERATION: # Check asset_category\r\n-                print(f\"    Skipping nodegroup content generation for asset '{asset_name}' (Category: '{asset_category}'). Tag added.\") # Use asset_category in log\r\n-                assets_processed += 1 # Still count as processed for summary, even if skipped\r\n-                continue # Skip the rest of the processing for this asset\r\n \r\n             # Apply Aspect Ratio Correction\r\n             aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n             if aspect_nodes:\r\n"
                }
            ],
            "date": 1745229222683,
            "name": "Commit-0",
            "content": "# Blender Script: Create/Update Node Groups from Asset Processor Output\r\n# Version: 1.0\r\n# Description: Scans a library processed by the Asset Processor Tool,\r\n#              reads metadata.json files, and creates/updates corresponding\r\n#              PBR node groups in the active Blender file.\r\n\r\nimport bpy\r\nimport os\r\nimport json\r\nfrom pathlib import Path\r\nimport time\r\nimport re # For parsing aspect ratio string\r\n\r\n# --- USER CONFIGURATION ---\r\n\r\n# Path to the root output directory of the Asset Processor Tool\r\n# Example: r\"G:\\Assets\\Processed\"\r\nPROCESSED_ASSET_LIBRARY_ROOT = r\"G:\\02 Content\\10-19 Content\\13 Textures Power of Two\\13.00\" # <<< CHANGE THIS PATH!\r\n\r\n# Names of the required node group templates in the Blender file\r\nPARENT_TEMPLATE_NAME = \"Template_PBRSET\"\r\nCHILD_TEMPLATE_NAME = \"Template_PBRTYPE\"\r\n\r\n# Labels of specific nodes within the PARENT template\r\nASPECT_RATIO_NODE_LABEL = \"AspectRatioCorrection\" # Value node for UV X-scaling factor\r\nSTATS_NODE_PREFIX = \"Histogram-\" # Prefix for Combine XYZ nodes storing stats (e.g., \"Histogram-ROUGH\")\r\n\r\n# Enable/disable the manifest system to track processed assets/maps\r\n# If enabled, requires the blend file to be saved.\r\nENABLE_MANIFEST = True\r\n\r\n# Map PBR type strings (from metadata) to Blender color spaces\r\n# Add more mappings as needed based on your metadata types\r\nPBR_COLOR_SPACE_MAP = {\r\n    \"AO\": \"Non-Color\", # Usually Non-Color, but depends on workflow\r\n    \"COL\": \"sRGB\",\r\n    \"DISP\": \"Non-Color\",\r\n    \"NRM\": \"Non-Color\",\r\n    \"REFL\": \"Non-Color\", # Reflection/Specular\r\n    \"ROUGH\": \"Non-Color\",\r\n    \"METAL\": \"Non-Color\",\r\n    \"OPC\": \"Non-Color\", # Opacity/Alpha\r\n    \"TRN\": \"Non-Color\", # Transmission\r\n    \"SSS\": \"sRGB\",      # Subsurface Color\r\n    \"EMISS\": \"sRGB\",    # Emission Color\r\n    # Add other types like GLOSS, HEIGHT, etc. if needed\r\n}\r\nDEFAULT_COLOR_SPACE = \"sRGB\" # Fallback if map type not in the dictionary\r\n\r\n# Map types for which stats should be applied (if found in metadata and node exists)\r\nAPPLY_STATS_FOR_MAP_TYPES = [\"ROUGH\", \"DISP\", \"METAL\"] # Add others if needed\r\n\r\n# --- END USER CONFIGURATION ---\r\n\r\n\r\n# --- Helper Functions ---\r\n\r\ndef find_nodes_by_label(node_tree, label, node_type=None):\r\n    \"\"\"Finds ALL nodes in a node tree matching the label and optionally type.\"\"\"\r\n    if not node_tree:\r\n        return []\r\n    matching_nodes = []\r\n    for node in node_tree.nodes:\r\n        # Use node.label for labeled nodes, node.name for non-labeled (like Group Input/Output)\r\n        node_identifier = node.label if node.label else node.name\r\n        if node_identifier and node_identifier == label:\r\n            if node_type is None or node.bl_idname == node_type or node.type == node_type: # Check bl_idname and type for flexibility\r\n                matching_nodes.append(node)\r\n    return matching_nodes\r\n\r\ndef add_tag_if_new(asset_data, tag_name):\r\n    \"\"\"Adds a tag to the asset data if it's not None/empty and doesn't already exist.\"\"\"\r\n    if not asset_data or not tag_name or not isinstance(tag_name, str):\r\n        return False\r\n    cleaned_tag_name = tag_name.strip()\r\n    if not cleaned_tag_name:\r\n        return False\r\n\r\n    # Check if tag already exists (case-insensitive check might be better sometimes)\r\n    if cleaned_tag_name not in [t.name for t in asset_data.tags]:\r\n        try:\r\n            asset_data.tags.new(cleaned_tag_name)\r\n            print(f\"        + Added Asset Tag: '{cleaned_tag_name}'\")\r\n            return True\r\n        except Exception as e:\r\n            print(f\"        Error adding tag '{cleaned_tag_name}': {e}\")\r\n            return False\r\n    return False # Tag already existed\r\n\r\ndef get_color_space(map_type):\r\n    \"\"\"Returns the appropriate Blender color space name for a given map type string.\"\"\"\r\n    return PBR_COLOR_SPACE_MAP.get(map_type.upper(), DEFAULT_COLOR_SPACE)\r\n\r\ndef calculate_factor_from_string(aspect_string):\r\n    \"\"\"\r\n    Parses the aspect_ratio_change_string from metadata and returns the\r\n    appropriate UV X-scaling factor needed to correct distortion.\r\n    Assumes the string format documented in Asset Processor Tool readme.md:\r\n    \"EVEN\", \"Xnnn\", \"Ynnn\", \"XnnnYnnn\".\r\n    Returns 1.0 if the string is invalid or \"EVEN\".\r\n    \"\"\"\r\n    if not aspect_string or aspect_string.upper() == \"EVEN\":\r\n        return 1.0\r\n\r\n    x_factor = 1.0\r\n    y_factor = 1.0\r\n\r\n    # Regex to find X and Y scaling parts\r\n    match_x = re.search(r\"X(\\d+)\", aspect_string, re.IGNORECASE)\r\n    match_y = re.search(r\"Y(\\d+)\", aspect_string, re.IGNORECASE)\r\n\r\n    try:\r\n        if match_x:\r\n            amount_x = int(match_x.group(1))\r\n            if amount_x > 0:\r\n                x_factor = amount_x / 100.0\r\n            else:\r\n                 print(f\"        Warn: Invalid X amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n\r\n        if match_y:\r\n            amount_y = int(match_y.group(1))\r\n            if amount_y > 0:\r\n                y_factor = amount_y / 100.0\r\n            else:\r\n                 print(f\"        Warn: Invalid Y amount in aspect string '{aspect_string}'. Using 1.0.\")\r\n\r\n        # The correction factor for the U (X) coordinate is Y/X\r\n        # If X was scaled by 1.5 (X150), U needs to be divided by 1.5 (multiplied by 1/1.5)\r\n        # If Y was scaled by 1.5 (Y150), U needs to be multiplied by 1.5\r\n        if x_factor == 0: # Avoid division by zero\r\n             print(f\"        Warn: X factor is zero in aspect string '{aspect_string}'. Cannot calculate correction. Returning 1.0.\")\r\n             return 1.0\r\n\r\n        correction_factor = y_factor / x_factor\r\n        return correction_factor\r\n\r\n    except ValueError:\r\n        print(f\"        Warn: Invalid number in aspect string '{aspect_string}'. Returning 1.0.\")\r\n        return 1.0\r\n    except Exception as e:\r\n        print(f\"        Error parsing aspect string '{aspect_string}': {e}. Returning 1.0.\")\r\n        return 1.0\r\n\r\n\r\n# --- Manifest Functions ---\r\n\r\ndef get_manifest_path(context):\r\n    \"\"\"Gets the expected path for the manifest JSON file.\"\"\"\r\n    if not context or not context.blend_data or not context.blend_data.filepath:\r\n        return None # Cannot determine path if blend file is not saved\r\n    blend_path = Path(context.blend_data.filepath)\r\n    manifest_filename = f\"{blend_path.stem}_manifest.json\"\r\n    return blend_path.parent / manifest_filename\r\n\r\ndef load_manifest(context):\r\n    \"\"\"Loads the manifest data from the JSON file.\"\"\"\r\n    if not ENABLE_MANIFEST:\r\n        return {} # Manifest disabled\r\n\r\n    manifest_path = get_manifest_path(context)\r\n    if not manifest_path:\r\n        print(\"   Manifest Info: Blend file not saved. Cannot load manifest.\")\r\n        return {} # Cannot load without a path\r\n\r\n    if not manifest_path.exists():\r\n        print(f\"   Manifest Info: No manifest file found at '{manifest_path.name}'. Starting fresh.\")\r\n        return {} # No manifest file exists yet\r\n\r\n    try:\r\n        with open(manifest_path, 'r', encoding='utf-8') as f:\r\n            data = json.load(f)\r\n        print(f\"   Manifest Loaded from: {manifest_path.name}\")\r\n        # Basic validation (check if it's a dictionary)\r\n        if not isinstance(data, dict):\r\n             print(f\"!!! WARNING: Manifest file '{manifest_path.name}' has invalid format (not a dictionary). Starting fresh. !!!\")\r\n             return {}\r\n        return data\r\n    except json.JSONDecodeError:\r\n        print(f\"!!! WARNING: Manifest file '{manifest_path.name}' is corrupted. Starting fresh. !!!\")\r\n        return {}\r\n    except Exception as e:\r\n        print(f\"!!! ERROR: Could not load manifest file '{manifest_path.name}': {e} !!!\")\r\n        return {} # Treat as starting fresh on error\r\n\r\ndef save_manifest(context, manifest_data):\r\n    \"\"\"Saves the manifest data to the JSON file.\"\"\"\r\n    if not ENABLE_MANIFEST or not manifest_data: # Don't save if disabled or empty\r\n        return False\r\n\r\n    manifest_path = get_manifest_path(context)\r\n    if not manifest_path:\r\n        print(\"   Manifest Error: Blend file not saved. Cannot save manifest.\")\r\n        return False\r\n\r\n    try:\r\n        with open(manifest_path, 'w', encoding='utf-8') as f:\r\n            json.dump(manifest_data, f, indent=2, sort_keys=True) # Use indent and sort for readability\r\n        print(f\"   Manifest Saved to: {manifest_path.name}\")\r\n        return True\r\n    except Exception as e:\r\n        print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\"\r\n              f\"!!! Manifest save FAILED to '{manifest_path.name}': {e} !!!\\n\"\r\n              f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\r\n        return False\r\n\r\ndef is_asset_processed(manifest_data, asset_name):\r\n    \"\"\"Checks if the entire asset (all its maps/resolutions) is marked as processed.\"\"\"\r\n    if not ENABLE_MANIFEST: return False\r\n    # For now, we process map-by-map. An asset is considered processed\r\n    # if its entry exists, but we rely on map checks.\r\n    # This could be enhanced later if needed.\r\n    return asset_name in manifest_data\r\n\r\ndef is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n    \"\"\"Checks if a specific map type and resolution for an asset is processed.\"\"\"\r\n    if not ENABLE_MANIFEST: return False\r\n    return resolution in manifest_data.get(asset_name, {}).get(map_type, [])\r\n\r\ndef update_manifest(manifest_data, asset_name, map_type=None, resolution=None):\r\n    \"\"\"Updates the manifest dictionary in memory.\"\"\"\r\n    if not ENABLE_MANIFEST: return False\r\n\r\n    # Ensure asset entry exists\r\n    if asset_name not in manifest_data:\r\n        manifest_data[asset_name] = {}\r\n\r\n    # If map_type and resolution are provided, update the specific map entry\r\n    if map_type and resolution:\r\n        if map_type not in manifest_data[asset_name]:\r\n            manifest_data[asset_name][map_type] = []\r\n\r\n        if resolution not in manifest_data[asset_name][map_type]:\r\n            manifest_data[asset_name][map_type].append(resolution)\r\n            manifest_data[asset_name][map_type].sort() # Keep sorted\r\n            return True # Indicate that a change was made\r\n    return False # No change made to this specific map/res\r\n\r\n\r\n# --- Core Logic ---\r\n\r\ndef process_library(context):\r\n    \"\"\"Scans the library, reads metadata, creates/updates node groups.\"\"\"\r\n    start_time = time.time()\r\n    print(f\"\\n--- Starting Node Group Processing ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---\")\r\n\r\n    # --- Pre-run Checks ---\r\n    print(\"Performing pre-run checks...\")\r\n    valid_setup = True\r\n    # 1. Check Library Root Path\r\n    root_path = Path(PROCESSED_ASSET_LIBRARY_ROOT)\r\n    if not root_path.is_dir():\r\n        print(f\"!!! ERROR: Processed asset library root directory not found or not a directory:\")\r\n        print(f\"!!!        '{PROCESSED_ASSET_LIBRARY_ROOT}'\")\r\n        valid_setup = False\r\n    else:\r\n        print(f\"   Asset Library Root: '{root_path}'\")\r\n\r\n    # 2. Check Templates\r\n    template_parent = bpy.data.node_groups.get(PARENT_TEMPLATE_NAME)\r\n    template_child = bpy.data.node_groups.get(CHILD_TEMPLATE_NAME)\r\n    if not template_parent:\r\n        print(f\"!!! ERROR: Parent template node group '{PARENT_TEMPLATE_NAME}' not found in this Blender file.\")\r\n        valid_setup = False\r\n    if not template_child:\r\n        print(f\"!!! ERROR: Child template node group '{CHILD_TEMPLATE_NAME}' not found in this Blender file.\")\r\n        valid_setup = False\r\n    if template_parent and template_child:\r\n         print(f\"   Found Templates: '{PARENT_TEMPLATE_NAME}', '{CHILD_TEMPLATE_NAME}'\")\r\n\r\n    # 3. Check Blend File Saved (if manifest enabled)\r\n    if ENABLE_MANIFEST and not context.blend_data.filepath:\r\n        print(f\"!!! WARNING: Manifest is enabled, but the current Blender file is not saved.\")\r\n        print(f\"!!!          Manifest cannot be loaded or saved. Processing will continue without manifest checks.\")\r\n        global ENABLE_MANIFEST # Allow modification of global\r\n        ENABLE_MANIFEST = False # Disable manifest for this run\r\n\r\n    if not valid_setup:\r\n        print(\"\\n--- Script aborted due to configuration errors. Please fix the issues above. ---\")\r\n        return False\r\n    print(\"Pre-run checks passed.\")\r\n    # --- End Pre-run Checks ---\r\n\r\n    manifest_data = load_manifest(context)\r\n    manifest_needs_saving = False\r\n\r\n    # --- Initialize Counters ---\r\n    metadata_files_found = 0\r\n    assets_processed = 0\r\n    assets_skipped_manifest = 0\r\n    parent_groups_created = 0\r\n    parent_groups_updated = 0\r\n    child_groups_created = 0\r\n    child_groups_updated = 0\r\n    images_loaded = 0\r\n    images_assigned = 0\r\n    maps_processed = 0\r\n    maps_skipped_manifest = 0\r\n    errors_encountered = 0\r\n    # --- End Counters ---\r\n\r\n    print(f\"\\nScanning for metadata files in '{root_path}'...\")\r\n\r\n    # --- Scan for metadata.json ---\r\n    # Using rglob to find all metadata.json files recursively\r\n    metadata_paths = list(root_path.rglob('metadata.json'))\r\n    metadata_files_found = len(metadata_paths)\r\n    print(f\"Found {metadata_files_found} metadata.json files.\")\r\n\r\n    if metadata_files_found == 0:\r\n        print(\"No metadata files found. Nothing to process.\")\r\n        print(\"--- Script Finished ---\")\r\n        return True # No work needed is considered success\r\n\r\n    # --- Process Each Metadata File ---\r\n    for metadata_path in metadata_paths:\r\n        print(f\"\\n--- Processing Metadata: {metadata_path.relative_to(root_path)} ---\")\r\n        try:\r\n            with open(metadata_path, 'r', encoding='utf-8') as f:\r\n                metadata = json.load(f)\r\n\r\n            # --- Extract Key Info ---\r\n            asset_name = metadata.get(\"asset_name\")\r\n            supplier_name = metadata.get(\"supplier_name\")\r\n            archetype = metadata.get(\"archetype\")\r\n            maps_data = metadata.get(\"maps\")\r\n            aspect_string = metadata.get(\"aspect_ratio_change_string\")\r\n\r\n            if not asset_name or not maps_data:\r\n                print(f\"    !!! ERROR: Metadata file is missing 'asset_name' or 'maps' data. Skipping.\")\r\n                errors_encountered += 1\r\n                continue\r\n\r\n            print(f\"  Asset Name: {asset_name}\")\r\n\r\n            # --- Manifest Check (Asset Level) ---\r\n            # We primarily check maps, but can skip the whole asset if needed later\r\n            if ENABLE_MANIFEST and is_asset_processed(manifest_data, asset_name):\r\n                # Check if ALL maps/resolutions are actually done, otherwise proceed\r\n                all_maps_done = True\r\n                for map_type, res_data in maps_data.items():\r\n                    for resolution in res_data.keys():\r\n                        if not is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n                            all_maps_done = False\r\n                            break\r\n                    if not all_maps_done:\r\n                        break\r\n                if all_maps_done:\r\n                    print(f\"  Skipping asset '{asset_name}' (already fully processed according to manifest).\")\r\n                    assets_skipped_manifest += 1\r\n                    continue # Skip to next metadata file\r\n\r\n            # --- Parent Group Handling ---\r\n            target_parent_name = f\"PBRSET_{asset_name}\"\r\n            parent_group = bpy.data.node_groups.get(target_parent_name)\r\n            is_new_parent = False\r\n\r\n            if parent_group is None:\r\n                print(f\"  Creating new parent group: '{target_parent_name}'\")\r\n                parent_group = template_parent.copy()\r\n                if not parent_group:\r\n                    print(f\"    !!! ERROR: Failed to copy parent template '{PARENT_TEMPLATE_NAME}'. Skipping asset '{asset_name}'.\")\r\n                    errors_encountered += 1\r\n                    continue\r\n                parent_group.name = target_parent_name\r\n                parent_groups_created += 1\r\n                is_new_parent = True\r\n            else:\r\n                print(f\"  Updating existing parent group: '{target_parent_name}'\")\r\n                parent_groups_updated += 1\r\n\r\n            # Ensure marked as asset\r\n            if not parent_group.asset_data:\r\n                try:\r\n                    parent_group.asset_mark()\r\n                    print(f\"    Marked '{parent_group.name}' as asset.\")\r\n                except Exception as e_mark:\r\n                    print(f\"    !!! ERROR: Failed to mark group '{parent_group.name}' as asset: {e_mark}\")\r\n                    # Continue processing other parts if possible\r\n\r\n            # Apply Asset Tags\r\n            if parent_group.asset_data:\r\n                if supplier_name:\r\n                    add_tag_if_new(parent_group.asset_data, supplier_name)\r\n                if archetype:\r\n                    add_tag_if_new(parent_group.asset_data, archetype)\r\n                # Add other tags if needed\r\n            # else: print(f\"    Warn: Cannot apply tags, asset_data not available for '{parent_group.name}'.\") # Optional warning\r\n\r\n            # Apply Aspect Ratio Correction\r\n            aspect_nodes = find_nodes_by_label(parent_group, ASPECT_RATIO_NODE_LABEL, 'ShaderNodeValue')\r\n            if aspect_nodes:\r\n                aspect_node = aspect_nodes[0]\r\n                correction_factor = calculate_factor_from_string(aspect_string)\r\n                # Check if update is needed (avoids unnecessary console spam)\r\n                if abs(aspect_node.outputs[0].default_value - correction_factor) > 0.0001:\r\n                    aspect_node.outputs[0].default_value = correction_factor\r\n                    print(f\"    Set '{ASPECT_RATIO_NODE_LABEL}' value to {correction_factor:.4f} (from string '{aspect_string}')\")\r\n            # else: print(f\"    Warn: Aspect ratio node '{ASPECT_RATIO_NODE_LABEL}' not found in parent group.\") # Optional\r\n\r\n            # Apply Stats\r\n            for map_type_to_stat in APPLY_STATS_FOR_MAP_TYPES:\r\n                if map_type_to_stat in maps_data:\r\n                    # Find the stats node in the parent group\r\n                    stats_node_label = f\"{STATS_NODE_PREFIX}{map_type_to_stat}\"\r\n                    stats_nodes = find_nodes_by_label(parent_group, stats_node_label, 'ShaderNodeCombineXYZ')\r\n                    if stats_nodes:\r\n                        stats_node = stats_nodes[0]\r\n                        # Find the stats data in the metadata (usually from a specific resolution)\r\n                        # Let's assume the stats are stored directly under the map_type entry\r\n                        map_metadata = maps_data[map_type_to_stat]\r\n                        stats = map_metadata.get(\"stats\") # Expecting {\"min\": float, \"max\": float, \"mean\": float}\r\n                        if stats and isinstance(stats, dict):\r\n                            min_val = stats.get(\"min\")\r\n                            max_val = stats.get(\"max\")\r\n                            mean_val = stats.get(\"mean\") # Often stored as 'mean' or 'avg'\r\n\r\n                            updated_stat = False\r\n                            if min_val is not None and abs(stats_node.inputs[0].default_value - min_val) > 0.0001:\r\n                                stats_node.inputs[0].default_value = min_val\r\n                                updated_stat = True\r\n                            if max_val is not None and abs(stats_node.inputs[1].default_value - max_val) > 0.0001:\r\n                                stats_node.inputs[1].default_value = max_val\r\n                                updated_stat = True\r\n                            if mean_val is not None and abs(stats_node.inputs[2].default_value - mean_val) > 0.0001:\r\n                                stats_node.inputs[2].default_value = mean_val\r\n                                updated_stat = True\r\n\r\n                            if updated_stat:\r\n                                print(f\"    Set stats in '{stats_node_label}': Min={min_val:.4f}, Max={max_val:.4f}, Mean={mean_val:.4f}\")\r\n                        # else: print(f\"    Info: No 'stats' dictionary found for map type '{map_type_to_stat}' in metadata.\") # Optional\r\n                    # else: print(f\"    Warn: Stats node '{stats_node_label}' not found in parent group.\") # Optional\r\n                # else: print(f\"    Info: Map type '{map_type_to_stat}' not present in metadata for stats application.\") # Optional\r\n\r\n\r\n            # --- Child Group Handling ---\r\n            processed_asset_flag = True # Track if any map within the asset was actually processed in this run\r\n            for map_type, type_data in maps_data.items():\r\n                print(f\"    Processing Map Type: {map_type}\")\r\n\r\n                # Find placeholder node in parent\r\n                # Placeholders might be labeled directly with the map type (e.g., \"NRM\", \"COL\")\r\n                holder_nodes = find_nodes_by_label(parent_group, map_type, 'ShaderNodeGroup')\r\n                if not holder_nodes:\r\n                    print(f\"      !!! WARNING: No placeholder node labeled '{map_type}' found in parent group '{parent_group.name}'. Skipping this map type.\")\r\n                    continue\r\n                holder_node = holder_nodes[0] # Assume first is correct\r\n\r\n                # Determine child group name\r\n                target_child_name = f\"PBRTYPE_{asset_name}_{map_type}\"\r\n                child_group = bpy.data.node_groups.get(target_child_name)\r\n                is_new_child = False\r\n\r\n                if child_group is None:\r\n                    # print(f\"      Creating new child group: '{target_child_name}'\") # Verbose\r\n                    child_group = template_child.copy()\r\n                    if not child_group:\r\n                        print(f\"        !!! ERROR: Failed to copy child template '{CHILD_TEMPLATE_NAME}'. Skipping map type '{map_type}'.\")\r\n                        errors_encountered += 1\r\n                        continue\r\n                    child_group.name = target_child_name\r\n                    child_groups_created += 1\r\n                    is_new_child = True\r\n                else:\r\n                    # print(f\"      Updating existing child group: '{target_child_name}'\") # Verbose\r\n                    child_groups_updated += 1\r\n\r\n                # Assign child group to placeholder if needed\r\n                if holder_node.node_tree != child_group:\r\n                    try:\r\n                        holder_node.node_tree = child_group\r\n                        print(f\"      Assigned child group '{child_group.name}' to placeholder '{holder_node.label}'.\")\r\n                    except TypeError as e_assign: # Catch potential type errors if placeholder isn't a group node\r\n                         print(f\"        !!! ERROR: Could not assign child group to placeholder '{holder_node.label}'. Is it a Group Node? Error: {e_assign}\")\r\n                         continue # Skip this map type if assignment fails\r\n\r\n                # Link placeholder output to parent output socket\r\n                try:\r\n                    # Find parent's output node\r\n                    group_output_node = next((n for n in parent_group.nodes if n.type == 'GROUP_OUTPUT'), None)\r\n                    if group_output_node:\r\n                        # Get the specific output socket on the placeholder (usually index 0 or named 'Color'/'Value')\r\n                        source_socket = holder_node.outputs.get(\"Color\") or holder_node.outputs.get(\"Value\") or holder_node.outputs[0]\r\n                        # Get the specific input socket on the parent output node (matching map_type)\r\n                        target_socket = group_output_node.inputs.get(map_type)\r\n\r\n                        if source_socket and target_socket:\r\n                            # Check if link already exists\r\n                            link_exists = any(link.from_socket == source_socket and link.to_socket == target_socket for link in parent_group.links)\r\n                            if not link_exists:\r\n                                parent_group.links.new(source_socket, target_socket)\r\n                                print(f\"        Linked '{holder_node.label}' output to parent output socket '{map_type}'.\")\r\n                        # else: # Optional warnings\r\n                        #     if not source_socket: print(f\"        Warn: Could not find suitable output socket on placeholder '{holder_node.label}'.\")\r\n                        #     if not target_socket: print(f\"        Warn: Could not find input socket '{map_type}' on parent output node.\")\r\n                    # else: print(f\"      Warn: Parent group '{parent_group.name}' has no Group Output node.\")\r\n\r\n                except Exception as e_link:\r\n                    print(f\"        !!! ERROR linking sockets for '{map_type}': {e_link}\")\r\n\r\n                # Ensure parent output socket type is Color (if it exists)\r\n                try:\r\n                    # Use the interface API for modern Blender versions\r\n                    item = parent_group.interface.items_tree.get(map_type)\r\n                    if item and item.item_type == 'SOCKET' and item.in_out == 'OUTPUT':\r\n                         # Common useful types: 'NodeSocketColor', 'NodeSocketVector', 'NodeSocketFloat'\r\n                         # Defaulting to Color seems reasonable for most PBR outputs\r\n                        if item.socket_type != 'NodeSocketColor':\r\n                            item.socket_type = 'NodeSocketColor'\r\n                            # print(f\"        Set parent output socket '{map_type}' type to Color.\") # Optional info\r\n                except Exception as e_sock_type:\r\n                    print(f\"        Warn: Could not verify/set socket type for '{map_type}': {e_sock_type}\")\r\n\r\n\r\n                # --- Image Node Handling (Inside Child Group) ---\r\n                # 'type_data' should be the dictionary containing resolutions and paths for this map_type\r\n                if not isinstance(type_data, dict):\r\n                     print(f\"      !!! ERROR: Invalid format for map type '{map_type}' data in metadata. Skipping.\")\r\n                     continue\r\n\r\n                for resolution, map_info in type_data.items():\r\n                    # 'map_info' should be the dictionary containing 'path', 'stats', etc. for this resolution\r\n                    if not isinstance(map_info, dict) or \"path\" not in map_info:\r\n                        print(f\"        !!! WARNING: Invalid or missing path data for {map_type}/{resolution}. Skipping.\")\r\n                        continue\r\n\r\n                    image_path_str = map_info[\"path\"]\r\n\r\n                    # --- Manifest Check (Map/Resolution Level) ---\r\n                    if ENABLE_MANIFEST and is_map_processed(manifest_data, asset_name, map_type, resolution):\r\n                        # print(f\"        Skipping {resolution} (Manifest)\") # Verbose\r\n                        maps_skipped_manifest += 1\r\n                        continue\r\n\r\n                    print(f\"        Processing Resolution: {resolution}\")\r\n\r\n                    # Find image texture node within the CHILD group (labeled by resolution, e.g., \"4K\")\r\n                    image_nodes = find_nodes_by_label(child_group, resolution, 'ShaderNodeTexImage')\r\n                    if not image_nodes:\r\n                        print(f\"          !!! WARNING: No Image Texture node labeled '{resolution}' found in child group '{child_group.name}'. Cannot assign image.\")\r\n                        continue # Skip this resolution if node not found\r\n\r\n                    # --- Load Image ---\r\n                    img = None\r\n                    image_load_failed = False\r\n                    try:\r\n                        image_path = Path(image_path_str)\r\n                        if not image_path.is_file():\r\n                            print(f\"          !!! ERROR: Image file not found: {image_path_str}\")\r\n                            image_load_failed = True\r\n                        else:\r\n                            # Use check_existing=True to reuse existing datablocks if path matches\r\n                            img = bpy.data.images.load(str(image_path), check_existing=True)\r\n                            if not img:\r\n                                print(f\"          !!! ERROR: Failed loading image via bpy.data.images.load: {image_path_str}\")\r\n                                image_load_failed = True\r\n                            else:\r\n                                images_loaded += 1 # Count successful loads\r\n                    except RuntimeError as e_runtime_load:\r\n                        # Catch specific Blender runtime errors (e.g., unsupported format)\r\n                        print(f\"          !!! ERROR loading image '{image_path_str}': {e_runtime_load}\")\r\n                        image_load_failed = True\r\n                    except Exception as e_gen_load:\r\n                        print(f\"          !!! UNEXPECTED ERROR loading image '{image_path_str}': {e_gen_load}\")\r\n                        image_load_failed = True\r\n                        errors_encountered += 1\r\n\r\n                    # --- Assign Image & Set Color Space ---\r\n                    if not image_load_failed and img:\r\n                        assigned_count_this_res = 0\r\n                        for image_node in image_nodes:\r\n                            if image_node.image != img:\r\n                                image_node.image = img\r\n                                assigned_count_this_res += 1\r\n\r\n                        if assigned_count_this_res > 0:\r\n                             images_assigned += assigned_count_this_res\r\n                             print(f\"          Assigned image '{img.name}' to {assigned_count_this_res} node(s).\")\r\n\r\n                        # Set Color Space\r\n                        correct_color_space = get_color_space(map_type)\r\n                        try:\r\n                            if img.colorspace_settings.name != correct_color_space:\r\n                                img.colorspace_settings.name = correct_color_space\r\n                                print(f\"          Set '{img.name}' color space -> {correct_color_space}\")\r\n                        except TypeError as e_cs: # Handle case where colorspace name is invalid\r\n                             print(f\"          !!! WARNING: Could not set color space '{correct_color_space}' for image '{img.name}'. Is the color space available in Blender? Error: {e_cs}\")\r\n                        except Exception as e_cs_gen:\r\n                             print(f\"          !!! ERROR setting color space for image '{img.name}': {e_cs_gen}\")\r\n\r\n\r\n                        # --- Update Manifest (Map/Resolution Level) ---\r\n                        if update_manifest(manifest_data, asset_name, map_type, resolution):\r\n                            manifest_needs_saving = True\r\n                            # print(f\"          Marked {map_type}/{resolution} processed in manifest.\") # Verbose\r\n                        maps_processed += 1\r\n                        processed_asset_flag = False # Mark that something was processed for this asset\r\n\r\n                    else:\r\n                        errors_encountered += 1\r\n                        processed_asset_flag = False # Still counts as an attempt for this asset\r\n\r\n                # --- End Resolution Loop ---\r\n            # --- End Map Type Loop ---\r\n\r\n            # --- Update Manifest (Asset Level - if fully processed) ---\r\n            # This logic might be redundant if we always check map level, but can be added\r\n            # if needed to mark the whole asset as 'touched' or 'completed'.\r\n            # For now, we rely on map-level updates.\r\n            # if not processed_asset_flag: # If any map was processed (or attempted)\r\n            #     update_manifest(manifest_data, asset_name) # Mark asset as processed\r\n            #     manifest_needs_saving = True\r\n\r\n            assets_processed += 1\r\n\r\n        except FileNotFoundError:\r\n            print(f\"    !!! ERROR: Metadata file not found (should not happen if scan worked): {metadata_path}\")\r\n            errors_encountered += 1\r\n        except json.JSONDecodeError:\r\n            print(f\"    !!! ERROR: Invalid JSON in metadata file: {metadata_path}\")\r\n            errors_encountered += 1\r\n        except Exception as e_main_loop:\r\n            print(f\"    !!! UNEXPECTED ERROR processing asset from {metadata_path}: {e_main_loop}\")\r\n            import traceback\r\n            traceback.print_exc() # Print detailed traceback for debugging\r\n            errors_encountered += 1\r\n            # Continue to the next asset\r\n\r\n    # --- End Metadata File Loop ---\r\n\r\n    # --- Final Manifest Save ---\r\n    if ENABLE_MANIFEST and manifest_needs_saving:\r\n        print(\"\\nAttempting final manifest save...\")\r\n        save_manifest(context, manifest_data)\r\n    elif ENABLE_MANIFEST:\r\n        print(\"\\nManifest is enabled, but no changes require saving.\")\r\n    # --- End Final Manifest Save ---\r\n\r\n    # --- Final Summary ---\r\n    end_time = time.time()\r\n    duration = end_time - start_time\r\n    print(\"\\n--- Script Run Finished ---\")\r\n    print(f\"Duration: {duration:.2f} seconds\")\r\n    print(f\"Metadata Files Found: {metadata_files_found}\")\r\n    print(f\"Assets Processed/Attempted: {assets_processed}\")\r\n    if ENABLE_MANIFEST:\r\n        print(f\"Assets Skipped (Manifest): {assets_skipped_manifest}\")\r\n        print(f\"Maps Skipped (Manifest): {maps_skipped_manifest}\")\r\n    print(f\"Parent Groups Created: {parent_groups_created}\")\r\n    print(f\"Parent Groups Updated: {parent_groups_updated}\")\r\n    print(f\"Child Groups Created: {child_groups_created}\")\r\n    print(f\"Child Groups Updated: {child_groups_updated}\")\r\n    print(f\"Images Loaded: {images_loaded}\")\r\n    print(f\"Image Nodes Assigned: {images_assigned}\")\r\n    print(f\"Individual Maps Processed: {maps_processed}\")\r\n    if errors_encountered > 0:\r\n        print(f\"!!! Errors Encountered: {errors_encountered} !!!\")\r\n    print(\"---------------------------\")\r\n\r\n    return True\r\n\r\n\r\n# --- Execution Block ---\r\n\r\nif __name__ == \"__main__\":\r\n    # Ensure we are running within Blender\r\n    try:\r\n        import bpy\r\n    except ImportError:\r\n        print(\"!!! ERROR: This script must be run from within Blender. !!!\")\r\n    else:\r\n        process_library(bpy.context)"
        }
    ]
}