9fcddeca02
update amznchartools
304 lines
11 KiB
Python
304 lines
11 KiB
Python
"""Operator definitions for AMZN Character Tools."""
|
|
import importlib
|
|
from pathlib import Path
|
|
import runpy
|
|
import traceback
|
|
|
|
import bpy
|
|
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:
|
|
"""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__")
|
|
|
|
|
|
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
|
|
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": "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": "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",
|
|
"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": "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",
|
|
"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:
|
|
# 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}")
|
|
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]
|
|
|