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
+56 -27
View File
@@ -6,14 +6,17 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
import bpy
from bpy.types import Context, Object, Operator, ShapeKey
from .. import utils
from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone
from ..core.model import FnModel, Model
from ..core.morph import FnMorph
from ....core.logging_setup import logger
class SelectObject(bpy.types.Operator):
@@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
logger.debug(f"Selecting object: {self.name}")
utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"}
@@ -43,41 +47,43 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod
def set_index(cls, obj, index):
def set_index(cls, obj: Object, index: int) -> None:
m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
@classmethod
def get_name(cls, obj, prefix=None):
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
@classmethod
def normalize_indices(cls, objects):
def normalize_indices(cls, objects: List[Object]) -> None:
for i, x in enumerate(objects):
cls.set_index(x, i)
@classmethod
def poll(cls, context):
return context.active_object
def poll(cls, context: Context) -> bool:
return context.active_object is not None
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
objects = self.__get_objects(obj)
if obj not in objects:
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name)
logger.error(f'Cannot move object "{obj.name}"')
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
return {"CANCELLED"}
objects.sort(key=lambda x: x.name)
logger.debug(f"Moving object {obj.name} {self.type}")
self.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects)
return {"FINISHED"}
def __get_objects(self, obj):
def __get_objects(self, obj: Object) -> Any:
class __MovableList(list):
def move(self, index_old, index_new):
def move(self, index_old: int, index_new: int) -> None:
item = self[index_old]
self.remove(item)
self.insert(index_new, item)
@@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod
def __can_remove(key_block):
def __can_remove(key_block: ShapeKey) -> bool:
if key_block.relative_key == key_block:
return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
@@ -114,20 +120,24 @@ class CleanShapeKeys(bpy.types.Operator):
return False
return True
def __shape_key_clean(self, obj, key_blocks):
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
for kb in key_blocks:
if self.__can_remove(kb):
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1:
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context):
obj: bpy.types.Object
def execute(self, context: Context) -> Set[str]:
logger.info("Cleaning shape keys for selected objects")
obj: Object
for obj in context.selected_objects:
if obj.type != "MESH" or obj.data.shape_keys is None:
continue
if not obj.data.shape_keys.use_relative:
continue # not be considered yet
logger.debug(f"Processing shape keys for {obj.name}")
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"}
@@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "MESH"
def __separate_by_materials(self, obj):
def __separate_by_materials(self, obj: Object) -> None:
logger.info(f"Separating {obj.name} by materials")
utils.separateByMaterials(obj)
if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
logger.debug("No root object found, separating single object")
self.__separate_by_materials(obj)
else:
logger.debug(f"Root object found: {root.name}, preparing for separation")
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -171,9 +185,11 @@ class SeparateByMaterials(bpy.types.Operator):
if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None))
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes()
return {"FINISHED"}
@@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator):
default=True,
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
logger.info(f"Joining meshes for model: {root.name}")
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -205,9 +223,11 @@ class JoinMeshes(bpy.types.Operator):
rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list:
logger.error("No meshes found in the model")
self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"}
active_mesh = meshes_list[0]
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
FnContext.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh)
@@ -216,15 +236,19 @@ class JoinMeshes(bpy.types.Operator):
for m in meshes_list[1:]:
for mat in m.data.materials:
if mat not in active_mesh.data.materials[:]:
logger.debug(f"Adding material {mat.name} to active mesh")
active_mesh.data.materials.append(mat)
# Join selected meshes
logger.debug("Joining meshes")
bpy.ops.object.join()
if self.sort_shape_keys:
logger.debug("Sorting shape keys")
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
active_mesh.active_shape_key_index = 0
for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes()
return {"FINISHED"}
@@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object)
if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
armObj = FnModel.find_armature_object(root)
if armObj is None:
logger.error("Model armature not found")
self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"}
logger.info(f"Attaching meshes to model: {root.name}")
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"}
@@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"}
@@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "ARMATURE"
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
def draw(self, context):
def draw(self, context: Context) -> None:
layout = self.layout
c = layout.column()
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
c.label(text="Click [OK] to run the operation.")
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"}