diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a4f195..bd7a79f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,9 @@ jobs: - name: Copy LICENSE file run: cp -p LICENSE mmd_tools/ + - name: Remove typings for development + run: rm -rf mmd_tools/typings + - name: Create a zip run: zip -r -9 mmd_tools-${GITHUB_REF_NAME}.zip mmd_tools/ diff --git a/mmd_tools/__init__.py b/mmd_tools/__init__.py index 9bda25a..c511533 100644 --- a/mmd_tools/__init__.py +++ b/mmd_tools/__init__.py @@ -20,7 +20,7 @@ "name": "mmd_tools", "author": "sugiany", "version": (4, 0, 0), - "blender": (4, 3, 0), + "blender": (4, 1, 1), "location": "View3D > Sidebar > MMD Panel", "description": "Utility tools for MMD model editing. (UuuNyaa's forked version)", "warning": "", diff --git a/mmd_tools/bpyutils.py b/mmd_tools/bpyutils.py index 5058e68..3e1e53d 100644 --- a/mmd_tools/bpyutils.py +++ b/mmd_tools/bpyutils.py @@ -2,7 +2,7 @@ # Copyright 2013 MMD Tools authors # This file is part of MMD Tools. -from typing import Optional, Union +from typing import List, Optional, Union import bpy @@ -107,10 +107,7 @@ def __exit__(self, _type, _value, _traceback): def addon_preferences(attrname, default=None): - if hasattr(bpy.context, "preferences"): - addon = bpy.context.preferences.addons.get(__package__, None) - else: - addon = bpy.context.user_preferences.addons.get(__package__, None) + addon = bpy.context.preferences.addons.get(__package__, None) return getattr(addon.preferences, attrname, default) if addon else default @@ -164,26 +161,7 @@ def activate_layer_collection(target: Union[bpy.types.Object, bpy.types.LayerCol def duplicateObject(obj, total_len): - for i in bpy.context.selected_objects: - i.select_set(False) - obj.select_set(True) - assert len(bpy.context.selected_objects) == 1 - assert bpy.context.selected_objects[0] == obj - last_selected = objs = [obj] - while len(objs) < total_len: - bpy.ops.object.duplicate() - objs.extend(bpy.context.selected_objects) - remain = total_len - len(objs) - len(bpy.context.selected_objects) - if remain < 0: - last_selected = bpy.context.selected_objects - for i in range(-remain): - last_selected[i].select_set(False) - else: - for i in range(min(remain, len(last_selected))): - last_selected[i].select_set(True) - last_selected = bpy.context.selected_objects - assert len(objs) == total_len - return objs + return FnContext.duplicate_object(bpy.context, obj, total_len) def makeCapsuleBak(segment=16, ring_count=8, radius=1.0, height=1.0, target_scene=None): @@ -240,11 +218,8 @@ def makeCapsuleBak(segment=16, ring_count=8, radius=1.0, height=1.0, target_scen def createObject(name="Object", object_data=None, target_scene=None): - target_scene = SceneOp(target_scene) - obj = bpy.data.objects.new(name=name, object_data=object_data) - target_scene.link_object(obj) - target_scene.active_object = obj - return obj + context = FnContext.ensure_context(target_scene) + return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data)) def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None): @@ -484,13 +459,21 @@ def id_objects(self): class FnContext: + def __init__(self): + raise NotImplementedError("This class is not expected to be instantiated.") + + @staticmethod + def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context: + return context or bpy.context + @staticmethod def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]: return context.active_object @staticmethod - def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> None: + def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: context.view_layer.objects.active = obj + return obj @staticmethod def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects: @@ -504,7 +487,7 @@ def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy. if obj not in context.selectable_objects: - def __layer_check(layer_collection: bpy.types.LayerCollection): + def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool: for lc in layer_collection.children: if __layer_check(lc): lc.hide_viewport = False @@ -523,13 +506,11 @@ def __layer_check(layer_collection: bpy.types.LayerCollection): for i in context.selected_objects: if i not in selected_objects: i.select_set(False) - return obj @staticmethod def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: - FnContext.ensure_selectable(context, obj) - obj.select_set(True) + FnContext.ensure_selectable(context, obj).select_set(True) return obj @staticmethod @@ -537,3 +518,43 @@ def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> b for i in context.selected_objects: i.select_set(False) return FnContext.select_object(context, obj) + + @staticmethod + def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + context.collection.objects.link(obj) + return obj + + @staticmethod + def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object: + return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data)) + + @staticmethod + def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]: + """Duplicate object + Args: + context (bpy.types.Context): context + obj (bpy.types.Object): object to duplicate + target_count (int): target count of duplicated objects + Returns: + List[bpy.types.Object]: duplicated objects + """ + for o in context.selected_objects: + o.select_set(False) + object_to_duplicate.select_set(True) + assert len(context.selected_objects) == 1 + assert context.selected_objects[0] == object_to_duplicate + last_selected_objects = result_objects = [object_to_duplicate] + while len(result_objects) < target_count: + bpy.ops.object.duplicate() + result_objects.extend(context.selected_objects) + remain = target_count - len(result_objects) - len(context.selected_objects) + if remain < 0: + last_selected_objects = context.selected_objects + for i in range(-remain): + last_selected_objects[i].select_set(False) + else: + for i in range(min(remain, len(last_selected_objects))): + last_selected_objects[i].select_set(True) + last_selected_objects = context.selected_objects + assert len(result_objects) == target_count + return result_objects diff --git a/mmd_tools/core/bone.py b/mmd_tools/core/bone.py index 04ff63f..9476951 100644 --- a/mmd_tools/core/bone.py +++ b/mmd_tools/core/bone.py @@ -149,7 +149,7 @@ def sync_bone_collections_from_armature(armature_object: bpy.types.Object): from mmd_tools.core.model import FnModel - root_object: bpy.types.Object = FnModel.find_root(armature_object) + root_object: bpy.types.Object = FnModel.find_root_object(armature_object) mmd_root = root_object.mmd_root bones = armature.bones @@ -198,6 +198,7 @@ def apply_bone_fixed_axis(armature_object: bpy.types.Object): force_align = True with bpyutils.edit_object(armature_object) as data: + bone: bpy.types.EditBone for bone in data.edit_bones: if bone.name not in bone_map: bone.select = False @@ -254,6 +255,7 @@ def apply_bone_local_axes(armature_object: bpy.types.Object): bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z) with bpyutils.edit_object(armature_object) as data: + bone: bpy.types.EditBone for bone in data.edit_bones: if bone.name not in bone_map: bone.select = False @@ -283,6 +285,7 @@ def apply_auto_bone_roll(armature): if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j): bone_names.append(b.name) with bpyutils.edit_object(armature) as data: + bone: bpy.types.EditBone for bone in data.edit_bones: if bone.name not in bone_names: continue diff --git a/mmd_tools/core/model.py b/mmd_tools/core/model.py index 705faf4..69fa0c6 100644 --- a/mmd_tools/core/model.py +++ b/mmd_tools/core/model.py @@ -9,11 +9,11 @@ import bpy import idprop -import mathutils import rna_prop_ui +from mathutils import Vector from mmd_tools import MMD_TOOLS_VERSION, bpyutils -from mmd_tools.bpyutils import Props, SceneOp +from mmd_tools.bpyutils import FnContext, Props, SceneOp from mmd_tools.core import rigid_body from mmd_tools.core.morph import FnMorph from mmd_tools.core.rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC @@ -23,54 +23,92 @@ from properties.rigid_body import MMDRigidBody -class InvalidRigidSettingException(ValueError): - pass - - class FnModel: @staticmethod def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None): FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {}) @staticmethod - def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]: - if not obj: - return None - if obj.mmd_type == "ROOT": - return obj - return FnModel.find_root(obj.parent) + def find_root_object(obj: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]: + """Find the root object of the model. + Args: + obj (bpy.types.Object): The object to start searching from. + Returns: + 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": + obj = obj.parent + return obj @staticmethod - def find_armature(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + """Find the armature object of the model. + Args: + root_object (bpy.types.Object): The root object of the model. + Returns: + Optional[bpy.types.Object]: The armature object of the model. If the model does not have an armature, None is returned. + """ for o in root_object.children: if o.type == "ARMATURE": return o return None @staticmethod - def find_rigid_group(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def find_rigid_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: for o in root_object.children: if o.type == "EMPTY" and o.mmd_type == "RIGID_GRP_OBJ": return o return None @staticmethod - def find_joint_group(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def __new_group_object(context: bpy.types.Context, name: str, mmd_type: str, parent: bpy.types.Object) -> bpy.types.Object: + group_object = FnContext.new_and_link_object(context, name=name, object_data=None) + group_object.mmd_type = mmd_type + group_object.parent = parent + group_object.hide_set(True) + group_object.hide_select = True + group_object.lock_rotation = group_object.lock_location = group_object.lock_scale = [True, True, True] + return group_object + + @staticmethod + def ensure_rigid_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + rigid_group_object = FnModel.find_rigid_group_object(root_object) + if rigid_group_object is not None: + return rigid_group_object + return FnModel.__new_group_object(context, name="rigidbodies", mmd_type="RIGID_GRP_OBJ", parent=root_object) + + @staticmethod + def find_joint_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: for o in root_object.children: if o.type == "EMPTY" and o.mmd_type == "JOINT_GRP_OBJ": return o return None @staticmethod - def find_temporary_group(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def ensure_joint_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + joint_group_object = FnModel.find_joint_group_object(root_object) + if joint_group_object is not None: + return joint_group_object + return FnModel.__new_group_object(context, name="joints", mmd_type="JOINT_GRP_OBJ", parent=root_object) + + @staticmethod + def find_temporary_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: for o in root_object.children: if o.type == "EMPTY" and o.mmd_type == "TEMPORARY_GRP_OBJ": return o return None + @staticmethod + def ensure_temporary_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + temporary_group_object = FnModel.find_temporary_group_object(root_object) + if temporary_group_object is not None: + return temporary_group_object + return FnModel.__new_group_object(context, name="temporary", mmd_type="TEMPORARY_GRP_OBJ", parent=root_object) + @staticmethod def find_bone_order_mesh_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: - armature_object = FnModel.find_armature(root_object) + armature_object = FnModel.find_armature_object(root_object) if armature_object is None: return None @@ -78,75 +116,74 @@ def find_bone_order_mesh_object(root_object: bpy.types.Object) -> Optional[bpy.t return next(filter(lambda o: o.type == "MESH" and "mmd_bone_order_override" in o.modifiers, armature_object.children), None) @staticmethod - def find_mesh_by_name(root_object: bpy.types.Object, name: str) -> Optional[bpy.types.Object]: - armature_object = FnModel.find_armature(root_object) - if armature_object is None: - return None - for o in FnModel.child_meshes(armature_object): + def find_mesh_object_by_name(root_object: bpy.types.Object, name: str) -> Optional[bpy.types.Object]: + for o in FnModel.iterate_mesh_objects(root_object): if o.name != name: continue return o return None @staticmethod - def all_children(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: - child: bpy.types.Object + def iterate_child_objects(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: for child in obj.children: yield child - yield from FnModel.all_children(child) + yield from FnModel.iterate_child_objects(child) @staticmethod - def filtered_children(condition_function: Callable[[bpy.types.Object], bool], obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: + def iterate_filtered_child_objects(condition_function: Callable[[bpy.types.Object], bool], obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: if obj is None: - return - child: bpy.types.Object + return iter(()) + return FnModel.__iterate_filtered_child_objects_internal(condition_function, obj) + + @staticmethod + def __iterate_filtered_child_objects_internal(condition_function: Callable[[bpy.types.Object], bool], obj: bpy.types.Object) -> Iterator[bpy.types.Object]: for child in obj.children: if condition_function(child): yield child - else: - yield from FnModel.filtered_children(condition_function, child) + yield from FnModel.__iterate_filtered_child_objects_internal(condition_function, child) @staticmethod - def child_meshes(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: - return FnModel.filtered_children(FnModel.is_mesh_object, obj) + def __iterate_child_mesh_objects(obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: + return FnModel.iterate_filtered_child_objects(FnModel.is_mesh_object, obj) + + @staticmethod + def iterate_mesh_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: + return FnModel.__iterate_child_mesh_objects(FnModel.find_armature_object(root_object)) @staticmethod def iterate_rigid_body_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: if root_object.mmd_root.is_built: return itertools.chain( - FnModel.filtered_children(FnModel.is_rigid_body_object, FnModel.find_armature(root_object)), - FnModel.filtered_children(FnModel.is_rigid_body_object, FnModel.find_rigid_group(root_object)), + FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_armature_object(root_object)), + FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)), ) - return FnModel.filtered_children(FnModel.is_rigid_body_object, FnModel.find_rigid_group(root_object)) + return FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)) @staticmethod def iterate_joint_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: - return FnModel.filtered_children(FnModel.is_joint_object, FnModel.find_joint_group(root_object)) + return FnModel.iterate_filtered_child_objects(FnModel.is_joint_object, FnModel.find_joint_group_object(root_object)) @staticmethod def iterate_temporary_objects(root_object: bpy.types.Object, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]: - rigid_body_objects = FnModel.filtered_children(FnModel.is_temporary_object, FnModel.find_rigid_group(root_object)) + rigid_body_objects = FnModel.iterate_filtered_child_objects(FnModel.is_temporary_object, FnModel.find_rigid_group_object(root_object)) if rigid_track_only: return rigid_body_objects - temporary_group_object = FnModel.find_temporary_group(root_object) + temporary_group_object = FnModel.find_temporary_group_object(root_object) if temporary_group_object is None: return rigid_body_objects - return itertools.chain(rigid_body_objects, FnModel.filtered_children(FnModel.is_temporary_object, temporary_group_object)) + return itertools.chain(rigid_body_objects, FnModel.__iterate_filtered_child_objects_internal(FnModel.is_temporary_object, temporary_group_object)) @staticmethod - def iterate_materials(root_object: bpy.types.Object) -> Iterable[bpy.types.Material]: - armature_object = FnModel.find_armature(root_object) - if armature_object is None: - return [] - return (material for mesh_object in FnModel.child_meshes(armature_object) for material in cast(bpy.types.Mesh, mesh_object.data).materials if material is not None) + def iterate_materials(root_object: bpy.types.Object) -> Iterator[bpy.types.Material]: + return (material for mesh_object in FnModel.iterate_mesh_objects(root_object) for material in cast(bpy.types.Mesh, mesh_object.data).materials if material is not None) @staticmethod - def iterate_unique_materials(root_object: bpy.types.Object) -> Iterable[bpy.types.Material]: + def iterate_unique_materials(root_object: bpy.types.Object) -> Iterator[bpy.types.Material]: materials: Dict[bpy.types.Material, None] = {} # use dict because set does not guarantee the order materials.update((material, None) for material in FnModel.iterate_materials(root_object)) - return materials.keys() + return iter(materials.keys()) @staticmethod def is_root_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: @@ -168,32 +205,9 @@ def is_temporary_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types. def is_mesh_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" - @staticmethod - def get_rigid_body_size(obj: bpy.types.Object): - assert obj.mmd_type == "RIGID_BODY" - - x0, y0, z0 = obj.bound_box[0] - x1, y1, z1 = obj.bound_box[6] - assert x1 >= x0 and y1 >= y0 and z1 >= z0 - - shape = obj.mmd_rigid.shape - if shape == "SPHERE": - radius = (z1 - z0) / 2 - return (radius, 0.0, 0.0) - elif shape == "BOX": - x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2 - return (x, y, z) - elif shape == "CAPSULE": - diameter = x1 - x0 - radius = diameter / 2 - height = abs((z1 - z0) - diameter) - return (radius, height, 0.0) - else: - raise ValueError(f"Invalid shape type: {shape}") - @staticmethod def join_models(parent_root_object: bpy.types.Object, child_root_objects: List[bpy.types.Object]): - parent_armature_object = FnModel.find_armature(parent_root_object) + parent_armature_object = FnModel.find_armature_object(parent_root_object) with bpy.context.temp_override( active_object=parent_armature_object, selected_editable_objects=[parent_armature_object], @@ -228,7 +242,7 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos child_root_object: bpy.types.Object for child_root_object in child_root_objects: - child_armature_object = FnModel.find_armature(child_root_object) + child_armature_object = FnModel.find_armature_object(child_root_object) child_pose_bones = child_armature_object.pose.bones child_bone_morphs = child_root_object.mmd_root.bone_morphs @@ -257,7 +271,7 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos try: # replace mesh armature modifier.object mesh: bpy.types.Object - for mesh in FnModel.child_meshes(child_armature_object): + for mesh in FnModel.__iterate_child_mesh_objects(child_armature_object): with bpy.context.temp_override( active_object=mesh, selected_editable_objects=[mesh], @@ -276,15 +290,15 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos ): bpy.ops.object.join() - for mesh in FnModel.child_meshes(parent_armature_object): + for mesh in FnModel.__iterate_child_mesh_objects(parent_armature_object): armature_modifier: bpy.types.ArmatureModifier = mesh.modifiers["mmd_bone_order_override"] if "mmd_bone_order_override" in mesh.modifiers else mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") if armature_modifier.object is None: armature_modifier.object = parent_armature_object mesh.matrix_parent_inverse = child_armature_matrix - child_rigid_group_object = FnModel.find_rigid_group(child_root_object) + child_rigid_group_object = FnModel.find_rigid_group_object(child_root_object) if child_rigid_group_object is not None: - parent_rigid_group_object = FnModel.find_rigid_group(parent_root_object) + parent_rigid_group_object = FnModel.find_rigid_group_object(parent_root_object) with bpy.context.temp_override( object=parent_rigid_group_object, @@ -293,9 +307,9 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.data.objects.remove(child_rigid_group_object) - child_joint_group_object = FnModel.find_joint_group(child_root_object) + child_joint_group_object = FnModel.find_joint_group_object(child_root_object) if child_joint_group_object is not None: - parent_joint_group_object = FnModel.find_joint_group(parent_root_object) + parent_joint_group_object = FnModel.find_joint_group_object(parent_root_object) with bpy.context.temp_override( object=parent_joint_group_object, selected_editable_objects=[parent_joint_group_object, *FnModel.iterate_joint_objects(child_root_object)], @@ -303,16 +317,16 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.data.objects.remove(child_joint_group_object) - child_temporary_group_object = FnModel.find_temporary_group(child_root_object) + child_temporary_group_object = FnModel.find_temporary_group_object(child_root_object) if child_temporary_group_object is not None: - parent_temporary_group_object = FnModel.find_temporary_group(parent_root_object) + parent_temporary_group_object = FnModel.find_temporary_group_object(parent_root_object) with bpy.context.temp_override( object=parent_temporary_group_object, selected_editable_objects=[parent_temporary_group_object, *FnModel.iterate_temporary_objects(child_root_object)], ): bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) - for obj in list(FnModel.all_children(child_temporary_group_object)): + for obj in list(FnModel.iterate_child_objects(child_temporary_group_object)): bpy.data.objects.remove(obj) bpy.data.objects.remove(child_temporary_group_object) @@ -324,11 +338,13 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos @staticmethod def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier: - if any(m.type == "ARMATURE" for m in mesh_object.modifiers): + for m in mesh_object.modifiers: + if m.type != "ARMATURE": + continue # already has armature modifier. - return + return cast(bpy.types.ArmatureModifier, m) - modifier: bpy.types.ArmatureModifier = mesh_object.modifiers.new(name="Armature", type="ARMATURE") + modifier = cast(bpy.types.ArmatureModifier, mesh_object.modifiers.new(name="Armature", type="ARMATURE")) modifier.object = armature_object modifier.use_vertex_groups = True modifier.name = "mmd_bone_order_override" @@ -336,8 +352,10 @@ def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.t return modifier @staticmethod - def attach_meshes(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool): - armature_object: bpy.types.Object = FnModel.find_armature(parent_root_object) + def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool): + armature_object = FnModel.find_armature_object(parent_root_object) + if armature_object is None: + raise ValueError(f"Armature object not found in {parent_root_object}") def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: if obj.parent is None: @@ -345,11 +363,10 @@ def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: return __get_root_object(obj.parent) for mesh_object in mesh_objects: - if mesh_object.type != "MESH": + if not FnModel.is_mesh_object(mesh_object): continue - if mesh_object.mmd_type != "NONE": - continue - if FnModel.find_root(mesh_object) is not None: + + if FnModel.find_root_object(mesh_object) is not None: continue mesh_root_object = __get_root_object(mesh_object) @@ -363,15 +380,17 @@ def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: @staticmethod def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool): + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + raise ValueError(f"Armature object not found in {root_object}") + vertex_group_names: Set[str] = set() - search_meshes = FnModel.child_meshes(root_object) if search_in_all_meshes else [mesh_object] + search_meshes = FnModel.iterate_mesh_objects(root_object) if search_in_all_meshes else [mesh_object] for search_mesh in search_meshes: vertex_group_names.update(search_mesh.vertex_groups.keys()) - armature_object = FnModel.find_armature(root_object) - pose_bone: bpy.types.PoseBone for pose_bone in armature_object.pose.bones: pose_bone_name = pose_bone.name @@ -392,10 +411,9 @@ def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: if new_ik_loop_factor == old_ik_loop_factor: return - armature_object = FnModel.find_armature(root_object) + armature_object = FnModel.find_armature_object(root_object) for pose_bone in armature_object.pose.bones: - constraint: bpy.types.KinematicConstraint - for constraint in (c for c in pose_bone.constraints if c.type == "IK"): + for constraint in (cast(bpy.types.KinematicConstraint, c) for c in pose_bone.constraints if c.type == "IK"): iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor) logging.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations) constraint.iterations = iterations @@ -472,6 +490,35 @@ def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_pr else: raise ValueError(f"Unsupported destination: {destination}") + @staticmethod + def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True): + frames = root_object.mmd_root.display_item_frames + if reset and len(frames) > 0: + root_object.mmd_root.active_display_item_frame = 0 + frames.clear() + + frame_names = {"Root": "Root", "表情": "Facial"} + + for frame_name, frame_name_e in frame_names.items(): + frame = frames.get(frame_name, None) or frames.add() + frame.name = frame_name + frame.name_e = frame_name_e + frame.is_special = True + + arm = FnModel.find_armature_object(root_object) + if arm is not None and len(arm.data.bones) > 0 and len(frames[0].data) < 1: + item = frames[0].data.add() + item.type = "BONE" + item.name = arm.data.bones[0].name + + if not reset: + frames.move(frames.find("Root"), 0) + frames.move(frames.find("表情"), 1) + + @staticmethod + def get_empty_display_size(root_object: bpy.types.Object) -> float: + return getattr(root_object, Props.empty_display_size) + class MigrationFnModel: """Migration Functions for old MMD models broken by bugs or issues""" @@ -485,7 +532,7 @@ def update_mmd_ik_loop_factor(cls): if "mmd_ik_loop_factor" not in armature_object: return - FnModel.find_root(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1) + FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1) del armature_object["mmd_ik_loop_factor"] @staticmethod @@ -560,35 +607,12 @@ def create(name, name_e="", scale=1, obj_name=None, armature=None, add_root_bone bpyutils.select_object(root) return Model(root) - @classmethod - def findRoot(cls, obj): - return FnModel.find_root(obj) + @staticmethod + def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]: + return FnModel.find_root_object(obj) def initialDisplayFrames(self, reset=True): - frames = self.__root.mmd_root.display_item_frames - if reset and len(frames): - self.__root.mmd_root.active_display_item_frame = 0 - frames.clear() - - frame_root = frames.get("Root", None) or frames.add() - frame_root.name = "Root" - frame_root.name_e = "Root" - frame_root.is_special = True - - frame_facial = frames.get("表情", None) or frames.add() - frame_facial.name = "表情" - frame_facial.name_e = "Facial" - frame_facial.is_special = True - - arm = self.armature() - if arm and len(arm.data.bones) and len(frame_root.data) < 1: - item = frame_root.data.add() - item.type = "BONE" - item.name = arm.data.bones[0].name - - if not reset: - frames.move(frames.find("Root"), 0) - frames.move(frames.find("表情"), 1) + FnModel.initalize_display_item_frames(self.__root, reset=reset) @property def morph_slider(self): @@ -597,201 +621,6 @@ def morph_slider(self): def loadMorphs(self): FnMorph.load_morphs(self) - def createRigidBodyPool(self, counts: int) -> List[bpy.types.Object]: - if counts < 1: - return [] - obj = bpyutils.createObject(name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody")) - obj.parent = self.rigidGroupObject() - obj.mmd_type = "RIGID_BODY" - obj.rotation_mode = "YXZ" - setattr(obj, Props.display_type, "SOLID") - obj.show_transparent = True - obj.hide_render = True - if hasattr(obj, "display"): - obj.display.show_shadows = False - if hasattr(obj, "cycles_visibility"): - for attr_name in ("camera", "diffuse", "glossy", "scatter", "shadow", "transmission"): - if hasattr(obj.cycles_visibility, attr_name): - setattr(obj.cycles_visibility, attr_name, False) - - bpy.ops.rigidbody.object_add(type="ACTIVE") - if counts == 1: - return [obj] - return bpyutils.duplicateObject(obj, counts) - - def createRigidBody(self, **kwargs): - """Create a object for MMD rigid body dynamics. - ### Parameters ### - @param shape_type the shape type. - @param location location of the rigid body object. - @param rotation rotation of the rigid body object. - @param size - @param dynamics_type the type of dynamics mode. (STATIC / DYNAMIC / DYNAMIC2) - @param collision_group_number - @param collision_group_mask list of boolean values. (length:16) - @param name Object name (Optional) - @param name_e English object name (Optional) - @param bone - """ - - shape_type = kwargs["shape_type"] - location = kwargs["location"] - rotation = kwargs["rotation"] - size = kwargs["size"] - dynamics_type = kwargs["dynamics_type"] - collision_group_number = kwargs.get("collision_group_number") - collision_group_mask = kwargs.get("collision_group_mask") - name = kwargs.get("name") - name_e = kwargs.get("name_e") - bone = kwargs.get("bone") - - friction = kwargs.get("friction") - mass = kwargs.get("mass") - angular_damping = kwargs.get("angular_damping") - linear_damping = kwargs.get("linear_damping") - bounce = kwargs.get("bounce") - - obj: Optional[bpy.types.Object] = kwargs.get("obj", None) - if obj is None: - obj = self.createRigidBodyPool(1)[0] - - obj.location = location - obj.rotation_euler = rotation - - obj.mmd_rigid.shape = rigid_body.collisionShape(shape_type) - obj.mmd_rigid.size = size - obj.mmd_rigid.type = str(dynamics_type) if dynamics_type in range(3) else "1" - - if collision_group_number is not None: - obj.mmd_rigid.collision_group_number = collision_group_number - if collision_group_mask is not None: - obj.mmd_rigid.collision_group_mask = collision_group_mask - if name is not None: - obj.name = name - obj.mmd_rigid.name_j = name - if name_e is not None: - obj.mmd_rigid.name_e = name_e - - obj.mmd_rigid.bone = bone if bone else "" - - rb = obj.rigid_body - if friction is not None: - rb.friction = friction - if mass is not None: - rb.mass = mass - if angular_damping is not None: - rb.angular_damping = angular_damping - if linear_damping is not None: - rb.linear_damping = linear_damping - if bounce: - rb.restitution = bounce - - obj.select_set(False) - return obj - - def createJointPool(self, counts): - if counts < 1: - return [] - obj = bpyutils.createObject(name="Joint", object_data=None) - obj.parent = self.jointGroupObject() - obj.mmd_type = "JOINT" - obj.rotation_mode = "YXZ" - setattr(obj, Props.empty_display_type, "ARROWS") - setattr(obj, Props.empty_display_size, 0.1 * getattr(self.__root, Props.empty_display_size)) - obj.hide_render = True - - if bpy.ops.rigidbody.world_add.poll(): - bpy.ops.rigidbody.world_add() - bpy.ops.rigidbody.constraint_add(type="GENERIC_SPRING") - rbc = obj.rigid_body_constraint - rbc.disable_collisions = False - rbc.use_limit_ang_x = True - rbc.use_limit_ang_y = True - rbc.use_limit_ang_z = True - rbc.use_limit_lin_x = True - rbc.use_limit_lin_y = True - rbc.use_limit_lin_z = True - rbc.use_spring_x = True - rbc.use_spring_y = True - rbc.use_spring_z = True - if hasattr(rbc, "use_spring_ang_x"): - rbc.use_spring_ang_x = True - rbc.use_spring_ang_y = True - rbc.use_spring_ang_z = True - if counts == 1: - return [obj] - return bpyutils.duplicateObject(obj, counts) - - def createJoint(self, **kwargs): - """Create a joint object for MMD rigid body dynamics. - ### Parameters ### - @param shape_type the shape type. - @param location location of the rigid body object. - @param rotation rotation of the rigid body object. - @param size - @param dynamics_type the type of dynamics mode. (STATIC / DYNAMIC / DYNAMIC2) - @param collision_group_number - @param collision_group_mask list of boolean values. (length:16) - @param name Object name - @param name_e English object name (Optional) - @param bone - """ - - location = kwargs["location"] - rotation = kwargs["rotation"] - - rigid_a = kwargs["rigid_a"] - rigid_b = kwargs["rigid_b"] - - max_loc = kwargs["maximum_location"] - min_loc = kwargs["minimum_location"] - max_rot = kwargs["maximum_rotation"] - min_rot = kwargs["minimum_rotation"] - spring_angular = kwargs["spring_angular"] - spring_linear = kwargs["spring_linear"] - - name = kwargs["name"] - name_e = kwargs.get("name_e") - - obj = kwargs.get("obj", None) - if obj is None: - obj = self.createJointPool(1)[0] - - obj.name = "J." + name - obj.mmd_joint.name_j = name - if name_e is not None: - obj.mmd_joint.name_e = name_e - - obj.location = location - obj.rotation_euler = rotation - - rbc = obj.rigid_body_constraint - - rbc.object1 = rigid_a - rbc.object2 = rigid_b - - rbc.limit_lin_x_upper = max_loc[0] - rbc.limit_lin_y_upper = max_loc[1] - rbc.limit_lin_z_upper = max_loc[2] - - rbc.limit_lin_x_lower = min_loc[0] - rbc.limit_lin_y_lower = min_loc[1] - rbc.limit_lin_z_lower = min_loc[2] - - rbc.limit_ang_x_upper = max_rot[0] - rbc.limit_ang_y_upper = max_rot[1] - rbc.limit_ang_z_upper = max_rot[2] - - rbc.limit_ang_x_lower = min_rot[0] - rbc.limit_ang_y_lower = min_rot[1] - rbc.limit_ang_z_lower = min_rot[2] - - obj.mmd_joint.spring_linear = spring_linear - obj.mmd_joint.spring_angular = spring_angular - - obj.select_set(False) - return obj - def create_ik_constraint(self, bone, ik_target): """create IK constraint @@ -814,22 +643,22 @@ def allObjects(self, obj: Optional[bpy.types.Object] = None) -> Iterator[bpy.typ if obj is None: obj: bpy.types.Object = self.__root yield obj - yield from FnModel.all_children(obj) + yield from FnModel.iterate_child_objects(obj) def rootObject(self): return self.__root def armature(self): if self.__arm is None: - self.__arm = FnModel.find_armature(self.__root) + self.__arm = FnModel.find_armature_object(self.__root) return self.__arm def hasRigidGroupObject(self): - return FnModel.find_rigid_group(self.__root) is not None + return FnModel.find_rigid_group_object(self.__root) is not None def rigidGroupObject(self): if self.__rigid_grp is None: - self.__rigid_grp = FnModel.find_rigid_group(self.__root) + self.__rigid_grp = FnModel.find_rigid_group_object(self.__root) if self.__rigid_grp is None: rigids = bpy.data.objects.new(name="rigidbodies", object_data=None) SceneOp(bpy.context).link_object(rigids) @@ -842,11 +671,11 @@ def rigidGroupObject(self): return self.__rigid_grp def hasJointGroupObject(self): - return FnModel.find_joint_group(self.__root) is not None + return FnModel.find_joint_group_object(self.__root) is not None def jointGroupObject(self): if self.__joint_grp is None: - self.__joint_grp = FnModel.find_joint_group(self.__root) + self.__joint_grp = FnModel.find_joint_group_object(self.__root) if self.__joint_grp is None: joints = bpy.data.objects.new(name="joints", object_data=None) SceneOp(bpy.context).link_object(joints) @@ -859,11 +688,11 @@ def jointGroupObject(self): return self.__joint_grp def hasTemporaryGroupObject(self): - return FnModel.find_temporary_group(self.__root) is not None + return FnModel.find_temporary_group_object(self.__root) is not None def temporaryGroupObject(self): if self.__temporary_grp is None: - self.__temporary_grp = FnModel.find_temporary_group(self.__root) + self.__temporary_grp = FnModel.find_temporary_group_object(self.__root) if self.__temporary_grp is None: temporarys = bpy.data.objects.new(name="temporary", object_data=None) SceneOp(bpy.context).link_object(temporarys) @@ -876,13 +705,10 @@ def temporaryGroupObject(self): return self.__temporary_grp def meshes(self): - arm = self.armature() - if arm is None: - return [] - return FnModel.child_meshes(arm) + return FnModel.iterate_mesh_objects(self.__root) def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True): - FnModel.attach_meshes(self.rootObject(), meshes, add_armature_modifier) + FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier) def firstMesh(self): for i in self.meshes(): @@ -1033,31 +859,6 @@ def __removeTemporaryObjects(self): with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()): bpy.ops.object.delete() - def __removeChildrenOfTemporaryGroupObject(self): - tmp_grp_obj = self.temporaryGroupObject() - tmp_cnt = len(tmp_grp_obj.children) - if tmp_cnt == 0: - return - logging.debug(" Removing %d children of temporary group object", tmp_cnt) - start_time = time.time() - total_cnt = len(bpy.data.objects) - layer_index = bpy.context.scene.active_layer - try: - bpy.ops.object.mode_set(mode="OBJECT") - except Exception: - pass - for i in bpy.context.selected_objects: - i.select_set(False) - for i in tmp_grp_obj.children: - i.hide_select = False - i.hide_set(False) - i.select_set(True) - i.layers[layer_index] = True - assert len(bpy.context.selected_objects) == tmp_cnt - bpy.ops.object.delete() - assert len(bpy.data.objects) == total_cnt - tmp_cnt - logging.debug(" - Done in %f seconds.", time.time() - start_time) - def __restoreTransforms(self, obj): for attr in ("location", "rotation_euler"): attr_name = "__backup_%s__" % attr @@ -1241,7 +1042,7 @@ def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float): rb.collision_shape = rigid.shape def __getRigidRange(self, obj): - return (mathutils.Vector(obj.bound_box[0]) - mathutils.Vector(obj.bound_box[6])).length + return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length def __createNonCollisionConstraint(self, nonCollisionJointTable): total_len = len(nonCollisionJointTable) diff --git a/mmd_tools/core/pmx/exporter.py b/mmd_tools/core/pmx/exporter.py index 58bbe36..0edf644 100644 --- a/mmd_tools/core/pmx/exporter.py +++ b/mmd_tools/core/pmx/exporter.py @@ -975,28 +975,8 @@ def __triangulate(mesh, custom_normals): @staticmethod def __get_normals(mesh, matrix): - custom_normals = None - if hasattr(mesh, "has_custom_normals"): - logging.debug(" - Calculating normals split...") - mesh.calc_normals_split() - custom_normals = [(matrix @ l.normal).normalized() for l in mesh.loops] - mesh.free_normals_split() - elif mesh.use_auto_smooth: - logging.debug(" - Calculating normals split (angle:%f)...", mesh.auto_smooth_angle) - mesh.calc_normals_split(mesh.auto_smooth_angle) - custom_normals = [(matrix @ l.normal).normalized() for l in mesh.loops] - mesh.free_normals_split() - else: - logging.debug(" - Calculating normals...") - mesh.calc_normals() - custom_normals = [] - for f in mesh.polygons: - if f.use_smooth: - for v in f.vertices: - custom_normals.append((matrix @ mesh.vertices[v].normal).normalized()) - else: - for v in f.vertices: - custom_normals.append((matrix @ f.normal).normalized()) + logging.debug(" - Get normals...") + custom_normals = [(matrix @ cn.vector).normalized() for cn in mesh.corner_normals] logging.debug(" - Done (polygons:%d)", len(mesh.polygons)) return custom_normals diff --git a/mmd_tools/core/pmx/importer.py b/mmd_tools/core/pmx/importer.py index 86217fa..b9086e6 100644 --- a/mmd_tools/core/pmx/importer.py +++ b/mmd_tools/core/pmx/importer.py @@ -6,6 +6,7 @@ import logging import os import time +from typing import List, TYPE_CHECKING import bpy from mathutils import Matrix, Vector @@ -16,10 +17,14 @@ from mmd_tools.core.material import FnMaterial from mmd_tools.core.model import FnModel, Model from mmd_tools.core.morph import FnMorph +from mmd_tools.core.rigid_body import FnRigidBody from mmd_tools.core.vmd.importer import BoneConverter from mmd_tools.operators.display_item import DisplayItemQuickSetup from mmd_tools.operators.misc import MoveObject +if TYPE_CHECKING: + from mmd_tools.properties.pose_bone import MMDBone + class PMXImporter: CATEGORIES = { @@ -282,8 +287,8 @@ def _VectorXZY(v): return nameTable, specialTipBones - def __sortPoseBonesByBoneIndex(self, pose_bones, bone_names): - r = [] + def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names): + r: List[bpy.types.PoseBone] = [] for i in bone_names: r.append(pose_bones[i]) return r @@ -418,7 +423,7 @@ def __importBones(self): self.__boneTable = pose_bones for i, pmx_bone in sorted(enumerate(pmxModel.bones), key=lambda x: x[1].transform_order): b_bone = pose_bones[i] - mmd_bone = b_bone.mmd_bone + mmd_bone: MMDBone = b_bone.mmd_bone mmd_bone.name_j = b_bone.name # pmx_bone.name mmd_bone.name_e = pmx_bone.name_e mmd_bone.is_controllable = pmx_bone.isControllable @@ -467,24 +472,24 @@ def __importBones(self): def __importRigids(self): start_time = time.time() self.__rigidTable = {} - rigid_pool = self.__rig.createRigidBodyPool(len(self.__model.rigids)) + context = bpyutils.FnContext.ensure_context() + rigid_pool = FnRigidBody.new_rigid_body_objects(context, FnModel.ensure_rigid_group_object(context, self.__rig.rootObject()), len(self.__model.rigids)) for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)): loc = Vector(rigid.location).xzy * self.__scale rot = Vector(rigid.rotation).xzy * -1 size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size) - obj = self.__rig.createRigidBody( + obj = FnRigidBody.setup_rigid_body_object( obj=rigid_obj, - name=rigid.name, - name_e=rigid.name_e, shape_type=rigid.type, - dynamics_type=rigid.mode, location=loc, rotation=rot, size=size * self.__scale, + dynamics_type=rigid.mode, + name=rigid.name, + name_e=rigid.name_e, collision_group_number=rigid.collision_group_number, collision_group_mask=[rigid.collision_group_mask & (1 << i) == 0 for i in range(16)], - arm_obj=self.__armObj, mass=rigid.mass, friction=rigid.friction, angular_damping=rigid.rotation_attenuation, @@ -500,12 +505,13 @@ def __importRigids(self): def __importJoints(self): start_time = time.time() - joint_pool = self.__rig.createJointPool(len(self.__model.joints)) + context = bpyutils.FnContext.ensure_context() + joint_pool = FnRigidBody.new_joint_objects(context, FnModel.ensure_joint_group_object(context, self.__rig.rootObject()), len(self.__model.joints), FnModel.get_empty_display_size(self.__rig.rootObject())) for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)): loc = Vector(joint.location).xzy * self.__scale rot = Vector(joint.rotation).xzy * -1 - obj = self.__rig.createJoint( + obj = FnRigidBody.setup_joint_object( obj=joint_obj, name=joint.name, name_e=joint.name_e, @@ -783,10 +789,7 @@ def __addArmatureModifier(self, meshObj, armObj): armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0 def __assignCustomNormals(self): - mesh = self.__meshObj.data - if not hasattr(mesh, "has_custom_normals"): - logging.info(" * No support for custom normals!!") - return + mesh: bpy.types.Mesh = self.__meshObj.data logging.info("Setting custom normals...") if self.__vertex_map: verts, faces = self.__model.vertices, self.__model.faces @@ -795,7 +798,6 @@ def __assignCustomNormals(self): else: custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices] mesh.normals_split_custom_set_from_vertices(custom_normals) - mesh.use_auto_smooth = True logging.info(" - Done!!") def __renameLRBones(self, use_underscore): diff --git a/mmd_tools/core/rigid_body.py b/mmd_tools/core/rigid_body.py index fa12460..98df678 100644 --- a/mmd_tools/core/rigid_body.py +++ b/mmd_tools/core/rigid_body.py @@ -2,7 +2,12 @@ # Copyright 2014 MMD Tools authors # This file is part of MMD Tools. +from typing import List, Optional + import bpy +from mathutils import Euler, Vector + +from mmd_tools.bpyutils import FnContext, Props SHAPE_SPHERE = 0 SHAPE_BOX = 1 @@ -75,3 +80,207 @@ def getMaterial(cls, number): else: mat = bpy.data.materials[material_name] return mat + + +class FnRigidBody: + @staticmethod + def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]: + if count < 1: + return [] + + obj = FnRigidBody.new_rigid_body_object(context, parent_object) + + if count == 1: + return [obj] + + return FnContext.duplicate_object(context, obj, count) + + @staticmethod + def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object: + obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody")) + obj.parent = parent_object + obj.mmd_type = "RIGID_BODY" + obj.rotation_mode = "YXZ" + setattr(obj, Props.display_type, "SOLID") + obj.show_transparent = True + obj.hide_render = True + obj.display.show_shadows = False + + with context.temp_override(object=obj): + bpy.ops.rigidbody.object_add(type="ACTIVE") + + return obj + + @staticmethod + def setup_rigid_body_object( + obj: bpy.types.Object, + shape_type: str, + location: Vector, + rotation: Euler, + size: Vector, + dynamics_type: str, + collision_group_number: Optional[int] = None, + collision_group_mask: Optional[List[bool]] = None, + name: Optional[str] = None, + name_e: Optional[str] = None, + bone: Optional[str] = None, + friction: Optional[float] = None, + mass: Optional[float] = None, + angular_damping: Optional[float] = None, + linear_damping: Optional[float] = None, + bounce: Optional[float] = None, + ) -> bpy.types.Object: + obj.location = location + obj.rotation_euler = rotation + + obj.mmd_rigid.shape = collisionShape(shape_type) + obj.mmd_rigid.size = size + obj.mmd_rigid.type = str(dynamics_type) if dynamics_type in range(3) else "1" + + if collision_group_number is not None: + obj.mmd_rigid.collision_group_number = collision_group_number + + if collision_group_mask is not None: + obj.mmd_rigid.collision_group_mask = collision_group_mask + + if name is not None: + obj.name = name + obj.mmd_rigid.name_j = name + obj.data.name = name + + if name_e is not None: + obj.mmd_rigid.name_e = name_e + + if bone is not None: + obj.mmd_rigid.bone = bone + else: + obj.mmd_rigid.bone = "" + + rb = obj.rigid_body + if friction is not None: + rb.friction = friction + if mass is not None: + rb.mass = mass + if angular_damping is not None: + rb.angular_damping = angular_damping + if linear_damping is not None: + rb.linear_damping = linear_damping + if bounce is not None: + rb.restitution = bounce + + return obj + + @staticmethod + def get_rigid_body_size(obj: bpy.types.Object): + assert obj.mmd_type == "RIGID_BODY" + + x0, y0, z0 = obj.bound_box[0] + x1, y1, z1 = obj.bound_box[6] + assert x1 >= x0 and y1 >= y0 and z1 >= z0 + + shape = obj.mmd_rigid.shape + if shape == "SPHERE": + radius = (z1 - z0) / 2 + return (radius, 0.0, 0.0) + elif shape == "BOX": + x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2 + return (x, y, z) + elif shape == "CAPSULE": + diameter = x1 - x0 + radius = diameter / 2 + height = abs((z1 - z0) - diameter) + return (radius, height, 0.0) + else: + raise ValueError(f"Invalid shape type: {shape}") + + @staticmethod + def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object: + obj = FnContext.new_and_link_object(context, name="Joint", object_data=None) + obj.parent = parent_object + obj.mmd_type = "JOINT" + obj.rotation_mode = "YXZ" + setattr(obj, Props.empty_display_type, "ARROWS") + setattr(obj, Props.empty_display_size, 0.1 * empty_display_size) + obj.hide_render = True + + with context.temp_override(): + context.view_layer.objects.active = obj + bpy.ops.rigidbody.constraint_add(type="GENERIC_SPRING") + + rigid_body_constraint = obj.rigid_body_constraint + rigid_body_constraint.disable_collisions = False + rigid_body_constraint.use_limit_ang_x = True + rigid_body_constraint.use_limit_ang_y = True + rigid_body_constraint.use_limit_ang_z = True + rigid_body_constraint.use_limit_lin_x = True + rigid_body_constraint.use_limit_lin_y = True + rigid_body_constraint.use_limit_lin_z = True + rigid_body_constraint.use_spring_x = True + rigid_body_constraint.use_spring_y = True + rigid_body_constraint.use_spring_z = True + rigid_body_constraint.use_spring_ang_x = True + rigid_body_constraint.use_spring_ang_y = True + rigid_body_constraint.use_spring_ang_z = True + + return obj + + @staticmethod + def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]: + if count < 1: + return [] + + obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size) + + if count == 1: + return [obj] + + return FnContext.duplicate_object(context, obj, count) + + @staticmethod + def setup_joint_object( + obj: bpy.types.Object, + location: Vector, + rotation: Euler, + rigid_a: bpy.types.Object, + rigid_b: bpy.types.Object, + maximum_location: Vector, + minimum_location: Vector, + maximum_rotation: Euler, + minimum_rotation: Euler, + spring_angular: Vector, + spring_linear: Vector, + name: str, + name_e: Optional[str] = None, + ) -> bpy.types.Object: + obj.name = f"J.{name}" + + obj.location = location + obj.rotation_euler = rotation + + rigid_body_constraint = obj.rigid_body_constraint + rigid_body_constraint.object1 = rigid_a + rigid_body_constraint.object2 = rigid_b + rigid_body_constraint.limit_lin_x_upper = maximum_location.x + rigid_body_constraint.limit_lin_y_upper = maximum_location.y + rigid_body_constraint.limit_lin_z_upper = maximum_location.z + + rigid_body_constraint.limit_lin_x_lower = minimum_location.x + rigid_body_constraint.limit_lin_y_lower = minimum_location.y + rigid_body_constraint.limit_lin_z_lower = minimum_location.z + + rigid_body_constraint.limit_ang_x_upper = maximum_rotation.x + rigid_body_constraint.limit_ang_y_upper = maximum_rotation.y + rigid_body_constraint.limit_ang_z_upper = maximum_rotation.z + + rigid_body_constraint.limit_ang_x_lower = minimum_rotation.x + rigid_body_constraint.limit_ang_y_lower = minimum_rotation.y + rigid_body_constraint.limit_ang_z_lower = minimum_rotation.z + + obj.mmd_joint.name_j = name + if name_e is not None: + obj.mmd_joint.name_e = name_e + + obj.mmd_joint.spring_linear = spring_linear + obj.mmd_joint.spring_angular = spring_angular + + return obj diff --git a/mmd_tools/core/shader.py b/mmd_tools/core/shader.py index d2d9028..cab9dba 100644 --- a/mmd_tools/core/shader.py +++ b/mmd_tools/core/shader.py @@ -6,7 +6,7 @@ class _NodeTreeUtils: - def __init__(self, shader): + def __init__(self, shader: bpy.types.NodeTree): self.shader, self.nodes, self.links = shader, shader.nodes, shader.links def _find_node(self, node_type): @@ -53,7 +53,7 @@ def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None): class _NodeGroupUtils(_NodeTreeUtils): - def __init__(self, shader): + def __init__(self, shader: bpy.types.NodeTree): super().__init__(shader) self.__node_input = self.__node_output = None diff --git a/mmd_tools/core/translations.py b/mmd_tools/core/translations.py index d410dda..a5853b4 100644 --- a/mmd_tools/core/translations.py +++ b/mmd_tools/core/translations.py @@ -116,7 +116,7 @@ def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTran @classmethod def collect_data(cls, mmd_translation: "MMDTranslation"): - armature_object: bpy.types.Object = FnModel.find_armature(mmd_translation.id_data) + armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) pose_bone: bpy.types.PoseBone for index, pose_bone in enumerate(armature_object.pose.bones): if not any(c.is_visible for c in pose_bone.bone.collections): @@ -279,7 +279,7 @@ def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTran def collect_data(cls, mmd_translation: "MMDTranslation"): checked_materials: Set[bpy.types.Material] = set() mesh_object: bpy.types.Object - for mesh_object in FnModel.child_meshes(FnModel.find_armature(mmd_translation.id_data)): + for mesh_object in FnModel.iterate_mesh_objects(mmd_translation.id_data): material: bpy.types.Material for index, material in enumerate(mesh_object.data.materials): if material in checked_materials: @@ -369,7 +369,7 @@ def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTran @classmethod def collect_data(cls, mmd_translation: "MMDTranslation"): - armature_object: bpy.types.Object = FnModel.find_armature(mmd_translation.id_data) + armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) bone_collection: bpy.types.BoneCollection for index, bone_collection in enumerate(armature_object.data.collections): mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() @@ -559,10 +559,12 @@ def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTran @classmethod def collect_data(cls, mmd_translation: "MMDTranslation"): root_object: bpy.types.Object = mmd_translation.id_data - armature_object: bpy.types.Object = FnModel.find_armature(root_object) + info_objects = [root_object] + armature_object = FnModel.find_armature_object(root_object) + if armature_object is not None: + info_objects.append(armature_object) - info_object: bpy.types.Object - for info_object in itertools.chain([root_object, armature_object], FnModel.child_meshes(armature_object)): + for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)): mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.INFO.name mmd_translation_element.object = info_object diff --git a/mmd_tools/core/vpd/exporter.py b/mmd_tools/core/vpd/exporter.py index f01fca0..7dd37a8 100644 --- a/mmd_tools/core/vpd/exporter.py +++ b/mmd_tools/core/vpd/exporter.py @@ -57,9 +57,10 @@ def __exportBones(self, armObj, converters=None, matrix_basis_map=None): vpd_bones.append(vpd.VpdBone(bone_name, location, [x, y, z, w])) return vpd_bones - def __exportPoseLib(self, armObj, pose_type, filepath, use_pose_mode=False): + def __exportPoseLib(self, armObj: bpy.types.Object, pose_type, filepath, use_pose_mode=False): if armObj is None: return None + # FIXME: armObj.pose_library is None when the armature is not in pose mode if armObj.pose_library is None: return None diff --git a/mmd_tools/core/vpd/importer.py b/mmd_tools/core/vpd/importer.py index 6fd9a43..8bd413c 100644 --- a/mmd_tools/core/vpd/importer.py +++ b/mmd_tools/core/vpd/importer.py @@ -34,7 +34,7 @@ def __assignToArmaturePoseMode(self, armObj): for bone, matrix_basis in pose_orig.items(): bone.matrix_basis = matrix_basis - def __assignToArmatureSimple(self, armObj, reset_transform=True): + def __assignToArmatureSimple(self, armObj: bpy.types.Object, reset_transform=True): logging.info(' - assigning to armature "%s"', armObj.name) pose_bones = armObj.pose.bones @@ -61,6 +61,7 @@ def __assignToArmatureSimple(self, armObj, reset_transform=True): elif reset_transform: bone.matrix_basis.identity() + # FIXME: armObj.pose_library is None when the armature is not in pose mode if armObj.pose_library is None: armObj.pose_library = bpy.data.actions.new(name="PoseLib") diff --git a/mmd_tools/operators/display_item.py b/mmd_tools/operators/display_item.py index b5f3854..ab7013a 100644 --- a/mmd_tools/operators/display_item.py +++ b/mmd_tools/operators/display_item.py @@ -242,7 +242,7 @@ def execute(self, context): mmd_root.active_morph_type = item.morph_type mmd_root.active_morph = index else: - utils.selectSingleBone(context, mmd_model.FnModel.find_armature(root), item.name) + utils.selectSingleBone(context, mmd_model.FnModel.find_armature_object(root), item.name) return {"FINISHED"} diff --git a/mmd_tools/operators/misc.py b/mmd_tools/operators/misc.py index b4b8eba..2e22721 100644 --- a/mmd_tools/operators/misc.py +++ b/mmd_tools/operators/misc.py @@ -240,17 +240,17 @@ class AttachMeshesToMMD(Operator): add_armature_modifier: bpy.props.BoolProperty(default=True) def execute(self, context: bpy.types.Context): - root = mmd_model.FnModel.find_root(context.active_object) + root = mmd_model.FnModel.find_root_object(context.active_object) if root is None: self.report({"ERROR"}, "Select a MMD model") return {"CANCELLED"} - armObj = mmd_model.FnModel.find_armature(root) + armObj = mmd_model.FnModel.find_armature_object(root) if armObj is None: self.report({"ERROR"}, "Model Armature not found") return {"CANCELLED"} - mmd_model.FnModel.attach_meshes(root, context.visible_objects, self.add_armature_modifier) + mmd_model.FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) return {"FINISHED"} @@ -270,16 +270,16 @@ class ChangeMMDIKLoopFactor(Operator): @classmethod def poll(cls, context): - return mmd_model.FnModel.find_root(context.active_object) is not None + return mmd_model.FnModel.find_root_object(context.active_object) is not None def invoke(self, context, event): - root_object = mmd_model.FnModel.find_root(context.active_object) + root_object = mmd_model.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): - root_object = mmd_model.FnModel.find_root(context.active_object) + root_object = mmd_model.FnModel.find_root_object(context.active_object) mmd_model.FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) return {"FINISHED"} diff --git a/mmd_tools/operators/model.py b/mmd_tools/operators/model.py index b83056e..ff1b0e5 100644 --- a/mmd_tools/operators/model.py +++ b/mmd_tools/operators/model.py @@ -200,11 +200,11 @@ class AddMissingVertexGroupsFromBones(Operator): @classmethod def poll(cls, context: bpy.types.Context): - return mmd_model.FnModel.find_root(context.active_object) is not None + return mmd_model.FnModel.find_root_object(context.active_object) is not None def execute(self, context: bpy.types.Context): active_object: bpy.types.Object = context.active_object - root_object = mmd_model.FnModel.find_root(active_object) + root_object = mmd_model.FnModel.find_root_object(active_object) bone_order_mesh_object = mmd_model.FnModel.find_bone_order_mesh_object(root_object) if bone_order_mesh_object is None: @@ -402,15 +402,15 @@ def execute(self, context: bpy.types.Context): mmd_root_object.hide_set(False) - rigid_group_object = mmd_model.FnModel.find_rigid_group(mmd_root_object) + rigid_group_object = mmd_model.FnModel.find_rigid_group_object(mmd_root_object) if rigid_group_object: rigid_group_object.hide_set(True) - joint_group_object = mmd_model.FnModel.find_joint_group(mmd_root_object) + joint_group_object = mmd_model.FnModel.find_joint_group_object(mmd_root_object) if joint_group_object: joint_group_object.hide_set(True) - temporary_group_object = mmd_model.FnModel.find_temporary_group(mmd_root_object) + temporary_group_object = mmd_model.FnModel.find_temporary_group_object(mmd_root_object) if temporary_group_object: temporary_group_object.hide_set(True) diff --git a/mmd_tools/operators/model_edit.py b/mmd_tools/operators/model_edit.py index 6ec6f63..6149c13 100644 --- a/mmd_tools/operators/model_edit.py +++ b/mmd_tools/operators/model_edit.py @@ -64,8 +64,8 @@ def execute(self, context: bpy.types.Context): def join(self, context: bpy.types.Context): bpy.ops.object.mode_set(mode="OBJECT") - parent_root_object = FnModel.find_root(context.active_object) - child_root_objects = {FnModel.find_root(o) for o in context.selected_objects} + parent_root_object = FnModel.find_root_object(context.active_object) + child_root_objects = {FnModel.find_root_object(o) for o in context.selected_objects} child_root_objects.remove(parent_root_object) if parent_root_object is None or len(child_root_objects) == 0: @@ -120,7 +120,7 @@ def poll(cls, context: bpy.types.Context): if active_object.type != "ARMATURE": return False - if FnModel.find_root(active_object) is None: + if FnModel.find_root_object(active_object) is None: return False return len(context.selected_pose_bones) > 0 @@ -154,7 +154,7 @@ def separate(self, context: bpy.types.Context): 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} - mmd_root_object: bpy.types.Object = FnModel.find_root(context.active_object) + mmd_root_object: bpy.types.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()) diff --git a/mmd_tools/operators/morph.py b/mmd_tools/operators/morph.py index 1d12569..bbe7e3c 100644 --- a/mmd_tools/operators/morph.py +++ b/mmd_tools/operators/morph.py @@ -2,6 +2,7 @@ # Copyright 2015 MMD Tools authors # This file is part of MMD Tools. +from typing import Optional, cast import bpy from bpy.types import Operator from mathutils import Quaternion, Vector @@ -169,7 +170,7 @@ def poll(cls, context): def execute(self, context): root = mmd_model.Model.findRoot(context.active_object) - FnMorph.overwrite_bone_morphs_from_pose_library(mmd_model.FnModel.find_armature(root)) + FnMorph.overwrite_bone_morphs_from_pose_library(mmd_model.FnModel.find_armature_object(root)) return {"FINISHED"} @@ -448,7 +449,7 @@ def execute(self, context): utils.selectSingleBone(context, armature, None, True) morph = mmd_root.bone_morphs[mmd_root.active_morph] for morph_data in morph.data: - p_bone = armature.pose.bones.get(morph_data.bone, None) + p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) if p_bone: p_bone.bone.select = True mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() @@ -583,10 +584,10 @@ def execute(self, context): bpy.ops.mmd_tools.clear_uv_morph_view() selected = meshObj.select_get() - with bpyutils.select_object(meshObj) as data: + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) morph = mmd_root.uv_morphs[mmd_root.active_morph] - mesh = meshObj.data - uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers) + uv_textures = mesh.uv_layers base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")] if morph.uv_index >= len(base_uv_layers): @@ -678,22 +679,22 @@ def execute(self, context): mmd_root = root.mmd_root meshObj = obj - selected = meshObj.select - with bpyutils.select_object(meshObj) as data: + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.select_mode(type="VERT", action="ENABLE") bpy.ops.mesh.reveal() # unhide all vertices bpy.ops.mesh.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="OBJECT") - vertices = meshObj.data.vertices - for l, d in zip(meshObj.data.loops, meshObj.data.uv_layers.active.data): + vertices = mesh.vertices + for l, d in zip(mesh.loops, mesh.uv_layers.active.data): if d.select: vertices[l.vertex_index].select = True - if not hasattr(meshObj.data, "uv_textures"): - polygons = meshObj.data.polygons - polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active) + polygons = mesh.polygons + polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active) bpy.ops.object.mode_set(mode="EDIT") meshObj.select_set(selected) @@ -721,10 +722,10 @@ def execute(self, context): mmd_root = root.mmd_root meshObj = obj - selected = meshObj.select - with bpyutils.select_object(meshObj) as data: + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) morph = mmd_root.uv_morphs[mmd_root.active_morph] - mesh = meshObj.data base_uv_name = mesh.uv_layers.active.name[5:] if base_uv_name not in mesh.uv_layers: @@ -764,7 +765,7 @@ def poll(cls, context): return mmd_model.Model.findRoot(context.active_object) is not None def execute(self, context: bpy.types.Context): - mmd_root_object = mmd_model.FnModel.find_root(context.active_object) + mmd_root_object = mmd_model.FnModel.find_root_object(context.active_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object) return {"FINISHED"} diff --git a/mmd_tools/operators/rigid_body.py b/mmd_tools/operators/rigid_body.py index cb88884..5fab631 100644 --- a/mmd_tools/operators/rigid_body.py +++ b/mmd_tools/operators/rigid_body.py @@ -3,13 +3,15 @@ # This file is part of MMD Tools. import math -from typing import Dict +from typing import Dict, Optional, Tuple, cast import bpy +from mathutils import Euler, Vector from mmd_tools import utils -from mmd_tools.bpyutils import Props, activate_layer_collection +from mmd_tools.bpyutils import FnContext, Props, activate_layer_collection from mmd_tools.core import rigid_body +from mmd_tools.core.rigid_body import FnRigidBody from mmd_tools.core.model import FnModel, Model @@ -48,7 +50,7 @@ def poll(cls, context): def execute(self, context): obj = context.active_object - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) if root is None: self.report({"ERROR"}, "The model root can't be found") return {"CANCELLED"} @@ -168,15 +170,17 @@ class AddRigidBody(bpy.types.Operator): default=0.1, ) - def __add_rigid_body(self, rig, arm_obj=None, pose_bone=None): - name_j = self.name_j - name_e = self.name_e + def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None): + name_j: str = self.name_j + name_e: str = self.name_e size = self.size.copy() - loc = (0.0, 0.0, 0.0) - rot = (0.0, 0.0, 0.0) - bone_name = None + loc = Vector((0.0, 0.0, 0.0)) + rot = Euler((0.0, 0.0, 0.0)) + bone_name: Optional[str] = None - if pose_bone: + if pose_bone is None: + size *= getattr(root_object, Props.empty_display_size) + else: bone_name = pose_bone.name mmd_bone = pose_bone.mmd_bone name_j = name_j.replace("$name_j", mmd_bone.name_j or bone_name) @@ -198,17 +202,16 @@ def __add_rigid_body(self, rig, arm_obj=None, pose_bone=None): size.z *= 0.8 elif self.rigid_shape == "CAPSULE": size.x /= 3 - else: - size *= getattr(rig.rootObject(), Props.empty_display_size) - return rig.createRigidBody( - name=name_j, - name_e=name_e, + return FnRigidBody.setup_rigid_body_object( + obj=FnRigidBody.new_rigid_body_object(context, FnModel.ensure_rigid_group_object(context, root_object)), + shape_type=rigid_body.shapeType(self.rigid_shape), location=loc, rotation=rot, size=size, - shape_type=rigid_body.shapeType(self.rigid_shape), dynamics_type=int(self.rigid_type), + name=name_j, + name_e=name_e, collision_group_number=self.collision_group_number, collision_group_mask=self.collision_group_mask, mass=self.mass, @@ -219,28 +222,40 @@ def __add_rigid_body(self, rig, arm_obj=None, pose_bone=None): bone=bone_name, ) + @classmethod + def poll(cls, context): + root_object = FnModel.find_root_object(context.active_object) + if root_object is None: + return False + + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return False + + return True + def execute(self, context): - obj = context.active_object - root = FnModel.find_root(obj) - rig = Model(root) - arm = rig.armature() - if obj != arm: - utils.selectAObject(root) - root.select_set(False) - elif arm.mode != "POSE": + 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)) + + if active_object != armature_object: + FnContext.select_single_object(context, root_object).select_set(False) + elif armature_object.mode != "POSE": bpy.ops.object.mode_set(mode="POSE") selected_pose_bones = [] if context.selected_pose_bones: selected_pose_bones = context.selected_pose_bones - arm.select_set(False) + armature_object.select_set(False) if len(selected_pose_bones) > 0: for pose_bone in selected_pose_bones: - rigid = self.__add_rigid_body(rig, arm, pose_bone) + rigid = self.__add_rigid_body(context, root_object, pose_bone) rigid.select_set(True) else: - rigid = self.__add_rigid_body(rig) + rigid = self.__add_rigid_body(context, root_object) rigid.select_set(True) return {"FINISHED"} @@ -275,7 +290,7 @@ def poll(cls, context): def execute(self, context): obj = context.active_object - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) utils.selectAObject(obj) # ensure this is the only one object select bpy.ops.object.delete(use_global=True) if root: @@ -365,7 +380,7 @@ class AddJoint(bpy.types.Operator): min=0, ) - def __enumerate_rigid_pair(self, bone_map): + def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]): obj_seq = tuple(bone_map.keys()) for rigid_a, bone_a in bone_map.items(): for rigid_b, bone_b in bone_map.items(): @@ -378,8 +393,9 @@ def __enumerate_rigid_pair(self, bone_map): else: yield obj_seq - def __add_joint(self, rig, rigid_pair, bone_map): - loc, rot = None, [0, 0, 0] + def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map): + loc: Optional[Vector] = None + rot = Euler((0.0, 0.0, 0.0)) rigid_a, rigid_b = rigid_pair bone_a = bone_map[rigid_a] bone_b = bone_map[rigid_b] @@ -397,7 +413,9 @@ def __add_joint(self, rig, rigid_pair, bone_map): name_j = rigid_b.mmd_rigid.name_j or rigid_b.name name_e = rigid_b.mmd_rigid.name_e or rigid_b.name - return rig.createJoint( + + return FnRigidBody.setup_joint_object( + obj=FnRigidBody.new_joint_object(context, FnModel.ensure_joint_group_object(context, root_object), FnModel.get_empty_display_size(root_object)), name=name_j, name_e=name_e, location=loc, @@ -412,28 +430,35 @@ def __add_joint(self, rig, rigid_pair, bone_map): spring_angular=self.spring_angular, ) - def execute(self, context): - obj = context.active_object - root = FnModel.find_root(obj) - rig = Model(root) + @classmethod + def poll(cls, context): + root_object = FnModel.find_root_object(context.active_object) + if root_object is None: + return False - arm = rig.armature() - bone_map = {} - for i in context.selected_objects: - if FnModel.is_rigid_body_object(i): - bone_map[i] = arm.data.bones.get(i.mmd_rigid.bone, None) + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return False + + return True + + def execute(self, context): + 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)) + bones = cast(bpy.types.Armature, armature_object.data).bones + bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()} if len(bone_map) < 2: self.report({"ERROR"}, "Please select two or more mmd rigid objects") return {"CANCELLED"} - utils.selectAObject(root) - root.select_set(False) + FnContext.select_single_object(context, root_object).select_set(False) if context.scene.rigidbody_world is None: bpy.ops.rigidbody.world_add() for pair in self.__enumerate_rigid_pair(bone_map): - joint = self.__add_joint(rig, pair, bone_map) + joint = self.__add_joint(context, root_object, pair, bone_map) joint.select_set(True) return {"FINISHED"} @@ -455,7 +480,7 @@ def poll(cls, context): def execute(self, context): obj = context.active_object - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) utils.selectAObject(obj) # ensure this is the only one object select bpy.ops.object.delete(use_global=True) if root: @@ -521,7 +546,7 @@ def _references(obj): if not _update_group(i, rb_objs): continue - rb_map = table.setdefault(FnModel.find_root(i), {}) + rb_map = table.setdefault(FnModel.find_root_object(i), {}) if i in rb_map: # means rb_map[i] will replace i rb_objs.unlink(i) continue @@ -535,7 +560,7 @@ def _references(obj): if not _update_group(i, rbc_objs): continue - rbc, root = i.rigid_body_constraint, FnModel.find_root(i) + rbc, root = i.rigid_body_constraint, FnModel.find_root_object(i) rb_map = table.get(root, {}) rbc.object1 = rb_map.get(rbc.object1, rbc.object1) rbc.object2 = rb_map.get(rbc.object2, rbc.object2) diff --git a/mmd_tools/operators/sdef.py b/mmd_tools/operators/sdef.py index bae50de..fbbb51f 100644 --- a/mmd_tools/operators/sdef.py +++ b/mmd_tools/operators/sdef.py @@ -19,17 +19,15 @@ def _get_target_objects(context): selected_objects.add(i) continue - root = FnModel.find_root(i) - if root not in {i, i.parent}: + root_object = FnModel.find_root_object(i) + if root_object is None: continue - - root_objects.add(root) - - arm = FnModel.find_armature(root) - if arm is None: + if root_object in root_objects: continue - selected_objects |= set(FnModel.child_meshes(arm)) + root_objects.add(root_object) + + selected_objects |= set(FnModel.iterate_mesh_objects(root_object)) return selected_objects, root_objects diff --git a/mmd_tools/operators/translations.py b/mmd_tools/operators/translations.py index 4313666..f28a04a 100644 --- a/mmd_tools/operators/translations.py +++ b/mmd_tools/operators/translations.py @@ -83,7 +83,7 @@ def execute(self, context): return {"CANCELLED"} obj = context.active_object - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) rig = Model(root) if "MMD" in self.modes: @@ -207,7 +207,7 @@ class RestoreMMDDataReferenceOperator(bpy.types.Operator): restore_value: bpy.props.StringProperty() def execute(self, context: bpy.types.Context): - root_object = FnModel.find_root(context.object) + root_object = FnModel.find_root_object(context.object) mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index] setattr(mmd_translation_element, self.prop_name, self.restore_value) @@ -222,7 +222,7 @@ class GlobalTranslationPopup(bpy.types.Operator): @classmethod def poll(cls, context): - return FnModel.find_root(context.object) is not None + return FnModel.find_root_object(context.object) is not None def draw(self, _context): layout = self.layout @@ -287,7 +287,7 @@ def draw(self, _context): row.prop(mmd_translation, "dictionary", text="replace") def invoke(self, context: bpy.types.Context, _event): - root_object = FnModel.find_root(context.object) + root_object = FnModel.find_root_object(context.object) if root_object is None: return {"CANCELLED"} @@ -300,7 +300,7 @@ def invoke(self, context: bpy.types.Context, _event): return context.window_manager.invoke_props_dialog(self, width=800) def execute(self, context): - root_object = FnModel.find_root(context.object) + root_object = FnModel.find_root_object(context.object) if root_object is None: return {"CANCELLED"} @@ -316,7 +316,7 @@ class ExecuteTranslationBatchOperator(bpy.types.Operator): bl_options = {"INTERNAL"} def execute(self, context: bpy.types.Context): - root = FnModel.find_root(context.object) + root = FnModel.find_root_object(context.object) if root is None: return {"CANCELLED"} diff --git a/mmd_tools/panels/prop_object.py b/mmd_tools/panels/prop_object.py index c348ea4..4a2e0de 100644 --- a/mmd_tools/panels/prop_object.py +++ b/mmd_tools/panels/prop_object.py @@ -16,13 +16,13 @@ class MMDModelObjectPanel(bpy.types.Panel): @classmethod def poll(cls, context): - return FnModel.find_root(context.active_object) + return FnModel.find_root_object(context.active_object) def draw(self, context): layout = self.layout obj = context.active_object - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) c = layout.column() c.prop(root.mmd_root, "name") diff --git a/mmd_tools/panels/prop_physics.py b/mmd_tools/panels/prop_physics.py index 291d02d..6acdf36 100644 --- a/mmd_tools/panels/prop_physics.py +++ b/mmd_tools/panels/prop_physics.py @@ -37,14 +37,14 @@ def draw(self, context): row = c.row(align=True) row.prop(obj.mmd_rigid, "type", expand=True) - root = mmd_model.FnModel.find_root(obj) + root = mmd_model.FnModel.find_root_object(obj) if root is None: row = c.row(align=True) row.enabled = False row.prop(obj.mmd_rigid, "bone", text="", icon="BONE_DATA") else: row = c.row(align=True) - armature = mmd_model.FnModel.find_armature(root) + armature = mmd_model.FnModel.find_armature_object(root) row.prop_search(obj.mmd_rigid, "bone", text="", search_data=armature.pose, search_property="bones", icon="BONE_DATA") c = layout.column(align=True) diff --git a/mmd_tools/panels/sidebar.py b/mmd_tools/panels/sidebar.py index 8eb5145..cbcb96c 100644 --- a/mmd_tools/panels/sidebar.py +++ b/mmd_tools/panels/sidebar.py @@ -173,7 +173,7 @@ def __get_toggle_items(self, mmd_root_object: bpy.types.Object): self.__toggle_items_ttl = time.time() + 10 self.__toggle_items_cache = [] - armature_object = model.FnModel.find_armature(mmd_root_object) + armature_object = model.FnModel.find_armature_object(mmd_root_object) pose_bones = armature_object.pose.bones ik_map = {pose_bones[c.subtarget]: (b.bone, c.chain_count, not c.is_valid) for b in pose_bones for c in b.constraints if c.type == "IK" and c.subtarget in pose_bones} diff --git a/mmd_tools/panels/tool.py b/mmd_tools/panels/tool.py index c6ecea9..725fdc9 100644 --- a/mmd_tools/panels/tool.py +++ b/mmd_tools/panels/tool.py @@ -2,6 +2,7 @@ # Copyright 2015 MMD Tools authors # This file is part of MMD Tools. +from typing import Optional import bpy from bpy.types import Menu, Panel, UIList @@ -127,11 +128,11 @@ class MMD_ROOT_UL_display_items(UIList): ) @staticmethod - def draw_bone_special(layout, armature, bone_name, mmd_name=None): + def draw_bone_special(layout: bpy.types.UILayout, armature: bpy.types.Object, bone_name: str, mmd_name: Optional[str] = None): if armature is None: return row = layout.row(align=True) - p_bone = armature.pose.bones.get(bone_name, None) + p_bone: bpy.types.PoseBone = armature.pose.bones.get(bone_name, None) if p_bone: bone = p_bone.bone if mmd_name: diff --git a/mmd_tools/panels/util_tools.py b/mmd_tools/panels/util_tools.py index f251285..a6cb23d 100644 --- a/mmd_tools/panels/util_tools.py +++ b/mmd_tools/panels/util_tools.py @@ -305,12 +305,12 @@ class MMDBoneOrder(_PanelBase, Panel): def draw(self, context): layout = self.layout active_obj = context.active_object - root = FnModel.find_root(active_obj) + root = FnModel.find_root_object(active_obj) if root is None: layout.label(text="Select a MMD Model") return - armature = FnModel.find_armature(root) + armature = FnModel.find_armature_object(root) if armature is None: layout.label(text="The armature object of active MMD model can't be found", icon="ERROR") return diff --git a/mmd_tools/properties/material.py b/mmd_tools/properties/material.py index 6f0354e..1fba427 100644 --- a/mmd_tools/properties/material.py +++ b/mmd_tools/properties/material.py @@ -74,7 +74,7 @@ def _mmd_material_get_name_j(prop: "MMDMaterial"): def _mmd_material_set_name_j(prop: "MMDMaterial", value: str): prop_value = value if prop_value and prop_value != prop.get("name_j"): - root = FnModel.find_root(bpy.context.active_object) + root = FnModel.find_root_object(bpy.context.active_object) if root is None: prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials}) else: diff --git a/mmd_tools/properties/morph.py b/mmd_tools/properties/morph.py index 81ead6d..d58be7e 100644 --- a/mmd_tools/properties/morph.py +++ b/mmd_tools/properties/morph.py @@ -30,11 +30,9 @@ def _morph_base_set_name(prop: "_MorphBase", value: str): if prop_name is not None: if morph_type == "vertex_morphs": kb_list = {} - armature_object = FnModel.find_armature(prop.id_data) - if armature_object is not None: - for mesh in FnModel.child_meshes(armature_object): - for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): - kb_list.setdefault(kb.name, []).append(kb) + 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) if prop_name in kb_list: value = utils.unique_name(value, used_names | kb_list.keys()) @@ -43,11 +41,9 @@ def _morph_base_set_name(prop: "_MorphBase", value: str): elif morph_type == "uv_morphs": vg_list = {} - armature_object = FnModel.find_armature(prop.id_data) - if armature_object is not None: - for mesh in FnModel.child_meshes(armature_object): - for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): - vg_list.setdefault(n, []).append(vg) + 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) if prop_name in vg_list: value = utils.unique_name(value, used_names | vg_list.keys()) @@ -106,7 +102,7 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: if bone_id < 0: return "" root_object = prop.id_data - armature_object = FnModel.find_armature(root_object) + armature_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) @@ -117,7 +113,7 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): root = prop.id_data - arm = FnModel.find_armature(root) + arm = 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. @@ -208,7 +204,7 @@ def _material_morph_data_set_material(prop: "MaterialMorphData", value: str): def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str): - mesh = FnModel.find_mesh_by_name(prop.id_data, value) + mesh = FnModel.find_mesh_object_by_name(prop.id_data, value) if mesh is not None: prop["related_mesh_data"] = mesh.data else: diff --git a/mmd_tools/properties/rigid_body.py b/mmd_tools/properties/rigid_body.py index fcc1678..04c5f9d 100644 --- a/mmd_tools/properties/rigid_body.py +++ b/mmd_tools/properties/rigid_body.py @@ -8,6 +8,7 @@ from mmd_tools import bpyutils from mmd_tools.core import rigid_body +from mmd_tools.core.rigid_body import RigidBodyMaterial, FnRigidBody from mmd_tools.core.model import FnModel from mmd_tools.properties import patch_library_overridable @@ -16,9 +17,9 @@ def _updateCollisionGroup(prop, context): obj = prop.id_data materials = obj.data.materials if len(materials) == 0: - materials.append(rigid_body.RigidBodyMaterial.getMaterial(prop.collision_group_number)) + materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) else: - obj.material_slots[0].material = rigid_body.RigidBodyMaterial.getMaterial(prop.collision_group_number) + obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) def _updateType(prop, context): @@ -62,9 +63,9 @@ def _set_bone(prop, value): arm = relation.target if arm is None: - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) if root: - arm = relation.target = FnModel.find_armature(root) + arm = relation.target = FnModel.find_armature_object(root) if arm is not None and bone_name in arm.data.bones: relation.subtarget = bone_name @@ -77,7 +78,7 @@ def _set_bone(prop, value): def _get_size(prop): if prop.id_data.mmd_type != "RIGID_BODY": return (0, 0, 0) - return FnModel.get_rigid_body_size(prop.id_data) + return FnRigidBody.get_rigid_body_size(prop.id_data) def _set_size(prop, value): @@ -125,7 +126,7 @@ def _set_size(prop, value): z0 = -z if z0 < 0 else z v.co = [x0, y0, z0] elif shape == "CAPSULE": - r0, h0, xx = FnModel.get_rigid_body_size(prop.id_data) + r0, h0, xx = FnRigidBody.get_rigid_body_size(prop.id_data) h0 *= 0.5 radius = max(value[0], 1e-3) height = max(value[1], 1e-3) * 0.5 diff --git a/mmd_tools/properties/root.py b/mmd_tools/properties/root.py index 3bcf6b8..6574746 100644 --- a/mmd_tools/properties/root.py +++ b/mmd_tools/properties/root.py @@ -37,7 +37,7 @@ def __add_single_prop(variables, id_obj, data_path, prefix): def _toggleUsePropertyDriver(self: "MMDRoot", _context): root_object: bpy.types.Object = self.id_data - armature_object = FnModel.find_armature(root_object) + armature_object = FnModel.find_armature_object(root_object) if armature_object is None: ik_map = {} @@ -55,7 +55,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context): if c: 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 - for i in FnModel.child_meshes(root_object): + for i in FnModel.iterate_mesh_objects(root_object): for prop_hide in ("hide_viewport", "hide_render"): driver, variables = __driver_variables(i, prop_hide) driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name @@ -67,7 +67,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context): c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) if c: c.driver_remove("influence") - for i in FnModel.child_meshes(root_object): + for i in FnModel.iterate_mesh_objects(root_object): for prop_hide in ("hide_viewport", "hide_render"): i.driver_remove(prop_hide) @@ -79,7 +79,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context): def _toggleUseToonTexture(self: "MMDRoot", _context): use_toon = self.use_toon_texture - for i in FnModel.child_meshes(self.id_data): + 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) @@ -87,7 +87,7 @@ def _toggleUseToonTexture(self: "MMDRoot", _context): def _toggleUseSphereTexture(self: "MMDRoot", _context): use_sphere = self.use_sphere_texture - for i in FnModel.child_meshes(self.id_data): + 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) @@ -95,14 +95,14 @@ def _toggleUseSphereTexture(self: "MMDRoot", _context): def _toggleUseSDEF(self: "MMDRoot", _context): mute_sdef = not self.use_sdef - for i in FnModel.child_meshes(self.id_data): + 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): root = self.id_data hide = not self.show_meshes - for i in FnModel.child_meshes(self.id_data): + for i in FnModel.iterate_mesh_objects(self.id_data): i.hide_set(hide) i.hide_render = hide if hide and context.active_object is None: @@ -153,7 +153,7 @@ def _toggleShowNamesOfJoints(self: "MMDRoot", _context): def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool): root = prop.id_data - arm = FnModel.find_armature(root) + arm = FnModel.find_armature_object(root) if arm is None: return if not v and bpy.context.active_object == arm: @@ -164,7 +164,7 @@ def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool): def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"): if prop.id_data.mmd_type != "ROOT": return False - arm = FnModel.find_armature(prop.id_data) + arm = FnModel.find_armature_object(prop.id_data) return arm and not arm.hide_get() diff --git a/mmd_tools/typings/mmd_tools/properties/morph.pyi b/mmd_tools/typings/mmd_tools/properties/morph.pyi new file mode 100644 index 0000000..1d556c1 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/morph.pyi @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +class MaterialMorph: + pass + +class UVMorph: + pass + +class BoneMorph: + pass + +class GroupMorph: + pass + +class VertexMorph: + pass diff --git a/mmd_tools/typings/mmd_tools/properties/pose_bone.pyi b/mmd_tools/typings/mmd_tools/properties/pose_bone.pyi new file mode 100644 index 0000000..30fdf50 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/pose_bone.pyi @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from mathutils import Vector + +class MMDBone: + name_j: str + name_e: str + bone_id: int + transform_order: int + is_controllable: bool + transform_after_dynamics: bool + enabled_fixed_axis: bool + fixed_axis: Vector + enabled_local_axes: bool + local_axis_x: Vector + local_axis_z: Vector + is_tip: bool + ik_rotation_constraint: float + has_additional_rotation: bool + has_additional_location: bool + additional_transform_bone: str + additional_transform_bone_id: int + additional_transform_influence: float + is_additional_transform_dirty: bool diff --git a/mmd_tools/typings/mmd_tools/properties/root.pyi b/mmd_tools/typings/mmd_tools/properties/root.pyi new file mode 100644 index 0000000..1072f80 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/root.pyi @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from typing import List + +from mmd_tools.properties.morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph +from mmd_tools.properties.translations import MMDTranslation + +class MMDDisplayItemFrame: + pass + +class MMDRoot: + name: str + name_j: str + name_e: str + comment_text: str + comment_e_text: str + ik_lookup_factor: int + show_meshes: bool + show_rigid_bodies: bool + show_joints: bool + show_temporary_objects: bool + show_armature: bool + show_names_of_rigid_bodies: bool + show_names_of_joints: bool + use_toon_texture: bool + use_sphere_texture: bool + use_sdef: bool + use_property_driver: bool + is_built: bool + active_rigidbody_index: int + active_joint_index: int + display_item_frames: List[MMDDisplayItemFrame] + active_display_item_frame: int + material_morphs: List[MaterialMorph] + uv_morphs: List[UVMorph] + bone_morphs: List[BoneMorph] + vertex_morphs: List[VertexMorph] + group_morphs: List[GroupMorph] + + active_morph_type: str # TODO: Replace with StrEnum + active_morph: int + morph_panel_show_settings: bool + active_mesh_index: int + translation: MMDTranslation + + @staticmethod + def register() -> None: ... + @staticmethod + def unregister() -> None: ... diff --git a/mmd_tools/typings/mmd_tools/properties/translations.pyi b/mmd_tools/typings/mmd_tools/properties/translations.pyi new file mode 100644 index 0000000..407fe9f --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/translations.pyi @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from typing import List + +import bpy + +class MMDTranslationElement: + type: str + object: bpy.types.Object + data_path: str + name: str + name_j: str + name_e: str + +class MMDTranslationElementIndex(bpy.types.PropertyGroup): + value: int + +class MMDTranslation: + id_data: bpy.types.Object + translation_elements: List[MMDTranslationElement] + filtered_translation_element_indices_active_index: int + filtered_translation_element_indices: List[MMDTranslationElementIndex] + + filter_japanese_blank: bool + filter_english_blank: bool + filter_restorable: bool + filter_selected: bool + filter_visible: bool + filter_types: str + + dictionary: str + + batch_operation_target: str + + batch_operation_script_preset: str + + batch_operation_script: str diff --git a/mmd_tools/utils.py b/mmd_tools/utils.py index 7eaaa06..c5370db 100644 --- a/mmd_tools/utils.py +++ b/mmd_tools/utils.py @@ -50,14 +50,14 @@ def selectSingleBone(context, armature, bone_name, reset_pose=False): if reset_pose: for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() - armature_bones = armature.data.bones + armature_bones: bpy.types.ArmatureBones = armature.data.bones + i: bpy.types.Bone for i in armature_bones: i.select = i.name == bone_name i.select_head = i.select_tail = i.select if i.select: armature_bones.active = i i.hide = False - # armature.data.layers[list(i.layers).index(True)] = True __CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") @@ -107,42 +107,10 @@ def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name): pass -def __getCustomNormalKeeper(mesh): - if hasattr(mesh, "has_custom_normals") and mesh.use_auto_smooth: - - class _CustomNormalKeeper: - def __init__(self, mesh): - mesh.calc_normals_split() - self.__normals = tuple(zip((l.normal.copy() for l in mesh.loops), (p.material_index for p in mesh.polygons for v in p.vertices))) - mesh.free_normals_split() - self.__material_map = {} - materials = mesh.materials - for i, m in enumerate(materials): - if m is None or m.name in self.__material_map: - materials[i] = bpy.data.materials.new("_mmd_tmp_") - self.__material_map[materials[i].name] = (i, getattr(m, "name", "")) - - def restore_custom_normals(self, mesh): - materials = mesh.materials - for i, m in enumerate(materials): - mat_id, mat_name_orig = self.__material_map[m.name] - if m.name != mat_name_orig: - materials[i] = bpy.data.materials.get(mat_name_orig, None) - m.user_clear() - bpy.data.materials.remove(m) - if len(materials) == 1: - mesh.normals_split_custom_set([n for n, x in self.__normals if x == mat_id]) - mesh.update() - - return _CustomNormalKeeper(mesh) # This fixes the issue that "SeparateByMaterials" could break custom normals - return None - - def separateByMaterials(meshObj): if len(meshObj.data.materials) < 2: selectAObject(meshObj) return - custom_normal_keeper = __getCustomNormalKeeper(meshObj.data) matrix_parent_inverse = meshObj.matrix_parent_inverse.copy() prev_parent = meshObj.parent dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)