Updated Operations and Properties

- Updated Operations and Properties with tpying and logging.

I have not updated translation files, this is because i want to gut MMD Tools system and replace it with our own, however I want to make MMD Tools more simple and ajust it to our needs only. This is going to take a while and my aim for this is Alpha 4, also the MMD Translation system hurt my head....

- Fixes a couple of bugs as well, with quick access and the PMX importer.
This commit is contained in:
Yusarina
2025-04-23 00:43:38 +01:00
parent 61e4269764
commit cfe760e8df
21 changed files with 689 additions and 347 deletions
+72 -31
View File
@@ -7,13 +7,17 @@
import itertools
from operator import itemgetter
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional, Set, Tuple, Any
import bmesh
import bpy
import numpy as np
import numpy.typing as npt
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from ..bpyutils import FnContext
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception):
@@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
@@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting model join by bones operation")
self.join(context)
logger.info("Model join by bones completed successfully")
except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def join(self, context: bpy.types.Context):
def join(self, context: Context) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object)
@@ -74,6 +81,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected")
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects)
@@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
# Connect child bones
if self.join_type == "CONNECTED":
parent_edit_bone: bpy.types.EditBone = context.active_bone
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone)
child_edit_bone: bpy.types.EditBone
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
child_edit_bone: EditBone
for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True
@@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
@@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting model separate by bones operation")
self.separate(context)
logger.info("Model separate by bones completed successfully")
except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def separate(self, context: bpy.types.Context):
def separate(self, context: Context) -> None:
weight_threshold: float = self.weight_threshold
mmd_scale = 0.08
target_armature_object: bpy.types.Object = context.active_object
target_armature_object: Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
root_bones: Set[EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
if self.include_descendant_bones:
logger.debug("Including descendant bones")
for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
logger.debug(f"Total bones to separate: {len(separate_bones)}")
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones
separate_armature_object: Optional[bpy.types.Object]
separate_armature_object: Optional[Object]
if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True)
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints
separate_joints: Set[bpy.types.Object] = {
separate_joints: Set[Object] = {
joint_object
for joint_object in mmd_model.joints()
if boundary_joint_owner_condition(
@@ -187,35 +210,43 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
]
)
}
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[bpy.types.Object]
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object]
separate_mesh_objects: Set[Object]
model2separate_mesh_objects: Dict[Object, Object]
if len(mmd_model_mesh_objects) == 0:
logger.debug("No mesh objects to separate")
separate_mesh_objects = set()
model2separate_mesh_objects = dict()
else:
# select meshes
obj: bpy.types.Object
logger.debug("Selecting meshes for separation")
obj: Object
for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
bpy.ops.object.mode_set(mode="OBJECT")
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
logger.debug(f"Creating new model with scale {mmd_scale}")
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature()
logger.debug(f"Created separate model with root: {separate_root_object.name}")
if self.separate_armature:
logger.debug("Joining separate armature to new model")
with context.temp_override(
active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
@@ -223,6 +254,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.join()
# add mesh
logger.debug("Parenting separate mesh objects to new model")
with context.temp_override(
object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
@@ -230,19 +262,23 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
for separate_mesh in separate_mesh_objects:
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
if armature_modifier is None:
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model")
with context.temp_override(
object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model")
with context.temp_override(
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
@@ -257,10 +293,12 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name:
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
separate_layer_collection.collection.objects.link(separate_object)
mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root(
separate_root_object,
mmd_root_object,
@@ -271,13 +309,15 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
},
)
def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict()
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
"""Select vertices weighted to the bones to be separated"""
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
mesh2selected_vertex_count: Dict[Object, int] = dict()
target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
mesh: bpy.types.Mesh = mesh_object.data
mesh: Mesh = mesh_object.data
target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify()
@@ -304,6 +344,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
vert.select_set(True)
if selected_vertex_count > 0:
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh)