2026-02-16
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
"""AMZN Character Tools - Main addon entry point."""
|
||||
import bpy
|
||||
|
||||
from .ui import OPERATOR_CLASSES, PANEL_CLASSES
|
||||
from .ui import OPERATOR_CLASSES, PANEL_CLASSES, AMZN_AddonPreferences
|
||||
|
||||
|
||||
CLASSES = (*OPERATOR_CLASSES, *PANEL_CLASSES)
|
||||
CLASSES = (*OPERATOR_CLASSES, *PANEL_CLASSES, AMZN_AddonPreferences)
|
||||
|
||||
|
||||
def register():
|
||||
"""Register all addon classes."""
|
||||
# Set preferences bl_idname to match __package__ (like Rainys_Bulk_Scene_Tools)
|
||||
AMZN_AddonPreferences.bl_idname = __package__
|
||||
|
||||
# Register all classes
|
||||
for cls in CLASSES:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
@@ -16,7 +20,11 @@ def register():
|
||||
def unregister():
|
||||
"""Unregister all addon classes."""
|
||||
for cls in reversed(CLASSES):
|
||||
bpy.utils.unregister_class(cls)
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
# Class may not have been registered, skip it
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
|
||||
id = "amzncharactertools"
|
||||
name = "AMZNCharacterTools"
|
||||
tagline = "AMZNCharacterTools"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "Nathan Lindsay"
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
import bpy
|
||||
import math
|
||||
import os
|
||||
|
||||
try:
|
||||
from ..utils import get_addon_preferences
|
||||
except (ImportError, ValueError):
|
||||
# Fallback if import fails (e.g., when run as script)
|
||||
def get_addon_preferences():
|
||||
test_names = ["bl_ext.vscode_development.AmazonCharacterTools", "amzncharactertools", "AmazonCharacterTools"]
|
||||
for addon_name in test_names:
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences') and hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
# Search all addons
|
||||
for addon_name in bpy.context.preferences.addons.keys():
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences') and hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
return None
|
||||
|
||||
def append_and_parent_device():
|
||||
# First, find and rename the existing Device to Device-Old
|
||||
@@ -31,8 +49,17 @@ def append_and_parent_device():
|
||||
old_parent_type = 'OBJECT'
|
||||
old_parent_bone = ''
|
||||
|
||||
# Append the new Device asset
|
||||
device_blend_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\device_v2.blend"
|
||||
# Get path from addon preferences
|
||||
prefs = get_addon_preferences()
|
||||
if not prefs:
|
||||
print("Error: Could not access addon preferences")
|
||||
return
|
||||
|
||||
device_blend_path = prefs.amzn_device_path
|
||||
|
||||
if not device_blend_path or not os.path.exists(device_blend_path):
|
||||
print(f"Error: Device library path not set or file not found: {device_blend_path}")
|
||||
return
|
||||
|
||||
# Append the Device object
|
||||
with bpy.data.libraries.load(device_blend_path, link=False) as (data_from, data_to):
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
import bpy
|
||||
import math
|
||||
import os
|
||||
|
||||
try:
|
||||
from ..utils import get_addon_preferences
|
||||
except (ImportError, ValueError):
|
||||
# Fallback if import fails (e.g., when run as script)
|
||||
def get_addon_preferences():
|
||||
test_names = ["bl_ext.vscode_development.AmazonCharacterTools", "amzncharactertools", "AmazonCharacterTools"]
|
||||
for addon_name in test_names:
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences') and hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
# Search all addons
|
||||
for addon_name in bpy.context.preferences.addons.keys():
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences') and hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
return None
|
||||
|
||||
def append_and_parent_device():
|
||||
# Append the Device asset
|
||||
device_blend_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\device_v2.blend"
|
||||
# Get path from addon preferences
|
||||
prefs = get_addon_preferences()
|
||||
if not prefs:
|
||||
print("Error: Could not access addon preferences")
|
||||
return
|
||||
|
||||
device_blend_path = prefs.amzn_device_path
|
||||
|
||||
if not device_blend_path or not os.path.exists(device_blend_path):
|
||||
print(f"Error: Device library path not set or file not found: {device_blend_path}")
|
||||
return
|
||||
|
||||
# Append the Device object
|
||||
with bpy.data.libraries.load(device_blend_path, link=False) as (data_from, data_to):
|
||||
@@ -51,8 +78,17 @@ def append_and_parent_device():
|
||||
print(f"Successfully parented 'Device' to {active_armature.name} bone 'DEF-forearm.L'")
|
||||
|
||||
def append_and_parent_finger_scanner():
|
||||
# Append the Finger-Scanner asset
|
||||
scanner_blend_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\amazon-3Dworld-assets_v4.0.blend"
|
||||
# Get path from addon preferences
|
||||
prefs = get_addon_preferences()
|
||||
if not prefs:
|
||||
print("Error: Could not access addon preferences")
|
||||
return
|
||||
|
||||
scanner_blend_path = prefs.amzn_scanner_assets_path
|
||||
|
||||
if not scanner_blend_path or not os.path.exists(scanner_blend_path):
|
||||
print(f"Error: Scanner assets library path not set or file not found: {scanner_blend_path}")
|
||||
return
|
||||
|
||||
# Append the Finger-Scanner object
|
||||
with bpy.data.libraries.load(scanner_blend_path, link=False) as (data_from, data_to):
|
||||
|
||||
@@ -80,9 +80,17 @@ def create_settings_bone():
|
||||
# Deselect all bones first
|
||||
bpy.ops.pose.select_all(action='DESELECT')
|
||||
|
||||
# Select the Settings bone
|
||||
# Select the Settings bone (compatible with Blender 4.2/4.5 LTS and 5.0+)
|
||||
pose_bone = armature_obj.pose.bones['Settings']
|
||||
|
||||
# Blender 5.0+ uses pose_bone.select, older versions use pose_bone.bone.select
|
||||
if hasattr(pose_bone, 'select'):
|
||||
# Blender 5.0+
|
||||
pose_bone.select = True
|
||||
elif hasattr(pose_bone.bone, 'select'):
|
||||
# Blender 4.2 LTS, 4.5 LTS
|
||||
pose_bone.bone.select = True
|
||||
|
||||
armature_obj.data.bones.active = pose_bone.bone
|
||||
|
||||
# Create widget for the Settings bone
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Operator modules for AMZN Character Tools."""
|
||||
@@ -1,7 +1,24 @@
|
||||
import bpy
|
||||
import os
|
||||
from mathutils import Matrix
|
||||
|
||||
ASSET_BLEND_PATH = r"A:\\1 Amazon_Active_Projects\\1 BlenderAssets\\Amazon\\amazon-asset_Hard-Hat_v1.1.blend"
|
||||
try:
|
||||
from ..utils import get_addon_preferences
|
||||
except (ImportError, ValueError):
|
||||
# Fallback if import fails (e.g., when run as script)
|
||||
def get_addon_preferences():
|
||||
test_names = ["bl_ext.vscode_development.AmazonCharacterTools", "amzncharactertools", "AmazonCharacterTools"]
|
||||
for addon_name in test_names:
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences') and hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
# Search all addons
|
||||
for addon_name in bpy.context.preferences.addons.keys():
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences') and hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
return None
|
||||
|
||||
ASSET_OBJECT_NAME = "hard-hat"
|
||||
GN_MOD_NAME = "hard-hat-transforms"
|
||||
|
||||
@@ -17,7 +34,19 @@ def find_armature_with_head():
|
||||
|
||||
|
||||
def append_hard_hat():
|
||||
with bpy.data.libraries.load(ASSET_BLEND_PATH, link=False, assets_only=True) as (data_from, data_to):
|
||||
# Get path from addon preferences
|
||||
prefs = get_addon_preferences()
|
||||
if not prefs:
|
||||
print("Error: Could not access addon preferences")
|
||||
return None
|
||||
|
||||
asset_blend_path = prefs.amzn_hardhat_asset_path
|
||||
|
||||
if not asset_blend_path or not os.path.exists(asset_blend_path):
|
||||
print(f"Error: Hard hat asset library path not set or file not found: {asset_blend_path}")
|
||||
return None
|
||||
|
||||
with bpy.data.libraries.load(asset_blend_path, link=False, assets_only=True) as (data_from, data_to):
|
||||
if ASSET_OBJECT_NAME in data_from.objects:
|
||||
data_to.objects = [ASSET_OBJECT_NAME]
|
||||
else:
|
||||
|
||||
@@ -23,7 +23,7 @@ def remap_vector_fonts():
|
||||
print(f"Found {len(existing_fonts)} existing font(s) starting with '{target_font_name}'")
|
||||
|
||||
# Check for numbered variants (.001, .002, etc)
|
||||
numbered_pattern = re.compile(rf"^{re.escape(target_font_name)}(\.\d{{3}})?$")
|
||||
numbered_pattern = re.compile(rf"^{re.escape(target_font_name)}(\.\d{{3}})$")
|
||||
numbered_fonts = []
|
||||
exact_match = None
|
||||
|
||||
@@ -33,35 +33,72 @@ def remap_vector_fonts():
|
||||
elif numbered_pattern.match(font.name):
|
||||
numbered_fonts.append(font)
|
||||
|
||||
# If we have an exact match, use it
|
||||
if exact_match:
|
||||
target_font = exact_match
|
||||
print(f" Using exact match: '{target_font.name}'")
|
||||
elif numbered_fonts:
|
||||
# Sort by the number suffix to find the lowest one
|
||||
def get_number(font_name):
|
||||
match = re.search(r"\.(\d{3})$", font_name)
|
||||
return int(match.group(1)) if match else 0
|
||||
|
||||
# Always use the lowest numbered variant as the base, rename it to base name
|
||||
# If no numbered variants exist, use exact match
|
||||
def get_number(font_name):
|
||||
match = re.search(r"\.(\d{3})$", font_name)
|
||||
return int(match.group(1)) if match else 999999
|
||||
|
||||
if numbered_fonts:
|
||||
# Sort by number to find the lowest numbered variant
|
||||
numbered_fonts.sort(key=lambda f: get_number(f.name))
|
||||
base_font = numbered_fonts[0]
|
||||
|
||||
# Rename the base font to remove the number suffix
|
||||
if base_font.name != target_font_name:
|
||||
print(f" Renaming base font '{base_font.name}' to '{target_font_name}'")
|
||||
base_font.name = target_font_name
|
||||
# First, remap the exact match (if it exists) to the lowest numbered variant
|
||||
# This avoids naming conflicts when we rename the numbered variant
|
||||
if exact_match and exact_match != base_font:
|
||||
if exact_match.users > 0:
|
||||
print(f" Remapping exact match '{exact_match.name}' ({exact_match.users} users) to '{base_font.name}'")
|
||||
else:
|
||||
print(f" Remapping unused exact match '{exact_match.name}' to '{base_font.name}'")
|
||||
try:
|
||||
exact_match.user_remap(base_font)
|
||||
except Exception as e:
|
||||
print(f" Error remapping '{exact_match.name}': {e}")
|
||||
|
||||
# Remap all other numbered variants to the base font
|
||||
for font in numbered_fonts[1:]:
|
||||
if font.users > 0:
|
||||
print(f" Remapping '{font.name}' to '{target_font_name}'")
|
||||
font.user_remap(base_font)
|
||||
# Now rename the lowest numbered variant to the base name
|
||||
old_name = base_font.name
|
||||
if base_font.name != target_font_name:
|
||||
if base_font.library:
|
||||
print(f" Warning: Cannot rename linked font '{old_name}' (from {base_font.library.filepath})")
|
||||
print(f" Using linked font as-is: '{old_name}'")
|
||||
else:
|
||||
print(f" Renaming lowest numbered variant '{old_name}' to '{target_font_name}'")
|
||||
base_font.name = target_font_name
|
||||
|
||||
target_font = base_font
|
||||
|
||||
# Show linked status if applicable
|
||||
if target_font.library:
|
||||
print(f" Target font is linked from: {target_font.library.filepath}")
|
||||
|
||||
# 2. Remap all other numbered variants to the base font
|
||||
other_variants = numbered_fonts[1:]
|
||||
if other_variants:
|
||||
print(f" Consolidating {len(other_variants)} other numbered variant(s) to '{target_font_name}'")
|
||||
for font in other_variants:
|
||||
if font.users > 0:
|
||||
print(f" Remapping '{font.name}' ({font.users} users) to '{target_font_name}'")
|
||||
else:
|
||||
print(f" Remapping unused variant '{font.name}' to '{target_font_name}'")
|
||||
try:
|
||||
font.user_remap(target_font)
|
||||
except Exception as e:
|
||||
print(f" Error remapping '{font.name}': {e}")
|
||||
elif exact_match:
|
||||
# No numbered variants, just use exact match
|
||||
target_font = exact_match
|
||||
if target_font.library:
|
||||
print(f" Using exact match: '{target_font.name}' ({target_font.users} users) [LINKED from {target_font.library.filepath}]")
|
||||
else:
|
||||
print(f" Using exact match: '{target_font.name}' ({target_font.users} users)")
|
||||
else:
|
||||
# Use the first one found
|
||||
# Use the first one found (shouldn't happen, but safety fallback)
|
||||
target_font = existing_fonts[0]
|
||||
print(f" Using existing font: '{target_font.name}'")
|
||||
if target_font.library:
|
||||
print(f" Using existing font: '{target_font.name}' [LINKED from {target_font.library.filepath}]")
|
||||
else:
|
||||
print(f" Using existing font: '{target_font.name}'")
|
||||
else:
|
||||
# Try to load from C:\Windows\Fonts
|
||||
print(f"Target font '{target_font_name}' not found in scene")
|
||||
@@ -70,41 +107,125 @@ def remap_vector_fonts():
|
||||
fonts_dir = r"C:\Windows\Fonts"
|
||||
if os.path.exists(fonts_dir):
|
||||
# Look for font files that might match
|
||||
# Try multiple search strategies since font filenames may not match font names exactly
|
||||
font_files = []
|
||||
search_terms = [
|
||||
# Exact match
|
||||
("amazon", "ember", "heavy"),
|
||||
# Abbreviated version (AmazonEmber_He.ttf where "He" = "Heavy")
|
||||
("amazon", "ember", "he"),
|
||||
# Just ember and heavy (in case filename doesn't have "amazon")
|
||||
("ember", "heavy"),
|
||||
# Just ember with "he" abbreviation
|
||||
("ember", "he"),
|
||||
# Just "heavy" (in case it's just "Ember Heavy")
|
||||
("heavy",),
|
||||
# Just "he" abbreviation
|
||||
("he",),
|
||||
]
|
||||
|
||||
for file in os.listdir(fonts_dir):
|
||||
if file.lower().endswith(('.ttf', '.otf', '.ttc')):
|
||||
# Check if filename contains "Amazon Ember Heavy" or similar
|
||||
if "amazon" in file.lower() and "ember" in file.lower() and "heavy" in file.lower():
|
||||
font_files.append(os.path.join(fonts_dir, file))
|
||||
file_lower = file.lower()
|
||||
# Try each search strategy
|
||||
for terms in search_terms:
|
||||
if all(term in file_lower for term in terms):
|
||||
full_path = os.path.join(fonts_dir, file)
|
||||
if full_path not in font_files:
|
||||
font_files.append(full_path)
|
||||
break # Found a match, no need to try other strategies for this file
|
||||
|
||||
if font_files:
|
||||
print(f" Found {len(font_files)} potential font file(s):")
|
||||
for f in font_files:
|
||||
print(f" - {f}")
|
||||
# Try to load the first matching font file
|
||||
font_file = font_files[0]
|
||||
print(f" Found font file: {font_file}")
|
||||
try:
|
||||
target_font = fonts.load(font_file)
|
||||
# Rename it to the target name
|
||||
target_font.name = target_font_name
|
||||
print(f" Successfully loaded '{font_file}' as '{target_font_name}'")
|
||||
loaded_font = bpy.data.fonts.load(font_file)
|
||||
# The loaded font might have a different name, so find it in the collection
|
||||
# and rename it to the target name
|
||||
if loaded_font:
|
||||
old_name = loaded_font.name
|
||||
print(f" Font loaded with name: '{old_name}'")
|
||||
# Rename to target name
|
||||
loaded_font.name = target_font_name
|
||||
# Get the font by the new name to ensure we have the right reference
|
||||
if target_font_name in bpy.data.fonts:
|
||||
target_font = bpy.data.fonts[target_font_name]
|
||||
print(f" Successfully loaded '{font_file}' (was '{old_name}') as '{target_font_name}'")
|
||||
else:
|
||||
print(f" Error: Font renamed but not found in collection as '{target_font_name}'")
|
||||
return
|
||||
else:
|
||||
print(f" Error: Font loaded but returned None")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f" Error loading font file '{font_file}': {e}")
|
||||
else:
|
||||
# Try loading by name (Blender will search system fonts)
|
||||
try:
|
||||
target_font = fonts.load(target_font_name)
|
||||
target_font.name = target_font_name
|
||||
print(f" Successfully loaded '{target_font_name}' from system fonts")
|
||||
except Exception as e:
|
||||
print(f" Error loading font: {e}")
|
||||
print(f" Please ensure '{target_font_name}' is available in your system")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return
|
||||
else:
|
||||
print(f" No matching font files found in '{fonts_dir}'")
|
||||
print(f" Listing all font files containing 'ember' or 'heavy' for debugging:")
|
||||
# List all fonts with ember or heavy for debugging
|
||||
debug_fonts = []
|
||||
for file in os.listdir(fonts_dir):
|
||||
if file.lower().endswith(('.ttf', '.otf', '.ttc')):
|
||||
if "ember" in file.lower() or "heavy" in file.lower():
|
||||
debug_fonts.append(file)
|
||||
if debug_fonts:
|
||||
for f in sorted(debug_fonts):
|
||||
print(f" - {f}")
|
||||
else:
|
||||
print(f" (none found)")
|
||||
|
||||
print(f"\n Error: Could not find 'Amazon Ember Heavy' font file")
|
||||
print(f" Please ensure the font is installed in Windows Fonts directory")
|
||||
print(f" Expected filename should contain: 'amazon', 'ember', and 'heavy'")
|
||||
print(f" Or at least: 'ember' and 'heavy'")
|
||||
return
|
||||
else:
|
||||
print(f" Error: Fonts directory '{fonts_dir}' not found")
|
||||
return
|
||||
|
||||
# Verify the font was actually loaded and is accessible
|
||||
if target_font_name not in bpy.data.fonts:
|
||||
print(f" Error: Font '{target_font_name}' was not found in fonts collection after loading")
|
||||
return
|
||||
target_font = bpy.data.fonts[target_font_name]
|
||||
|
||||
# Check if the loaded font is linked
|
||||
if target_font.library:
|
||||
print(f" Note: Font '{target_font_name}' is linked from library: {target_font.library.filepath}")
|
||||
|
||||
if not target_font:
|
||||
print("Error: Could not find or load target font")
|
||||
return
|
||||
|
||||
# Re-validate target font after consolidation (it might have been removed)
|
||||
# Look up the font by name to ensure it still exists
|
||||
if target_font_name not in fonts:
|
||||
print(f"Error: Target font '{target_font_name}' was removed or is not in the fonts collection")
|
||||
return
|
||||
|
||||
# Get the font from the collection to ensure we have a valid reference
|
||||
target_font = fonts[target_font_name]
|
||||
|
||||
# Verify the font datablock is valid
|
||||
try:
|
||||
_ = target_font.name
|
||||
_ = target_font.users
|
||||
except (ReferenceError, KeyError):
|
||||
print(f"Error: Target font '{target_font_name}' is not a valid font datablock")
|
||||
return
|
||||
|
||||
# Ensure target font name is correct
|
||||
if target_font.name != target_font_name:
|
||||
print(f" Ensuring target font name is '{target_font_name}'")
|
||||
target_font.name = target_font_name
|
||||
|
||||
# Find all text objects and remap their fonts
|
||||
text_objects = [obj for obj in bpy.data.objects if obj.type == 'FONT']
|
||||
|
||||
@@ -122,27 +243,61 @@ def remap_vector_fonts():
|
||||
else:
|
||||
skipped_count += 1
|
||||
|
||||
# Also remap font datablocks directly (for any other uses)
|
||||
# Exclude fonts that start with the target name (they're already correct or being handled)
|
||||
# Check if target font is linked
|
||||
target_is_linked = target_font.library is not None
|
||||
if target_is_linked:
|
||||
print(f"\nTarget font '{target_font_name}' is linked from library: {target_font.library.filepath}")
|
||||
print(" Remapping local fonts to linked font...")
|
||||
|
||||
# Remap all font datablocks directly (regardless of user count)
|
||||
# Exclude fonts that start with the target name (they're already correct)
|
||||
# Exclude linked fonts (we shouldn't try to remap fonts in linked libraries)
|
||||
fonts_to_remap = [
|
||||
font for font in fonts
|
||||
if font != target_font
|
||||
and not font.name.startswith(target_font_name)
|
||||
and font.users > 0
|
||||
and font.library is None # Don't remap linked fonts
|
||||
]
|
||||
|
||||
# Count linked fonts that were skipped
|
||||
linked_fonts_skipped = [
|
||||
font for font in fonts
|
||||
if font != target_font
|
||||
and not font.name.startswith(target_font_name)
|
||||
and font.library is not None
|
||||
]
|
||||
|
||||
if linked_fonts_skipped:
|
||||
print(f" Skipping {len(linked_fonts_skipped)} linked font(s) (cannot remap fonts in linked libraries)")
|
||||
|
||||
if fonts_to_remap:
|
||||
print(f"\nRemapping {len(fonts_to_remap)} font datablocks...")
|
||||
print(f"\nRemapping {len(fonts_to_remap)} local font datablocks...")
|
||||
for font in fonts_to_remap:
|
||||
users_before = font.users
|
||||
if users_before > 0:
|
||||
try:
|
||||
font.user_remap(target_font)
|
||||
users_after = font.users
|
||||
remapped_users = users_before - users_after
|
||||
print(f" Remapped '{font.name}': {remapped_users} users")
|
||||
except Exception as e:
|
||||
print(f" Error remapping '{font.name}': {e}")
|
||||
try:
|
||||
font.user_remap(target_font)
|
||||
if target_is_linked:
|
||||
print(f" Remapped '{font.name}' -> '{target_font_name}' (linked font)")
|
||||
else:
|
||||
print(f" Remapped '{font.name}' -> '{target_font_name}'")
|
||||
except Exception as e:
|
||||
print(f" Error remapping '{font.name}': {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Final cleanup: remap any remaining unused numbered variants (don't remove them)
|
||||
numbered_pattern_final = re.compile(rf"^{re.escape(target_font_name)}\.\d{{3}}$")
|
||||
remaining_variants = [
|
||||
font for font in fonts
|
||||
if numbered_pattern_final.match(font.name) and font != target_font and font.users == 0
|
||||
]
|
||||
if remaining_variants:
|
||||
print(f"\nRemapping {len(remaining_variants)} remaining unused numbered variant(s) to '{target_font_name}'...")
|
||||
for font in remaining_variants:
|
||||
print(f" Remapping unused variant: '{font.name}'")
|
||||
try:
|
||||
font.user_remap(target_font)
|
||||
except Exception as e:
|
||||
print(f" Error remapping '{font.name}': {e}")
|
||||
|
||||
print(f"\n=== REMAPPING SUMMARY ===")
|
||||
print(f"Target font: '{target_font.name}'")
|
||||
|
||||
@@ -2,10 +2,46 @@ import bpy
|
||||
import re
|
||||
import os
|
||||
|
||||
try:
|
||||
from ..utils import get_addon_preferences
|
||||
except (ImportError, ValueError):
|
||||
# Fallback if import fails (e.g., when run as script) - try to find addon by searching for our preferences
|
||||
def get_addon_preferences():
|
||||
"""Fallback function to get addon preferences."""
|
||||
# Try extension format first (most likely for vscode_development)
|
||||
test_names = [
|
||||
"bl_ext.vscode_development.AmazonCharacterTools",
|
||||
"amzncharactertools",
|
||||
"AmazonCharacterTools",
|
||||
]
|
||||
for addon_name in test_names:
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences'):
|
||||
if hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
|
||||
# Search all addons for one with our preferences class
|
||||
for addon_name in bpy.context.preferences.addons.keys():
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences'):
|
||||
if hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
return None
|
||||
|
||||
def link_bsdf_materials():
|
||||
"""Link all materials from the BSDF library file"""
|
||||
|
||||
library_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\MATERIALS_BSDF_pallette_v1.0.blend"
|
||||
# Get path from addon preferences
|
||||
prefs = get_addon_preferences()
|
||||
if not prefs:
|
||||
print("Error: Could not access addon preferences")
|
||||
return []
|
||||
|
||||
library_path = prefs.amzn_bsdf_materials_path
|
||||
|
||||
if not library_path:
|
||||
print("Warning: BSDF materials library path not set in addon preferences")
|
||||
return []
|
||||
|
||||
if not os.path.exists(library_path):
|
||||
print(f"Warning: Library file not found at {library_path}")
|
||||
@@ -186,12 +222,132 @@ def remap_missing_datablocks():
|
||||
print(f"Remapped {remapping_count} missing datablocks to valid linked materials")
|
||||
return remapping_count
|
||||
|
||||
def consolidate_duplicate_target_materials():
|
||||
"""Consolidate duplicate target materials using datablock-utils operators.
|
||||
|
||||
Uses DBU operators to find and merge duplicate materials:
|
||||
1. Finds similar and duplicate materials
|
||||
2. Merges the duplicates
|
||||
"""
|
||||
|
||||
print("\n=== CONSOLIDATING DUPLICATE TARGET MATERIALS (using datablock-utils) ===")
|
||||
|
||||
# Check if DBU operators are available
|
||||
if not hasattr(bpy.ops.scene, 'dbu_find_similar_and_duplicates'):
|
||||
print("Error: datablock-utils addon not available or operators not found")
|
||||
print(" Please ensure the 'Data-Block Utilities' addon is installed and enabled")
|
||||
return 0
|
||||
|
||||
if not hasattr(bpy.ops.scene, 'dbu_merge_duplicates'):
|
||||
print("Error: dbu_merge_duplicates operator not found")
|
||||
return 0
|
||||
|
||||
try:
|
||||
# Try to configure settings for materials
|
||||
# Check various possible property paths
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Try common property paths for DBU settings
|
||||
settings_configured = False
|
||||
if hasattr(scene, 'dbu_similar_settings'):
|
||||
if hasattr(scene.dbu_similar_settings, 'id_type'):
|
||||
scene.dbu_similar_settings.id_type = 'MATERIAL'
|
||||
settings_configured = True
|
||||
print(" Configured DBU settings: id_type = 'MATERIAL'")
|
||||
elif hasattr(scene, 'dbu_settings'):
|
||||
if hasattr(scene.dbu_settings, 'id_type'):
|
||||
scene.dbu_settings.id_type = 'MATERIAL'
|
||||
settings_configured = True
|
||||
print(" Configured DBU settings: id_type = 'MATERIAL'")
|
||||
|
||||
if not settings_configured:
|
||||
print(" Note: Could not find DBU settings property, operators may use defaults")
|
||||
|
||||
# Count duplicates before consolidation
|
||||
def count_duplicates():
|
||||
"""Count materials with duplicate names."""
|
||||
material_names = {}
|
||||
for mat in bpy.data.materials:
|
||||
if mat.name not in material_names:
|
||||
material_names[mat.name] = []
|
||||
material_names[mat.name].append(mat)
|
||||
duplicates = {name: mats for name, mats in material_names.items() if len(mats) > 1}
|
||||
return len(duplicates), sum(len(mats) - 1 for mats in duplicates.values())
|
||||
|
||||
initial_duplicate_groups, initial_duplicate_count = count_duplicates()
|
||||
print(f" Initial duplicate groups: {initial_duplicate_groups}, total duplicate materials: {initial_duplicate_count}")
|
||||
|
||||
# Call operators multiple times until no more duplicates are found
|
||||
max_iterations = 10
|
||||
iteration = 0
|
||||
total_merged = 0
|
||||
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
print(f"\n Iteration {iteration}:")
|
||||
|
||||
# Step 1: Find similar and duplicates
|
||||
print(" Finding similar and duplicate materials...")
|
||||
try:
|
||||
result = bpy.ops.scene.dbu_find_similar_and_duplicates()
|
||||
if result != {'FINISHED'}:
|
||||
print(f" Warning: Find duplicates returned: {result}")
|
||||
except Exception as e:
|
||||
print(f" Error calling dbu_find_similar_and_duplicates: {e}")
|
||||
break
|
||||
|
||||
# Step 2: Merge duplicates
|
||||
print(" Merging duplicate materials...")
|
||||
try:
|
||||
result = bpy.ops.scene.dbu_merge_duplicates()
|
||||
if result != {'FINISHED'}:
|
||||
print(f" Warning: Merge duplicates returned: {result}")
|
||||
except Exception as e:
|
||||
print(f" Error calling dbu_merge_duplicates: {e}")
|
||||
break
|
||||
|
||||
# Check if there are still duplicates
|
||||
remaining_groups, remaining_count = count_duplicates()
|
||||
print(f" Remaining duplicate groups: {remaining_groups}, remaining duplicates: {remaining_count}")
|
||||
|
||||
# If no more duplicates, we're done
|
||||
if remaining_groups == 0:
|
||||
print(f" All duplicates merged after {iteration} iteration(s)")
|
||||
break
|
||||
|
||||
# If no progress was made, stop
|
||||
if iteration > 1 and remaining_count >= initial_duplicate_count:
|
||||
print(f" No progress made, stopping after {iteration} iteration(s)")
|
||||
break
|
||||
|
||||
initial_duplicate_count = remaining_count
|
||||
|
||||
final_groups, final_count = count_duplicates()
|
||||
if final_groups > 0:
|
||||
print(f"\n Warning: {final_groups} duplicate groups still remain after consolidation")
|
||||
print(f" These may be linked materials from different libraries that cannot be automatically merged")
|
||||
else:
|
||||
print(f"\n All duplicates successfully consolidated")
|
||||
|
||||
print(" Consolidation complete using datablock-utils operators")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error during consolidation: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 0
|
||||
|
||||
def replace_cel_materials():
|
||||
"""Replace all CEL materials with their BSDF counterparts using Blender's user remapping"""
|
||||
|
||||
# First, link BSDF materials from library
|
||||
linked_materials = link_bsdf_materials()
|
||||
|
||||
# Consolidate duplicate target materials FIRST (before any scanning/remapping)
|
||||
# This ensures all duplicates are merged before we start building material mappings
|
||||
duplicate_consolidations = consolidate_duplicate_target_materials()
|
||||
|
||||
# Then, remap any missing datablocks
|
||||
missing_remaps = remap_missing_datablocks()
|
||||
|
||||
@@ -207,8 +363,11 @@ def replace_cel_materials():
|
||||
"Wheel-White": "BSDF_WHITE",
|
||||
"Bag Colors": "BSDF_Bag Colors",
|
||||
"cardboard": "Package_Cardboard",
|
||||
"shuttle-cardboard": "Shuttle_Cardboard_1",
|
||||
"cardboard2": "Shuttle_Cardboard_2",
|
||||
"wood": "Pallet_Wood",
|
||||
"Package_Cardboard": "Package_Cardboard",
|
||||
"Pallet_Wood": "Pallet_Wood",
|
||||
"blue (triton)": "BSDF_blue-2_TRITON",
|
||||
"gray (snow)": "BSDF_gray-6_SNOW",
|
||||
"gray (storm)": "BSDF_gray-2_STORM",
|
||||
@@ -290,8 +449,13 @@ def replace_cel_materials():
|
||||
return None
|
||||
|
||||
# Helper function to find target material
|
||||
def find_target_material(name):
|
||||
"""Find target material (can be linked or local)."""
|
||||
def find_target_material(name, require_linked=False):
|
||||
"""Find target material (can be linked or local).
|
||||
|
||||
Args:
|
||||
name: Material name to find
|
||||
require_linked: If True, only return linked materials (used when remapping same-name materials)
|
||||
"""
|
||||
candidates = []
|
||||
# Try exact match first
|
||||
if name in exact_material_map:
|
||||
@@ -304,26 +468,39 @@ def replace_cel_materials():
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
# Return the first one (or prefer linked if available)
|
||||
# Prefer linked materials, or require them if specified
|
||||
linked = [m for m in candidates if m.library is not None]
|
||||
if require_linked:
|
||||
if linked:
|
||||
return linked[0]
|
||||
else:
|
||||
return None # No linked version found
|
||||
|
||||
return linked[0] if linked else candidates[0]
|
||||
|
||||
# Process custom mappings first
|
||||
for source_name, target_name in custom_mappings.items():
|
||||
source_mat = find_best_source_material(source_name)
|
||||
target_mat = find_target_material(target_name)
|
||||
|
||||
# If source and target have the same name, we need a linked target (remapping local -> linked)
|
||||
require_linked_target = (source_name == target_name)
|
||||
target_mat = find_target_material(target_name, require_linked=require_linked_target)
|
||||
|
||||
if source_mat:
|
||||
if target_mat:
|
||||
# Skip if source and target are the same material
|
||||
if source_mat == target_mat:
|
||||
print(f"Skipping remapping: '{source_name}' is already '{target_name}'")
|
||||
print(f"Skipping remapping: '{source_name}' is already '{target_name}' (same material object)")
|
||||
else:
|
||||
material_mapping[source_mat] = target_mat
|
||||
print(f"Found custom mapping: {source_mat.name} ({'local' if source_mat.library is None else 'linked'}) -> {target_mat.name}")
|
||||
print(f"Found custom mapping: {source_mat.name} ({'local' if source_mat.library is None else 'linked'}) -> {target_mat.name} ({'local' if target_mat.library is None else 'linked'})")
|
||||
else:
|
||||
missing_targets.append(f"{source_name} -> {target_name}")
|
||||
print(f"Warning: Target material '{target_name}' not found for custom mapping '{source_name}'")
|
||||
if require_linked_target:
|
||||
print(f"Warning: Target material '{target_name}' not found as linked material for custom mapping '{source_name}' -> '{target_name}'")
|
||||
print(f" (Need linked version to remap local material to linked)")
|
||||
else:
|
||||
missing_targets.append(f"{source_name} -> {target_name}")
|
||||
print(f"Warning: Target material '{target_name}' not found for custom mapping '{source_name}'")
|
||||
else:
|
||||
# Check if there are any materials with this name (even if linked)
|
||||
if source_name in exact_material_map or source_name.lower() in case_insensitive_map:
|
||||
@@ -362,11 +539,20 @@ def replace_cel_materials():
|
||||
continue
|
||||
|
||||
print(f"Remapping all users of {source_material.name} to {target_material.name}")
|
||||
print(f" Source: {source_material.name} (local={source_material.library is None}, users={source_material.users})")
|
||||
print(f" Target: {target_material.name} (local={target_material.library is None}, users={target_material.users})")
|
||||
|
||||
# Verify target is linked when source and target have same name
|
||||
if source_material.name == target_material.name and target_material.library is None:
|
||||
print(f" ERROR: Target material '{target_material.name}' is not linked! Cannot remap local->local with same name.")
|
||||
print(f" Skipping this remapping")
|
||||
continue
|
||||
|
||||
# Get user count before remapping
|
||||
users_before = source_material.users
|
||||
users_before_source = source_material.users
|
||||
users_before_target = target_material.users
|
||||
|
||||
if users_before == 0:
|
||||
if users_before_source == 0:
|
||||
print(f" Material has no users, skipping remapping")
|
||||
continue
|
||||
|
||||
@@ -376,21 +562,28 @@ def replace_cel_materials():
|
||||
source_material.user_remap(target_material)
|
||||
except Exception as e:
|
||||
print(f" Error during remapping: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
# Get user count after remapping
|
||||
users_after = source_material.users
|
||||
users_after_source = source_material.users
|
||||
users_after_target = target_material.users
|
||||
|
||||
replacements_this_material = users_before - users_after
|
||||
replacements_this_material = users_before_source - users_after_source
|
||||
replacements_made += replacements_this_material
|
||||
|
||||
print(f" Users before: {users_before}, after: {users_after}")
|
||||
print(f" Source users: {users_before_source} -> {users_after_source}")
|
||||
print(f" Target users: {users_before_target} -> {users_after_target}")
|
||||
print(f" Remapped {replacements_this_material} users")
|
||||
|
||||
if replacements_this_material == 0 and users_before > 0:
|
||||
print(f" Warning: Remapping reported 0 users remapped despite {users_before} users before")
|
||||
if replacements_this_material == 0 and users_before_source > 0:
|
||||
print(f" Warning: Remapping reported 0 users remapped despite {users_before_source} users before")
|
||||
print(f" Source material library: {source_material.library}")
|
||||
print(f" Target material library: {target_material.library}")
|
||||
elif users_after_target != users_before_target + replacements_this_material:
|
||||
print(f" Warning: Target user count doesn't match expected increase!")
|
||||
print(f" Expected target users: {users_before_target + replacements_this_material}, actual: {users_after_target}")
|
||||
|
||||
# Optional: Remove unused source materials after remapping
|
||||
print(f"\nCleaning up unused source materials...")
|
||||
@@ -409,6 +602,7 @@ def replace_cel_materials():
|
||||
print(f"Materials linked from library: {len(linked_materials)}")
|
||||
print(f"Missing datablocks remapped: {missing_remaps}")
|
||||
print(f"Appended->Linked remappings: {appended_remaps}")
|
||||
print(f"Duplicate target consolidations: {duplicate_consolidations}")
|
||||
print(f"Total materials mapped: {len(material_mapping)}")
|
||||
print(f"Successful mappings: {len(material_mapping)}")
|
||||
print(f"Total user remappings: {replacements_made}")
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import bpy
|
||||
|
||||
# Delete the 'Dual Node Background' world if it exists
|
||||
if 'Dual Node Background' in bpy.data.worlds:
|
||||
world_to_delete = bpy.data.worlds['Dual Node Background']
|
||||
bpy.data.worlds.remove(world_to_delete, do_unlink=True)
|
||||
|
||||
# Create new world
|
||||
new_world = bpy.data.worlds.new(name="World")
|
||||
|
||||
# Set world color to pure white (#FFFFFFFF)
|
||||
new_world.use_nodes = True
|
||||
nodes = new_world.node_tree.nodes
|
||||
links = new_world.node_tree.links
|
||||
|
||||
# Clear existing nodes
|
||||
nodes.clear()
|
||||
|
||||
# Create background node
|
||||
background_node = nodes.new(type='ShaderNodeBackground')
|
||||
background_node.inputs[0].default_value = (1.0, 1.0, 1.0, 1.0) # Pure white RGBA
|
||||
|
||||
# Create output node
|
||||
output_node = nodes.new(type='ShaderNodeOutputWorld')
|
||||
|
||||
# Link background to output
|
||||
links.new(background_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
# Set as active world
|
||||
bpy.context.scene.world = new_world
|
||||
|
||||
# Purge orphaned data
|
||||
bpy.ops.outliner.orphans_purge()
|
||||
|
||||
# Enable transparent film rendering
|
||||
bpy.context.scene.render.film_transparent = True
|
||||
|
||||
print("World 'Dual Node Background' deleted and new white world created successfully!")
|
||||
print("Orphaned data purged and transparent film rendering enabled.")
|
||||
@@ -1,6 +1,7 @@
|
||||
"""UI module for AMZN Character Tools."""
|
||||
from .operators import OPERATOR_CLASSES
|
||||
from .panels import PANEL_CLASSES
|
||||
from .preferences import AMZN_AddonPreferences
|
||||
|
||||
__all__ = ["OPERATOR_CLASSES", "PANEL_CLASSES"]
|
||||
__all__ = ["OPERATOR_CLASSES", "PANEL_CLASSES", "AMZN_AddonPreferences"]
|
||||
|
||||
|
||||
@@ -50,15 +50,6 @@ OP_SPECS = [
|
||||
"panel": "general",
|
||||
"large": True,
|
||||
},
|
||||
{
|
||||
"name": "WhiteWorld",
|
||||
"id": "white_world",
|
||||
"desc": "Removes Dual Node Background world and replaces with pure white world",
|
||||
"script": "white_world.py",
|
||||
"button": "White World",
|
||||
"icon": "WORLD",
|
||||
"panel": "scene",
|
||||
},
|
||||
{
|
||||
"name": "ApplySubdivWgt",
|
||||
"id": "apply_subdiv_wgt",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Preferences for AMZN Character Tools."""
|
||||
import bpy
|
||||
from bpy.types import AddonPreferences
|
||||
from bpy.props import StringProperty
|
||||
|
||||
|
||||
class AMZN_AddonPreferences(AddonPreferences):
|
||||
"""Addon preferences for AMZN Character Tools."""
|
||||
|
||||
# bl_idname will be set in __init__.py during registration
|
||||
# This ensures it uses the correct root package name
|
||||
bl_idname = "" # Set dynamically
|
||||
|
||||
amzn_bsdf_materials_path: StringProperty(
|
||||
name="BSDF Materials Library",
|
||||
description="Path to MATERIALS_BSDF_pallette_v1.0.blend",
|
||||
subtype='FILE_PATH',
|
||||
default=r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\MATERIALS_BSDF_pallette_v1.0.blend",
|
||||
)
|
||||
|
||||
amzn_device_path: StringProperty(
|
||||
name="Device Library",
|
||||
description="Path to device_v2.blend",
|
||||
subtype='FILE_PATH',
|
||||
default=r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\device_v2.blend",
|
||||
)
|
||||
|
||||
amzn_scanner_assets_path: StringProperty(
|
||||
name="Scanner Assets Library",
|
||||
description="Path to amazon-3Dworld-assets_v4.0.blend",
|
||||
subtype='FILE_PATH',
|
||||
default=r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\amazon-3Dworld-assets_v4.0.blend",
|
||||
)
|
||||
|
||||
amzn_hardhat_asset_path: StringProperty(
|
||||
name="Hard Hat Asset Library",
|
||||
description="Path to amazon-asset_Hard-Hat_v1.1.blend",
|
||||
subtype='FILE_PATH',
|
||||
default=r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\amazon-asset_Hard-Hat_v1.1.blend",
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
"""Draw the preferences UI."""
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Blend File Paths:")
|
||||
layout.separator()
|
||||
|
||||
# BSDF Materials path
|
||||
row = layout.row()
|
||||
row.prop(self, "amzn_bsdf_materials_path")
|
||||
|
||||
# Device path
|
||||
row = layout.row()
|
||||
row.prop(self, "amzn_device_path")
|
||||
|
||||
# Scanner assets path
|
||||
row = layout.row()
|
||||
row.prop(self, "amzn_scanner_assets_path")
|
||||
|
||||
# Hard hat asset path
|
||||
row = layout.row()
|
||||
row.prop(self, "amzn_hardhat_asset_path")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Utility functions for AMZN Character Tools."""
|
||||
import bpy
|
||||
|
||||
|
||||
def get_addon_preferences():
|
||||
"""Get the addon preferences, finding the correct addon identifier automatically.
|
||||
|
||||
Returns:
|
||||
The addon preferences object, or None if not found.
|
||||
"""
|
||||
# Try common identifiers first
|
||||
test_names = [
|
||||
"bl_ext.vscode_development.AmazonCharacterTools", # Extension format
|
||||
"amzncharactertools", # Manifest ID
|
||||
"AmazonCharacterTools", # Module name
|
||||
]
|
||||
|
||||
for addon_name in test_names:
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences'):
|
||||
# Verify it's our preferences class
|
||||
if hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
|
||||
# If not found, search all addons for one with our preferences
|
||||
for addon_name in bpy.context.preferences.addons.keys():
|
||||
addon_prefs = bpy.context.preferences.addons.get(addon_name)
|
||||
if addon_prefs and hasattr(addon_prefs, 'preferences'):
|
||||
if hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
|
||||
return addon_prefs.preferences
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user