import bpy import os from mathutils import Matrix 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" def find_armature_with_head(): active = bpy.context.active_object if active and active.type == 'ARMATURE' and 'head' in active.pose.bones: return active for obj in bpy.data.objects: if obj.type == 'ARMATURE' and 'head' in obj.pose.bones: return obj return None def append_hard_hat(): # 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: print(f"Error: '{ASSET_OBJECT_NAME}' not found in asset file") return None obj = bpy.data.objects.get(ASSET_OBJECT_NAME) if not obj: print("Error: hard-hat object failed to append") return None # Link to current collection (temporary) if obj.name not in bpy.context.collection.objects: bpy.context.collection.objects.link(obj) # Make it no longer an asset try: obj.asset_clear() except Exception: pass return obj def strip_geonodes(obj): for mod in list(obj.modifiers): if mod.type == 'NODES' and (mod.name == GN_MOD_NAME or (mod.node_group and mod.node_group.name == GN_MOD_NAME)): obj.modifiers.remove(mod) def align_to_head(obj, armature): head_pb = armature.pose.bones.get('head') if not head_pb: print("Error: head pose bone not found") return # Apply transforms: X-90°, Y90°, Z offset -0.07 import math from mathutils import Euler, Vector # Get head bone matrix head_matrix = armature.matrix_world @ head_pb.matrix # Apply rotation offsets rotation_offset = Euler((math.radians(-90), math.radians(90), 0), 'XYZ') rotation_matrix = rotation_offset.to_matrix().to_4x4() # First set rotation relative to head obj.matrix_world = head_matrix @ rotation_matrix # Then apply global offsets (world space) obj.matrix_world.translation += Vector((0.0, 0.0, -0.07)) # global Z obj.matrix_world.translation += Vector((0.0, -0.004, 0.0)) # global Y # Apply uniform scale before parenting so it sticks try: obj.scale *= 1.4 except Exception: obj.scale = (1.4, 1.4, 1.4) def get_accessories_collection(): # Prefer an existing 'Accessories' collection that already holds known items candidates = [c for c in bpy.data.collections if c.name == 'Accessories'] def has_known(c): names = {o.name for o in c.objects} return any(n in names for n in ('Device', 'device-band', 'Finger-Scanner', 'Lanyard', 'Vest')) for c in candidates: if has_known(c): return c if candidates: return candidates[0] # Create under scene root if not found coll = bpy.data.collections.new('Accessories') bpy.context.scene.collection.children.link(coll) return coll def move_to_accessories(obj): acc = get_accessories_collection() # Unlink from other collections to avoid duplicates for c in list(obj.users_collection): try: c.objects.unlink(obj) except Exception: pass # Link to Accessories if obj.name not in acc.objects: acc.objects.link(obj) def parent_to_head(obj, armature): head_pb = armature.pose.bones.get('head') if not head_pb: print("Error: head pose bone not found") return obj.parent = armature obj.parent_type = 'BONE' obj.parent_bone = 'head' obj.matrix_parent_inverse = (armature.matrix_world @ head_pb.matrix).inverted() def run(): arm = find_armature_with_head() if not arm: print("Error: No armature with 'head' bone found") return hat = append_hard_hat() if not hat: return strip_geonodes(hat) align_to_head(hat, arm) parent_to_head(hat, arm) move_to_accessories(hat) bpy.context.view_layer.objects.active = hat hat.select_set(True) print("HH appended, cleaned, aligned, and parented to head.") run()