update amznchartools
This commit is contained in:
2026-03-18 18:03:21 -06:00
parent eea7cc1969
commit 9fcddeca02
26 changed files with 461 additions and 110 deletions
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
id = "amzncharactertools"
name = "AMZNCharacterTools"
tagline = "AMZNCharacterTools"
version = "0.10.2"
version = "0.11.0"
type = "add-on"
maintainer = "Nathan Lindsay"
@@ -5,7 +5,7 @@ def add_body_masks():
body_obj = bpy.data.objects.get('CC_Base_Body')
if not body_obj:
print("Error: CC_Base_Body object not found")
return
return {"success": False, "reason": "NO_BODY"}
print(f"Found body object: {body_obj.name}")
@@ -124,6 +124,8 @@ def add_body_masks():
print("\nBody masking completed!")
print("Main_Mask: Shows head, arms, and chest")
print("Hand_Mask: Shows head, arms, chest, and hands")
return {"success": True}
# Execute the operation
add_body_masks()
if __name__ == "__main__":
# When run as a script, execute for side-effects and keep existing prints.
add_body_masks()
@@ -0,0 +1,10 @@
import bpy
# Simple operator script that removes the 'Devices' custom property from the active pose bone.
# Usage: enter Pose Mode, select the Settings pose bone, then click the button in the Devices panel.
try:
bpy.ops.wm.properties_remove(data_path="active_pose_bone", property_name="Devices")
except Exception as e:
# This will fail if the context isn't correct; keep it minimal and print for debugging.
print(f"Failed to remove 'Devices' property via wm.properties_remove: {e}")
@@ -8,14 +8,17 @@ print(f"Auto-execution enabled: {bpy.context.preferences.filepaths.use_scripts_a
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
# Get the active object and selected objects
active_obj = bpy.context.active_object
selected_objects = bpy.context.selected_objects
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}")
print(f"Found {len(selected_objects)} selected object(s): {[obj.name for obj in selected_objects]}")
# Find the armature object in the scene
armature_obj = None
@@ -92,61 +95,65 @@ print(f"✓ Created '{property_name}' custom property with library override supp
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}")
# Set up drivers for all selected objects
for obj in selected_objects:
print(f" Setting up drivers for {obj.name}...")
# Clear any existing drivers
try:
obj.driver_remove('hide_render')
print(f" Removed existing hide_render driver")
except:
print(" No existing hide_render driver to remove")
try:
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 = 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 {obj.name}")
# Create hide_viewport driver (same logic)
driver_fcurve = 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 {obj.name}")
except Exception as e:
print(f" Error creating {active_obj.name} drivers: {e}")
print(f" Error creating drivers: {e}")
print("✓ Driver setup complete")
@@ -159,28 +166,31 @@ 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}")
for obj in selected_objects:
print(f" {obj.name}: hide_render={obj.hide_render}, hide_viewport={obj.hide_viewport}")
# Test toggle to False (object hidden)
print(f"\nToggling to False ({active_obj.name} hidden)...")
# Test toggle to False (objects hidden)
print(f"\nToggling to False (objects 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}")
for obj in selected_objects:
print(f" {obj.name}: hide_render={obj.hide_render}, hide_viewport={obj.hide_viewport}")
# Test toggle back to True (object visible)
print(f"\nToggling to True ({active_obj.name} visible)...")
# Test toggle back to True (objects visible)
print(f"\nToggling to True (objects 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}")
for obj in selected_objects:
print(f" {obj.name}: hide_render={obj.hide_render}, hide_viewport={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}")
print(f"Use checkbox to toggle visibility of {len(selected_objects)} selected object(s): {[obj.name for obj in selected_objects]}")
print(f" - {property_name} ON (checked): Shows all selected objects")
print(f" - {property_name} OFF (unchecked): Hides all selected objects")
@@ -53,9 +53,16 @@ def link_bsdf_materials():
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
try:
with bpy.data.libraries.load(library_path, link=True) as (data_from, data_to):
# Link all materials
data_to.materials = data_from.materials
print(f" Library loaded successfully, materials in file: {len(data_from.materials)}")
except Exception as e:
print(f"Error loading library: {e}")
import traceback
traceback.print_exc()
return []
# Get list of newly linked materials
materials_after = set(bpy.data.materials.keys())
@@ -368,6 +375,8 @@ def replace_cel_materials():
"wood": "Pallet_Wood",
"Package_Cardboard": "Package_Cardboard",
"Pallet_Wood": "Pallet_Wood",
"Shuttle_Cardboard_1": "Shuttle_Cardboard_1",
"Shuttle_Cardboard_2": "Shuttle_Cardboard_2",
"blue (triton)": "BSDF_blue-2_TRITON",
"gray (snow)": "BSDF_gray-6_SNOW",
"gray (storm)": "BSDF_gray-2_STORM",
@@ -608,6 +617,13 @@ def replace_cel_materials():
print(f"Total user remappings: {replacements_made}")
print(f"Source materials removed: {removed_materials}")
# Print a highly visible summary message
print(f"\n{'='*60}")
print(f" CEL REPLACEMENT COMPLETE")
print(f" {len(material_mapping)} materials replaced")
print(f" {replacements_made} total users remapped")
print(f"{'='*60}\n")
if missing_targets:
print(f"\nMissing target materials for:")
for mapping in missing_targets:
@@ -618,19 +634,19 @@ def replace_cel_materials():
# 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)")
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,173 @@
"""Add Ambassador Vest Color to the active object's vest color node group."""
import bpy
import os
try:
from ..utils import get_addon_preferences
except (ImportError, ValueError):
# Fallback if import fails
def get_addon_preferences():
test_names = [
"bl_ext.vscode_development.AmazonCharacterTools",
"amzncharactertools",
"AmazonCharacterTools",
]
for addon_name in test_names:
addon_prefs = bpy.context.preferences.addons.get(addon_name)
if addon_prefs and hasattr(addon_prefs, 'preferences'):
if hasattr(addon_prefs.preferences, 'amzn_bsdf_materials_path'):
return addon_prefs.preferences
return None
def add_vest_ambassador_color():
"""Add blue (ambassador) vest color option to the active object's vest color node group."""
# Get active object
obj = bpy.context.active_object
if not obj:
print("Error: No active object selected")
return {"success": False, "reason": "NO_ACTIVE_OBJECT"}
print(f"Working on object: {obj.name}")
# Find the amazon-vest-color node group on the object
vest_node_group = None
for mod in obj.modifiers:
if mod.type == 'NODES' and mod.node_group:
if 'amazon-vest-color' in mod.node_group.name.lower():
vest_node_group = mod.node_group
print(f"Found vest color node group: {vest_node_group.name}")
break
if not vest_node_group:
print("Error: No amazon-vest-color node group found on active object")
print("Available geometry node groups:")
for mod in obj.modifiers:
if mod.type == 'NODES' and mod.node_group:
print(f" - {mod.node_group.name}")
return {"success": False, "reason": "NO_NODE_GROUP"}
# Link the Vest-blue material from MAT_Char.blend
library_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Mat\MAT_Char.blend"
if not os.path.exists(library_path):
print(f"Error: Library file not found at {library_path}")
return {"success": False, "reason": "LIBRARY_NOT_FOUND"}
print(f"Linking Vest-blue material from: {library_path}")
# Check if material already exists
materials_before = set(bpy.data.materials.keys())
try:
with bpy.data.libraries.load(library_path, link=True) as (data_from, data_to):
if 'Vest-blue' in data_from.materials:
data_to.materials = ['Vest-blue']
print(" Vest-blue material linked successfully")
else:
print(" Warning: Vest-blue not found in library, trying alternate names...")
# Try to find any vest-related material
for mat_name in data_from.materials:
if 'vest' in mat_name.lower() and 'blue' in mat_name.lower():
data_to.materials = [mat_name]
print(f" Found and linked alternate: {mat_name}")
break
except Exception as e:
print(f"Error loading library: {e}")
import traceback
traceback.print_exc()
return {"success": False, "reason": "LIBRARY_LOAD_FAILED"}
# Get the linked material
materials_after = set(bpy.data.materials.keys())
newly_linked = materials_after - materials_before
if not newly_linked:
# Check if Vest-blue already exists
if 'Vest-blue' in bpy.data.materials:
vest_material = bpy.data.materials['Vest-blue']
print(f"Vest-blue already exists: {vest_material.name}")
else:
print("Error: Could not link Vest-blue material")
return {"success": False, "reason": "MATERIAL_NOT_FOUND"}
else:
vest_material = bpy.data.materials[list(newly_linked)[0]]
print(f"Linked material: {vest_material.name}")
# Find the menu switch node in the node group
# This is typically a node with an enum property or index-based switch
menu_switch_node = None
for node in vest_node_group.nodes:
# Look for common menu/switch node types
if node.type in ('MENU_SWITCH', 'MENU_SWITCH_ITEM', 'SWITCH', 'INDEX_SWITCH'):
menu_switch_node = node
print(f"Found menu switch node: {node.name} (type: {node.type})")
break
# Also check for nodes with "color" or "menu" in the name
if 'color' in node.name.lower() or 'menu' in node.name.lower():
print(f"Potential menu node: {node.name} (type: {node.type})")
if not menu_switch_node:
menu_switch_node = node
if not menu_switch_node:
print("Error: Could not find menu/switch node in vest color node group")
print("Available nodes:")
for node in vest_node_group.nodes:
print(f" - {node.name} (type: {node.type})")
return {"success": False, "reason": "NO_MENU_SWITCH"}
node_type = menu_switch_node.type
print(f"Adding 'blue (ambassador)' option to {menu_switch_node.name} (type: {node_type})...")
added = False
item_name = "blue (ambassador)"
if node_type == "MENU_SWITCH" and hasattr(menu_switch_node, "enum_items"):
existing_names = [item.name for item in menu_switch_node.enum_items]
if item_name in existing_names:
print(" Option 'blue (ambassador)' already exists")
added = True
else:
try:
# Blender API for GeometryNodeMenuSwitch: enum_items.new(name)
menu_switch_node.enum_items.new(item_name)
print(" Added enum item to menu switch")
added = True
except Exception as e:
print(f" Failed to add enum item: {e}")
elif node_type in {"SWITCH", "INDEX_SWITCH"}:
print(f" Switch node type '{node_type}' is not a geometry menu switch; cannot add named menu item")
else:
print(f" Node type '{node_type}' does not support enum menu items")
# For MATERIAL menu switches, assign the linked material to the corresponding input socket default.
if added:
for socket in menu_switch_node.inputs:
if socket.name == item_name and hasattr(socket, "default_value"):
try:
socket.default_value = vest_material
print(f" Assigned material '{vest_material.name}' to input '{socket.name}'")
except Exception as e:
print(f" Warning: could not assign material to '{socket.name}': {e}")
break
if added:
print("\n" + "="*60)
print(" VEST AMBASSADOR COLOR ADDED")
print(f" Material: {vest_material.name}")
print(f" Target: {obj.name}")
print("="*60 + "\n")
else:
print("\n" + "="*60)
print(" WARNING: Could not add menu item")
print(f" Material: {vest_material.name} was linked but not assigned")
print(" The menu API may need manual configuration")
print("="*60 + "\n")
if added:
return {"success": True, "reason": "OK"}
return {"success": False, "reason": "MENU_ITEM_ADD_FAILED"}
if __name__ == "__main__":
add_vest_ambassador_color()
@@ -1,4 +1,5 @@
"""Operator definitions for AMZN Character Tools."""
import importlib
from pathlib import Path
import runpy
import traceback
@@ -8,6 +9,7 @@ from bpy.types import Operator
OPS_DIR = Path(__file__).parent.parent / "ops"
BASE_PACKAGE = (__package__ or "").rsplit(".", 1)[0]
def run_script(script_name: str) -> None:
@@ -25,6 +27,15 @@ def run_script(script_name: str) -> None:
runpy.run_path(str(script_path), run_name="__main__")
def _import_ops_module(module_name: str):
"""Import and reload an ops module from this addon package."""
if not BASE_PACKAGE:
raise RuntimeError("Cannot resolve addon base package for module imports")
full_name = f"{BASE_PACKAGE}.ops.{module_name}"
module = importlib.import_module(full_name)
return importlib.reload(module)
# Icon mapping from old indices to icon names
ICON_MAP = {
144: "PREFERENCES", # Settings/configuration operations
@@ -86,6 +97,15 @@ OP_SPECS = [
"icon": "FILE_REFRESH",
"panel": "devices",
},
{
"name": "RemoveDevicesSettings",
"id": "remove_devices_settings",
"desc": "Removes the 'Devices' custom property from SettingsBone",
"script": "RemoveDevicesSettings.py",
"button": "Remove Devices Settings",
"icon": "CANCEL",
"panel": "devices",
},
{
"name": "GeoSeparator",
"id": "geo_separator",
@@ -122,6 +142,15 @@ OP_SPECS = [
"icon": "PREFERENCES",
"panel": "geo",
},
{
"name": "AddVestAmbassadorColor",
"id": "add_vest_ambassador_color",
"desc": "Adds blue (ambassador) vest color option to active object's vest color node group",
"script": "vest_ambassador_color.py",
"button": "Add Vest Ambassador Color",
"icon": "MATERIAL",
"panel": "vest",
},
{
"name": "HHSpawn",
"id": "hh_spawn",
@@ -192,7 +221,65 @@ def _make_operator(spec: dict) -> type[Operator]:
"""Create an operator class from a specification dictionary."""
def _execute(self, context):
try:
run_script(spec["script"])
# Special handling for operators that need result capture
if spec["script"] == "replace_cel_with_bsdf.py":
script_path = OPS_DIR / spec["script"]
if script_path.exists():
module = _import_ops_module("replace_cel_with_bsdf")
materials_mapped, users_remapped = module.replace_cel_materials()
self.report({"INFO"}, f"Replaced CEL: {materials_mapped} materials, {users_remapped} users remapped")
else:
run_script(spec["script"])
self.report({"INFO"}, f"{spec['button']} complete")
elif spec["script"] == "vest_ambassador_color.py":
script_path = OPS_DIR / spec["script"]
if script_path.exists():
module = _import_ops_module("vest_ambassador_color")
result = module.add_vest_ambassador_color()
success = bool(result.get("success")) if isinstance(result, dict) else bool(result)
reason = result.get("reason") if isinstance(result, dict) else ""
if success:
self.report({"INFO"}, f"{spec['button']} complete")
else:
if reason == "NO_NODE_GROUP":
self.report({"INFO"}, "No vest color node group found on active object")
elif reason == "NO_ACTIVE_OBJECT":
self.report({"INFO"}, "No active object selected")
elif reason == "NO_MENU_SWITCH":
self.report({"INFO"}, "No vest menu switch node found in vest color node group")
else:
self.report({"WARNING"}, "Vest-blue material linked but menu item was not added")
else:
run_script(spec["script"])
self.report({"INFO"}, f"{spec['button']} complete")
elif spec["script"] == "BodyMasker.py":
script_path = OPS_DIR / spec["script"]
if script_path.exists():
module = _import_ops_module("BodyMasker")
result = module.add_body_masks()
if isinstance(result, dict):
if result.get("success"):
self.report({"INFO"}, f"{spec['button']} complete")
else:
reason = result.get("reason")
if reason == "NO_BODY":
self.report({"ERROR"}, "CC_Base_Body not found in scene")
else:
self.report({"WARNING"}, f"{spec['button']} incomplete: {reason}")
else:
# Unexpected return type; fall back to running the script for side-effects
run_script(spec["script"])
self.report({"INFO"}, f"{spec['button']} complete")
elif spec.get("id") == "remove_devices_settings":
# Pre-check: ensure CC_Base_Body exists in the scene. If not, inform the user.
if "CC_Base_Body" not in bpy.data.objects:
self.report({"ERROR"}, "CC_Base_Body not found in scene")
return {"CANCELLED"}
run_script(spec["script"])
self.report({"INFO"}, f"{spec['button']} complete")
else:
run_script(spec["script"])
self.report({"INFO"}, f"{spec['button']} complete")
except Exception as exc: # pragma: no cover - best effort logging
traceback.print_exc()
self.report({"ERROR"}, f"{spec['button']} failed: {exc}")
@@ -5,7 +5,7 @@ from bpy.types import Panel
from .operators import OP_SPECS
PANEL_KEYS = ("scene", "general", "core", "devices", "geo", "helmet")
PANEL_KEYS = ("scene", "general", "core", "devices", "geo", "helmet", "vest")
PANEL_BUTTONS = {key: [spec for spec in OP_SPECS if spec["panel"] == key] for key in PANEL_KEYS}
@@ -87,5 +87,13 @@ class AMZN_PT_Helmet(_AMZN_BasePanel):
panel_key = "helmet"
PANEL_CLASSES = (AMZN_PT_Main, AMZN_PT_Scene, AMZN_PT_General, AMZN_PT_Devices, AMZN_PT_Geo, AMZN_PT_Helmet)
class AMZN_PT_Vest(_AMZN_BasePanel):
"""Vest panel."""
bl_idname = "AMZN_PT_VEST"
bl_label = "Vest"
bl_parent_id = "AMZN_PT_MAIN"
panel_key = "vest"
PANEL_CLASSES = (AMZN_PT_Main, AMZN_PT_Scene, AMZN_PT_General, AMZN_PT_Devices, AMZN_PT_Geo, AMZN_PT_Helmet, AMZN_PT_Vest)
@@ -15,7 +15,7 @@ class AMZN_AddonPreferences(AddonPreferences):
name="BSDF Materials Library",
description="Path to MATERIALS_BSDF_pallette_v1.0.blend",
subtype='FILE_PATH',
default=r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\MATERIALS_BSDF_pallette_v1.0.blend",
default=r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\Mat\MATERIALS_BSDF_pallette_v1.0.blend",
)
amzn_device_path: StringProperty(