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
+101 -72
View File
@@ -1,23 +1,20 @@
# -*- 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.
# Copyright 2012 MMD Tools authors
# This file is part of MMD Tools.
from ...core.logging_setup import logger
import os
import re
from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any
import string
from typing import Callable, Optional, Set
import bpy
from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup
import numpy as np
from ..logging_setup import logger
from .bpyutils import FnContext
## 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj: Object) -> None:
# 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj):
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
@@ -27,14 +24,14 @@ def selectAObject(obj: Object) -> None:
FnContext.set_active_object(FnContext.ensure_context(), obj)
## 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj: Object) -> None:
# 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj):
selectAObject(obj)
if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
def setParentToBone(obj, parent, bone_name):
selectAObject(obj)
FnContext.set_active_object(FnContext.ensure_context(), parent)
bpy.ops.object.mode_set(mode="POSE")
@@ -43,11 +40,11 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
def selectSingleBone(context, armature, bone_name, reset_pose=False):
try:
bpy.ops.object.mode_set(mode="OBJECT")
except:
pass
except Exception as e:
logger.warning(f"Failed to set object mode: {e}")
for i in context.selected_objects:
i.select_set(False)
FnContext.set_active_object(context, armature)
@@ -55,21 +52,21 @@ def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: st
if reset_pose:
for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity()
# Blender 5.0: bone selection in Pose mode now uses pose.bones[].select
armature_bones: bpy.types.ArmatureBones = armature.data.bones
for p_bone in armature.pose.bones:
p_bone.select = p_bone.name == bone_name
if p_bone.select:
armature_bones.active = armature_bones[p_bone.name]
p_bone.hide = False
is_target = p_bone.name == bone_name
p_bone.select = is_target
if is_target:
armature.data.bones.active = p_bone.bone
p_bone.bone.hide = False
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
__CONVERT_NAME_TO_L_REGEXP = re.compile(r"^(.*)左(.*)$")
__CONVERT_NAME_TO_R_REGEXP = re.compile(r"^(.*)右(.*)$")
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
# 日本語で左右を命名されている名前をblender方式のL(R)に変更する
def convertNameToLR(name, use_underscore=False):
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
delimiter = "_" if use_underscore else "."
if m:
@@ -84,7 +81,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<aft
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
def convertLRToName(name: str) -> str:
def convertLRToName(name):
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
if match:
return f"{name[0:match.start()]}{match['after']}{name[match.end():]}"
@@ -96,8 +93,8 @@ def convertLRToName(name: str) -> str:
return name
## src_vertex_groupのWeightをdest_vertex_groupにaddする
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
# src_vertex_groupのWeightをdest_vertex_groupにaddする
def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
mesh = meshObj.data
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
@@ -111,43 +108,73 @@ def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_gr
pass
def separateByMaterials(meshObj: Object) -> None:
if len(meshObj.data.materials) < 2:
def separateByMaterials(meshObj: bpy.types.Object, keep_normals: bool = False):
meshData = meshObj.data
if len(meshData.materials) < 2:
selectAObject(meshObj)
return
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
prev_parent = meshObj.parent
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
meshObj.parent = dummy_parent
meshObj.active_shape_key_index = 0
dummy_parent = None
try:
enterEditMode(meshObj)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.separate(type="MATERIAL")
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
prev_parent = meshObj.parent
meshObj.parent = dummy_parent
meshObj.active_shape_key_index = 0
mmd_normal_name = None # To avoid conflict ("mmd_normal.001", etc.)
if keep_normals:
existing_custom_normal = meshData.attributes.get("custom_normal")
if existing_custom_normal:
if existing_custom_normal.data_type == "INT16_2D":
normals_data = np.empty(len(meshData.loops) * 2, dtype=np.int16)
existing_custom_normal.data.foreach_get("value", normals_data)
mmd_normal = meshData.attributes.new("mmd_normal", "INT16_2D", "CORNER")
mmd_normal_name = mmd_normal.name
mmd_normal.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
try:
enterEditMode(meshObj)
bpy.ops.mesh.separate(type="MATERIAL")
finally:
bpy.ops.object.mode_set(mode="OBJECT")
for i in dummy_parent.children:
materials = i.data.materials
i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
i.parent = prev_parent
i.matrix_parent_inverse = matrix_parent_inverse
if keep_normals and mmd_normal_name:
mmd_normal = i.data.attributes.get(mmd_normal_name)
if mmd_normal:
if mmd_normal.data_type == "INT16_2D":
normals_data = np.empty(len(i.data.loops) * 2, dtype=np.int16)
mmd_normal.data.foreach_get("value", normals_data)
custom_normal_attr = i.data.attributes.get("custom_normal")
if not custom_normal_attr:
custom_normal_attr = i.data.attributes.new("custom_normal", "INT16_2D", "CORNER")
custom_normal_attr.data.foreach_set("value", normals_data)
else:
raise TypeError(f"Unsupported custom_normal data type: '{mmd_normal.data_type}'. Supported types: 'INT16_2D'")
i.data.attributes.remove(mmd_normal)
finally:
bpy.ops.object.mode_set(mode="OBJECT")
for i in dummy_parent.children:
materials = i.data.materials
i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
i.parent = prev_parent
i.matrix_parent_inverse = matrix_parent_inverse
bpy.data.objects.remove(dummy_parent)
if dummy_parent and dummy_parent.name in bpy.data.objects:
bpy.data.objects.remove(dummy_parent)
def clearUnusedMeshes() -> None:
meshes_to_delete = []
for mesh in bpy.data.meshes:
if mesh.users == 0:
meshes_to_delete.append(mesh)
def clearUnusedMeshes():
meshes_to_delete = [mesh for mesh in bpy.data.meshes if mesh.users == 0]
for mesh in meshes_to_delete:
bpy.data.meshes.remove(mesh)
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
# Maintain backward compatibility with mmd_tools v0.4.x or older.
def makePmxBoneMap(armObj):
# Maintain backward compatibility with mmd_tools_local v0.4.x or older.
return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones}
@@ -155,7 +182,7 @@ __REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")
def unique_name(name: str, used_names: Set[str]) -> str:
"""Helper function for storing unique names.
"""Generate a unique name from the given name.
This function is a limited and simplified version of bpy_extras.io_utils.unique_name.
Args:
@@ -175,13 +202,11 @@ def unique_name(name: str, used_names: Set[str]) -> str:
return new_name
def int2base(x: int, base: int, width: int = 0) -> str:
def int2base(x, base, width=0):
"""
Method to convert an int to a base
Convert an int to a base
Source: http://stackoverflow.com/questions/2267362
"""
import string
digs = string.digits + string.ascii_uppercase
assert 2 <= base <= len(digs)
digits, negtive = "", False
@@ -198,7 +223,7 @@ def int2base(x: int, base: int, width: int = 0) -> str:
return digits
def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
def saferelpath(path, start, strategy="inside"):
"""
On Windows relpath will raise a ValueError
when trying to calculate the relative path to a
@@ -225,15 +250,16 @@ def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
return os.path.relpath(path, start)
class ItemOp:
@staticmethod
def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]:
def get_by_index(items, index):
if 0 <= index < len(items):
return items[index]
return None
@staticmethod
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
def resize(items: bpy.types.bpy_prop_collection, length: int):
count = length - len(items)
if count > 0:
for i in range(count):
@@ -243,7 +269,7 @@ class ItemOp:
items.remove(length)
@staticmethod
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]:
def add_after(items, index):
index_end = len(items)
index = max(0, min(index_end, index + 1))
items.add()
@@ -265,8 +291,7 @@ class ItemMoveOp:
)
@staticmethod
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
index_min: int = 0, index_max: Optional[int] = None) -> int:
def move(items, index, move_type, index_min=0, index_max=None):
if index_max is None:
index_max = len(items) - 1
else:
@@ -276,7 +301,7 @@ class ItemMoveOp:
if index < index_min:
items.move(index, index_min)
return index_min
elif index > index_max:
if index > index_max:
items.move(index, index_max)
return index_max
@@ -295,8 +320,8 @@ class ItemMoveOp:
return index_new
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable:
"""Decorator to mark a function as deprecated.
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
"""Mark a function as deprecated.
Args:
deprecated_in (Optional[str]): Version in which the function was deprecated.
details (Optional[str]): Additional details about the deprecation.
@@ -304,8 +329,8 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
Callable: The decorated function.
"""
def _function_wrapper(function: Callable) -> Callable:
def _inner_wrapper(*args: Any, **kwargs: Any) -> Any:
def _function_wrapper(function: Callable):
def _inner_wrapper(*args, **kwargs):
warn_deprecation(function.__name__, deprecated_in, details)
return function(*args, **kwargs)
@@ -315,7 +340,7 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None:
"""Reports a deprecation warning.
"""Report a deprecation warning.
Args:
function_name (str): Name of the deprecated function.
deprecated_in (Optional[str]): Version in which the function was deprecated.
@@ -329,3 +354,7 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
stack_info=True,
stacklevel=4,
)
# import warnings # pylint: disable=import-outside-toplevel
# warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)