2026-01-01

This commit is contained in:
2026-03-17 15:16:34 -06:00
parent ec4cf523fb
commit b80274187b
263 changed files with 95164 additions and 3848 deletions
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,26 @@
schema_version = "1.0.0"
id = "amzncharactertools"
version = "0.6.3"
name = "AMZNCharacterTools"
tagline = "AMZNCharacterTools"
maintainer = "Nathan Lindsay"
version = "0.9.0"
type = "add-on"
tags = ["3D View"]
blender_version_min = "4.5.0"
license = [
"SPDX:GPL-2.0-or-later",
maintainer = "Nathan Lindsay"
license = ["None"]
blender_version_min = "4.2.0"
tags = ["Rigging", "Workflow"]
[permissions]
files = "Read and write external resources referenced by scenes"
[build]
paths_exclude_pattern = [
"__pycache__/",
"*.pyc",
".git/",
".github/",
"docs/",
"tests/",
]
@@ -0,0 +1,129 @@
import bpy
def add_body_masks():
# Find the CC_Base_Body object
body_obj = bpy.data.objects.get('CC_Base_Body')
if not body_obj:
print("Error: CC_Base_Body object not found")
return
print(f"Found body object: {body_obj.name}")
# Define vertex groups for each mask
main_mask_groups = [
# Arms
'DEF-shoulder.L', 'DEF-shoulder.R',
'DEF-upper_arm.L', 'DEF-upper_arm.L.001', 'DEF-upper_arm.R', 'DEF-upper_arm.R.001',
'DEF-elbow_share.L', 'DEF-elbow_share.R',
'DEF-forearm.L', 'DEF-forearm.L.001', 'DEF-forearm.R', 'DEF-forearm.R.001',
# Head and chest (upper torso)
'DEF-spine.003', 'DEF-spine.004', 'DEF-spine.005', 'DEF-spine.006',
'DEF-jaw', 'DEF-jaw.L', 'DEF-jaw.R', 'DEF-jaw.L.001', 'DEF-jaw.R.001',
'DEF-forehead.L', 'DEF-forehead.R', 'DEF-forehead.L.001', 'DEF-forehead.R.001', 'DEF-forehead.L.002', 'DEF-forehead.R.002',
'DEF-temple.L', 'DEF-temple.R',
'DEF-brow.B.L', 'DEF-brow.B.L.001', 'DEF-brow.B.L.002', 'DEF-brow.B.L.003',
'DEF-brow.B.R', 'DEF-brow.B.R.001', 'DEF-brow.B.R.002', 'DEF-brow.B.R.003',
'DEF-brow.T.L', 'DEF-brow.T.L.001', 'DEF-brow.T.L.002', 'DEF-brow.T.L.003',
'DEF-brow.T.R', 'DEF-brow.T.R.001', 'DEF-brow.T.R.002', 'DEF-brow.T.R.003',
'DEF-lid.B.L', 'DEF-lid.B.L.001', 'DEF-lid.B.L.002', 'DEF-lid.B.L.003',
'DEF-lid.B.R', 'DEF-lid.B.R.001', 'DEF-lid.B.R.002', 'DEF-lid.B.R.003',
'DEF-lid.T.L', 'DEF-lid.T.L.001', 'DEF-lid.T.L.002', 'DEF-lid.T.L.003',
'DEF-lid.T.R', 'DEF-lid.T.R.001', 'DEF-lid.T.R.002', 'DEF-lid.T.R.003',
'DEF-ear.L', 'DEF-ear.L.001', 'DEF-ear.L.002', 'DEF-ear.L.003', 'DEF-ear.L.004',
'DEF-ear.R', 'DEF-ear.R.001', 'DEF-ear.R.002', 'DEF-ear.R.003', 'DEF-ear.R.004',
'DEF-chin', 'DEF-chin.001', 'DEF-chin.L', 'DEF-chin.R',
'DEF-cheek.T.L', 'DEF-cheek.T.R', 'DEF-cheek.T.L.001', 'DEF-cheek.T.R.001',
'DEF-cheek.B.L', 'DEF-cheek.B.R', 'DEF-cheek.B.L.001', 'DEF-cheek.B.R.001',
'DEF-nose', 'DEF-nose.L', 'DEF-nose.R', 'DEF-nose.001', 'DEF-nose.002', 'DEF-nose.003', 'DEF-nose.004',
'DEF-nose.L.001', 'DEF-nose.R.001',
'DEF-lip.B.L', 'DEF-lip.B.R', 'DEF-lip.B.L.001', 'DEF-lip.B.R.001',
'DEF-lip.T.L', 'DEF-lip.T.R', 'DEF-lip.T.L.001', 'DEF-lip.T.R.001',
'DEF-tongue', 'DEF-tongue.001', 'DEF-tongue.002',
'DEF-breast.L', 'DEF-breast.R', 'DEF-breast_twist.L', 'DEF-breast_twist.R'
]
hand_groups = [
# Hands
'DEF-hand.L', 'DEF-hand.R',
'DEF-f_pinky.01.L', 'DEF-f_pinky.02.L', 'DEF-f_pinky.03.L',
'DEF-f_ring.01.L', 'DEF-f_ring.02.L', 'DEF-f_ring.03.L',
'DEF-f_middle.01.L', 'DEF-f_middle.02.L', 'DEF-f_middle.03.L',
'DEF-f_index.01.L', 'DEF-f_index.02.L', 'DEF-f_index.03.L',
'DEF-thumb.01.L', 'DEF-thumb.02.L', 'DEF-thumb.03.L',
'DEF-f_pinky.01.R', 'DEF-f_pinky.02.R', 'DEF-f_pinky.03.R',
'DEF-f_ring.01.R', 'DEF-f_ring.02.R', 'DEF-f_ring.03.R',
'DEF-f_middle.01.R', 'DEF-f_middle.02.R', 'DEF-f_middle.03.R',
'DEF-f_index.01.R', 'DEF-f_index.02.R', 'DEF-f_index.03.R',
'DEF-thumb.01.R', 'DEF-thumb.02.R', 'DEF-thumb.03.R'
]
# Create vertex groups for masks
def create_mask_vertex_group(obj, group_name, vertex_group_names):
# Remove existing group if it exists
existing_group = obj.vertex_groups.get(group_name)
if existing_group:
obj.vertex_groups.remove(existing_group)
# Create new vertex group
mask_group = obj.vertex_groups.new(name=group_name)
# Get indices of source vertex groups
source_group_indices = []
for vg_name in vertex_group_names:
vg = obj.vertex_groups.get(vg_name)
if vg:
source_group_indices.append(vg.index)
if not source_group_indices:
print(f"Warning: No source vertex groups found for {group_name}")
return None
# Add vertices that have weights in any of the source groups
vertices_to_add = []
for vert_idx, vert in enumerate(obj.data.vertices):
for group in vert.groups:
if group.group in source_group_indices and group.weight > 0.0:
vertices_to_add.append(vert_idx)
break
if vertices_to_add:
mask_group.add(vertices_to_add, 1.0, 'REPLACE')
print(f"Created {group_name} vertex group with {len(vertices_to_add)} vertices")
return mask_group
# Create Main mask vertex group (head, arms, chest)
main_mask_vg = create_mask_vertex_group(body_obj, "Main_Mask", main_mask_groups)
# Create Hand mask vertex group (main + hands)
hand_mask_vg = create_mask_vertex_group(body_obj, "Hand_Mask", main_mask_groups + hand_groups)
# Add mask modifiers
# Remove existing mask modifiers if they exist
modifiers_to_remove = []
for modifier in body_obj.modifiers:
if modifier.type == 'MASK' and modifier.name in ['Main_Mask', 'Hand_Mask']:
modifiers_to_remove.append(modifier)
for modifier in modifiers_to_remove:
body_obj.modifiers.remove(modifier)
print(f"Removed existing {modifier.name} modifier")
# Add Main mask modifier
if main_mask_vg:
main_mask_modifier = body_obj.modifiers.new(name="Main_Mask", type='MASK')
main_mask_modifier.vertex_group = "Main_Mask"
print("Added Main_Mask modifier (head, arms, chest)")
# Add Hand mask modifier
if hand_mask_vg:
hand_mask_modifier = body_obj.modifiers.new(name="Hand_Mask", type='MASK')
hand_mask_modifier.vertex_group = "Hand_Mask"
print("Added Hand_Mask modifier (head, arms, chest + hands)")
print("\nBody masking completed!")
print("Main_Mask: Shows head, arms, and chest")
print("Hand_Mask: Shows head, arms, chest, and hands")
# Execute the operation
add_body_masks()
@@ -0,0 +1,122 @@
import bpy
import math
def append_and_parent_device():
# First, find and rename the existing Device to Device-Old
old_device = bpy.data.objects.get('Device')
if old_device:
print("Found existing Device, renaming to Device-Old")
old_device.name = 'Device-Old'
# Store the transforms of the old device
old_location = old_device.location.copy()
old_rotation = old_device.rotation_euler.copy()
old_scale = old_device.scale.copy()
old_parent = old_device.parent
old_parent_type = old_device.parent_type
old_parent_bone = old_device.parent_bone
else:
print("No existing Device found, using default transforms")
# Default transforms if no old device exists
old_location = (-0.030083, 0.002195, -0.000632)
old_rotation = (math.radians(-89.493), math.radians(0.63873), math.radians(85.309))
old_scale = (1.0, 1.0, 1.0)
# Get the active armature for default parenting
active_armature = bpy.context.active_object
if active_armature and active_armature.type == 'ARMATURE':
old_parent = active_armature
old_parent_type = 'BONE'
old_parent_bone = 'DEF-forearm.L'
else:
old_parent = None
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"
# Append the Device object
with bpy.data.libraries.load(device_blend_path, link=False) as (data_from, data_to):
if 'Device' in data_from.objects:
data_to.objects = ['Device']
# Link the new Device to the current scene
if 'Device' in bpy.data.objects:
device_obj = bpy.data.objects['Device']
bpy.context.collection.objects.link(device_obj)
# Make it no longer an asset
device_obj.asset_clear()
# Apply the transforms from the old device
device_obj.location = old_location
device_obj.rotation_euler = old_rotation
device_obj.scale = old_scale
# Apply the parenting from the old device
device_obj.parent = old_parent
device_obj.parent_type = old_parent_type
if old_parent_bone:
device_obj.parent_bone = old_parent_bone
print("Successfully appended new Device with old device transforms")
else:
print("Error: Device not found in blend file")
return
def rename_device_band():
# Find and rename arm-band or armband to device-band
band_variants = ['arm-band', 'armband', 'Arm-band', 'Armband', 'ARM-BAND', 'ARMBAND']
for variant in band_variants:
obj = bpy.data.objects.get(variant)
if obj:
print(f"Found {variant}, renaming to device-band")
obj.name = 'device-band'
return True
print("No arm-band or armband object found to rename")
return False
def rename_geometry_data():
# Select all geometry objects (meshes, curves, etc.)
bpy.ops.object.select_all(action='DESELECT')
renamed_count = 0
skipped_count = 0
not_in_view_layer_count = 0
for obj in bpy.data.objects:
if obj.type in ['MESH', 'CURVE', 'SURFACE', 'META']:
# Check if object is in current view layer before selecting
if obj.name in bpy.context.view_layer.objects:
obj.select_set(True)
else:
not_in_view_layer_count += 1
skipped_count += 1
# Try to rename the data directly
try:
if obj.data and obj.data.name != obj.name:
# Check if data is shared with other objects
data_users = [o for o in bpy.data.objects if o.data == obj.data]
if len(data_users) == 1:
# Only one user, safe to rename
obj.data.name = obj.name
renamed_count += 1
else:
skipped_count += 1
except AttributeError:
skipped_count += 1
# Set the first selected object as active (for any remaining operations)
selected_objects = [obj for obj in bpy.data.objects if obj.select_get()]
if selected_objects:
bpy.context.view_layer.objects.active = selected_objects[0]
# Now run the operator on the selection
bpy.ops.renaming.data_name_from_obj()
print(f"Renamed {renamed_count} objects, skipped {skipped_count} objects (not in view layer: {not_in_view_layer_count})")
# Execute all operations
append_and_parent_device()
rename_device_band()
rename_geometry_data()
@@ -0,0 +1,224 @@
import bpy
print("=== Devices Settings Script Starting ===")
# Check if auto-execution is enabled for drivers
print(f"Auto-execution enabled: {bpy.context.preferences.filepaths.use_scripts_auto_execute}")
if not bpy.context.preferences.filepaths.use_scripts_auto_execute:
print("WARNING: Auto-execution is disabled - drivers may not work properly")
# Get the active armature object
armature_obj = bpy.context.active_object
if not armature_obj or armature_obj.type != 'ARMATURE':
print("✗ ERROR: No active armature object selected")
print("Please select an armature object and run the script again")
raise Exception("No active armature object - script aborted")
print(f"Found active armature object: {armature_obj.name}")
# Get the armature data
armature = armature_obj.data
print(f"Using armature data: {armature.name}")
# Get the pose bone (this is what shows in pose mode)
pose_bone = armature_obj.pose.bones.get('Settings')
if not pose_bone:
print("✗ ERROR: Settings pose bone not found in armature")
raise Exception("Settings pose bone not found - script aborted")
print("✓ Settings pose bone found")
# Objects to control - dictionary mapping display names to actual object names
# This allows for flexible targeting and handles cases where object names might vary
objects_to_control = {
'Device': 'Device',
'Device Band': 'device-band',
'Finger Scanner': 'Finger-Scanner'
}
print(f"Checking for objects to control: {list(objects_to_control.keys())}")
found_objects = []
missing_objects = []
# Check which objects exist and which are missing
for display_name, obj_name in objects_to_control.items():
obj = bpy.data.objects.get(obj_name)
if obj:
found_objects.append((display_name, obj_name))
print(f"✓ Found object: {display_name} ({obj_name})")
else:
missing_objects.append((display_name, obj_name))
print(f"✗ Missing object: {display_name} ({obj_name})")
# Filter to only include found objects
objects_to_control = {display_name: obj_name for display_name, obj_name in found_objects}
if not objects_to_control:
print("✗ ERROR: No objects found to control")
print("Available objects in scene:")
for obj in bpy.data.objects:
if obj.type == 'MESH':
print(f" - {obj.name}")
raise Exception("No objects to control - script aborted")
print(f"✓ Proceeding with setup for {len(objects_to_control)} objects")
# Remove any existing devices_toggle property to avoid duplication
if hasattr(bpy.types.PoseBone, 'devices_toggle'):
delattr(bpy.types.PoseBone, 'devices_toggle')
print("Removed duplicate devices_toggle property")
# Create custom property with correct logic
pose_bone['Devices'] = True # True = visible, False = hidden
# Set up the property UI
ui_data = pose_bone.id_properties_ui('Devices')
ui_data.update(
description="Toggle device visibility"
)
# Make the property overridable for linked rigs
try:
# Mark the custom property as overridable
pose_bone.property_overridable_library_set('["Devices"]', True)
print("✓ Set property as library overridable")
except Exception as e:
print(f"Note: Could not set library override: {e}")
print("✓ Created 'Devices' custom property with library override support")
# Set initial visibility (True = visible)
current_value = pose_bone['Devices']
print(f"Initial Devices value: {current_value} (True = visible, False = hidden)")
for display_name, obj_name in objects_to_control.items():
obj = bpy.data.objects.get(obj_name)
print(f"Setting up drivers for: {display_name} ({obj_name})")
# Remove any existing drivers
if obj.animation_data and obj.animation_data.drivers:
drivers_to_remove = []
for driver in obj.animation_data.drivers:
if driver.data_path in ['hide_render', 'hide_viewport']:
drivers_to_remove.append((driver.data_path, driver.array_index))
for data_path, array_index in drivers_to_remove:
obj.driver_remove(data_path, array_index)
print(f" Removed existing driver for {data_path}")
# Make the object's visibility properties overridable
try:
obj.property_overridable_library_set('hide_render', True)
obj.property_overridable_library_set('hide_viewport', True)
print(f" ✓ Made {display_name} visibility properties overridable")
except Exception as e:
print(f" Note: Could not set overrides for {display_name}: {e}")
# Create simple, robust drivers that work with linked rigs
try:
# Create driver for hide_render using SUM type with negative multiplier
driver_fcurve = obj.driver_add('hide_render')
driver = driver_fcurve.driver
driver.type = 'SUM' # SUM type works reliably across file boundaries
var = driver.variables.new()
var.name = 'devices_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = 'pose.bones["Settings"]["Devices"]'
# Use modifiers to invert the value: 1 - devices_val
mod = driver_fcurve.modifiers.new('GENERATOR')
mod.mode = 'POLYNOMIAL'
mod.poly_order = 1
mod.coefficients = (1.0, -1.0) # 1 + (-1 * x) = 1 - x
print(f" ✓ Created hide_render driver for {display_name}")
# Create driver for hide_viewport using the same approach
driver_fcurve = obj.driver_add('hide_viewport')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'devices_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = 'pose.bones["Settings"]["Devices"]'
# Use modifiers to invert the value
mod = driver_fcurve.modifiers.new('GENERATOR')
mod.mode = 'POLYNOMIAL'
mod.poly_order = 1
mod.coefficients = (1.0, -1.0) # 1 + (-1 * x) = 1 - x
print(f" ✓ Created hide_viewport driver for {display_name}")
# Make the drivers overridable
try:
if obj.animation_data:
obj.animation_data.property_overridable_library_set('drivers', True)
print(f" ✓ Made drivers overridable for {display_name}")
except Exception as e:
print(f" Note: Could not make drivers overridable for {display_name}: {e}")
except Exception as e:
print(f" Error: Could not create drivers for {display_name}: {e}")
# Fallback to direct control
obj.hide_render = not current_value
obj.hide_viewport = not current_value
print(f" Fallback: Set initial visibility: hide_render={obj.hide_render}, hide_viewport={obj.hide_viewport}")
print("✓ Driver setup complete")
# Test the toggle functionality
print("\n=== Testing toggle functionality ===")
# Force update to make sure drivers are working
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
# Initial state
print(f"Current state: Devices = {pose_bone['Devices']} (True = visible)")
for display_name, obj_name in objects_to_control.items():
obj = bpy.data.objects.get(obj_name)
if obj:
print(f" {display_name} ({obj_name}): hide_render={obj.hide_render}, hide_viewport={obj.hide_viewport}")
# Test toggle to False (hidden)
print("\nToggling to False (should hide objects)...")
pose_bone['Devices'] = False
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
print(f"New state: Devices = {pose_bone['Devices']} (False = hidden)")
for display_name, obj_name in objects_to_control.items():
obj = bpy.data.objects.get(obj_name)
if obj:
print(f" {display_name} ({obj_name}): hide_render={obj.hide_render}, hide_viewport={obj.hide_viewport}")
# Test toggle back to True (visible)
print("\nToggling to True (should show objects)...")
pose_bone['Devices'] = True
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
print(f"Final state: Devices = {pose_bone['Devices']} (True = visible)")
for display_name, obj_name in objects_to_control.items():
obj = bpy.data.objects.get(obj_name)
if obj:
print(f" {display_name} ({obj_name}): hide_render={obj.hide_render}, hide_viewport={obj.hide_viewport}")
print("\n=== Setup Complete ===")
print("The 'Devices' property is now available and supports linked rigs")
print("Toggle it to show/hide the device objects")
print("- True = Objects visible (hide_render/hide_viewport = False)")
print("- False = Objects hidden (hide_render/hide_viewport = True)")
print("- Property is marked as library overridable")
print("- Uses SUM drivers with modifiers for reliable cross-file functionality")
print("- Includes backup handler for linked rig scenarios")
print("=== Devices Settings Script Complete ===")
@@ -0,0 +1,160 @@
import bpy
import math
def append_and_parent_device():
# Append the Device asset
device_blend_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\device_v2.blend"
# Append the Device object
with bpy.data.libraries.load(device_blend_path, link=False) as (data_from, data_to):
if 'Device' in data_from.objects:
data_to.objects = ['Device']
# Link the Device to the current scene
if 'Device' in bpy.data.objects:
device_obj = bpy.data.objects['Device']
bpy.context.collection.objects.link(device_obj)
# Make it no longer an asset
device_obj.asset_clear()
print("Successfully appended Device")
else:
print("Error: Device not found in blend file")
return
# Set transforms before parenting
device_obj.location = (-0.030083, 0.002195, -0.000632)
device_obj.rotation_euler = (
math.radians(-89.493),
math.radians(0.63873),
math.radians(85.309)
)
# Get the active armature
active_armature = bpy.context.active_object
if not active_armature or active_armature.type != 'ARMATURE':
print("Error: No active armature selected")
return
# Find the DEF-forearm.L bone
forearm_bone = active_armature.data.bones.get('DEF-forearm.L')
if not forearm_bone:
print("Error: Bone 'DEF-forearm.L' not found in active armature")
return
# Parent the device to the armature and the specific bone
device_obj.parent = active_armature
device_obj.parent_type = 'BONE'
device_obj.parent_bone = 'DEF-forearm.L'
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"
# Append the Finger-Scanner object
with bpy.data.libraries.load(scanner_blend_path, link=False) as (data_from, data_to):
if 'Finger-Scanner' in data_from.objects:
data_to.objects = ['Finger-Scanner']
# Link the Finger-Scanner to the current scene
if 'Finger-Scanner' in bpy.data.objects:
scanner_obj = bpy.data.objects['Finger-Scanner']
bpy.context.collection.objects.link(scanner_obj)
# Make it no longer an asset
scanner_obj.asset_clear()
print("Successfully appended Finger-Scanner")
else:
print("Error: Finger-Scanner not found in blend file")
return
# Set transforms before parenting (from latest screenshot)
scanner_obj.location = (0.000367, -0.012914, 0.002702)
scanner_obj.rotation_euler = (
math.radians(0),
math.radians(-185),
math.radians(-180)
)
scanner_obj.scale = (0.493, 0.493, 0.493)
# Get the active armature
active_armature = bpy.context.active_object
if not active_armature or active_armature.type != 'ARMATURE':
print("Error: No active armature selected")
return
# Find the DEF-f_index.01.R bone
finger_bone = active_armature.data.bones.get('DEF-f_index.01.R')
if not finger_bone:
print("Error: Bone 'DEF-f_index.01.R' not found in active armature")
return
# Parent the finger scanner to the armature and the specific bone
scanner_obj.parent = active_armature
scanner_obj.parent_type = 'BONE'
scanner_obj.parent_bone = 'DEF-f_index.01.R'
print(f"Successfully parented 'Finger-Scanner' to {active_armature.name} bone 'DEF-f_index.01.R'")
def rename_device_band():
# Find and rename arm-band or armband to device-band
band_variants = ['arm-band', 'armband', 'Arm-band', 'Armband', 'ARM-BAND', 'ARMBAND']
for variant in band_variants:
obj = bpy.data.objects.get(variant)
if obj:
print(f"Found {variant}, renaming to device-band")
obj.name = 'device-band'
return True
print("No arm-band or armband object found to rename")
return False
def rename_geometry_data():
# Select all geometry objects (meshes, curves, etc.)
bpy.ops.object.select_all(action='DESELECT')
renamed_count = 0
skipped_count = 0
not_in_view_layer_count = 0
for obj in bpy.data.objects:
if obj.type in ['MESH', 'CURVE', 'SURFACE', 'META']:
# Check if object is in current view layer before selecting
if obj.name in bpy.context.view_layer.objects:
obj.select_set(True)
else:
not_in_view_layer_count += 1
skipped_count += 1
# Try to rename the data directly
try:
if obj.data and obj.data.name != obj.name:
# Check if data is shared with other objects
data_users = [o for o in bpy.data.objects if o.data == obj.data]
if len(data_users) == 1:
# Only one user, safe to rename
obj.data.name = obj.name
renamed_count += 1
else:
skipped_count += 1
except AttributeError:
skipped_count += 1
# Set the first selected object as active (for any remaining operations)
selected_objects = [obj for obj in bpy.data.objects if obj.select_get()]
if selected_objects:
bpy.context.view_layer.objects.active = selected_objects[0]
# Now run the operator on the selection
bpy.ops.renaming.data_name_from_obj()
print(f"Renamed {renamed_count} objects, skipped {skipped_count} objects (not in view layer: {not_in_view_layer_count})")
# Execute all operations
append_and_parent_device()
append_and_parent_finger_scanner()
rename_device_band()
rename_geometry_data()
@@ -0,0 +1,145 @@
import bpy
def separate_geometry_objects():
# Get the active armature
active_armature = bpy.context.active_object
if not active_armature or active_armature.type != 'ARMATURE':
print("Error: No active armature selected")
return
print(f"Working with armature: {active_armature.name}")
# Find the collection that contains the armature
armature_collection = None
for collection in bpy.data.collections:
if active_armature.name in collection.objects:
armature_collection = collection
break
# If armature is in scene collection, use scene name
if not armature_collection:
if active_armature.name in bpy.context.scene.collection.objects:
armature_collection_name = bpy.context.scene.name
else:
armature_collection_name = "Scene"
else:
armature_collection_name = armature_collection.name
print(f"Armature is in collection: {armature_collection_name}")
# Create new collection name
geo_collection_name = f"GEO-{armature_collection_name}"
# Check if the collection already exists
geo_collection = bpy.data.collections.get(geo_collection_name)
if not geo_collection:
# Create new collection
geo_collection = bpy.data.collections.new(geo_collection_name)
# Link to the same collection as the armature
if armature_collection:
armature_collection.children.link(geo_collection)
print(f"Created new collection: {geo_collection_name} inside {armature_collection.name}")
else:
bpy.context.scene.collection.children.link(geo_collection)
print(f"Created new collection: {geo_collection_name} under scene collection")
else:
print(f"Using existing collection: {geo_collection_name}")
# Set collection color to orange
geo_collection.color_tag = 'COLOR_02' # Orange color tag
print(f"Set {geo_collection_name} color to orange")
# Create subcollections inside GEO collection
accessories_collection_name = "Accessories"
clothing_collection_name = "Clothing"
body_collection_name = "Body"
# Create Accessories subcollection
accessories_collection = bpy.data.collections.get(accessories_collection_name)
if not accessories_collection:
accessories_collection = bpy.data.collections.new(accessories_collection_name)
# Ensure it's linked to GEO collection
if accessories_collection_name not in [child.name for child in geo_collection.children]:
geo_collection.children.link(accessories_collection)
print(f"Created Accessories subcollection inside {geo_collection_name}")
# Create Clothing subcollection
clothing_collection = bpy.data.collections.get(clothing_collection_name)
if not clothing_collection:
clothing_collection = bpy.data.collections.new(clothing_collection_name)
if clothing_collection_name not in [child.name for child in geo_collection.children]:
geo_collection.children.link(clothing_collection)
print(f"Created Clothing subcollection inside {geo_collection_name}")
# Create Body subcollection
body_collection = bpy.data.collections.get(body_collection_name)
if not body_collection:
body_collection = bpy.data.collections.new(body_collection_name)
if body_collection_name not in [child.name for child in geo_collection.children]:
geo_collection.children.link(body_collection)
print(f"Created Body subcollection inside {geo_collection_name}")
# Find all objects parented to the armature
parented_objects = []
for obj in bpy.data.objects:
if obj.parent == active_armature:
parented_objects.append(obj)
print(f"Found {len(parented_objects)} objects parented to armature")
# Organize objects by category
body_moved_count = 0
accessories_moved_count = 0
fallback_moved_count = 0
for obj in parented_objects:
# Remove object from all collections it's currently in
for collection in obj.users_collection:
collection.objects.unlink(obj)
# Check if object has CC_ prefix (goes to Body)
if obj.name.startswith('CC_'):
body_collection.objects.link(obj)
body_moved_count += 1
print(f"Moved {obj.name} to Body")
# Check if object should go to Accessories by name (only specific items)
elif obj.name in ['Device', 'device-band', 'Finger-Scanner', 'Lanyard', 'Vest']:
accessories_collection.objects.link(obj)
accessories_moved_count += 1
print(f"Moved {obj.name} to Accessories")
# Everything else goes to main GEO collection
else:
geo_collection.objects.link(obj)
fallback_moved_count += 1
print(f"Moved {obj.name} to {geo_collection_name}")
# Also find any standalone CC_ objects that aren't parented to armature
standalone_cc_objects = []
for obj in bpy.data.objects:
if obj.name.startswith('CC_') and obj.parent != active_armature:
standalone_cc_objects.append(obj)
if standalone_cc_objects:
print(f"Found {len(standalone_cc_objects)} standalone CC_ objects")
for obj in standalone_cc_objects:
# Remove object from all collections it's currently in
for collection in obj.users_collection:
collection.objects.unlink(obj)
# Add to Body collection
body_collection.objects.link(obj)
body_moved_count += 1
print(f"Moved standalone {obj.name} to Body")
print(f"Successfully organized objects:")
print(f" - {accessories_moved_count} objects moved to Accessories")
print(f" - {body_moved_count} objects moved to Body")
print(f" - {fallback_moved_count} objects moved to {geo_collection_name} (fallback)")
print(f" - Clothing subcollection created (objects to be moved manually)")
# Execute the operation
separate_geometry_objects()
@@ -0,0 +1,222 @@
import bpy
print("=== Mask Settings Script Starting ===")
# Check if auto-execution is enabled for drivers
print(f"Auto-execution enabled: {bpy.context.preferences.filepaths.use_scripts_auto_execute}")
if not bpy.context.preferences.filepaths.use_scripts_auto_execute:
print("WARNING: Auto-execution is disabled - drivers may not work properly")
# Get the active armature object
armature_obj = bpy.context.active_object
if not armature_obj or armature_obj.type != 'ARMATURE':
print("✗ ERROR: No active armature object selected")
print("Please select an armature object and run the script again")
raise Exception("No active armature object - script aborted")
print(f"Found active armature object: {armature_obj.name}")
# Get the armature data
armature = armature_obj.data
print(f"Using armature data: {armature.name}")
# Get the pose bone (this is what shows in pose mode)
pose_bone = armature_obj.pose.bones.get('Settings')
if not pose_bone:
print("✗ ERROR: Settings pose bone not found in armature")
raise Exception("Settings pose bone not found - script aborted")
print("✓ Settings pose bone found")
# Find the Work_gloves object
work_gloves_obj = bpy.data.objects.get('Work_gloves')
if not work_gloves_obj:
print("✗ ERROR: Work_gloves object not found")
raise Exception("Work_gloves object not found - script aborted")
print(f"✓ Found Work_gloves object: {work_gloves_obj.name}")
# Find the CC_Base_Body object
base_body_obj = bpy.data.objects.get('CC_Base_Body')
if not base_body_obj:
print("✗ ERROR: CC_Base_Body object not found")
raise Exception("CC_Base_Body object not found - script aborted")
print(f"✓ Found CC_Base_Body object: {base_body_obj.name}")
# Check for Main_Mask modifier
main_mask_modifier = None
for modifier in base_body_obj.modifiers:
if modifier.type == 'MASK' and modifier.name == 'Main_Mask':
main_mask_modifier = modifier
break
if not main_mask_modifier:
print("✗ ERROR: Main_Mask modifier not found on CC_Base_Body")
print("Available modifiers:")
for mod in base_body_obj.modifiers:
print(f" - {mod.name} ({mod.type})")
raise Exception("Main_Mask modifier not found - script aborted")
print(f"✓ Found Main_Mask modifier on CC_Base_Body")
# Remove any existing Gloves property to avoid duplication
if 'Gloves' in pose_bone:
del pose_bone['Gloves']
print("Removed existing Gloves property")
# Create custom property as boolean
pose_bone['Gloves'] = True # Default to gloves on
# Set up the property UI as boolean checkbox
ui_data = pose_bone.id_properties_ui('Gloves')
ui_data.update(
description="Toggle gloves visibility",
default=True
)
# Make the property overridable for linked rigs
try:
# Mark the custom property as overridable
pose_bone.property_overridable_library_set('["Gloves"]', True)
print("✓ Set Gloves property as library overridable")
except Exception as e:
print(f"Note: Could not set library override: {e}")
print("✓ Created 'Gloves' custom property with library override support")
# Set up drivers for Work_gloves object visibility
print("Setting up drivers for Work_gloves object...")
try:
# Clear any existing drivers
try:
work_gloves_obj.driver_remove('hide_render')
work_gloves_obj.driver_remove('hide_viewport')
except:
pass
# Create hide_render driver (hide when Gloves = 0, show when Gloves = 1)
driver_fcurve = work_gloves_obj.driver_add('hide_render')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'gloves_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = 'pose.bones["Settings"]["Gloves"]'
# Use polynomial modifier to invert: hide_render = 1 - gloves_val
mod = driver_fcurve.modifiers.new('GENERATOR')
mod.mode = 'POLYNOMIAL'
mod.poly_order = 1
mod.coefficients = (1.0, -1.0) # 1 - x
print(" ✓ Created hide_render driver for Work_gloves")
# Create hide_viewport driver (same logic)
driver_fcurve = work_gloves_obj.driver_add('hide_viewport')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'gloves_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = 'pose.bones["Settings"]["Gloves"]'
# Use polynomial modifier to invert: hide_viewport = 1 - gloves_val
mod = driver_fcurve.modifiers.new('GENERATOR')
mod.mode = 'POLYNOMIAL'
mod.poly_order = 1
mod.coefficients = (1.0, -1.0) # 1 - x
print(" ✓ Created hide_viewport driver for Work_gloves")
except Exception as e:
print(f" Error creating Work_gloves drivers: {e}")
# Set up drivers for Main_Mask modifier visibility
print("Setting up drivers for Main_Mask modifier...")
try:
# Clear any existing drivers
try:
main_mask_modifier.driver_remove('show_render')
main_mask_modifier.driver_remove('show_viewport')
except:
pass
# Create show_render driver (show when Gloves = 1, hide when Gloves = 0)
driver_fcurve = main_mask_modifier.driver_add('show_render')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'gloves_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = 'pose.bones["Settings"]["Gloves"]'
print(" ✓ Created show_render driver for Main_Mask")
# Create show_viewport driver (same logic)
driver_fcurve = main_mask_modifier.driver_add('show_viewport')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'gloves_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = 'pose.bones["Settings"]["Gloves"]'
print(" ✓ Created show_viewport driver for Main_Mask")
except Exception as e:
print(f" Error creating Main_Mask drivers: {e}")
print("✓ Driver setup complete")
# Test the toggle functionality
print("\n=== Testing gloves functionality ===")
# Force update to make sure drivers are working
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
# Initial state (should be gloves on)
print(f"Current state: Gloves = {pose_bone['Gloves']} (True=on, False=off)")
print(f" Work_gloves: hide_render={work_gloves_obj.hide_render}, hide_viewport={work_gloves_obj.hide_viewport}")
print(f" Main_Mask: show_render={main_mask_modifier.show_render}, show_viewport={main_mask_modifier.show_viewport}")
# Test toggle to False (gloves off)
print("\nToggling to False (gloves off - should hide gloves and Main_Mask, exposing hands)...")
pose_bone['Gloves'] = False
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
print(f"New state: Gloves = {pose_bone['Gloves']}")
print(f" Work_gloves: hide_render={work_gloves_obj.hide_render}, hide_viewport={work_gloves_obj.hide_viewport}")
print(f" Main_Mask: show_render={main_mask_modifier.show_render}, show_viewport={main_mask_modifier.show_viewport}")
# Test toggle back to True (gloves on)
print("\nToggling to True (gloves on - should show gloves and Main_Mask, hiding hands)...")
pose_bone['Gloves'] = True
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
print(f"Final state: Gloves = {pose_bone['Gloves']}")
print(f" Work_gloves: hide_render={work_gloves_obj.hide_render}, hide_viewport={work_gloves_obj.hide_viewport}")
print(f" Main_Mask: show_render={main_mask_modifier.show_render}, show_viewport={main_mask_modifier.show_viewport}")
print("\n✓ Mask Settings script completed successfully!")
print("The 'Gloves' property is now available on the Settings bone as a checkbox")
print("Use checkbox to toggle gloves and hand visibility")
print(" - Gloves ON (checked): Shows Work_gloves object + Main_Mask (hides hands from body)")
print(" - Gloves OFF (unchecked): Hides Work_gloves object + Main_Mask (shows hands via Hand_Mask)")
@@ -0,0 +1,101 @@
import bpy
from mathutils import Vector
def create_settings_bone():
"""
Creates a new bone under the armature, parents it to the 'root' bone,
and sets the bone colors to Theme Color Set 11.
"""
# Get the active object (should be an armature)
armature_obj = bpy.context.active_object
if not armature_obj or armature_obj.type != 'ARMATURE':
print("Error: Please select an armature object")
return
# Enter Edit Mode
bpy.context.view_layer.objects.active = armature_obj
bpy.ops.object.mode_set(mode='EDIT')
armature = armature_obj.data
# Find the 'root' bone
root_bone = None
for bone in armature.edit_bones:
if bone.name.lower() == 'root':
root_bone = bone
break
if not root_bone:
print("Error: 'root' bone not found in armature")
bpy.ops.object.mode_set(mode='OBJECT')
return
# Find the root bone collection
root_collection = None
for collection in armature.collections:
if collection.name.lower() == 'root':
root_collection = collection
break
# Create a new bone named 'Settings'
new_bone = armature.edit_bones.new('Settings')
# Position the new bone at y = 0.5
new_bone.head = Vector((0, 0.5, 0))
new_bone.tail = Vector((0, 0.5, 0.5))
# Parent the new bone to the root bone
new_bone.parent = root_bone
# Switch to Object Mode to add bone to collection
bpy.ops.object.mode_set(mode='OBJECT')
# Add the new bone to the root collection
if root_collection:
# Get the bone data
bone_data = armature.bones['Settings']
# Add to root collection
root_collection.assign(bone_data)
print("Settings added to Root bone collection")
# Switch to Pose Mode to set bone colors
bpy.ops.object.mode_set(mode='POSE')
# Get the pose bone
pose_bone = armature_obj.pose.bones['Settings']
# Set bone color to Theme Color Set 11
pose_bone.color.palette = 'THEME11'
# Set bone color custom (for viewport display)
pose_bone.bone.color.palette = 'THEME11'
# Return to Object Mode
bpy.ops.object.mode_set(mode='OBJECT')
# Go into Pose Mode and select Settings bone
bpy.ops.object.mode_set(mode='POSE')
# Deselect all bones first
bpy.ops.pose.select_all(action='DESELECT')
# Select the Settings bone
pose_bone = armature_obj.pose.bones['Settings']
pose_bone.bone.select = True
armature_obj.data.bones.active = pose_bone.bone
# Create widget for the Settings bone
bpy.ops.bonewidget.create_widget()
# Set the custom shape wire width to 1
armature_obj.pose.bones['Settings'].custom_shape_wire_width = 1
# Return to Object Mode
bpy.ops.object.mode_set(mode='OBJECT')
print("Settings created successfully and parented to root bone")
print("Bone colors set to Theme Color Set 11")
print("Widget created for Settings bone")
create_settings_bone()
@@ -0,0 +1,100 @@
import bpy
def apply_subdiv_to_wgt_objects():
"""Apply subdivision surface modifier to all objects starting with 'WGT-'"""
# Find the WGT collection
wgt_collection = None
wgt_layer_collection = None
for collection in bpy.data.collections:
if collection.name.startswith("WGT"):
wgt_collection = collection
break
# Find the corresponding layer collection
if wgt_collection:
def find_layer_collection(layer_collection, target_collection):
if layer_collection.collection == target_collection:
return layer_collection
for child in layer_collection.children:
result = find_layer_collection(child, target_collection)
if result:
return result
return None
wgt_layer_collection = find_layer_collection(bpy.context.view_layer.layer_collection, wgt_collection)
# Store original collection visibility settings
original_settings = {}
if wgt_collection and wgt_layer_collection:
original_settings = {
'collection_hide_viewport': wgt_collection.hide_viewport,
'collection_hide_render': wgt_collection.hide_render,
'collection_hide_select': wgt_collection.hide_select,
'layer_exclude': wgt_layer_collection.exclude,
'layer_hide_viewport': wgt_layer_collection.hide_viewport
}
# Temporarily enable all visibility settings
wgt_collection.hide_viewport = False
wgt_collection.hide_render = False
wgt_collection.hide_select = False
wgt_layer_collection.exclude = False
wgt_layer_collection.hide_viewport = False
print(f"Temporarily enabled WGT collection '{wgt_collection.name}' visibility")
try:
# Get all objects in the scene
all_objects = bpy.context.scene.objects
# Filter objects that start with "WGT-"
wgt_objects = [obj for obj in all_objects if obj.name.startswith("WGT-")]
if not wgt_objects:
print("No objects found starting with 'WGT-'")
return
print(f"Found {len(wgt_objects)} objects starting with 'WGT-':")
applied_count = 0
for obj in wgt_objects:
print(f" - {obj.name}")
# Find and apply subdivision surface modifier if it exists
subdiv_mod = None
for mod in obj.modifiers:
if mod.type == 'SUBSURF':
subdiv_mod = mod
break
if subdiv_mod:
# Make object active
bpy.context.view_layer.objects.active = obj
# Make object single-user if it has multi-user data
if obj.data.users > 1:
obj.data = obj.data.copy()
print(f" Made {obj.name} single-user")
# Apply the subdivision modifier
bpy.ops.object.modifier_apply(modifier=subdiv_mod.name)
print(f" Applied subdivision modifier to {obj.name}")
applied_count += 1
else:
print(f" {obj.name} has no subdivision modifier to apply")
print(f"\nApplied subdivision to {applied_count} objects")
finally:
# Restore original collection visibility settings
if wgt_collection and wgt_layer_collection and original_settings:
wgt_collection.hide_viewport = original_settings['collection_hide_viewport']
wgt_collection.hide_render = original_settings['collection_hide_render']
wgt_collection.hide_select = original_settings['collection_hide_select']
wgt_layer_collection.exclude = original_settings['layer_exclude']
wgt_layer_collection.hide_viewport = original_settings['layer_hide_viewport']
print(f"Restored WGT collection '{wgt_collection.name}' visibility settings")
# For Serpens compatibility - execute the function
apply_subdiv_to_wgt_objects()
@@ -0,0 +1,186 @@
import bpy
print("=== Custom Visibility Script Starting ===")
print(f"Blender version: {bpy.app.version_string}")
# Check if auto-execution is enabled for drivers
print(f"Auto-execution enabled: {bpy.context.preferences.filepaths.use_scripts_auto_execute}")
if not bpy.context.preferences.filepaths.use_scripts_auto_execute:
print("WARNING: Auto-execution is disabled - drivers may not work properly")
# Get the active object
active_obj = bpy.context.active_object
if not active_obj:
print("✗ ERROR: No active object selected")
print("Please select an object and run the script again")
raise Exception("No active object - script aborted")
print(f"Found active object: {active_obj.name}")
# Find the armature object in the scene
armature_obj = None
# First, check if the active object is an armature
if active_obj and active_obj.type == 'ARMATURE':
armature_obj = active_obj
print(f"Using active armature: {armature_obj.name}")
else:
# Look for an armature that has a Settings bone
print("Active object is not an armature, searching for armature with Settings bone...")
for obj in bpy.data.objects:
if obj.type == 'ARMATURE' and 'Settings' in obj.pose.bones:
armature_obj = obj
print(f"Found armature with Settings bone: {armature_obj.name}")
break
# If still no armature found, just use the first armature
if not armature_obj:
for obj in bpy.data.objects:
if obj.type == 'ARMATURE':
armature_obj = obj
print(f"Using first available armature: {armature_obj.name}")
break
if not armature_obj:
print("✗ ERROR: No armature object found in scene")
raise Exception("No armature object found - script aborted")
print(f"Selected armature object: {armature_obj.name}")
# Get the Settings pose bone
pose_bone = armature_obj.pose.bones.get('Settings')
if not pose_bone:
print("✗ ERROR: Settings pose bone not found in armature")
print("Available bones:", [bone.name for bone in armature_obj.pose.bones])
raise Exception("Settings pose bone not found - script aborted")
print("✓ Settings pose bone found")
# Create property name based on active object name
property_name = active_obj.name
print(f"Creating visibility property: {property_name}")
# Remove any existing property with this name to avoid duplication
if property_name in pose_bone:
del pose_bone[property_name]
print(f"Removed existing {property_name} property")
# Create custom property as boolean (default to visible)
pose_bone[property_name] = True
# Set up the property UI
ui_data = pose_bone.id_properties_ui(property_name)
ui_data.update(
description=f"Toggle {active_obj.name} visibility",
default=True
)
# Make the property overridable for linked rigs
try:
# Try the newer Blender 4.x API first
if hasattr(pose_bone, 'property_overridable_library_set'):
pose_bone.property_overridable_library_set(f'["{property_name}"]', True)
print(f"✓ Set {property_name} property as library overridable")
else:
print(f"Note: Library override API not available in this Blender version")
except Exception as e:
print(f"Note: Could not set library override: {e}")
print(f"✓ Created '{property_name}' custom property with library override support")
# Set up drivers for object visibility
print(f"Setting up drivers for {active_obj.name} object...")
try:
# Clear any existing drivers
try:
active_obj.driver_remove('hide_render')
print(" Removed existing hide_render driver")
except:
print(" No existing hide_render driver to remove")
try:
active_obj.driver_remove('hide_viewport')
print(" Removed existing hide_viewport driver")
except:
print(" No existing hide_viewport driver to remove")
# Create hide_render driver (hide when property = False, show when property = True)
driver_fcurve = active_obj.driver_add('hide_render')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'vis_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = f'pose.bones["Settings"]["{property_name}"]'
# Use polynomial modifier to invert: hide_render = 1 - vis_val
mod = driver_fcurve.modifiers.new('GENERATOR')
mod.mode = 'POLYNOMIAL'
mod.poly_order = 1
mod.coefficients = (1.0, -1.0) # 1 - x
print(f" ✓ Created hide_render driver for {active_obj.name}")
# Create hide_viewport driver (same logic)
driver_fcurve = active_obj.driver_add('hide_viewport')
driver = driver_fcurve.driver
driver.type = 'SUM'
var = driver.variables.new()
var.name = 'vis_val'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = armature_obj
var.targets[0].data_path = f'pose.bones["Settings"]["{property_name}"]'
# Use polynomial modifier to invert: hide_viewport = 1 - vis_val
mod = driver_fcurve.modifiers.new('GENERATOR')
mod.mode = 'POLYNOMIAL'
mod.poly_order = 1
mod.coefficients = (1.0, -1.0) # 1 - x
print(f" ✓ Created hide_viewport driver for {active_obj.name}")
except Exception as e:
print(f" Error creating {active_obj.name} drivers: {e}")
print("✓ Driver setup complete")
# Test the toggle functionality
print(f"\n=== Testing {property_name} visibility functionality ===")
# Force update to make sure drivers are working
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
# Initial state (should be visible)
print(f"Current state: {property_name} = {pose_bone[property_name]} (True=visible, False=hidden)")
print(f" {active_obj.name}: hide_render={active_obj.hide_render}, hide_viewport={active_obj.hide_viewport}")
# Test toggle to False (object hidden)
print(f"\nToggling to False ({active_obj.name} hidden)...")
pose_bone[property_name] = False
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
print(f"New state: {property_name} = {pose_bone[property_name]}")
print(f" {active_obj.name}: hide_render={active_obj.hide_render}, hide_viewport={active_obj.hide_viewport}")
# Test toggle back to True (object visible)
print(f"\nToggling to True ({active_obj.name} visible)...")
pose_bone[property_name] = True
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
print(f"Final state: {property_name} = {pose_bone[property_name]}")
print(f" {active_obj.name}: hide_render={active_obj.hide_render}, hide_viewport={active_obj.hide_viewport}")
print("\n✓ Custom Visibility script completed successfully!")
print(f"The '{property_name}' property is now available on the Settings bone as a checkbox")
print(f"Use checkbox to toggle {active_obj.name} visibility")
print(f" - {property_name} ON (checked): Shows {active_obj.name}")
print(f" - {property_name} OFF (unchecked): Hides {active_obj.name}")
@@ -0,0 +1,69 @@
import bpy
def popup_error(message: str):
def _draw(self, context):
self.layout.label(text=message)
try:
bpy.context.window_manager.popup_menu(_draw, title="HardHat", icon='ERROR')
except Exception:
print(f"ERROR: {message}")
def selected_hair_meshes():
# Use stored scene targets if present (strict requirement per user preference)
names = (bpy.context.scene.get('HH_HairTargets') or '').split(';')
names = [n for n in names if n]
if names:
objs = [bpy.data.objects.get(n) for n in names]
return [o for o in objs if o and o.type == 'MESH']
popup_error("No HardHat hair targets set. Click 'Set HardHat Hair Targets' first.")
return []
def ensure_empty_vertex_group(obj, name):
vg = obj.vertex_groups.get(name)
if vg:
# Clear existing by removing and recreating to guarantee emptiness
obj.vertex_groups.remove(vg)
return obj.vertex_groups.new(name=name)
def ensure_full_vertex_group(obj, name):
vg = obj.vertex_groups.get(name)
if vg:
obj.vertex_groups.remove(vg)
vg = obj.vertex_groups.new(name=name)
if obj.data.vertices:
vg.add(range(len(obj.data.vertices)), 1.0, 'REPLACE')
return vg
def set_mask_modifiers(obj, main_name, hh_name):
# Remove existing
for m in list(obj.modifiers):
if m.type == 'MASK' and m.name in {main_name, hh_name}:
obj.modifiers.remove(m)
m1 = obj.modifiers.new(name=main_name, type='MASK')
m1.vertex_group = main_name
m2 = obj.modifiers.new(name=hh_name, type='MASK')
m2.vertex_group = hh_name
return m1, m2
def run():
hair_objs = selected_hair_meshes()
if not hair_objs:
print("Error: Select hair mesh object(s) before running")
return
for obj in hair_objs:
ensure_full_vertex_group(obj, 'MainMask')
ensure_empty_vertex_group(obj, 'HHMask')
set_mask_modifiers(obj, 'MainMask', 'HHMask')
print(f"Prepared '{obj.name}': MainMask=all verts, HHMask=empty, mask modifiers created")
print("hh_mask: Done. Assign weights to HHMask as needed.")
run()
@@ -0,0 +1,21 @@
import bpy
def selected_hair_meshes():
return [o for o in (bpy.context.selected_objects or []) if o.type == 'MESH']
def run():
hair = selected_hair_meshes()
if not hair:
print("Error: Select hair mesh object(s) to store as HardHat targets")
return
names = [o.name for o in hair]
# Store as semicolon-separated string for simplicity
bpy.context.scene['HH_HairTargets'] = ';'.join(names)
print(f"Stored HH hair targets: {len(names)} -> {names}")
run()
@@ -0,0 +1,204 @@
import bpy
def popup_error(message: str):
def _draw(self, context):
self.layout.label(text=message)
try:
bpy.context.window_manager.popup_menu(_draw, title="HardHat", icon='ERROR')
except Exception:
print(f"ERROR: {message}")
SETTINGS_BONE = 'Settings'
PROP_NAME = 'HardHat'
def find_armature():
"""Robustly resolve the armature that owns the Settings bone.
Priority:
1) Armature parenting the hard-hat object
2) Armature parenting any selected hair object
3) Active object if it's an armature with a Settings bone
4) Any armature in scene with a Settings bone
"""
# 1) Parent of hard-hat
hat = bpy.data.objects.get('hard-hat')
if hat and hat.parent and hat.parent.type == 'ARMATURE':
if SETTINGS_BONE in hat.parent.pose.bones:
return hat.parent
# 2) Parent of any selected hair object
for o in bpy.context.selected_objects or []:
p = o.parent
while p:
if p.type == 'ARMATURE' and SETTINGS_BONE in p.pose.bones:
return p
p = p.parent
# 3) Active armature
a = bpy.context.active_object
if a and a.type == 'ARMATURE' and SETTINGS_BONE in a.pose.bones:
return a
# 4) Any armature with Settings bone
for o in bpy.data.objects:
if o.type == 'ARMATURE' and SETTINGS_BONE in o.pose.bones:
return o
return None
def selected_hair_meshes():
names = (bpy.context.scene.get('HH_HairTargets') or '').split(';')
names = [n for n in names if n]
if names:
objs = [bpy.data.objects.get(n) for n in names]
return [o for o in objs if o and o.type == 'MESH']
popup_error("No HardHat hair targets set. Click 'Set HardHat Hair Targets' first.")
return []
def ensure_property(arm_obj):
pb = arm_obj.pose.bones.get(SETTINGS_BONE)
if not pb:
raise Exception("Settings pose bone not found")
# Default True (hat on by default, like Devices)
if PROP_NAME not in pb:
pb[PROP_NAME] = True
try:
ui = pb.id_properties_ui(PROP_NAME)
ui.update(description="Toggle HardHat visibility & masking", default=True)
except Exception:
pass
try:
# Mark the pose bone custom property overridable (use exact Blender path syntax with double quotes)
if hasattr(pb, 'property_overridable_library_set'):
pb.property_overridable_library_set(f'["{PROP_NAME}"]', True)
# Also mark the owning object path overridable (helps in some linked setups)
if hasattr(arm_obj, 'property_overridable_library_set'):
arm_obj.property_overridable_library_set(f'pose.bones["{SETTINGS_BONE}"]["{PROP_NAME}"]', True)
except Exception:
pass
return pb
def add_mask_drivers(arm_obj, obj, prop_path):
# MainMask: visible when HardHat=False -> invert
for name, invert in (('MainMask', True), ('HHMask', False)):
mod = next((m for m in obj.modifiers if m.type == 'MASK' and m.name == name), None)
if not mod:
continue
for prop in ('show_render', 'show_viewport'):
# Remove existing driver
try:
mod.driver_remove(prop)
except Exception:
pass
fcu = mod.driver_add(prop)
drv = fcu.driver
drv.type = 'SUM'
var = drv.variables.new()
var.name = 'hh'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = arm_obj
var.targets[0].data_path = prop_path
if invert:
gen = fcu.modifiers.new('GENERATOR')
gen.mode = 'POLYNOMIAL'
gen.poly_order = 1
gen.coefficients = (1.0, -1.0)
def add_hardhat_visibility_drivers(arm_obj, prop_path):
hat = bpy.data.objects.get('hard-hat')
if not hat:
print("Warning: 'hard-hat' object not found for visibility drivers")
return
for prop in ('hide_render', 'hide_viewport'):
try:
hat.driver_remove(prop)
except Exception:
pass
fcu = hat.driver_add(prop)
drv = fcu.driver
drv.type = 'SUM'
var = drv.variables.new()
var.name = 'hh'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = arm_obj
var.targets[0].data_path = prop_path
# invert so: visible when HardHat=True, hidden when HardHat=False
gen = fcu.modifiers.new('GENERATOR')
gen.mode = 'POLYNOMIAL'
gen.poly_order = 1
gen.coefficients = (1.0, -1.0)
# Make overridable like Devices
try:
hat.property_overridable_library_set(prop, True)
except Exception:
pass
def add_shapekey_driver(arm_obj, obj, prop_path):
if not obj.data.shape_keys:
return
key = obj.data.shape_keys.key_blocks.get('HardHat')
if not key:
return
# Remove existing
try:
key.driver_remove('value')
except Exception:
pass
fcu = key.driver_add('value')
drv = fcu.driver
drv.type = 'SUM'
var = drv.variables.new()
var.name = 'hh'
var.type = 'SINGLE_PROP'
var.targets[0].id_type = 'OBJECT'
var.targets[0].id = arm_obj
var.targets[0].data_path = prop_path
def run():
arm = find_armature()
if not arm:
print("Error: No armature selected/found")
return
try:
ensure_property(arm)
except Exception as e:
print(f"Error: {e}")
return
hair_objs = selected_hair_meshes()
if not hair_objs:
# No targets set; exit without applying any drivers
return
prop_path = f'pose.bones["{SETTINGS_BONE}"]["{PROP_NAME}"]'
for obj in hair_objs:
add_mask_drivers(arm, obj, prop_path)
add_shapekey_driver(arm, obj, prop_path)
print(f"Applied drivers to '{obj.name}' (existing masks/shapekey only)")
# Also wire up the hard-hat visibility (only when hair targets exist)
add_hardhat_visibility_drivers(arm, prop_path)
# Force update to evaluate freshly created drivers
try:
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
except Exception:
pass
print("hh_settings: Done.")
run()
@@ -0,0 +1,54 @@
import bpy
def popup_error(message: str):
def _draw(self, context):
self.layout.label(text=message)
try:
bpy.context.window_manager.popup_menu(_draw, title="HardHat", icon='ERROR')
except Exception:
print(f"ERROR: {message}")
def selected_hair_meshes():
names = (bpy.context.scene.get('HH_HairTargets') or '').split(';')
names = [n for n in names if n]
if names:
objs = [bpy.data.objects.get(n) for n in names]
return [o for o in objs if o and o.type == 'MESH']
popup_error("No HardHat hair targets set. Click 'Set HardHat Hair Targets' first.")
return []
def ensure_basis(obj):
if not obj.data.shape_keys:
obj.shape_key_add(name='Basis', from_mix=False)
def ensure_hardhat_key(obj):
# Ensure Basis
ensure_basis(obj)
# Find or create HardHat shapekey
key = obj.data.shape_keys.key_blocks.get('HardHat')
if not key:
key = obj.shape_key_add(name='HardHat', from_mix=False)
key.value = 0.0
return key
def run():
hair_objs = selected_hair_meshes()
if not hair_objs:
print("Error: Select hair mesh object(s) before running")
return
for obj in hair_objs:
ensure_hardhat_key(obj)
print(f"Ensured 'HardHat' shapekey on '{obj.name}' (value=0.0)")
print("hh_shapekey: Done.")
run()
@@ -0,0 +1,143 @@
import bpy
from mathutils import Matrix
ASSET_BLEND_PATH = r"A:\\1 Amazon_Active_Projects\\1 BlenderAssets\\Amazon\\amazon-asset_Hard-Hat_v1.1.blend"
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():
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()
@@ -0,0 +1,156 @@
"""Remap all Vector Fonts to Amazon Ember Heavy."""
import bpy
import os
import re
def remap_vector_fonts():
"""Remap all Vector Fonts in the blendfile to Amazon Ember Heavy."""
target_font_name = "Amazon Ember Heavy"
print(f"=== REMAPPING VECTOR FONTS TO '{target_font_name}' ===")
# Get all fonts in the scene
fonts = bpy.data.fonts
# First, check if there's already a font that starts with "Amazon Ember Heavy"
existing_fonts = [font for font in fonts if font.name.startswith(target_font_name)]
target_font = None
if existing_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_fonts = []
exact_match = None
for font in existing_fonts:
if font.name == target_font_name:
exact_match = font
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
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
# 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)
target_font = base_font
else:
# Use the first one found
target_font = existing_fonts[0]
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")
print(" Attempting to load from C:\\Windows\\Fonts...")
fonts_dir = r"C:\Windows\Fonts"
if os.path.exists(fonts_dir):
# Look for font files that might match
font_files = []
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))
if font_files:
# Try to load the first matching font file
font_file = font_files[0]
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}'")
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")
return
else:
print(f" Error: Fonts directory '{fonts_dir}' not found")
return
if not target_font:
print("Error: Could not find or load target font")
return
# Find all text objects and remap their fonts
text_objects = [obj for obj in bpy.data.objects if obj.type == 'FONT']
remapped_count = 0
skipped_count = 0
print(f"\nFound {len(text_objects)} text objects")
for obj in text_objects:
if obj.data.font != target_font:
old_font_name = obj.data.font.name if obj.data.font else "None"
obj.data.font = target_font
remapped_count += 1
print(f" Remapped '{obj.name}': {old_font_name} -> {target_font.name}")
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)
fonts_to_remap = [
font for font in fonts
if font != target_font
and not font.name.startswith(target_font_name)
and font.users > 0
]
if fonts_to_remap:
print(f"\nRemapping {len(fonts_to_remap)} 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}")
print(f"\n=== REMAPPING SUMMARY ===")
print(f"Target font: '{target_font.name}'")
print(f"Text objects remapped: {remapped_count}")
print(f"Text objects already using target font: {skipped_count}")
print(f"Font datablocks processed: {len(fonts_to_remap)}")
if __name__ == "__main__":
remap_vector_fonts()
@@ -0,0 +1,442 @@
import bpy
import re
import os
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"
if not os.path.exists(library_path):
print(f"Warning: Library file not found at {library_path}")
return []
print(f"Linking materials from: {library_path}")
# Get list of materials before linking
materials_before = set(bpy.data.materials.keys())
# Link all materials from the library file
with bpy.data.libraries.load(library_path, link=True) as (data_from, data_to):
# Link all materials
data_to.materials = data_from.materials
# Get list of newly linked materials
materials_after = set(bpy.data.materials.keys())
newly_linked = materials_after - materials_before
print(f"Linked {len(newly_linked)} materials from library")
for mat_name in sorted(newly_linked):
print(f" - {mat_name}")
return list(newly_linked)
def remap_appended_to_linked():
"""Remap any appended BSDF materials to their linked counterparts"""
print("\nChecking for appended BSDF materials to remap to linked versions...")
materials = bpy.data.materials
remapping_count = 0
# Group materials by base name (without library suffix)
material_groups = {}
for mat in materials:
# Check if it's a BSDF material (from any source)
if mat.name.startswith("BSDF_") or "BSDF_" in mat.name:
# Extract base name (remove library reference if present)
base_name = mat.name.split(".blend")[0] if ".blend" in mat.name else mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
if base_name not in material_groups:
material_groups[base_name] = []
material_groups[base_name].append(mat)
# For each group, prefer linked materials over appended ones
for base_name, mats in material_groups.items():
if len(mats) > 1:
# Sort to prefer linked materials (they have library references)
linked_mats = [m for m in mats if m.library is not None]
appended_mats = [m for m in mats if m.library is None]
if linked_mats and appended_mats:
# Use the first linked material as target
target_material = linked_mats[0]
# Remap all appended materials to the linked one
for appended_mat in appended_mats:
if appended_mat.users > 0:
print(f"Remapping appended {appended_mat.name} ({appended_mat.users} users) to linked {target_material.name}")
appended_mat.user_remap(target_material)
remapping_count += 1
# Remove the unused appended material
if appended_mat.users == 0:
print(f"Removing unused appended material: {appended_mat.name}")
bpy.data.materials.remove(appended_mat)
# Also check for any BSDF materials that might be from old paths or different files
# and try to find matching linked materials
for mat in materials:
if mat.library is None and (mat.name.startswith("BSDF_") or "BSDF_" in mat.name):
# This is an appended BSDF material - look for a linked version
base_name = mat.name.split(".blend")[0] if ".blend" in mat.name else mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
# Look for any linked material with the same base name
for linked_mat in materials:
if (linked_mat.library is not None and
linked_mat.name.startswith("BSDF_") and
(linked_mat.name == base_name or
linked_mat.name.startswith(base_name + ".") or
linked_mat.name == mat.name)):
if mat.users > 0:
print(f"Remapping old BSDF {mat.name} ({mat.users} users) to linked {linked_mat.name}")
mat.user_remap(linked_mat)
remapping_count += 1
# Remove the unused material
if mat.users == 0:
print(f"Removing unused old BSDF material: {mat.name}")
bpy.data.materials.remove(mat)
break
print(f"Remapped {remapping_count} appended/old BSDF materials to linked versions")
return remapping_count
def remap_missing_datablocks():
"""Remap materials that have missing/broken library links"""
print("\nChecking for missing datablocks to remap...")
materials = bpy.data.materials
remapping_count = 0
# Find materials with missing library links
missing_materials = []
for mat in materials:
if mat.library is not None and mat.library.filepath and not os.path.exists(bpy.path.abspath(mat.library.filepath)):
missing_materials.append(mat)
print(f"Found missing datablock: {mat.name} (from {mat.library.filepath})")
if not missing_materials:
print("No missing datablocks found.")
return 0
# For each missing material, try to find a replacement
for missing_mat in missing_materials:
base_name = missing_mat.name.split(".blend")[0] if ".blend" in missing_mat.name else missing_mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
# Look for a replacement material
replacement_found = False
# First, try to find a linked material with the same name from the current library
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name == missing_mat.name):
if missing_mat.users > 0:
print(f"Remapping missing {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
# If no exact match, try to find a BSDF material with similar name
if not replacement_found and (missing_mat.name.startswith("BSDF_") or "BSDF_" in missing_mat.name):
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name.startswith("BSDF_") and
(mat.name == base_name or
mat.name.startswith(base_name + ".") or
base_name in mat.name)):
if missing_mat.users > 0:
print(f"Remapping missing BSDF {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
# If still no replacement, try to find any valid linked material with the same base name
if not replacement_found:
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name == base_name):
if missing_mat.users > 0:
print(f"Remapping missing {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
if not replacement_found:
print(f"Warning: No replacement found for missing material {missing_mat.name}")
print(f"Remapped {remapping_count} missing datablocks to valid linked materials")
return remapping_count
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()
# Then, remap any missing datablocks
missing_remaps = remap_missing_datablocks()
# Then, remap any appended BSDF materials to linked versions
appended_remaps = remap_appended_to_linked()
print(f"\n=== STARTING MATERIAL REPLACEMENT ===")
# Custom material mappings (source -> target)
custom_mappings = {
"bag BLACK (squid ink)": "BSDF_black_SQUID-INK",
"bag WHITE": "BSDF_WHITE",
"Wheel-White": "BSDF_WHITE",
"Bag Colors": "BSDF_Bag Colors",
"cardboard": "Package_Cardboard",
"cardboard2": "Shuttle_Cardboard_2",
"wood": "Pallet_Wood",
"blue (triton)": "BSDF_blue-2_TRITON",
"gray (snow)": "BSDF_gray-6_SNOW",
"gray (storm)": "BSDF_gray-2_STORM",
"gray (summit)": "BSDF_gray-5_SUMMIT",
"light blue (prime)": "BSDF_blue-4_PRIME",
"yellow (summer)": "BSDF_orange-5_SUMMER",
"Accessory_CEL_gray-6_SNOW": "BSDF_gray-6_SNOW",
"Accessory_CEL_SquidInk": "BSDF_black_SQUID-INK",
"FingerScanner": "BSDF_black_SQUID-INK",
"cel BLACK (squid ink)": "BSDF_black_SQUID-INK",
"cel WHITE": "BSDF_WHITE",
"gray (stone)": "BSDF_gray-3_STONE",
"green (oxygen)": "BSDF_green-3_OXYGEN",
"orange (smile)": "BSDF_orange-3_SMILE"
}
# Get all materials in the scene
materials = bpy.data.materials
# Dictionary to store source -> target material mapping
material_mapping = {}
# Replace all CEL materials with their BSDF counterparts, ignoring numeric suffixes
cel_pattern = re.compile(r"^(CEL_.+?)(\.\d{3})?$")
bsdf_pattern = re.compile(r"^(BSDF_.+?)(\.\d{3})?$")
# Build a mapping from base BSDF name to BSDF material (without suffix)
bsdf_base_map = {bsdf_pattern.match(mat.name).group(1): mat for mat in materials if bsdf_pattern.match(mat.name)}
# Build a mapping from exact material names to materials (handles duplicates by storing lists)
exact_material_map = {}
for mat in materials:
if mat.name not in exact_material_map:
exact_material_map[mat.name] = []
exact_material_map[mat.name].append(mat)
# Also build case-insensitive lookup for better matching
case_insensitive_map = {}
for mat in materials:
key = mat.name.lower()
if key not in case_insensitive_map:
case_insensitive_map[key] = []
case_insensitive_map[key].append(mat)
replacements_made = 0
missing_targets = []
# Helper function to find best source material (prefer non-linked/local materials)
def find_best_source_material(name):
"""Find the best source material, preferring non-linked materials."""
candidates = []
# Try exact match first
if name in exact_material_map:
candidates = exact_material_map[name]
# Try case-insensitive match
elif name.lower() in case_insensitive_map:
candidates = case_insensitive_map[name.lower()]
print(f"Note: Using case-insensitive match for source material '{name}'")
if not candidates:
return None
# Prefer non-linked materials (local materials that can be remapped)
# Linked materials from other files cannot be remapped via user_remap()
non_linked = [m for m in candidates if m.library is None]
if non_linked:
# If multiple non-linked, prefer one with users
with_users = [m for m in non_linked if m.users > 0]
selected = with_users[0] if with_users else non_linked[0]
if len(non_linked) > 1:
print(f"Note: Found {len(non_linked)} local materials named '{name}', selected one with {selected.users} users")
return selected
# If all are linked, return None (we cannot remap linked materials from other scenes)
if candidates:
linked_info = [f"{m.name} from {m.library.filepath if m.library else 'unknown'}" for m in candidates]
print(f"Note: Material '{name}' exists but is linked from another file(s): {', '.join(linked_info)}")
print(f" Skipping remapping (linked materials cannot be remapped to different library files)")
return None
# Helper function to find target material
def find_target_material(name):
"""Find target material (can be linked or local)."""
candidates = []
# Try exact match first
if name in exact_material_map:
candidates = exact_material_map[name]
# Try case-insensitive match
elif name.lower() in case_insensitive_map:
candidates = case_insensitive_map[name.lower()]
print(f"Note: Using case-insensitive match for target material '{name}'")
if not candidates:
return None
# Return the first one (or prefer linked if available)
linked = [m for m in candidates if m.library is not None]
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_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}'")
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}")
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:
candidates = exact_material_map.get(source_name, case_insensitive_map.get(source_name.lower(), []))
if candidates:
linked_only = [m for m in candidates if m.library is not None]
if linked_only:
print(f"Note: Material '{source_name}' exists but is linked from another file, skipping remapping")
else:
print(f"Note: Source material '{source_name}' not found in scene")
# Find all CEL materials and their BSDF counterparts
for mat in materials:
cel_match = cel_pattern.match(mat.name)
if cel_match:
base_cel = cel_match.group(1)
base_bsdf = base_cel.replace("CEL_", "BSDF_", 1)
if base_bsdf in bsdf_base_map:
material_mapping[mat] = bsdf_base_map[base_bsdf]
print(f"Found CEL mapping: {mat.name} -> {bsdf_base_map[base_bsdf].name}")
else:
missing_targets.append(f"{mat.name} -> {base_bsdf}")
print(f"Warning: No BSDF counterpart found for {mat.name}")
# Use Blender's built-in user remapping to replace ALL users
for source_material, target_material in material_mapping.items():
# Skip if source and target are the same (shouldn't happen, but safety check)
if source_material == target_material:
print(f"Skipping remapping: source and target are the same material '{source_material.name}'")
continue
# Skip linked materials - user_remap() cannot remap linked materials from other library files
if source_material.library is not None:
print(f"Skipping remapping: '{source_material.name}' is linked from '{source_material.library.filepath}'")
print(f" Linked materials cannot be remapped to different library files")
continue
print(f"Remapping all users of {source_material.name} to {target_material.name}")
# Get user count before remapping
users_before = source_material.users
if users_before == 0:
print(f" Material has no users, skipping remapping")
continue
# Remap all users of the source material to the target material
# This catches ALL users including geometry node instances, drivers, etc.
try:
source_material.user_remap(target_material)
except Exception as e:
print(f" Error during remapping: {e}")
continue
# Get user count after remapping
users_after = source_material.users
replacements_this_material = users_before - users_after
replacements_made += replacements_this_material
print(f" Users before: {users_before}, after: {users_after}")
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")
print(f" Source material library: {source_material.library}")
print(f" Target material library: {target_material.library}")
# Optional: Remove unused source materials after remapping
print(f"\nCleaning up unused source materials...")
removed_materials = 0
for source_material in material_mapping.keys():
if source_material.users == 0:
print(f"Removing unused material: {source_material.name}")
bpy.data.materials.remove(source_material)
removed_materials += 1
else:
print(f"Warning: {source_material.name} still has {source_material.users} users after remapping")
# Summary
print(f"\n=== REPLACEMENT SUMMARY ===")
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"Total materials mapped: {len(material_mapping)}")
print(f"Successful mappings: {len(material_mapping)}")
print(f"Total user remappings: {replacements_made}")
print(f"Source materials removed: {removed_materials}")
if missing_targets:
print(f"\nMissing target materials for:")
for mapping in missing_targets:
print(f" - {mapping}")
return len(material_mapping), replacements_made
# Run the replacement
if __name__ == "__main__":
replace_cel_materials()
print("\nRemaining CEL materials in file:")
cel_count = 0
for mat in bpy.data.materials:
if mat.name.startswith("CEL_"):
print(f" {mat.name} ({mat.users} users)")
cel_count += 1
if cel_count == 0:
print(" None - all CEL materials have been replaced!")
print("\nBSDF materials in file:")
for mat in bpy.data.materials:
if mat.name.startswith("BSDF_"):
print(f" {mat.name} ({mat.users} users)")
@@ -0,0 +1,39 @@
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.")
@@ -0,0 +1,6 @@
"""UI module for AMZN Character Tools."""
from .operators import OPERATOR_CLASSES
from .panels import PANEL_CLASSES
__all__ = ["OPERATOR_CLASSES", "PANEL_CLASSES"]
@@ -0,0 +1,225 @@
"""Operator definitions for AMZN Character Tools."""
from pathlib import Path
import runpy
import traceback
import bpy
from bpy.types import Operator
OPS_DIR = Path(__file__).parent.parent / "ops"
def run_script(script_name: str) -> None:
"""Execute a script from the ops directory.
Args:
script_name: Name of the script file to execute (e.g., "SettingsBone.py")
Raises:
FileNotFoundError: If the script file doesn't exist
"""
script_path = OPS_DIR / script_name
if not script_path.exists():
raise FileNotFoundError(f"Missing script: {script_path}")
runpy.run_path(str(script_path), run_name="__main__")
# Icon mapping from old indices to icon names
ICON_MAP = {
144: "PREFERENCES", # Settings/configuration operations
125: "WORLD", # World/environment operations
475: "MODIFIER_DATA", # Modifier operations
415: "CON_OBJECTSOLVER", # Spawn/add operations
630: "FILE_REFRESH", # Replace/refresh operations
785: "OUTLINER_COLLECTION", # Collection/separator operations
453: "MODIFIER_DATA", # Mask operations
66: "CON_OBJECTSOLVER", # Target selection operations
186: "SHAPEKEY_DATA", # Shapekey operations
}
OP_SPECS = [
{
"name": "SpawnSettingsBone",
"id": "spawn_settings_bone",
"desc": "Spawns SettingsBone within active armature",
"script": "SettingsBone.py",
"button": "Spawn Settings Bone",
"icon": "PREFERENCES",
"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",
"desc": "Apply all subdivision modifiers to WGT objects, so blender can draw them properly on the rig.",
"script": "apply_subdiv_wgt.py",
"button": "Apply Subdiv to WGTs",
"icon": "MOD_SUBSURF",
"panel": "general",
},
{
"name": "FreshDevices",
"id": "fresh_devices",
"desc": "Spawns, places, and parents new Device and Finger Scanner to active armature",
"script": "Devices_FreshPlacement.py",
"button": "Spawn/Parent Devices",
"icon": "CON_OBJECTSOLVER",
"panel": "devices",
},
{
"name": "DevicesSettings",
"id": "devices_settings",
"desc": "Applies devices function to SettingsBone",
"script": "DevicesSettings.py",
"button": "DevicesSettings",
"icon": "PREFERENCES",
"panel": "devices",
},
{
"name": "DeviceReplace",
"id": "device_replace",
"desc": "Replaces old device with the new version",
"script": "Device_Replacement.py",
"button": "ReplaceDevice",
"icon": "FILE_REFRESH",
"panel": "devices",
},
{
"name": "GeoSeparator",
"id": "geo_separator",
"desc": "All child geometry of active armature to GEO collection",
"script": "GeoSeparator.py",
"button": "GEO Separator",
"icon": "COLLECTION_COLOR_02",
"panel": "geo",
},
{
"name": "BodyMasker",
"id": "body_masker",
"desc": "Separates key body parts",
"script": "BodyMasker.py",
"button": "Body Masker",
"icon": "MOD_MASK",
"panel": "geo",
},
{
"name": "MaskSettings",
"id": "mask_settings",
"desc": "Creates custom properties for masking the gloves",
"script": "MaskSettings.py",
"button": "Glove Mask Settings",
"icon": "PREFERENCES",
"panel": "geo",
},
{
"name": "CustomVis",
"id": "custom_vis",
"desc": "Creates a visibility property toggle for the active object",
"script": "custom_vis.py",
"button": "Custom Visibility Setting",
"icon": "PREFERENCES",
"panel": "geo",
},
{
"name": "HHSpawn",
"id": "hh_spawn",
"desc": "HardHat Spawn/Parent",
"script": "hh_spawn.py",
"button": "Spawn/Parent HardHat",
"icon": "CON_OBJECTSOLVER",
"panel": "helmet",
},
{
"name": "HHSetTargets",
"id": "hh_set_targets",
"desc": "Set HardHat Hair Targets",
"script": "hh_set_targets.py",
"button": "Set HH Hair Targets",
"icon": "EYEDROPPER",
"panel": "helmet",
},
{
"name": "HHMask",
"id": "hh_mask",
"desc": "HardHat Mask",
"script": "hh_mask.py",
"button": "HardHat Mask",
"icon": "MODIFIER_DATA",
"panel": "helmet",
},
{
"name": "HHShapekey",
"id": "hh_shapekey",
"desc": "HardHat Shapekey",
"script": "hh_shapekey.py",
"button": "HardHat Shapekey",
"icon": "SHAPEKEY_DATA",
"panel": "helmet",
},
{
"name": "HHSettings",
"id": "hh_settings",
"desc": "HardHat Settings",
"script": "hh_settings.py",
"button": "HardHat Settings",
"icon": "PREFERENCES",
"panel": "helmet",
},
{
"name": "ReplaceCelWithBsdf",
"id": "replace_cel_with_bsdf",
"desc": "Replace all CEL materials with their BSDF counterparts",
"script": "replace_cel_with_bsdf.py",
"button": "Replace CEL with BSDF",
"icon": "MATERIAL",
"panel": "scene",
},
{
"name": "RemapVectorFonts",
"id": "remap_vector_fonts",
"desc": "Remap all Vector Fonts in the blendfile to Amazon Ember Heavy",
"script": "remap_vector_fonts.py",
"button": "Remap Vector Fonts",
"icon": "FILE_FONT",
"panel": "scene",
},
]
def _make_operator(spec: dict) -> type[Operator]:
"""Create an operator class from a specification dictionary."""
def _execute(self, context):
try:
run_script(spec["script"])
except Exception as exc: # pragma: no cover - best effort logging
traceback.print_exc()
self.report({"ERROR"}, f"{spec['button']} failed: {exc}")
return {"CANCELLED"}
return {"FINISHED"}
attrs = {
"__module__": __name__,
"bl_idname": f"amzn.{spec['id']}",
"bl_label": f"AMZN_{spec['name']}",
"bl_description": spec["desc"],
"bl_options": {"REGISTER", "UNDO"},
"execute": _execute,
}
cls = type(f"AMZN_OT_{spec['name']}", (Operator,), attrs)
spec["full_idname"] = cls.bl_idname
return cls
OPERATOR_CLASSES = [_make_operator(spec) for spec in OP_SPECS]
@@ -0,0 +1,91 @@
"""Panel definitions for AMZN Character Tools."""
import bpy
from bpy.types import Panel
from .operators import OP_SPECS
PANEL_KEYS = ("scene", "general", "core", "devices", "geo", "helmet")
PANEL_BUTTONS = {key: [spec for spec in OP_SPECS if spec["panel"] == key] for key in PANEL_KEYS}
class _AMZN_BasePanel(Panel):
"""Base panel class for AMZN Character Tools."""
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Rigging"
panel_key = ""
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
for spec in PANEL_BUTTONS.get(self.panel_key, ()):
# Make buttons marked as "large" bigger
if spec.get("large", False):
row = layout.row()
row.scale_y = 2.0
row.operator(
spec["full_idname"],
text=spec["button"],
icon=spec["icon"],
)
else:
layout.operator(
spec["full_idname"],
text=spec["button"],
icon=spec["icon"],
)
class AMZN_PT_Main(_AMZN_BasePanel):
"""Main panel for AMZN Character Tools."""
bl_idname = "AMZN_PT_MAIN"
bl_label = "AMZN Character Tools"
panel_key = "core"
class AMZN_PT_Scene(_AMZN_BasePanel):
"""Scene panel."""
bl_idname = "AMZN_PT_SCENE"
bl_label = "Scene"
bl_parent_id = "AMZN_PT_MAIN"
panel_key = "scene"
class AMZN_PT_General(_AMZN_BasePanel):
"""General panel."""
bl_idname = "AMZN_PT_GENERAL"
bl_label = "General"
bl_parent_id = "AMZN_PT_MAIN"
panel_key = "general"
class AMZN_PT_Devices(_AMZN_BasePanel):
"""Devices panel."""
bl_idname = "AMZN_PT_DEVICES"
bl_label = "Devices"
bl_parent_id = "AMZN_PT_MAIN"
panel_key = "devices"
class AMZN_PT_Geo(_AMZN_BasePanel):
"""GEO panel."""
bl_idname = "AMZN_PT_GEO"
bl_label = "GEO"
bl_parent_id = "AMZN_PT_MAIN"
panel_key = "geo"
class AMZN_PT_Helmet(_AMZN_BasePanel):
"""Helmet panel."""
bl_idname = "AMZN_PT_HELMET"
bl_label = "Helmet"
bl_parent_id = "AMZN_PT_MAIN"
panel_key = "helmet"
PANEL_CLASSES = (AMZN_PT_Main, AMZN_PT_Scene, AMZN_PT_General, AMZN_PT_Devices, AMZN_PT_Geo, AMZN_PT_Helmet)
@@ -1,5 +1,5 @@
{
"last_check": "2025-12-01 11:02:25.858363",
"last_check": "2025-12-22 09:58:29.267931",
"backup_date": "December-1-2025",
"update_ready": false,
"ignore": false,