Holy shit this was a pain

- Truly fixes PMX Import lol, i messed up completely
- Updated MMD Tools to use Cats One
This commit is contained in:
Yusarina
2025-11-19 06:35:06 +00:00
parent f0bda259d3
commit a929f68ad4
38 changed files with 4479 additions and 2709 deletions
+47 -68
View File
@@ -1,22 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
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):
@@ -32,8 +25,7 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context: Context) -> Set[str]:
logger.debug(f"Selecting object: {self.name}")
def execute(self, context):
utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"}
@@ -47,43 +39,41 @@ 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: Object, index: int) -> None:
def set_index(cls, obj, index):
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)
obj.name = f"{utils.int2base(index, 36, 3)}_{name}"
@classmethod
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
def get_name(cls, obj, prefix=None):
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: List[Object]) -> None:
def normalize_indices(cls, objects):
for i, x in enumerate(objects):
cls.set_index(x, i)
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
return context.active_object is not None
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
objects = self.__get_objects(obj)
if obj not in objects:
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: Object) -> Any:
def __get_objects(self, obj):
class __MovableList(list):
def move(self, index_old: int, index_new: int) -> None:
def move(self, index_old, index_new):
item = self[index_old]
self.remove(item)
self.insert(index_new, item)
@@ -108,43 +98,40 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod
def __can_remove(key_block: ShapeKey) -> bool:
def __can_remove(key_block):
if key_block.relative_key == key_block:
return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
for v0, v1 in zip(key_block.relative_key.data, key_block.data, strict=False):
if v0.co != v1.co:
return False
return True
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
def __shape_key_clean(self, obj, key_blocks):
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: Context) -> Set[str]:
logger.info("Cleaning shape keys for selected objects")
obj: Object
def execute(self, context):
obj: bpy.types.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"}
class SeparateByMaterials(bpy.types.Operator):
bl_idname = "mmd_tools.separate_by_materials"
bl_label = "Separate By Materials"
bl_label = "Sep by Mat(High Risk)"
bl_description = "Separate by Materials (High Risk)\nSeparate the mesh into multiple objects based on materials.\nHIGH RISK & BUGGY: This operation is not reversible and may cause various issues. It splits adjacent geometry by material, and merging later will not reconnect shared edges.\nKnown issues include potential mesh corruption, UV mapping problems, and other unpredictable behaviors. Use with extreme caution and backup your work first."
bl_options = {"REGISTER", "UNDO"}
clean_shape_keys: bpy.props.BoolProperty(
@@ -153,26 +140,32 @@ class SeparateByMaterials(bpy.types.Operator):
default=True,
)
@classmethod
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "MESH"
keep_normals: bpy.props.BoolProperty(
name="Keep Normals",
default=True,
)
def __separate_by_materials(self, obj: Object) -> None:
logger.info(f"Separating {obj.name} by materials")
utils.separateByMaterials(obj)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj is not None and obj.type == "MESH"
def __separate_by_materials(self, obj):
utils.separateByMaterials(obj, self.keep_normals)
if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
# Sep by Mat crashes Blender if used after morph assembly
rig = Model(root)
rig.morph_slider.unbind()
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()
@@ -185,11 +178,9 @@ 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"}
@@ -207,15 +198,13 @@ class JoinMeshes(bpy.types.Operator):
default=True,
)
def execute(self, context: Context) -> Set[str]:
def execute(self, context):
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()
@@ -223,11 +212,9 @@ 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)
@@ -236,19 +223,15 @@ 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"}
@@ -262,20 +245,17 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: Context) -> Set[str]:
def execute(self, context: bpy.types.Context):
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"}
@@ -295,18 +275,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
)
@classmethod
def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def poll(cls, context):
root = FnModel.find_root_object(context.active_object)
return root is not None
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
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: Context) -> Set[str]:
def execute(self, context):
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"}
@@ -318,22 +298,21 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context: Context) -> bool:
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "ARMATURE"
return obj is not None and obj.type == "ARMATURE"
def invoke(self, context: Context, event: Any) -> Set[str]:
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
def draw(self, context):
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: Context) -> Set[str]:
def execute(self, context):
arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"}