From cfe760e8df7b4ea8c909caad47d7ecb824fc74c9 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Wed, 23 Apr 2025 00:43:38 +0100 Subject: [PATCH] Updated Operations and Properties - Updated Operations and Properties with tpying and logging. I have not updated translation files, this is because i want to gut MMD Tools system and replace it with our own, however I want to make MMD Tools more simple and ajust it to our needs only. This is going to take a while and my aim for this is Alpha 4, also the MMD Translation system hurt my head.... - Fixes a couple of bugs as well, with quick access and the PMX importer. --- blender_manifest.toml | 2 +- core/armature_validation.py | 29 +++++++- core/mmd/core/model.py | 6 +- core/mmd/operators/material.py | 120 ++++++++++++++++++++---------- core/mmd/operators/misc.py | 83 ++++++++++++++------- core/mmd/operators/model.py | 85 +++++++++++++++------ core/mmd/operators/model_edit.py | 103 +++++++++++++++++-------- core/mmd/operators/morph.py | 91 ++++++++++++++-------- core/mmd/operators/rigid_body.py | 62 +++++++++------ core/mmd/operators/sdef.py | 28 ++++--- core/mmd/operators/view.py | 76 ++++++++++--------- core/mmd/properties/material.py | 44 ++++++----- core/mmd/properties/morph.py | 50 +++++++------ core/mmd/properties/pose_bone.py | 30 +++++--- core/mmd/properties/rigid_body.py | 64 +++++++++------- core/mmd/properties/root.py | 66 +++++++++------- core/updater.py | 2 +- resources/translations/en_US.json | 9 ++- resources/translations/ja_JP.json | 9 ++- resources/translations/ko_KR.json | 9 ++- ui/quick_access_panel.py | 68 ++++++++++++++--- 21 files changed, 689 insertions(+), 347 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index b6e9679..77dd551 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" id = "avatar_toolkit" -version = "0.2.1" +version = "0.3.0" name = "Avatar Toolkit" tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." maintainer = "Team NekoNeo" diff --git a/core/armature_validation.py b/core/armature_validation.py index ad1212b..9abf0d7 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -25,12 +25,18 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio non_standard_messages: List[str] = [] scale_messages: List[str] = [] + # Check if this is a PMX model + is_pmx_model = False + if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')): + is_pmx_model = True + logger.debug("Detected PMX model, using specialized validation") + if validation_mode == 'NONE': logger.debug("Validation mode is NONE, skipping validation") if detailed_messages: - return True, [], False, [], [], [] + return True, [t("Validation.mode.none")], False, [], [], [] else: - return True, [], False + return True, [t("Validation.mode.none")], False if not armature or armature.type != 'ARMATURE' or not armature.data.bones: logger.warning("Basic armature check failed") @@ -125,6 +131,21 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio non_standard_messages.append(t("Armature.validation.standardize_note.line2")) non_standard_messages.append(t("Armature.validation.standardize_note.line3")) + # Special handling for PMX models + if is_pmx_model: + logger.info("PMX model detected, applying specialized validation") + # For PMX models, we'll be more lenient with validation + # and provide specific guidance for these models + if not messages: + messages = [t("Armature.validation.pmx_model_detected")] + + # Add PMX-specific messages + if validation_mode == 'STRICT': + messages.append(t("Armature.validation.pmx_model_strict")) + messages.append(t("Armature.validation.pmx_model_standardize")) + else: + messages.append(t("Armature.validation.pmx_model_basic")) + # Combine messages in correct order messages.extend(non_standard_messages) @@ -149,6 +170,10 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio else: return True, messages, True + # Ensure messages has at least one element + if not messages: + messages = [t("Armature.validation.unknown_format")] + logger.info(f"Armature validation complete. Valid: {is_valid}") if detailed_messages: return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages diff --git a/core/mmd/core/model.py b/core/mmd/core/model.py index c60f929..ab22433 100644 --- a/core/mmd/core/model.py +++ b/core/mmd/core/model.py @@ -41,9 +41,11 @@ class FnModel: Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned. Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT". """ - while obj is not None and obj.mmd_type != "ROOT": + while obj is not None: + if hasattr(obj, 'mmd_type') and obj.mmd_type == "ROOT": + return obj obj = obj.parent - return obj + return None @staticmethod def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: diff --git a/core/mmd/operators/material.py b/core/mmd/operators/material.py index 23f2d49..a6ea15a 100644 --- a/core/mmd/operators/material.py +++ b/core/mmd/operators/material.py @@ -6,13 +6,16 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import bpy -from bpy.props import BoolProperty, StringProperty -from bpy.types import Operator +from bpy.props import BoolProperty, StringProperty, FloatProperty +from bpy.types import Operator, Context, Object, Material + +from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast from .. import cycles_converter from ..core.exceptions import MaterialNotFoundError from ..core.material import FnMaterial from ..core.shader import _NodeGroupUtils +from ....core.logging_setup import logger class ConvertMaterialsForCycles(Operator): @@ -21,14 +24,14 @@ class ConvertMaterialsForCycles(Operator): bl_description = "Convert materials of selected objects for Cycles." bl_options = {"REGISTER", "UNDO"} - use_principled: bpy.props.BoolProperty( + use_principled: BoolProperty( name="Convert to Principled BSDF", description="Convert MMD shader nodes to Principled BSDF as well if enabled", default=False, options={"SKIP_SAVE"}, ) - clean_nodes: bpy.props.BoolProperty( + clean_nodes: BoolProperty( name="Clean Nodes", description="Remove redundant nodes as well if enabled. Disable it to keep node data.", default=False, @@ -36,22 +39,27 @@ class ConvertMaterialsForCycles(Operator): ) @classmethod - def poll(cls, context): - return next((x for x in context.selected_objects if x.type == "MESH"), None) + def poll(cls, context: Context) -> bool: + return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None - def draw(self, context): + def draw(self, context: Context) -> None: layout = self.layout layout.prop(self, "use_principled") layout.prop(self, "clean_nodes") - def execute(self, context): + def execute(self, context: Context) -> Set[str]: try: context.scene.render.engine = "CYCLES" - except: + except Exception as e: + logger.error(f"Failed to change to Cycles render engine: {str(e)}") self.report({"ERROR"}, " * Failed to change to Cycles render engine.") return {"CANCELLED"} + + logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}") for obj in (x for x in context.selected_objects if x.type == "MESH"): + logger.debug(f"Converting materials for object: {obj.name}") cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes) + return {"FINISHED"} @@ -61,21 +69,21 @@ class ConvertMaterials(Operator): bl_description = "Convert materials of selected objects." bl_options = {"REGISTER", "UNDO"} - use_principled: bpy.props.BoolProperty( + use_principled: BoolProperty( name="Convert to Principled BSDF", description="Convert MMD shader nodes to Principled BSDF as well if enabled", default=True, options={"SKIP_SAVE"}, ) - clean_nodes: bpy.props.BoolProperty( + clean_nodes: BoolProperty( name="Clean Nodes", description="Remove redundant nodes as well if enabled. Disable it to keep node data.", default=True, options={"SKIP_SAVE"}, ) - subsurface: bpy.props.FloatProperty( + subsurface: FloatProperty( name="Subsurface", default=0.001, soft_min=0.000, @@ -85,13 +93,15 @@ class ConvertMaterials(Operator): ) @classmethod - def poll(cls, context): - return next((x for x in context.selected_objects if x.type == "MESH"), None) + def poll(cls, context: Context) -> bool: + return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None - def execute(self, context): + def execute(self, context: Context) -> Set[str]: + logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}") for obj in context.selected_objects: if obj.type != "MESH": continue + logger.debug(f"Converting materials for object: {obj.name}") cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface) return {"FINISHED"} @@ -102,20 +112,22 @@ class ConvertBSDFMaterials(Operator): bl_options = {'REGISTER', 'UNDO'} @classmethod - def poll(cls, context): - return next((x for x in context.selected_objects if x.type == 'MESH'), None) + def poll(cls, context: Context) -> bool: + return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None - def execute(self, context): + def execute(self, context: Context) -> Set[str]: + logger.info("Converting BSDF materials to MMD shader") for obj in context.selected_objects: if obj.type != 'MESH': continue + logger.debug(f"Converting BSDF materials for object: {obj.name}") cycles_converter.convertToMMDShader(obj) return {'FINISHED'} class _OpenTextureBase: """Create a texture for mmd model material.""" - bl_options = {"REGISTER", "UNDO", "INTERNAL"} + bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"} filepath: StringProperty( name="File Path", @@ -129,7 +141,7 @@ class _OpenTextureBase: options={"HIDDEN"}, ) - def invoke(self, context, event): + def invoke(self, context: Context, event: Any) -> Set[str]: context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} @@ -139,8 +151,13 @@ class OpenTexture(Operator, _OpenTextureBase): bl_label = "Open Texture" bl_description = "Create main texture of active material" - def execute(self, context): + def execute(self, context: Context) -> Set[str]: mat = context.active_object.active_material + if not mat: + logger.error("No active material found") + return {"CANCELLED"} + + logger.info(f"Creating texture for material: {mat.name} from {self.filepath}") fnMat = FnMaterial(mat) fnMat.create_texture(self.filepath) return {"FINISHED"} @@ -154,8 +171,13 @@ class RemoveTexture(Operator): bl_description = "Remove main texture of active material" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: Context) -> Set[str]: mat = context.active_object.active_material + if not mat: + logger.error("No active material found") + return {"CANCELLED"} + + logger.info(f"Removing texture from material: {mat.name}") fnMat = FnMaterial(mat) fnMat.remove_texture() return {"FINISHED"} @@ -168,8 +190,13 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase): bl_label = "Open Sphere Texture" bl_description = "Create sphere texture of active material" - def execute(self, context): + def execute(self, context: Context) -> Set[str]: mat = context.active_object.active_material + if not mat: + logger.error("No active material found") + return {"CANCELLED"} + + logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}") fnMat = FnMaterial(mat) fnMat.create_sphere_texture(self.filepath, context.active_object) return {"FINISHED"} @@ -183,8 +210,13 @@ class RemoveSphereTexture(Operator): bl_description = "Remove sphere texture of active material" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: Context) -> Set[str]: mat = context.active_object.active_material + if not mat: + logger.error("No active material found") + return {"CANCELLED"} + + logger.info(f"Removing sphere texture from material: {mat.name}") fnMat = FnMaterial(mat) fnMat.remove_sphere_texture() return {"FINISHED"} @@ -197,18 +229,21 @@ class MoveMaterialUp(Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: obj = context.active_object valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" - return valid_mesh and obj.active_material_index > 0 + return bool(valid_mesh and obj.active_material_index > 0) - def execute(self, context): + def execute(self, context: Context) -> Set[str]: obj = context.active_object current_idx = obj.active_material_index prev_index = current_idx - 1 + + logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}") try: FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True) except MaterialNotFoundError: + logger.error(f"Materials not found for indices {current_idx} and {prev_index}") self.report({"ERROR"}, "Materials not found") return {"CANCELLED"} obj.active_material_index = prev_index @@ -223,18 +258,21 @@ class MoveMaterialDown(Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: obj = context.active_object valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" - return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1 + return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1) - def execute(self, context): + def execute(self, context: Context) -> Set[str]: obj = context.active_object current_idx = obj.active_material_index next_index = current_idx + 1 + + logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}") try: FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True) except MaterialNotFoundError: + logger.error(f"Materials not found for indices {current_idx} and {next_index}") self.report({"ERROR"}, "Materials not found") return {"CANCELLED"} obj.active_material_index = next_index @@ -257,26 +295,31 @@ class EdgePreviewSetup(Operator): default="CREATE", ) - def execute(self, context): + def execute(self, context: Context) -> Set[str]: from ..core.model import FnModel root = FnModel.find_root_object(context.active_object) if root is None: + logger.error("No MMD model root found") self.report({"ERROR"}, "Select a MMD model") return {"CANCELLED"} if self.action == "CLEAN": + logger.info(f"Cleaning toon edge for model: {root.name}") for obj in FnModel.iterate_mesh_objects(root): self.__clean_toon_edge(obj) else: from ..bpyutils import Props + logger.info(f"Creating toon edge for model: {root.name}") scale = 0.2 * getattr(root, Props.empty_display_size) counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root)) + logger.info(f"Created {counts} toon edge(s)") self.report({"INFO"}, "Created %d toon edge(s)" % counts) return {"FINISHED"} - def __clean_toon_edge(self, obj): + def __clean_toon_edge(self, obj: Object) -> None: + logger.debug(f"Cleaning toon edge for object: {obj.name}") if "mmd_edge_preview" in obj.modifiers: obj.modifiers.remove(obj.modifiers["mmd_edge_preview"]) @@ -285,7 +328,8 @@ class EdgePreviewSetup(Operator): FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge.")) - def __create_toon_edge(self, obj, scale=1.0): + def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int: + logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}") self.__clean_toon_edge(obj) materials = obj.data.materials material_offset = len(materials) @@ -310,10 +354,10 @@ class EdgePreviewSetup(Operator): mod.vertex_group = "mmd_edge_preview" return len(materials) - material_offset - def __create_edge_preview_group(self, obj): + def __create_edge_preview_group(self, obj: Object) -> None: vertices, materials = obj.data.vertices, obj.data.materials weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m} - scale_map = {} + scale_map: Dict[int, float] = {} vg_scale_index = obj.vertex_groups.find("mmd_edge_scale") if vg_scale_index >= 0: scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index} @@ -322,7 +366,7 @@ class EdgePreviewSetup(Operator): weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02 vg_edge_preview.add(index=[i], weight=weight, type="REPLACE") - def __get_edge_material(self, mat_name, edge_color, materials): + def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material: if mat_name in materials: return materials[mat_name] mat = bpy.data.materials.get(mat_name, None) @@ -340,7 +384,7 @@ class EdgePreviewSetup(Operator): self.__make_shader(mat) return mat - def __make_shader(self, m): + def __make_shader(self, m: Material) -> None: m.use_nodes = True nodes, links = m.node_tree.nodes, m.node_tree.links @@ -361,7 +405,7 @@ class EdgePreviewSetup(Operator): node_shader.inputs["Color"].default_value = m.mmd_material.edge_color node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3] - def __get_edge_preview_shader(self): + def __get_edge_preview_shader(self) -> bpy.types.NodeTree: group_name = "MMDEdgePreview" shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") if len(shader.nodes): diff --git a/core/mmd/operators/misc.py b/core/mmd/operators/misc.py index c59815e..83cfeff 100644 --- a/core/mmd/operators/misc.py +++ b/core/mmd/operators/misc.py @@ -6,14 +6,17 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import re +from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type import bpy +from bpy.types import Context, Object, Operator, ShapeKey from .. import utils from ..bpyutils import FnContext, FnObject from ..core.bone import FnBone from ..core.model import FnModel, Model from ..core.morph import FnMorph +from ....core.logging_setup import logger class SelectObject(bpy.types.Operator): @@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator): options={"HIDDEN", "SKIP_SAVE"}, ) - def execute(self, context): + def execute(self, context: Context) -> Set[str]: + logger.debug(f"Selecting object: {self.name}") utils.selectAObject(context.scene.objects[self.name]) return {"FINISHED"} @@ -43,41 +47,43 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp): __PREFIX_REGEXP = re.compile(r"(?P[0-9A-Z]{3}_)(?P.*)") @classmethod - def set_index(cls, obj, index): + def set_index(cls, obj: Object, index: int) -> None: m = cls.__PREFIX_REGEXP.match(obj.name) name = m.group("name") if m else obj.name obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name) @classmethod - def get_name(cls, obj, prefix=None): + def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str: m = cls.__PREFIX_REGEXP.match(obj.name) name = m.group("name") if m else obj.name return name[len(prefix) :] if prefix and name.startswith(prefix) else name @classmethod - def normalize_indices(cls, objects): + def normalize_indices(cls, objects: List[Object]) -> None: for i, x in enumerate(objects): cls.set_index(x, i) @classmethod - def poll(cls, context): - return context.active_object + def poll(cls, context: Context) -> bool: + return context.active_object is not None - def execute(self, context): + def execute(self, context: Context) -> Set[str]: obj = context.active_object objects = self.__get_objects(obj) if obj not in objects: - self.report({"ERROR"}, 'Can not move object "%s"' % obj.name) + logger.error(f'Cannot move object "{obj.name}"') + self.report({"ERROR"}, f'Can not move object "{obj.name}"') return {"CANCELLED"} objects.sort(key=lambda x: x.name) + logger.debug(f"Moving object {obj.name} {self.type}") self.move(objects, objects.index(obj), self.type) self.normalize_indices(objects) return {"FINISHED"} - def __get_objects(self, obj): + def __get_objects(self, obj: Object) -> Any: class __MovableList(list): - def move(self, index_old, index_new): + def move(self, index_old: int, index_new: int) -> None: item = self[index_old] self.remove(item) self.insert(index_new, item) @@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: return any(o.type == "MESH" for o in context.selected_objects) @staticmethod - def __can_remove(key_block): + def __can_remove(key_block: ShapeKey) -> bool: if key_block.relative_key == key_block: return False # Basis for v0, v1 in zip(key_block.relative_key.data, key_block.data): @@ -114,20 +120,24 @@ class CleanShapeKeys(bpy.types.Operator): return False return True - def __shape_key_clean(self, obj, key_blocks): + def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None: for kb in key_blocks: if self.__can_remove(kb): + logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}") FnObject.mesh_remove_shape_key(obj, kb) if len(key_blocks) == 1: + logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}") FnObject.mesh_remove_shape_key(obj, key_blocks[0]) - def execute(self, context): - obj: bpy.types.Object + def execute(self, context: Context) -> Set[str]: + logger.info("Cleaning shape keys for selected objects") + obj: Object for obj in context.selected_objects: if obj.type != "MESH" or obj.data.shape_keys is None: continue if not obj.data.shape_keys.use_relative: continue # not be considered yet + logger.debug(f"Processing shape keys for {obj.name}") self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks) return {"FINISHED"} @@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator): ) @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: obj = context.active_object return obj and obj.type == "MESH" - def __separate_by_materials(self, obj): + def __separate_by_materials(self, obj: Object) -> None: + logger.info(f"Separating {obj.name} by materials") utils.separateByMaterials(obj) if self.clean_shape_keys: + logger.debug("Cleaning shape keys after separation") bpy.ops.mmd_tools.clean_shape_keys() - def execute(self, context): + def execute(self, context: Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) if root is None: + logger.debug("No root object found, separating single object") self.__separate_by_materials(obj) else: + logger.debug(f"Root object found: {root.name}, preparing for separation") bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_uv_morph_view() @@ -171,9 +185,11 @@ class SeparateByMaterials(bpy.types.Operator): if len(mesh.data.materials) > 0: mat = mesh.data.materials[0] idx = mat_names.index(getattr(mat, "name", None)) + logger.debug(f"Setting index {idx} for mesh {mesh.name}") MoveObject.set_index(mesh, idx) for morph in root.mmd_root.material_morphs: + logger.debug(f"Updating material morph: {morph.name}") FnMorph(morph, rig).update_mat_related_mesh() utils.clearUnusedMeshes() return {"FINISHED"} @@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator): default=True, ) - def execute(self, context): + def execute(self, context: Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) if root is None: + logger.error("No MMD model found") self.report({"ERROR"}, "Select a MMD model") return {"CANCELLED"} + logger.info(f"Joining meshes for model: {root.name}") bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_uv_morph_view() @@ -205,9 +223,11 @@ class JoinMeshes(bpy.types.Operator): rig = Model(root) meshes_list = sorted(rig.meshes(), key=lambda x: x.name) if not meshes_list: + logger.error("No meshes found in the model") self.report({"ERROR"}, "The model does not have any meshes") return {"CANCELLED"} active_mesh = meshes_list[0] + logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active") FnContext.select_objects(context, *meshes_list) FnContext.set_active_object(context, active_mesh) @@ -216,15 +236,19 @@ class JoinMeshes(bpy.types.Operator): for m in meshes_list[1:]: for mat in m.data.materials: if mat not in active_mesh.data.materials[:]: + logger.debug(f"Adding material {mat.name} to active mesh") active_mesh.data.materials.append(mat) # Join selected meshes + logger.debug("Joining meshes") bpy.ops.object.join() if self.sort_shape_keys: + logger.debug("Sorting shape keys") FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys()) active_mesh.active_shape_key_index = 0 for morph in root.mmd_root.material_morphs: + logger.debug(f"Updating material morph: {morph.name}") FnMorph(morph, rig).update_mat_related_mesh(active_mesh) utils.clearUnusedMeshes() return {"FINISHED"} @@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator): add_armature_modifier: bpy.props.BoolProperty(default=True) - def execute(self, context: bpy.types.Context): + def execute(self, context: Context) -> Set[str]: root = FnModel.find_root_object(context.active_object) if root is None: + logger.error("No MMD model found") self.report({"ERROR"}, "Select a MMD model") return {"CANCELLED"} armObj = FnModel.find_armature_object(root) if armObj is None: + logger.error("Model armature not found") self.report({"ERROR"}, "Model Armature not found") return {"CANCELLED"} + logger.info(f"Attaching meshes to model: {root.name}") FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) return {"FINISHED"} @@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator): ) @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: return FnModel.find_root_object(context.active_object) is not None - def invoke(self, context, event): + def invoke(self, context: Context, event: Any) -> Set[str]: root_object = FnModel.find_root_object(context.active_object) self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor vm = context.window_manager return vm.invoke_props_dialog(self) - def execute(self, context): + def execute(self, context: Context) -> Set[str]: root_object = FnModel.find_root_object(context.active_object) + logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}") FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) return {"FINISHED"} @@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: obj = context.active_object return obj and obj.type == "ARMATURE" - def invoke(self, context, event): + def invoke(self, context: Context, event: Any) -> Set[str]: vm = context.window_manager return vm.invoke_props_dialog(self) - def draw(self, context): + def draw(self, context: Context) -> None: layout = self.layout c = layout.column() c.label(text="This operation will break existing f-curve/action.", icon="QUESTION") c.label(text="Click [OK] to run the operation.") - def execute(self, context): + def execute(self, context: Context) -> Set[str]: arm = context.active_object + logger.info(f"Recalculating bone roll for armature: {arm.name}") FnBone.apply_auto_bone_roll(arm) return {"FINISHED"} diff --git a/core/mmd/operators/model.py b/core/mmd/operators/model.py index 16fe3ba..c4edf30 100644 --- a/core/mmd/operators/model.py +++ b/core/mmd/operators/model.py @@ -6,10 +6,12 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import bpy +from typing import Optional, Set, Dict, Any, List, Tuple, Union from ..bpyutils import FnContext from ..core.bone import FnBone, MigrationFnBone from ..core.model import FnModel, Model +from ....core.logging_setup import logger class MorphSliderSetup(bpy.types.Operator): @@ -29,18 +31,22 @@ class MorphSliderSetup(bpy.types.Operator): default="CREATE", ) - def execute(self, context: bpy.types.Context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = FnModel.find_root_object(active_object) assert root_object is not None + logger.debug(f"Executing MorphSliderSetup with type: {self.type}") with FnContext.temp_override_active_layer_collection(context, root_object): rig = Model(root_object) if self.type == "BIND": + logger.info(f"Binding morph sliders for {root_object.name}") rig.morph_slider.bind() elif self.type == "UNBIND": + logger.info(f"Unbinding morph sliders for {root_object.name}") rig.morph_slider.unbind() else: + logger.info(f"Creating morph sliders for {root_object.name}") rig.morph_slider.create() FnContext.set_active_object(context, active_object) @@ -53,10 +59,11 @@ class CleanRiggingObjects(bpy.types.Operator): bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: root_object = FnModel.find_root_object(context.active_object) assert root_object is not None + logger.info(f"Cleaning rig for {root_object.name}") rig = Model(root_object) rig.clean() FnContext.set_active_object(context, root_object) @@ -86,9 +93,10 @@ class BuildRig(bpy.types.Operator): default=1e-06, ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: root_object = FnModel.find_root_object(context.active_object) + logger.info(f"Building rig for {root_object.name} with non_collision_distance_scale={self.non_collision_distance_scale}, collision_margin={self.collision_margin}") with FnContext.temp_override_active_layer_collection(context, root_object): rig = Model(root_object) rig.build(self.non_collision_distance_scale, self.collision_margin) @@ -103,11 +111,14 @@ class CleanAdditionalTransformConstraints(bpy.types.Operator): bl_description = "Delete shadow bones of selected object and revert bones to default MMD state" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = FnModel.find_root_object(active_object) assert root_object is not None - FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object)) + + logger.info(f"Cleaning additional transform constraints for {root_object.name}") + armature_object = FnModel.find_armature_object(root_object) + FnBone.clean_additional_transformation(armature_object) FnContext.set_active_object(context, active_object) return {"FINISHED"} @@ -118,11 +129,12 @@ class ApplyAdditionalTransformConstraints(bpy.types.Operator): bl_description = "Translate appended bones of selected object for Blender" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = FnModel.find_root_object(active_object) assert root_object is not None + logger.info(f"Applying additional transform constraints for {root_object.name}") armature_object = FnModel.find_armature_object(root_object) assert armature_object is not None @@ -149,12 +161,14 @@ class SetupBoneFixedAxes(bpy.types.Operator): default="LOAD", ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: armature_object = context.active_object if not armature_object or armature_object.type != "ARMATURE": self.report({"ERROR"}, "Active object is not an armature object") + logger.error("Setup Bone Fixed Axis failed: Active object is not an armature object") return {"CANCELLED"} + logger.info(f"Setting up bone fixed axes with type: {self.type}") if self.type == "APPLY": FnBone.apply_bone_fixed_axis(armature_object) FnBone.apply_additional_transformation(armature_object) @@ -180,12 +194,14 @@ class SetupBoneLocalAxes(bpy.types.Operator): default="LOAD", ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: armature_object = context.active_object if not armature_object or armature_object.type != "ARMATURE": self.report({"ERROR"}, "Active object is not an armature object") + logger.error("Setup Bone Local Axes failed: Active object is not an armature object") return {"CANCELLED"} + logger.info(f"Setting up bone local axes with type: {self.type}") if self.type == "APPLY": FnBone.apply_bone_local_axes(armature_object) FnBone.apply_additional_transformation(armature_object) @@ -207,16 +223,18 @@ class AddMissingVertexGroupsFromBones(bpy.types.Operator): ) @classmethod - def poll(cls, context: bpy.types.Context): + def poll(cls, context: bpy.types.Context) -> bool: return FnModel.find_root_object(context.active_object) is not None - def execute(self, context: bpy.types.Context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object: bpy.types.Object = context.active_object root_object = FnModel.find_root_object(active_object) assert root_object is not None + logger.info(f"Adding missing vertex groups from bones for {root_object.name}, search_in_all_meshes={self.search_in_all_meshes}") bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object) if bone_order_mesh_object is None: + logger.error("Failed to find bone order mesh object") return {"CANCELLED"} FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes) @@ -246,12 +264,13 @@ class CreateMMDModelRoot(bpy.types.Operator): default=0.08, ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: + logger.info(f"Creating MMD model root object with name_j={self.name_j}, name_e={self.name_e}, scale={self.scale}") rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True) rig.initialDisplayFrames() return {"FINISHED"} - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: vm = context.window_manager return vm.invoke_props_dialog(self) @@ -305,15 +324,16 @@ class ConvertToMMDModel(bpy.types.Operator): ) @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: obj = context.active_object return obj and obj.type == "ARMATURE" and obj.mode != "EDIT" - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: vm = context.window_manager return vm.invoke_props_dialog(self) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: + logger.info(f"Converting to MMD model with scale={self.scale}, convert_material_nodes={self.convert_material_nodes}") # TODO convert some basic MMD properties armature_object = context.active_object scale = self.scale @@ -321,29 +341,31 @@ class ConvertToMMDModel(bpy.types.Operator): root_object = FnModel.find_root_object(armature_object) if root_object is None or root_object != armature_object.parent: + logger.debug("Creating new MMD model") Model.create(model_name, model_name, scale, armature_object=armature_object) self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context)) self.__configure_rig(context, Model(armature_object.parent)) return {"FINISHED"} - def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects): - def __is_child_of_armature(mesh): + def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects) -> None: + def __is_child_of_armature(mesh: bpy.types.Object) -> bool: if mesh.parent is None: return False return mesh.parent == armature_object or __is_child_of_armature(mesh.parent) - def __is_using_armature(mesh): + def __is_using_armature(mesh: bpy.types.Object) -> bool: for m in mesh.modifiers: if m.type == "ARMATURE" and m.object == armature_object: return True return False - def __get_root(mesh): + def __get_root(mesh: bpy.types.Object) -> bpy.types.Object: if mesh.parent is None: return mesh return __get_root(mesh.parent) + attached_count = 0 for x in objects: if __is_using_armature(x) and not __is_child_of_armature(x): x_root = __get_root(x) @@ -351,27 +373,35 @@ class ConvertToMMDModel(bpy.types.Operator): x_root.parent_type = "OBJECT" x_root.parent = armature_object x_root.matrix_world = m + attached_count += 1 + + logger.debug(f"Attached {attached_count} meshes to armature") - def __configure_rig(self, context: bpy.types.Context, mmd_model: Model): + def __configure_rig(self, context: bpy.types.Context, mmd_model: Model) -> None: root_object = mmd_model.rootObject() armature_object = mmd_model.armature() mesh_objects = tuple(mmd_model.meshes()) + logger.info(f"Configuring rig for {root_object.name} with {len(mesh_objects)} meshes") mmd_model.loadMorphs() if self.middle_joint_bones_lock: vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups} + locked_bones = 0 for pose_bone in armature_object.pose.bones: if not pose_bone.parent: continue if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups: continue pose_bone.lock_location = (True, True, True) + locked_bones += 1 + logger.debug(f"Locked {locked_bones} middle joint bones") from ..core.material import FnMaterial FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes) try: + converted_materials = 0 for m in (x for mesh in mesh_objects for x in mesh.data.materials if x): FnMaterial.convert_to_mmd_material(m, context) mmd_material = m.mmd_material @@ -384,6 +414,8 @@ class ConvertToMMDModel(bpy.types.Operator): line_color = list(m.line_color) mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)] + converted_materials += 1 + logger.debug(f"Converted {converted_materials} materials") finally: FnMaterial.set_nodes_are_readonly(False) from .display_item import DisplayItemQuickSetup @@ -400,16 +432,17 @@ class ResetObjectVisibility(bpy.types.Operator): bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod - def poll(cls, context: bpy.types.Context): + def poll(cls, context: bpy.types.Context) -> bool: active_object: bpy.types.Object = context.active_object return FnModel.find_root_object(active_object) is not None - def execute(self, context: bpy.types.Context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object: bpy.types.Object = context.active_object mmd_root_object = FnModel.find_root_object(active_object) assert mmd_root_object is not None mmd_root = mmd_root_object.mmd_root + logger.info(f"Resetting object visibility for {mmd_root_object.name}") mmd_root_object.hide_set(False) rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object) @@ -440,11 +473,12 @@ class AssembleAll(bpy.types.Operator): bl_label = "Assemble All" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = FnModel.find_root_object(active_object) assert root_object is not None + logger.info(f"Assembling all components for {root_object.name}") with FnContext.temp_override_active_layer_collection(context, root_object) as context: rig = Model(root_object) MigrationFnBone.fix_mmd_ik_limit_override(rig.armature()) @@ -452,6 +486,7 @@ class AssembleAll(bpy.types.Operator): rig.build() rig.morph_slider.bind() + logger.debug("Binding SDEF weights") with context.temp_override(selected_objects=[active_object]): bpy.ops.mmd_tools.sdef_bind() root_object.mmd_root.use_property_driver = True @@ -466,13 +501,15 @@ class DisassembleAll(bpy.types.Operator): bl_label = "Disassemble All" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = FnModel.find_root_object(active_object) assert root_object is not None + logger.info(f"Disassembling all components for {root_object.name}") with FnContext.temp_override_active_layer_collection(context, root_object) as context: root_object.mmd_root.use_property_driver = False + logger.debug("Unbinding SDEF weights") with context.temp_override(selected_objects=[active_object]): bpy.ops.mmd_tools.sdef_unbind() diff --git a/core/mmd/operators/model_edit.py b/core/mmd/operators/model_edit.py index ca21046..632ae5e 100644 --- a/core/mmd/operators/model_edit.py +++ b/core/mmd/operators/model_edit.py @@ -7,13 +7,17 @@ import itertools from operator import itemgetter -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple, Any import bmesh import bpy +import numpy as np +import numpy.typing as npt +from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature from ..bpyutils import FnContext from ..core.model import FnModel, Model +from ....core.logging_setup import logger class MessageException(Exception): @@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator): ) @classmethod - def poll(cls, context: bpy.types.Context): - active_object: Optional[bpy.types.Object] = context.active_object + def poll(cls, context: Context) -> bool: + active_object: Optional[Object] = context.active_object if context.mode != "POSE": return False @@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator): return len(context.selected_pose_bones) > 0 - def invoke(self, context, event): + def invoke(self, context: Context, event: Any) -> Set[str]: return context.window_manager.invoke_props_dialog(self) - def execute(self, context: bpy.types.Context): + def execute(self, context: Context) -> Set[str]: try: + logger.info("Starting model join by bones operation") self.join(context) + logger.info("Model join by bones completed successfully") except MessageException as ex: + logger.error(f"Model join by bones failed: {str(ex)}") self.report(type={"ERROR"}, message=str(ex)) return {"CANCELLED"} return {"FINISHED"} - def join(self, context: bpy.types.Context): + def join(self, context: Context) -> None: bpy.ops.object.mode_set(mode="OBJECT") parent_root_object = FnModel.find_root_object(context.active_object) @@ -74,6 +81,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator): if parent_root_object is None or len(child_root_objects) == 0: raise MessageException("No MMD Models selected") + logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}") with FnContext.temp_override_active_layer_collection(context, parent_root_object): FnModel.join_models(parent_root_object, child_root_objects) @@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator): # Connect child bones if self.join_type == "CONNECTED": - parent_edit_bone: bpy.types.EditBone = context.active_bone - child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones) + parent_edit_bone: EditBone = context.active_bone + child_edit_bones: Set[EditBone] = set(context.selected_bones) child_edit_bones.remove(parent_edit_bone) - child_edit_bone: bpy.types.EditBone + logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}") + child_edit_bone: EditBone for child_edit_bone in child_edit_bones: child_edit_bone.use_connect = True @@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): ) @classmethod - def poll(cls, context: bpy.types.Context): - active_object: Optional[bpy.types.Object] = context.active_object + def poll(cls, context: Context) -> bool: + active_object: Optional[Object] = context.active_object if context.mode != "POSE": return False @@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): return len(context.selected_pose_bones) > 0 - def invoke(self, context, event): + def invoke(self, context: Context, event: Any) -> Set[str]: return context.window_manager.invoke_props_dialog(self) - def execute(self, context: bpy.types.Context): + def execute(self, context: Context) -> Set[str]: try: + logger.info("Starting model separate by bones operation") self.separate(context) + logger.info("Model separate by bones completed successfully") except MessageException as ex: + logger.error(f"Model separate by bones failed: {str(ex)}") self.report(type={"ERROR"}, message=str(ex)) return {"CANCELLED"} return {"FINISHED"} - def separate(self, context: bpy.types.Context): + def separate(self, context: Context) -> None: weight_threshold: float = self.weight_threshold mmd_scale = 0.08 - target_armature_object: bpy.types.Object = context.active_object + target_armature_object: Object = context.active_object + logger.debug(f"Target armature: {target_armature_object.name}") bpy.ops.object.mode_set(mode="EDIT") - root_bones: Set[bpy.types.EditBone] = set(context.selected_bones) + root_bones: Set[EditBone] = set(context.selected_bones) + logger.debug(f"Selected root bones: {len(root_bones)}") if self.include_descendant_bones: + logger.debug("Including descendant bones") for edit_bone in root_bones: with context.temp_override(active_bone=edit_bone): bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1) - separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones} - deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform} + separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones} + deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform} + logger.debug(f"Total bones to separate: {len(separate_bones)}") - mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object) + mmd_root_object: Object = FnModel.find_root_object(context.active_object) mmd_model = Model(mmd_root_object) - mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes()) + mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes()) + logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model") - mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys()) + mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold) + mmd_model_mesh_objects = list(mesh_selection_result.keys()) + logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices") # separate armature bones - separate_armature_object: Optional[bpy.types.Object] + separate_armature_object: Optional[Object] if self.separate_armature: + logger.debug("Separating armature") target_armature_object.select_set(True) bpy.ops.armature.separate() separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None) + if separate_armature_object: + logger.debug(f"Created separate armature: {separate_armature_object.name}") bpy.ops.object.mode_set(mode="OBJECT") # collect separate rigid bodies - separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones} + separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones} + logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate") boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all # collect separate joints - separate_joints: Set[bpy.types.Object] = { + separate_joints: Set[Object] = { joint_object for joint_object in mmd_model.joints() if boundary_joint_owner_condition( @@ -187,35 +210,43 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): ] ) } + logger.debug(f"Found {len(separate_joints)} joints to separate") - separate_mesh_objects: Set[bpy.types.Object] - model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] + separate_mesh_objects: Set[Object] + model2separate_mesh_objects: Dict[Object, Object] if len(mmd_model_mesh_objects) == 0: + logger.debug("No mesh objects to separate") separate_mesh_objects = set() model2separate_mesh_objects = dict() else: # select meshes - obj: bpy.types.Object + logger.debug("Selecting meshes for separation") + obj: Object for obj in context.view_layer.objects: obj.select_set(obj in mmd_model_mesh_objects) context.view_layer.objects.active = mmd_model_mesh_objects[0] # separate mesh by selected vertices + logger.debug("Separating meshes by selected vertices") bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.separate(type="SELECTED") - separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects] + separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects] bpy.ops.object.mode_set(mode="OBJECT") + logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects") model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects)) + logger.debug(f"Creating new model with scale {mmd_scale}") separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False) separate_model.initialDisplayFrames() separate_root_object = separate_model.rootObject() separate_root_object.matrix_world = mmd_root_object.matrix_world separate_model_armature_object = separate_model.armature() + logger.debug(f"Created separate model with root: {separate_root_object.name}") if self.separate_armature: + logger.debug("Joining separate armature to new model") with context.temp_override( active_object=separate_model_armature_object, selected_editable_objects=[separate_model_armature_object, separate_armature_object], @@ -223,6 +254,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): bpy.ops.object.join() # add mesh + logger.debug("Parenting separate mesh objects to new model") with context.temp_override( object=separate_model_armature_object, selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects], @@ -230,19 +262,23 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) # replace mesh armature modifier.object + logger.debug("Updating armature modifiers on separate meshes") for separate_mesh in separate_mesh_objects: armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None) if armature_modifier is None: + logger.debug(f"Creating new armature modifier for {separate_mesh.name}") armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") armature_modifier.object = separate_model_armature_object + logger.debug("Parenting rigid bodies to new model") with context.temp_override( object=separate_model.rigidGroupObject(), selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies], ): bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + logger.debug("Parenting joints to new model") with context.temp_override( object=separate_model.jointGroupObject(), selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints], @@ -257,10 +293,12 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): assert separate_layer_collection is not None if mmd_layer_collection.name != separate_layer_collection.name: + logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}") for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints): separate_layer_collection.collection.objects.link(separate_object) mmd_layer_collection.collection.objects.unlink(separate_object) + logger.debug("Copying MMD root properties") FnModel.copy_mmd_root( separate_root_object, mmd_root_object, @@ -271,13 +309,15 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): }, ) - def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]: - mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict() + def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]: + """Select vertices weighted to the bones to be separated""" + logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}") + mesh2selected_vertex_count: Dict[Object, int] = dict() target_bmesh: bmesh.types.BMesh = bmesh.new() for mesh_object in mmd_model_mesh_objects: vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups - mesh: bpy.types.Mesh = mesh_object.data + mesh: Mesh = mesh_object.data target_bmesh.from_mesh(mesh, face_normals=False) target_bmesh.select_mode |= {"VERT"} deform_layer = target_bmesh.verts.layers.deform.verify() @@ -304,6 +344,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator): vert.select_set(True) if selected_vertex_count > 0: + logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}") mesh2selected_vertex_count[mesh_object] = selected_vertex_count target_bmesh.select_flush_mode() target_bmesh.to_mesh(mesh) diff --git a/core/mmd/operators/morph.py b/core/mmd/operators/morph.py index 1b34420..1201659 100644 --- a/core/mmd/operators/morph.py +++ b/core/mmd/operators/morph.py @@ -5,7 +5,7 @@ # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. -from typing import Optional, cast +from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union import bpy from mathutils import Quaternion, Vector @@ -16,10 +16,11 @@ from ..core.exceptions import MaterialNotFoundError from ..core.material import FnMaterial from ..core.morph import FnMorph from ..utils import ItemMoveOp, ItemOp +from ....logging_setup import logger # Util functions -def divide_vector_components(vec1, vec2): +def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]: if len(vec1) != len(vec2): raise ValueError("Vectors should have the same number of components") result = [] @@ -33,7 +34,7 @@ def divide_vector_components(vec1, vec2): return result -def multiply_vector_components(vec1, vec2): +def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]: if len(vec1) != len(vec2): raise ValueError("Vectors should have the same number of components") result = [] @@ -42,7 +43,7 @@ def multiply_vector_components(vec1, vec2): return result -def special_division(n1, n2): +def special_division(n1: float, n2: float) -> float: """This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised""" if n2 == 0: if n1 == 0: @@ -58,7 +59,7 @@ class AddMorph(bpy.types.Operator): bl_description = "Add a morph item to active morph list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -68,6 +69,7 @@ class AddMorph(bpy.types.Operator): morph.name = "New Morph" if morph_type.startswith("uv"): morph.data_type = "VERTEX_GROUP" + logger.debug(f"Added new morph of type {morph_type}") return {"FINISHED"} @@ -84,7 +86,7 @@ class RemoveMorph(bpy.types.Operator): options={"SKIP_SAVE"}, ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -99,9 +101,11 @@ class RemoveMorph(bpy.types.Operator): if self.all: morphs.clear() mmd_root.active_morph = 0 + logger.debug(f"Removed all morphs of type {morph_type}") else: morphs.remove(mmd_root.active_morph) mmd_root.active_morph = max(0, mmd_root.active_morph - 1) + logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}") return {"FINISHED"} @@ -111,7 +115,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp): bl_description = "Move active morph item up/down in the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -120,6 +124,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp): mmd_root.active_morph, self.type, ) + logger.debug(f"Moved morph to index {mmd_root.active_morph}") return {"FINISHED"} @@ -129,7 +134,7 @@ class CopyMorph(bpy.types.Operator): bl_description = "Make a copy of active morph in the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -156,6 +161,7 @@ class CopyMorph(bpy.types.Operator): for k, v in morph.items(): morph_new[k] = v if k != "name" else name_tmp morph_new.name = name_orig + "_copy" # trigger name check + logger.debug(f"Copied morph {name_orig} to {morph_new.name}") return {"FINISHED"} @@ -165,17 +171,17 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator): bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: root = FnModel.find_root_object(context.active_object) if root is None: return False return root.mmd_root.active_morph_type == "bone_morphs" - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: root = FnModel.find_root_object(context.active_object) FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root)) - + logger.info("Overwrote bone morphs from active action pose") return {"FINISHED"} @@ -185,7 +191,7 @@ class AddMorphOffset(bpy.types.Operator): bl_description = "Add a morph offset item to the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -210,6 +216,7 @@ class AddMorphOffset(bpy.types.Operator): item.location = pose_bone.location item.rotation = pose_bone.rotation_quaternion + logger.debug(f"Added morph offset to {morph_type}") return {"FINISHED"} @@ -226,7 +233,7 @@ class RemoveMorphOffset(bpy.types.Operator): options={"SKIP_SAVE"}, ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -243,17 +250,21 @@ class RemoveMorphOffset(bpy.types.Operator): if morph_type.startswith("vertex"): for obj in FnModel.iterate_mesh_objects(root): FnMorph.remove_shape_key(obj, morph.name) + logger.debug(f"Removed all vertex morph offsets for {morph.name}") return {"FINISHED"} elif morph_type.startswith("uv"): if morph.data_type == "VERTEX_GROUP": for obj in FnModel.iterate_mesh_objects(root): FnMorph.store_uv_morph_data(obj, morph) + logger.debug(f"Removed all UV morph offsets for {morph.name}") return {"FINISHED"} morph.data.clear() morph.active_data = 0 + logger.debug(f"Cleared all morph offsets for {morph.name}") else: morph.data.remove(morph.active_data) morph.active_data = max(0, morph.active_data - 1) + logger.debug(f"Removed morph offset at index {morph.active_data}") return {"FINISHED"} @@ -269,7 +280,7 @@ class InitMaterialOffset(bpy.types.Operator): default=0, ) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -281,6 +292,7 @@ class InitMaterialOffset(bpy.types.Operator): mat_data.specular_color = mat_data.ambient_color = (val,) * 3 mat_data.shininess = mat_data.edge_weight = val mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 + logger.debug(f"Initialized material offset with value {val}") return {"FINISHED"} @@ -290,7 +302,7 @@ class ApplyMaterialOffset(bpy.types.Operator): bl_description = "Calculates the offsets and apply them, then the temporary material is removed" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -328,6 +340,7 @@ class ApplyMaterialOffset(bpy.types.Operator): except ZeroDivisionError: mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD + logger.warning("Zero division detected, switching to ADD offset type") except ValueError: self.report({"ERROR"}, "An unexpected error happened") # We should stop on our tracks and re-raise the exception @@ -345,6 +358,7 @@ class ApplyMaterialOffset(bpy.types.Operator): mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat) + logger.info(f"Applied material offset for {mat_data.material}") return {"FINISHED"} @@ -354,7 +368,7 @@ class CreateWorkMaterial(bpy.types.Operator): bl_description = "Creates a temporary material to edit this offset" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -413,6 +427,7 @@ class CreateWorkMaterial(bpy.types.Operator): work_mmd_mat.edge_color = list(edge_offset) work_mmd_mat.edge_weight += mat_data.edge_weight + logger.info(f"Created work material {work_mat_name}") return {"FINISHED"} @@ -422,13 +437,13 @@ class ClearTempMaterials(bpy.types.Operator): bl_description = "Clears all the temporary materials" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None for meshObj in FnModel.iterate_mesh_objects(root): - def __pre_remove(m): + def __pre_remove(m: Optional[bpy.types.Material]) -> bool: if m and "_temp" in m.name: base_mat_name = m.name.split("_temp")[0] try: @@ -439,6 +454,7 @@ class ClearTempMaterials(bpy.types.Operator): return False FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) + logger.info("Cleared all temporary materials") return {"FINISHED"} @@ -448,7 +464,7 @@ class ViewBoneMorph(bpy.types.Operator): bl_description = "View the result of active bone morph" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -463,6 +479,7 @@ class ViewBoneMorph(bpy.types.Operator): mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() mtx.translation = p_bone.location + morph_data.location p_bone.matrix_basis = mtx + logger.info(f"Viewing bone morph: {morph.name}") return {"FINISHED"} @@ -472,13 +489,14 @@ class ClearBoneMorphView(bpy.types.Operator): bl_description = "Reset transforms of all bones to their default values" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None armature = FnModel.find_armature_object(root) for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() + logger.info("Cleared bone morph view") return {"FINISHED"} @@ -488,7 +506,7 @@ class ApplyBoneMorph(bpy.types.Operator): bl_description = "Apply current pose to active bone morph" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -506,6 +524,7 @@ class ApplyBoneMorph(bpy.types.Operator): p_bone.bone.select = True else: p_bone.bone.select = False + logger.info(f"Applied current pose to bone morph: {morph.name}") return {"FINISHED"} @@ -515,7 +534,7 @@ class SelectRelatedBone(bpy.types.Operator): bl_description = "Select the bone assigned to this offset in the armature" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -524,6 +543,7 @@ class SelectRelatedBone(bpy.types.Operator): morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] utils.selectSingleBone(context, armature, morph_data.bone) + logger.debug(f"Selected bone: {morph_data.bone}") return {"FINISHED"} @@ -533,7 +553,7 @@ class EditBoneOffset(bpy.types.Operator): bl_description = "Applies the location and rotation of this offset to the bone" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -546,6 +566,7 @@ class EditBoneOffset(bpy.types.Operator): mtx.translation = morph_data.location p_bone.matrix_basis = mtx utils.selectSingleBone(context, armature, p_bone.name) + logger.debug(f"Edited bone offset for {p_bone.name}") return {"FINISHED"} @@ -555,7 +576,7 @@ class ApplyBoneOffset(bpy.types.Operator): bl_description = "Stores the current bone location and rotation into this offset" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -567,6 +588,7 @@ class ApplyBoneOffset(bpy.types.Operator): p_bone = armature.pose.bones[morph_data.bone] morph_data.location = p_bone.location morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() + logger.debug(f"Applied bone offset for {p_bone.name}") return {"FINISHED"} @@ -576,7 +598,7 @@ class ViewUVMorph(bpy.types.Operator): bl_description = "View the result of active UV morph on current mesh object" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -627,6 +649,7 @@ class ViewUVMorph(bpy.types.Operator): uv_tex.active_render = True meshObj.hide_set(False) meshObj.select_set(selected) + logger.info(f"Viewing UV morph: {morph.name}") return {"FINISHED"} @@ -636,7 +659,7 @@ class ClearUVMorphView(bpy.types.Operator): bl_description = "Clear all temporary data of UV morphs" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None @@ -664,6 +687,7 @@ class ClearUVMorphView(bpy.types.Operator): for act in bpy.data.actions: if act.name.startswith("__uv.") and act.users < 1: bpy.data.actions.remove(act) + logger.info("Cleared UV morph view") return {"FINISHED"} @@ -674,14 +698,14 @@ class EditUVMorph(bpy.types.Operator): bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: obj = context.active_object if obj.type != "MESH": return False active_uv_layer = obj.data.uv_layers.active return active_uv_layer and active_uv_layer.name.startswith("__uv.") - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object meshObj = obj @@ -704,6 +728,7 @@ class EditUVMorph(bpy.types.Operator): bpy.ops.object.mode_set(mode="EDIT") meshObj.select_set(selected) + logger.info("Editing UV morph") return {"FINISHED"} @@ -714,14 +739,14 @@ class ApplyUVMorph(bpy.types.Operator): bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: obj = context.active_object if obj.type != "MESH": return False active_uv_layer = obj.data.uv_layers.active return active_uv_layer and active_uv_layer.name.startswith("__uv.") - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root @@ -756,6 +781,7 @@ class ApplyUVMorph(bpy.types.Operator): morph.data_type = "VERTEX_GROUP" meshObj.select_set(selected) + logger.info(f"Applied UV morph: {morph.name}") return {"FINISHED"} @@ -766,11 +792,12 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: return FnModel.find_root_object(context.active_object) is not None - def execute(self, context: bpy.types.Context): + def execute(self, context: bpy.types.Context) -> Set[str]: mmd_root_object = FnModel.find_root_object(context.active_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object) + logger.info("Cleaned duplicated material morphs") return {"FINISHED"} diff --git a/core/mmd/operators/rigid_body.py b/core/mmd/operators/rigid_body.py index 22e3515..ef91c47 100644 --- a/core/mmd/operators/rigid_body.py +++ b/core/mmd/operators/rigid_body.py @@ -6,7 +6,7 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import math -from typing import Dict, Optional, Tuple, cast +from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator import bpy from mathutils import Euler, Vector @@ -16,6 +16,7 @@ from ..bpyutils import FnContext, Props from ..core import rigid_body from ..core.model import FnModel, Model from ..core.rigid_body import FnRigidBody +from ...logging_setup import logger class SelectRigidBody(bpy.types.Operator): @@ -43,15 +44,15 @@ class SelectRigidBody(bpy.types.Operator): default=False, ) - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: vm = context.window_manager return vm.invoke_props_dialog(self) @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: return FnModel.is_rigid_body_object(context.active_object) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) if root is None: @@ -173,7 +174,7 @@ class AddRigidBody(bpy.types.Operator): default=0.1, ) - def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None): + def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object: name_j: str = self.name_j name_e: str = self.name_e size = self.size.copy() @@ -226,7 +227,7 @@ class AddRigidBody(bpy.types.Operator): ) @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: root_object = FnModel.find_root_object(context.active_object) if root_object is None: return False @@ -237,7 +238,7 @@ class AddRigidBody(bpy.types.Operator): return True - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) @@ -254,15 +255,17 @@ class AddRigidBody(bpy.types.Operator): armature_object.select_set(False) if len(selected_pose_bones) > 0: + logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones") for pose_bone in selected_pose_bones: rigid = self.__add_rigid_body(context, root_object, pose_bone) rigid.select_set(True) else: + logger.info("Adding a single rigid body without bone attachment") rigid = self.__add_rigid_body(context, root_object) rigid.select_set(True) return {"FINISHED"} - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: no_bone = True if context.selected_bones and len(context.selected_bones) > 0: no_bone = False @@ -288,12 +291,13 @@ class RemoveRigidBody(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: return FnModel.is_rigid_body_object(context.active_object) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) + logger.info(f"Removing rigid body: {obj.name}") utils.selectAObject(obj) # ensure this is the only one object select bpy.ops.object.delete(use_global=True) if root: @@ -306,7 +310,8 @@ class RigidBodyBake(bpy.types.Operator): bl_label = "Bake" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context: bpy.types.Context): + def execute(self, context: bpy.types.Context) -> Set[str]: + logger.info("Baking rigid body simulation") with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True) @@ -318,7 +323,8 @@ class RigidBodyDeleteBake(bpy.types.Operator): bl_label = "Delete Bake" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context: bpy.types.Context): + def execute(self, context: bpy.types.Context) -> Set[str]: + logger.info("Deleting rigid body simulation bake") with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): bpy.ops.ptcache.free_bake("INVOKE_DEFAULT") @@ -381,7 +387,7 @@ class AddJoint(bpy.types.Operator): min=0, ) - def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]): + def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]: obj_seq = tuple(bone_map.keys()) for rigid_a, bone_a in bone_map.items(): for rigid_b, bone_b in bone_map.items(): @@ -394,7 +400,7 @@ class AddJoint(bpy.types.Operator): else: yield obj_seq - def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map): + def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object: loc: Optional[Vector] = None rot = Euler((0.0, 0.0, 0.0)) rigid_a, rigid_b = rigid_pair @@ -432,7 +438,7 @@ class AddJoint(bpy.types.Operator): ) @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: root_object = FnModel.find_root_object(context.active_object) if root_object is None: return False @@ -443,7 +449,7 @@ class AddJoint(bpy.types.Operator): return True - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: active_object = context.active_object root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object)) @@ -456,15 +462,19 @@ class AddJoint(bpy.types.Operator): FnContext.select_single_object(context, root_object).select_set(False) if context.scene.rigidbody_world is None: + logger.info("Creating rigid body world") bpy.ops.rigidbody.world_add() + joint_count = 0 for pair in self.__enumerate_rigid_pair(bone_map): joint = self.__add_joint(context, root_object, pair, bone_map) joint.select_set(True) - + joint_count += 1 + + logger.info(f"Added {joint_count} joints between rigid bodies") return {"FINISHED"} - def invoke(self, context, event): + def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]: vm = context.window_manager return vm.invoke_props_dialog(self) @@ -476,12 +486,13 @@ class RemoveJoint(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @classmethod - def poll(cls, context): + def poll(cls, context: bpy.types.Context) -> bool: return FnModel.is_joint_object(context.active_object) - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: obj = context.active_object root = FnModel.find_root_object(obj) + logger.info(f"Removing joint: {obj.name}") utils.selectAObject(obj) # ensure this is the only one object select bpy.ops.object.delete(use_global=True) if root: @@ -496,7 +507,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} @staticmethod - def __get_rigid_body_world_objects(): + def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]: rigid_body.setRigidBodyWorldEnabled(True) rbw = bpy.context.scene.rigidbody_world if not rbw.collection: @@ -511,12 +522,12 @@ class UpdateRigidBodyWorld(bpy.types.Operator): return rbw.collection.objects, rbw.constraints.objects - def execute(self, context): + def execute(self, context: bpy.types.Context) -> Set[str]: scene = context.scene scene_objs = set(scene.objects) scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects) - def _update_group(obj, group): + def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool: if obj in scene_objs: if obj not in group.values(): group.link(obj) @@ -525,7 +536,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator): group.unlink(obj) return False - def _references(obj): + def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]: yield obj if getattr(obj, "proxy", None): yield from _references(obj.proxy) @@ -542,6 +553,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator): # Object.rigid_body are removed, # but Object.rigid_body_constraint are retained. # Therefore, it must be checked with Object.mmd_type. + logger.info("Updating rigid body world objects") for i in (x for x in objects if x.mmd_type == "RIGID_BODY"): if not _update_group(i, rb_objs): continue @@ -556,6 +568,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator): # TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters. # mass, friction, restitution, linear_dumping, angular_dumping + logger.info("Updating rigid body constraints") for i in (x for x in objects if x.rigid_body_constraint): if not _update_group(i, rbc_objs): continue @@ -566,6 +579,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator): rbc.object2 = rb_map.get(rbc.object2, rbc.object2) if need_rebuild_physics: + logger.info("Rebuilding physics for models") for root_object in scene.objects: if root_object.mmd_type != "ROOT": continue diff --git a/core/mmd/operators/sdef.py b/core/mmd/operators/sdef.py index e38badd..bb46807 100644 --- a/core/mmd/operators/sdef.py +++ b/core/mmd/operators/sdef.py @@ -5,18 +5,19 @@ # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. -from typing import Set +from typing import Set, Tuple import bpy -from bpy.types import Operator +from bpy.types import Operator, Context, Object from ..core.model import FnModel from ..core.sdef import FnSDEF +from ....core.logging_setup import logger -def _get_target_objects(context): - root_objects: Set[bpy.types.Object] = set() - selected_objects: Set[bpy.types.Object] = set() +def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]: + root_objects: Set[Object] = set() + selected_objects: Set[Object] = set() for i in context.selected_objects: if i.type == "MESH": selected_objects.add(i) @@ -40,11 +41,13 @@ class ResetSDEFCache(Operator): bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - def execute(self, context): + def execute(self, context: Context) -> Set[str]: target_meshes, _ = _get_target_objects(context) + logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects") for i in target_meshes: FnSDEF.clear_cache(i) FnSDEF.clear_cache(unused_only=True) + logger.debug("SDEF cache reset completed") return {"FINISHED"} @@ -75,19 +78,20 @@ class BindSDEF(Operator): default=False, ) - def invoke(self, context, event): + def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]: vm = context.window_manager return vm.invoke_props_dialog(self) - # TODO: Utility Functionalize - def execute(self, context): + def execute(self, context: Context) -> Set[str]: target_meshes, root_objects = _get_target_objects(context) + logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}") for r in root_objects: r.mmd_root.use_sdef = True param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale) count = sum(FnSDEF.bind(i, *param) for i in target_meshes) + logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes") self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)") return {"FINISHED"} @@ -98,13 +102,15 @@ class UnbindSDEF(Operator): bl_description = "Unbind MMD SDEF data of selected objects" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - # TODO: Utility Functionalize - def execute(self, context): + def execute(self, context: Context) -> Set[str]: target_meshes, root_objects = _get_target_objects(context) + logger.info(f"Unbinding SDEF for {len(target_meshes)} objects") + for i in target_meshes: FnSDEF.unbind(i) for r in root_objects: r.mmd_root.use_sdef = False + logger.debug("SDEF unbinding completed") return {"FINISHED"} diff --git a/core/mmd/operators/view.py b/core/mmd/operators/view.py index 0072312..3e82cf4 100644 --- a/core/mmd/operators/view.py +++ b/core/mmd/operators/view.py @@ -6,29 +6,32 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import re +from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator -from bpy.types import Operator -from mathutils import Matrix +from bpy.types import Operator, Context +from mathutils import Matrix, Vector, Quaternion + +from ...logging_setup import logger class _SetShadingBase: - bl_options = {"REGISTER", "UNDO"} + bl_options: Set[str] = {"REGISTER", "UNDO"} @staticmethod - def _get_view3d_spaces(context): + def _get_view3d_spaces(context: Context) -> Iterator[Any]: if getattr(context.area, "type", None) == "VIEW_3D": return (context.area.spaces[0],) return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D") @staticmethod - def _reset_color_management(context, use_display_device=True): + def _reset_color_management(context: Context, use_display_device: bool = True) -> None: try: context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device] except TypeError: pass @staticmethod - def _reset_material_shading(context, use_shadeless=False): + def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None: for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"): for s in i.material_slots: if s.material is None: @@ -36,10 +39,11 @@ class _SetShadingBase: s.material.use_nodes = False s.material.use_shadeless = use_shadeless - def execute(self, context): + def execute(self, context: Context) -> Dict[str, str]: context.scene.render.engine = "BLENDER_EEVEE_NEXT" + logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT") - shading_mode = getattr(self, "_shading_mode", None) + shading_mode: Optional[str] = getattr(self, "_shading_mode", None) for space in self._get_view3d_spaces(context): shading = space.shading shading.type = "SOLID" @@ -47,39 +51,40 @@ class _SetShadingBase: shading.color_type = "TEXTURE" if shading_mode else "MATERIAL" shading.show_object_outline = False shading.show_backface_culling = False + logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}") return {"FINISHED"} class SetGLSLShading(Operator, _SetShadingBase): - bl_idname = "mmd_tools.set_glsl_shading" - bl_label = "GLSL View" - bl_description = "Use GLSL shading with additional lighting" + bl_idname: str = "mmd_tools.set_glsl_shading" + bl_label: str = "GLSL View" + bl_description: str = "Use GLSL shading with additional lighting" - _shading_mode = "GLSL" + _shading_mode: str = "GLSL" class SetShadelessGLSLShading(Operator, _SetShadingBase): - bl_idname = "mmd_tools.set_shadeless_glsl_shading" - bl_label = "Shadeless GLSL View" - bl_description = "Use only toon shading" + bl_idname: str = "mmd_tools.set_shadeless_glsl_shading" + bl_label: str = "Shadeless GLSL View" + bl_description: str = "Use only toon shading" - _shading_mode = "SHADELESS" + _shading_mode: str = "SHADELESS" class ResetShading(Operator, _SetShadingBase): - bl_idname = "mmd_tools.reset_shading" - bl_label = "Reset View" - bl_description = "Reset to default Blender shading" + bl_idname: str = "mmd_tools.reset_shading" + bl_label: str = "Reset View" + bl_description: str = "Reset to default Blender shading" class FlipPose(Operator): - bl_idname = "mmd_tools.flip_pose" - bl_label = "Flip Pose" - bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis." - bl_options = {"REGISTER", "UNDO"} + bl_idname: str = "mmd_tools.flip_pose" + bl_label: str = "Flip Pose" + bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis." + bl_options: Set[str] = {"REGISTER", "UNDO"} # https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html - __LR_REGEX = [ + __LR_REGEX: List[Dict[str, Any]] = [ {"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1}, {"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2}, {"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0}, @@ -87,7 +92,7 @@ class FlipPose(Operator): {"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1}, {"re": re.compile(r"^(左|右)(.+)$"), "lr": 0}, ] - __LR_MAP = { + __LR_MAP: Dict[str, str] = { "RIGHT": "LEFT", "Right": "Left", "right": "left", @@ -103,7 +108,7 @@ class FlipPose(Operator): } @classmethod - def flip_name(cls, name): + def flip_name(cls, name: str) -> str: for regex in cls.__LR_REGEX: match = regex["re"].match(name) if match: @@ -121,17 +126,15 @@ class FlipPose(Operator): return "" @staticmethod - def __cmul(vec1, vec2): + def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]: return type(vec1)([x * y for x, y in zip(vec1, vec2)]) @staticmethod - def __matrix_compose(loc, rot, scale): + def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix: return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)]) @classmethod - def __flip_pose(cls, matrix_basis, bone_src, bone_dest): - from mathutils import Quaternion - + def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None: m = bone_dest.bone.matrix_local.to_3x3().transposed() mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted() loc, rot, scale = matrix_basis.decompose() @@ -140,11 +143,16 @@ class FlipPose(Operator): bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale) @classmethod - def poll(cls, context): + def poll(cls, context: Context) -> bool: return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE" - def execute(self, context): + def execute(self, context: Context) -> Dict[str, str]: + logger.info("Executing flip pose operation") pose_bones = context.active_object.pose.bones for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]: - self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b)) + flip_name = self.flip_name(b.name) + target_bone = pose_bones.get(flip_name, b) + logger.debug(f"Flipping pose from {b.name} to {target_bone.name}") + self.__flip_pose(mat, b, target_bone) + logger.info("Flip pose operation completed") return {"FINISHED"} diff --git a/core/mmd/properties/material.py b/core/mmd/properties/material.py index d3df3a3..b597c5d 100644 --- a/core/mmd/properties/material.py +++ b/core/mmd/properties/material.py @@ -6,81 +6,85 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import bpy +from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type from .. import utils from ..core import material from ..core.material import FnMaterial from ..core.model import FnModel from . import patch_library_overridable +from ....core.logging_setup import logger -def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context): +def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_ambient_color() -def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context): +def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_diffuse_color() -def _mmd_material_update_alpha(prop: "MMDMaterial", _context): +def _mmd_material_update_alpha(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_alpha() -def _mmd_material_update_specular_color(prop: "MMDMaterial", _context): +def _mmd_material_update_specular_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_specular_color() -def _mmd_material_update_shininess(prop: "MMDMaterial", _context): +def _mmd_material_update_shininess(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_shininess() -def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context): +def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_is_double_sided() -def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context): +def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object) -def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context): +def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_toon_texture() -def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context): +def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_drop_shadow() -def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context): +def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_self_shadow_map() -def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context): +def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_self_shadow() -def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context): +def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_enabled_toon_edge() -def _mmd_material_update_edge_color(prop: "MMDMaterial", _context): +def _mmd_material_update_edge_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_edge_color() -def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context): +def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context: bpy.types.Context) -> None: FnMaterial(prop.id_data).update_edge_weight() -def _mmd_material_get_name_j(prop: "MMDMaterial"): +def _mmd_material_get_name_j(prop: "MMDMaterial") -> str: return prop.get("name_j", "") -def _mmd_material_set_name_j(prop: "MMDMaterial", value: str): +def _mmd_material_set_name_j(prop: "MMDMaterial", value: str) -> None: prop_value = value if prop_value and prop_value != prop.get("name_j"): root = FnModel.find_root_object(bpy.context.active_object) if root is None: + logger.debug(f"No root object found, using unique name for material: {value}") prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials}) else: + logger.debug(f"Root object found, using unique name for material within model: {value}") prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)}) prop["name_j"] = prop_value @@ -275,13 +279,15 @@ class MMDMaterial(bpy.types.PropertyGroup): description="Comment", ) - def is_id_unique(self): + def is_id_unique(self) -> bool: return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None) @staticmethod - def register(): + def register() -> None: + logger.debug("Registering MMD material properties") bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial)) @staticmethod - def unregister(): + def unregister() -> None: + logger.debug("Unregistering MMD material properties") del bpy.types.Material.mmd_material diff --git a/core/mmd/properties/morph.py b/core/mmd/properties/morph.py index ba94350..e2be89b 100644 --- a/core/mmd/properties/morph.py +++ b/core/mmd/properties/morph.py @@ -6,33 +6,33 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import bpy +from typing import Optional, List, Dict, Any, Set, Tuple, Union, TypeVar, Type +from bpy.types import PropertyGroup, Object, ShapeKey from .. import utils from ..core.bone import FnBone from ..core.material import FnMaterial from ..core.model import FnModel, Model from ..core.morph import FnMorph +from ....core.logging_setup import logger def _morph_base_get_name(prop: "_MorphBase") -> str: return prop.get("name", "") -def _morph_base_set_name(prop: "_MorphBase", value: str): +def _morph_base_set_name(prop: "_MorphBase", value: str) -> None: mmd_root = prop.id_data.mmd_root - # morph_type = mmd_root.active_morph_type morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower() - # assert(prop.bl_rna.identifier.endswith('Morph')) - # logging.debug('_set_name: %s %s %s', prop, value, morph_type) prop_name = prop.get("name", None) if prop_name == value: return - used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop} + used_names: Set[str] = {x.name for x in getattr(mmd_root, morph_type) if x != prop} value = utils.unique_name(value, used_names) if prop_name is not None: if morph_type == "vertex_morphs": - kb_list = {} + kb_list: Dict[str, List[ShapeKey]] = {} for mesh in FnModel.iterate_mesh_objects(prop.id_data): for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): kb_list.setdefault(kb.name, []).append(kb) @@ -43,7 +43,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str): kb.name = value elif morph_type == "uv_morphs": - vg_list = {} + vg_list: Dict[str, List[Any]] = {} for mesh in FnModel.iterate_mesh_objects(prop.id_data): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): vg_list.setdefault(n, []).append(vg) @@ -72,6 +72,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str): kb.name = value prop["name"] = value + logger.debug(f"Renamed morph from '{prop_name}' to '{value}'") class _MorphBase: @@ -101,11 +102,11 @@ class _MorphBase: def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: - bone_id = prop.get("bone_id", -1) + bone_id: int = prop.get("bone_id", -1) if bone_id < 0: return "" - root_object = prop.id_data - armature_object = FnModel.find_armature_object(root_object) + root_object: Object = prop.id_data + armature_object: Optional[Object] = FnModel.find_armature_object(root_object) if armature_object is None: return "" pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id) @@ -114,9 +115,9 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: return pose_bone.name -def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): - root = prop.id_data - arm = FnModel.find_armature_object(root) +def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None: + root: Object = prop.id_data + arm: Optional[Object] = FnModel.find_armature_object(root) # Load the library_override file. This function is triggered when loading, but the arm obj cannot be found. # The arm obj is exist, but the relative relationship has not yet been established. @@ -128,9 +129,10 @@ def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): return pose_bone = arm.pose.bones[value] prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) + logger.debug(f"Set bone morph data bone to '{value}' with ID {prop['bone_id']}") -def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context): +def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context: bpy.types.Context) -> None: if not prop.name.startswith("mmd_bind"): return arm = FnModel(prop.id_data).morph_slider.dummy_armature @@ -139,6 +141,7 @@ def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context if bone: bone.location = prop.location bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency + logger.debug(f"Updated bone morph data location/rotation for '{prop.name}'") class BoneMorphData(bpy.types.PropertyGroup): @@ -188,40 +191,44 @@ class BoneMorph(_MorphBase, bpy.types.PropertyGroup): ) -def _material_morph_data_get_material(prop: "MaterialMorphData"): +def _material_morph_data_get_material(prop: "MaterialMorphData") -> str: mat_p = prop.get("material_data", None) if mat_p is not None: return mat_p.name return "" -def _material_morph_data_set_material(prop: "MaterialMorphData", value: str): +def _material_morph_data_set_material(prop: "MaterialMorphData", value: str) -> None: if value not in bpy.data.materials: prop["material_data"] = None prop["material_id"] = -1 + logger.debug(f"Material '{value}' not found, setting material_data to None") else: mat = bpy.data.materials[value] fnMat = FnMaterial(mat) prop["material_data"] = mat prop["material_id"] = fnMat.material_id + logger.debug(f"Set material morph data material to '{value}' with ID {fnMat.material_id}") -def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str): +def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str) -> None: mesh = FnModel.find_mesh_object_by_name(prop.id_data, value) if mesh is not None: prop["related_mesh_data"] = mesh.data + logger.debug(f"Set material morph data related mesh to '{value}'") else: prop["related_mesh_data"] = None + logger.debug(f"Mesh '{value}' not found, setting related_mesh_data to None") -def _material_morph_data_get_related_mesh(prop): +def _material_morph_data_get_related_mesh(prop: "MaterialMorphData") -> str: mesh_p = prop.get("related_mesh_data", None) if mesh_p is not None: return mesh_p.name return "" -def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context): +def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context: bpy.types.Context) -> None: if not prop.name.startswith("mmd_bind"): return from ..core.shader import _MaterialMorph @@ -229,9 +236,11 @@ def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _co mat = prop["material_data"] if mat is not None: _MaterialMorph.update_morph_inputs(mat, prop) + logger.debug(f"Updated material morph modifiable values for '{prop.name}'") else: for mat in FnModel(prop.id_data).materials(): _MaterialMorph.update_morph_inputs(mat, prop) + logger.debug(f"Updated material morph modifiable values for all materials") class MaterialMorphData(bpy.types.PropertyGroup): @@ -407,9 +416,6 @@ class UVMorphOffset(bpy.types.PropertyGroup): name="UV Offset", description="UV offset", size=4, - # min=-1, - # max=1, - # precision=3, step=0.1, default=[0, 0, 0, 0], ) diff --git a/core/mmd/properties/pose_bone.py b/core/mmd/properties/pose_bone.py index 3584c42..84a71ef 100644 --- a/core/mmd/properties/pose_bone.py +++ b/core/mmd/properties/pose_bone.py @@ -5,29 +5,33 @@ # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. -from typing import cast +from typing import cast, Optional, Any, Union import bpy +from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature from ..core.bone import FnBone from . import patch_library_overridable +from ....core.logging_setup import logger -def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context): +def _mmd_bone_update_additional_transform(prop: "MMDBone", context: Context) -> None: prop["is_additional_transform_dirty"] = True p_bone = context.active_pose_bone if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): + logger.debug(f"Applying additional transformation for {p_bone.name}") FnBone.apply_additional_transformation(prop.id_data) -def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context): +def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: Context) -> None: pose_bone = context.active_pose_bone if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer(): + logger.debug(f"Updating additional transform influence for {pose_bone.name}") FnBone.update_additional_transform_influence(pose_bone) else: prop["is_additional_transform_dirty"] = True -def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"): +def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str: arm = prop.id_data bone_id = prop.get("additional_transform_bone_id", -1) if bone_id < 0: @@ -38,7 +42,7 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"): return pose_bone.name -def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str): +def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str) -> None: arm = prop.id_data prop["is_additional_transform_dirty"] = True if value not in arm.pose.bones.keys(): @@ -48,7 +52,7 @@ def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str): prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) -class MMDBone(bpy.types.PropertyGroup): +class MMDBone(PropertyGroup): name_j: bpy.props.StringProperty( name="Name", description="Japanese Name", @@ -184,11 +188,12 @@ class MMDBone(bpy.types.PropertyGroup): is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True) - def is_id_unique(self): + def is_id_unique(self) -> bool: return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) @staticmethod - def register(): + def register() -> None: + logger.debug("Registering MMDBone properties") bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone)) bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False)) bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type")) @@ -202,20 +207,21 @@ class MMDBone(bpy.types.PropertyGroup): ) @staticmethod - def unregister(): + def unregister() -> None: + logger.debug("Unregistering MMDBone properties") del bpy.types.PoseBone.mmd_ik_toggle del bpy.types.PoseBone.mmd_shadow_bone_type del bpy.types.PoseBone.is_mmd_shadow_bone del bpy.types.PoseBone.mmd_bone -def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context): +def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context: Any) -> None: v = prop.mmd_ik_toggle - armature_object = cast(bpy.types.Object, prop.id_data) + armature_object = cast(Object, prop.id_data) for b in armature_object.pose.bones: for c in b.constraints: if c.type == "IK" and c.subtarget == prop.name: - # logging.debug(' %s %s', b.name, c.name) + logger.debug(f"Updating IK toggle for {b.name} {c.name}") c.influence = v b = b if c.use_tail else b.parent for b in ([b] + b.parent_recursive)[: c.chain_count]: diff --git a/core/mmd/properties/rigid_body.py b/core/mmd/properties/rigid_body.py index 3941657..87ef14d 100644 --- a/core/mmd/properties/rigid_body.py +++ b/core/mmd/properties/rigid_body.py @@ -8,32 +8,35 @@ """Properties for rigid bodies and joints""" import bpy +from typing import Optional, Any, Set, List, Dict, Tuple, Union +from bpy.types import Context, Object, PropertyGroup, Material from .. import bpyutils from ..core import rigid_body from ..core.rigid_body import RigidBodyMaterial, FnRigidBody from ..core.model import FnModel from . import patch_library_overridable +from ....core.logging_setup import logger -def _updateCollisionGroup(prop, _context): - obj = prop.id_data - materials = obj.data.materials +def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None: + obj: Object = prop.id_data + materials: List[Material] = obj.data.materials if len(materials) == 0: materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) else: obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) -def _updateType(prop, _context): - obj = prop.id_data +def _updateType(prop: PropertyGroup, _context: Context) -> None: + obj: Object = prop.id_data rb = obj.rigid_body if rb: rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC -def _updateShape(prop, _context): - obj = prop.id_data +def _updateShape(prop: PropertyGroup, _context: Context) -> None: + obj: Object = prop.id_data if len(obj.data.vertices) > 0: size = prop.size @@ -44,8 +47,8 @@ def _updateShape(prop, _context): rb.collision_shape = prop.shape -def _get_bone(prop): - obj = prop.id_data +def _get_bone(prop: PropertyGroup) -> str: + obj: Object = prop.id_data relation = obj.constraints.get("mmd_tools_rigid_parent", None) if relation: arm = relation.target @@ -55,9 +58,9 @@ def _get_bone(prop): return prop.get("bone", "") -def _set_bone(prop, value): - bone_name = value - obj = prop.id_data +def _set_bone(prop: PropertyGroup, value: str) -> None: + bone_name: str = value + obj: Object = prop.id_data relation = obj.constraints.get("mmd_tools_rigid_parent", None) if relation is None: relation = obj.constraints.new("CHILD_OF") @@ -78,16 +81,16 @@ def _set_bone(prop, value): prop["bone"] = bone_name -def _get_size(prop): +def _get_size(prop: PropertyGroup) -> Tuple[float, float, float]: if prop.id_data.mmd_type != "RIGID_BODY": return (0, 0, 0) return FnRigidBody.get_rigid_body_size(prop.id_data) -def _set_size(prop, value): - obj = prop.id_data +def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None: + obj: Object = prop.id_data assert obj.mode == "OBJECT" # not support other mode yet - shape = prop.shape + shape: str = prop.shape mesh = obj.data rb = obj.rigid_body @@ -146,15 +149,15 @@ def _set_size(prop, value): mesh.update() -def _get_rigid_name(prop): +def _get_rigid_name(prop: PropertyGroup) -> str: return prop.get("name", "") -def _set_rigid_name(prop, value): +def _set_rigid_name(prop: PropertyGroup, value: str) -> None: prop["name"] = value -class MMDRigidBody(bpy.types.PropertyGroup): +class MMDRigidBody(PropertyGroup): name_j: bpy.props.StringProperty( name="Name", description="Japanese Name", @@ -227,16 +230,18 @@ class MMDRigidBody(bpy.types.PropertyGroup): ) @staticmethod - def register(): + def register() -> None: + logger.debug("Registering MMDRigidBody property") bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody)) @staticmethod - def unregister(): + def unregister() -> None: + logger.debug("Unregistering MMDRigidBody property") del bpy.types.Object.mmd_rigid -def _updateSpringLinear(prop, context): - obj = prop.id_data +def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None: + obj: Object = prop.id_data rbc = obj.rigid_body_constraint if rbc: rbc.spring_stiffness_x = prop.spring_linear[0] @@ -244,8 +249,8 @@ def _updateSpringLinear(prop, context): rbc.spring_stiffness_z = prop.spring_linear[2] -def _updateSpringAngular(prop, context): - obj = prop.id_data +def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None: + obj: Object = prop.id_data rbc = obj.rigid_body_constraint if rbc and hasattr(rbc, "use_spring_ang_x"): rbc.spring_stiffness_ang_x = prop.spring_angular[0] @@ -253,7 +258,7 @@ def _updateSpringAngular(prop, context): rbc.spring_stiffness_ang_z = prop.spring_angular[2] -class MMDJoint(bpy.types.PropertyGroup): +class MMDJoint(PropertyGroup): name_j: bpy.props.StringProperty( name="Name", description="Japanese Name", @@ -287,9 +292,12 @@ class MMDJoint(bpy.types.PropertyGroup): ) @staticmethod - def register(): + def register() -> None: + logger.debug("Registering MMDJoint property") bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint)) @staticmethod - def unregister(): + def unregister() -> None: + logger.debug("Unregistering MMDJoint property") del bpy.types.Object.mmd_joint + diff --git a/core/mmd/properties/root.py b/core/mmd/properties/root.py index 8188ed1..679a9ff 100644 --- a/core/mmd/properties/root.py +++ b/core/mmd/properties/root.py @@ -8,6 +8,7 @@ """Properties for MMD model root object""" import bpy +from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast from .. import utils from ..bpyutils import FnContext @@ -17,9 +18,10 @@ from ..core.sdef import FnSDEF from . import patch_library_overridable from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph from .translations import MMDTranslation +from ....core.logging_setup import logger -def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1): +def __driver_variables(constraint: bpy.types.Constraint, path: str, index: int = -1) -> Tuple[bpy.types.Driver, Any]: d = constraint.driver_add(path, index) variables = d.driver.variables for x in variables: @@ -27,7 +29,7 @@ def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1): return d.driver, variables -def __add_single_prop(variables, id_obj, data_path, prefix): +def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str, prefix: str) -> Any: var = variables.new() var.name = prefix + str(len(variables)) var.type = "SINGLE_PROP" @@ -38,17 +40,18 @@ def __add_single_prop(variables, id_obj, data_path, prefix): return var -def _toggleUsePropertyDriver(self: "MMDRoot", _context): +def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None: root_object: bpy.types.Object = self.id_data armature_object = FnModel.find_armature_object(root_object) if armature_object is None: - ik_map = {} + ik_map: Dict[Any, Tuple[Any, Any]] = {} else: bones = armature_object.pose.bones ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones} if self.use_property_driver: + logger.debug("Enabling property drivers for %s", root_object.name) for ik, (b, c) in ik_map.items(): driver, variables = __driver_variables(c, "influence") driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name @@ -63,6 +66,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context): driver, variables = __driver_variables(i, prop_hide) driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name else: + logger.debug("Disabling property drivers for %s", root_object.name) for ik, (b, c) in ik_map.items(): c.driver_remove("influence") b = b if c.use_tail else b.parent @@ -80,31 +84,35 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context): # =========================================== -def _toggleUseToonTexture(self: "MMDRoot", _context): +def _toggleUseToonTexture(self: "MMDRoot", _context: bpy.types.Context) -> None: use_toon = self.use_toon_texture + logger.debug("Toggling toon texture to %s for %s", use_toon, self.id_data.name) for i in FnModel.iterate_mesh_objects(self.id_data): for m in i.data.materials: if m: FnMaterial(m).use_toon_texture(use_toon) -def _toggleUseSphereTexture(self: "MMDRoot", _context): +def _toggleUseSphereTexture(self: "MMDRoot", _context: bpy.types.Context) -> None: use_sphere = self.use_sphere_texture + logger.debug("Toggling sphere texture to %s for %s", use_sphere, self.id_data.name) for i in FnModel.iterate_mesh_objects(self.id_data): for m in i.data.materials: if m: FnMaterial(m).use_sphere_texture(use_sphere, i) -def _toggleUseSDEF(self: "MMDRoot", _context): +def _toggleUseSDEF(self: "MMDRoot", _context: bpy.types.Context) -> None: mute_sdef = not self.use_sdef + logger.debug("Toggling SDEF to %s for %s", not mute_sdef, self.id_data.name) for i in FnModel.iterate_mesh_objects(self.id_data): FnSDEF.mute_sdef_set(i, mute_sdef) -def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context): +def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> None: root = self.id_data hide = not self.show_meshes + logger.debug("Toggling mesh visibility to %s for %s", not hide, root.name) for i in FnModel.iterate_mesh_objects(self.id_data): i.hide_set(hide) i.hide_render = hide @@ -112,27 +120,30 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context): FnContext.set_active_object(context, root) -def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context): +def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context) -> None: root = self.id_data hide = not self.show_rigid_bodies + logger.debug("Toggling rigid body visibility to %s for %s", not hide, root.name) for i in FnModel.iterate_rigid_body_objects(root): i.hide_set(hide) if hide and context.active_object is None: FnContext.set_active_object(context, root) -def _toggleVisibilityOfJoints(self: "MMDRoot", context): +def _toggleVisibilityOfJoints(self: "MMDRoot", context: bpy.types.Context) -> None: root_object = self.id_data hide = not self.show_joints + logger.debug("Toggling joint visibility to %s for %s", not hide, root_object.name) for i in FnModel.iterate_joint_objects(root_object): i.hide_set(hide) if hide and context.active_object is None: FnContext.set_active_object(context, root_object) -def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context): +def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context) -> None: root_object: bpy.types.Object = self.id_data hide = not self.show_temporary_objects + logger.debug("Toggling temporary object visibility to %s for %s", not hide, root_object.name) with FnContext.temp_override_active_layer_collection(context, root_object): for i in FnModel.iterate_temporary_objects(root_object): i.hide_set(hide) @@ -140,45 +151,48 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont FnContext.set_active_object(context, root_object) -def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context): +def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context: bpy.types.Context) -> None: root = self.id_data show_names = root.mmd_root.show_names_of_rigid_bodies + logger.debug("Toggling rigid body names to %s for %s", show_names, root.name) for i in FnModel.iterate_rigid_body_objects(root): i.show_name = show_names -def _toggleShowNamesOfJoints(self: "MMDRoot", _context): +def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None: root = self.id_data show_names = root.mmd_root.show_names_of_joints + logger.debug("Toggling joint names to %s for %s", show_names, root.name) for i in FnModel.iterate_joint_objects(root): i.show_name = show_names -def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool): +def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None: root = prop.id_data arm = FnModel.find_armature_object(root) if arm is None: return if not v and bpy.context.active_object == arm: FnContext.set_active_object(bpy.context, root) + logger.debug("Setting armature visibility to %s for %s", v, root.name) arm.hide_set(not v) -def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"): +def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool: if prop.id_data.mmd_type != "ROOT": return False arm = FnModel.find_armature_object(prop.id_data) return arm and not arm.hide_get() -def _setActiveRigidbodyObject(prop: "MMDRoot", v: int): +def _setActiveRigidbodyObject(prop: "MMDRoot", v: int) -> None: obj = FnContext.get_scene_objects(bpy.context)[v] if FnModel.is_rigid_body_object(obj): FnContext.set_active_and_select_single_object(bpy.context, obj) prop["active_rigidbody_object_index"] = v -def _getActiveRigidbodyObject(prop: "MMDRoot"): +def _getActiveRigidbodyObject(prop: "MMDRoot") -> int: context = bpy.context active_obj = FnContext.get_active_object(context) if FnModel.is_rigid_body_object(active_obj): @@ -186,14 +200,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot"): return prop.get("active_rigidbody_object_index", 0) -def _setActiveJointObject(prop: "MMDRoot", v: int): +def _setActiveJointObject(prop: "MMDRoot", v: int) -> None: obj = FnContext.get_scene_objects(bpy.context)[v] if FnModel.is_joint_object(obj): FnContext.set_active_and_select_single_object(bpy.context, obj) prop["active_joint_object_index"] = v -def _getActiveJointObject(prop: "MMDRoot"): +def _getActiveJointObject(prop: "MMDRoot") -> int: context = bpy.context active_obj = FnContext.get_active_object(context) if FnModel.is_joint_object(active_obj): @@ -201,26 +215,26 @@ def _getActiveJointObject(prop: "MMDRoot"): return prop.get("active_joint_object_index", 0) -def _setActiveMorph(prop: "MMDRoot", v: bool): +def _setActiveMorph(prop: "MMDRoot", v: bool) -> None: if "active_morph_indices" not in prop: prop["active_morph_indices"] = [0] * 5 prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v -def _getActiveMorph(prop: "MMDRoot"): +def _getActiveMorph(prop: "MMDRoot") -> int: if "active_morph_indices" in prop: return prop["active_morph_indices"][prop.get("active_morph_type", 3)] return 0 -def _setActiveMeshObject(prop: "MMDRoot", v: int): +def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None: obj = FnContext.get_scene_objects(bpy.context)[v] if FnModel.is_mesh_object(obj): FnContext.set_active_and_select_single_object(bpy.context, obj) prop["active_mesh_index"] = v -def _getActiveMeshObject(prop: "MMDRoot"): +def _getActiveMeshObject(prop: "MMDRoot") -> int: context = bpy.context active_obj = FnContext.get_active_object(context) if FnModel.is_mesh_object(active_obj): @@ -520,7 +534,8 @@ class MMDRoot(bpy.types.PropertyGroup): prop.hide_viewport = value @staticmethod - def register(): + def register() -> None: + logger.debug("Registering MMDRoot property group") bpy.types.Object.mmd_type = patch_library_overridable( bpy.props.EnumProperty( name="Type", @@ -570,7 +585,8 @@ class MMDRoot(bpy.types.PropertyGroup): ) @staticmethod - def unregister(): + def unregister() -> None: + logger.debug("Unregistering MMDRoot property group") del bpy.types.Object.hide del bpy.types.Object.select del bpy.types.Object.mmd_root diff --git a/core/updater.py b/core/updater.py index 125cc7a..e1c30ec 100644 --- a/core/updater.py +++ b/core/updater.py @@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit" # Define which version series this installation can update to # For example: ["0.1"] means only look for 0.1.x updates # ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates -ALLOWED_VERSION_SERIES = ["0.2"] +ALLOWED_VERSION_SERIES = ["0.3"] is_checking_for_update: bool = False update_needed: bool = False diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index e29ef01..9e89d6c 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)", + "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.0)", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc3": "please report it on our Github.", @@ -63,6 +63,13 @@ "PoseMode.basis": "Basis", "Armature.validation.no_armature": "No armature selected", + "Armature.validation.pmx_model_detected": "PMX model detected. Japanese bone names may not match standard naming conventions.", + "Armature.validation.pmx_model_strict": "Consider using the 'Standardize Armature' option to convert Japanese bone names to standard names.", + "Armature.validation.pmx_model_standardize": "This will make the model compatible with standard avatar systems.", + "Armature.validation.pmx_model_basic": "PMX models use Japanese bone names which may not match standard naming conventions.", + "Armature.validation.unknown_format": "Unknown armature format detected.", + "Validation.mode.none": "Validation is disabled in settings.", + "Validation.no_messages": "No validation messages available.", "Armature.validation.not_armature": "Selected object is not an armature", "Armature.validation.no_bones": "Armature has no bones", "Armature.validation.basic_check_failed": "Basic armature validation failed", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 22282b0..6ba5a72 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)", + "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc3": "GitHubで報告してください。", @@ -63,6 +63,13 @@ "PoseMode.basis": "基本形", "Armature.validation.no_armature": "アーマチュアが選択されていません", + "Armature.validation.pmx_model_detected": "PMXモデルが検出されました。日本語の骨名が標準の命名規則と一致しない場合があります。", + "Armature.validation.pmx_model_strict": "「アーマチュアの標準化」オプションを使用して、日本語の骨名を標準名に変換することを検討してください。", + "Armature.validation.pmx_model_standardize": "これにより、モデルが標準的なアバターシステムと互換性を持つようになります。", + "Armature.validation.pmx_model_basic": "PMXモデルは日本語の骨名を使用しており、標準の命名規則と一致しない場合があります。", + "Armature.validation.unknown_format": "不明なアーマチュア形式が検出されました。", + "Validation.mode.none": "検証は設定で無効になっています。", + "Validation.no_messages": "検証メッセージはありません。", "Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません", "Armature.validation.no_bones": "アーマチュアにボーンがありません", "Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index f80a09d..ca35b5a 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)", + "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc3": "Github에 보고해 주세요.", @@ -63,6 +63,13 @@ "PoseMode.basis": "기본", "Armature.validation.no_armature": "선택된 아마추어 없음", + "Armature.validation.pmx_model_detected": "PMX 모델이 감지되었습니다. 일본어 본 이름이 표준 명명 규칙과 일치하지 않을 수 있습니다.", + "Armature.validation.pmx_model_strict": "'아마추어 표준화' 옵션을 사용하여 일본어 본 이름을 표준 이름으로 변환하는 것을 고려하세요.", + "Armature.validation.pmx_model_standardize": "이렇게 하면 모델이 표준 아바타 시스템과 호환됩니다.", + "Armature.validation.pmx_model_basic": "PMX 모델은 일본어 본 이름을 사용하며 표준 명명 규칙과 일치하지 않을 수 있습니다.", + "Armature.validation.unknown_format": "알 수 없는 아마추어 형식이 감지되었습니다.", + "Validation.mode.none": "유효성 검사가 설정에서 비활성화되었습니다.", + "Validation.no_messages": "사용 가능한 유효성 검사 메시지가 없습니다.", "Armature.validation.not_armature": "선택된 객체가 아마추어가 아님", "Armature.validation.no_bones": "아마추어에 본이 없음", "Armature.validation.basic_check_failed": "기본 아마추어 검증 실패", diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index d0d6755..2f8f625 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -89,16 +89,33 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): if active_armature: is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True) + # Check if this is a PMX model + is_pmx_model = False + if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')): + is_pmx_model = True + info_box = col.box() + # If it's a PMX model, display a prominent notice + if is_pmx_model: + pmx_box = info_box.box() + pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO') + + validation_mode = context.scene.avatar_toolkit.validation_mode + if validation_mode == 'STRICT': + pmx_box.label(text=t("Armature.validation.pmx_model_strict")) + pmx_box.label(text=t("Armature.validation.pmx_model_standardize")) + else: + pmx_box.label(text=t("Armature.validation.pmx_model_basic")) + if not is_valid: # Display non-standard bones and hierarchy issues - if len(messages) > 1: + if messages and len(messages) > 0: # Found Bones section validation_box = info_box.box() row = validation_box.row() row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False) - if props.show_found_bones: + if props.show_found_bones and len(messages) > 0: for line in messages[0].split('\n'): validation_box.label(text=line) @@ -127,15 +144,31 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False) if props.show_non_standard: - if non_standard_messages: + if non_standard_messages and len(non_standard_messages) > 0: for message in non_standard_messages: for line in message.split('\n'): sub_row = validation_box.row() sub_row.alert = True sub_row.label(text=line) else: - sub_row = validation_box.row() - sub_row.label(text=t("Validation.no_non_standard_issues")) + # For PMX models, if no non-standard messages but it's a PMX model, + # we should still indicate there might be non-standard bones + if is_pmx_model: + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=t("Armature.validation.pmx_model_basic")) + + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=t("Armature.validation.pmx_model_strict")) + + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=t("Armature.validation.pmx_model_standardize")) + + else: + sub_row = validation_box.row() + sub_row.label(text=t("Validation.no_non_standard_issues")) # Hierarchy Issues section validation_box = info_box.box() @@ -190,9 +223,14 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): row.label(text=msg.name) else: # If no specific issues, show acceptable message - info_box.label(text=messages[0], icon='INFO') - info_box.label(text=messages[1]) - info_box.label(text=messages[2]) + if messages and len(messages) > 0: + info_box.label(text=messages[0], icon='INFO') + if len(messages) > 1: + info_box.label(text=messages[1]) + if len(messages) > 2: + info_box.label(text=messages[2]) + else: + info_box.label(text=t("Validation.no_messages"), icon='INFO') elif is_valid and not is_acceptable: row = info_box.row() split = row.split(factor=0.6) @@ -204,9 +242,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') elif is_valid and is_acceptable: # Show acceptable standard message - info_box.label(text=messages[0], icon='INFO') - info_box.label(text=messages[1]) - info_box.label(text=messages[2]) + if messages and len(messages) > 0: + info_box.label(text=messages[0], icon='INFO') + + # Only try to access additional messages if they exist + if len(messages) > 1: + info_box.label(text=messages[1]) + if len(messages) > 2: + info_box.label(text=messages[2]) + else: + info_box.label(text=t("Validation.no_messages"), icon='INFO') # Add standardize button standardize_box = info_box.box() @@ -252,3 +297,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): button_row.scale_y = 1.5 button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT') button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT') +