173 lines
5.4 KiB
Python
173 lines
5.4 KiB
Python
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()
|
|
|
|
|