"""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]