2026-01-01
This commit is contained in:
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
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user