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
+102 -106
View File
@@ -1,18 +1,13 @@
# -*- 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 2013 MMD Tools authors
# This file is part of MMD Tools.
import contextlib
from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union
import math
from typing import Generator, List, Optional, TypeVar
import bmesh
import bpy
from bpy.types import Object, Context, ID, Key, ShapeKey, FCurve, LayerCollection, Collection
from bpy.types import AddonPreferences, Addon, WindowManager, Area, Region, Window
from ..logging_setup import logger
from mathutils import Matrix
class Props: # For API changes of only name changed properties
@@ -24,7 +19,7 @@ class Props: # For API changes of only name changed properties
class __EditMode:
def __init__(self, obj: Object):
def __init__(self, obj):
if not isinstance(obj, bpy.types.Object):
raise ValueError
self.__prevMode = obj.mode
@@ -34,10 +29,10 @@ class __EditMode:
if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
def __enter__(self) -> Any:
def __enter__(self):
return self.__obj.data
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
def __exit__(self, exc_type, exc_value, traceback):
if self.__prevMode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
bpy.ops.object.mode_set(mode=self.__prevMode)
@@ -45,43 +40,46 @@ class __EditMode:
class __SelectObjects:
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
if not isinstance(active_object, bpy.types.Object):
raise ValueError
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
logger.debug("Failed to set object mode")
pass
context = FnContext.ensure_context()
contenxt = FnContext.ensure_context()
for i in context.selected_objects:
for i in contenxt.selected_objects:
i.select_set(False)
self.__active_object = active_object
self.__selected_objects = tuple(set(selected_objects) | set([active_object])) if selected_objects else (active_object,)
self.__selected_objects = tuple(set(selected_objects) | {active_object}) if selected_objects else (active_object,)
self.__hides: List[bool] = []
for i in self.__selected_objects:
self.__hides.append(i.hide_get())
FnContext.select_object(context, i)
FnContext.set_active_object(context, active_object)
FnContext.select_object(contenxt, i)
FnContext.set_active_object(contenxt, active_object)
def __enter__(self) -> Object:
def __enter__(self) -> bpy.types.Object:
return self.__active_object
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
for i, j in zip(self.__selected_objects, self.__hides):
i.hide_set(j)
def __exit__(self, exc_type, exc_value, traceback):
for i, j in zip(self.__selected_objects, self.__hides, strict=False):
try:
i.hide_set(j)
except ReferenceError:
# Object may no longer exist, so skip restoring hidden state.
pass
def setParent(obj: Object, parent: Object) -> None:
def setParent(obj, parent):
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
def setParentToBone(obj, parent, bone_name):
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name]
@@ -89,7 +87,7 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
def edit_object(obj: Object) -> __EditMode:
def edit_object(obj):
"""Set the object interaction mode to 'EDIT'
It is recommended to use 'edit_object' with 'with' statement like the following code.
@@ -100,7 +98,7 @@ def edit_object(obj: Object) -> __EditMode:
return __EditMode(obj)
def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects:
def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
"""Select objects.
It is recommended to use 'select_object' with 'with' statement like the following code.
@@ -109,27 +107,26 @@ def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __Sele
with select_object(obj):
some functions...
"""
# TODO: Reimplement with bpy.context.temp_override (If it ain't broke, don't fix it.)
# TODO: Consider reimplementing with bpy.context.temp_override,
# but note that Blender's new API has stability issues.
# temp_override is prone to crashes, making the current approach safer.
# If it ain't broke, don't fix it.
return __SelectObjects(obj, objects)
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
def duplicateObject(obj, total_len):
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object:
def createObject(name="Object", object_data=None, target_scene=None):
context = FnContext.ensure_context(target_scene)
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
import bmesh
def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
if target_object is None:
target_object = createObject(name="Sphere")
logger.debug(f"Created new sphere object: {target_object.name}")
else:
logger.debug(f"Using existing object for sphere: {target_object.name}")
mesh_data = bpy.data.meshes.new("Sphere")
target_object = createObject(name="Sphere", object_data=mesh_data)
mesh = target_object.data
bm = bmesh.new()
@@ -146,15 +143,10 @@ def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, targe
return target_object
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
import bmesh
from mathutils import Matrix
def makeBox(size=(1, 1, 1), target_object=None):
if target_object is None:
target_object = createObject(name="Box")
logger.debug(f"Created new box object: {target_object.name}")
else:
logger.debug(f"Using existing object for box: {target_object.name}")
mesh_data = bpy.data.meshes.new("Box")
target_object = createObject(name="Box", object_data=mesh_data)
mesh = target_object.data
bm = bmesh.new()
@@ -170,16 +162,10 @@ def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optiona
return target_object
def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object:
import math
import bmesh
def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
if target_object is None:
target_object = createObject(name="Capsule")
logger.debug(f"Created new capsule object: {target_object.name}")
else:
logger.debug(f"Using existing object for capsule: {target_object.name}")
mesh_data = bpy.data.meshes.new("Capsule")
target_object = createObject(name="Capsule", object_data=mesh_data)
height = max(height, 1e-3)
mesh = target_object.data
@@ -188,8 +174,11 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
top = (0, 0, height / 2 + radius)
verts.new(top)
# f = lambda i: radius*i/ring_count
f = lambda i: radius * math.sin(0.5 * math.pi * i / ring_count)
# def f(i):
# return radius * i / ring_count
def f(i):
return radius * math.sin(0.5 * math.pi * i / ring_count)
for i in range(ring_count, 0, -1):
z = f(i - 1)
t = math.sqrt(radius**2 - z**2)
@@ -238,10 +227,10 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
class TransformConstraintOp:
__MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"}
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
@staticmethod
def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint:
def create(constraints, name, map_type):
c = constraints.get(name, None)
if c and c.type != "TRANSFORM":
constraints.remove(c)
@@ -259,7 +248,7 @@ class TransformConstraintOp:
return c
@classmethod
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
def min_max_attributes(cls, map_type, name_id=""):
key = (map_type, name_id)
ret = cls.__MIN_MAX_MAP.get(key, None)
if ret is None:
@@ -269,7 +258,7 @@ class TransformConstraintOp:
return ret
@classmethod
def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None:
def update_min_max(cls, constraint, value, influence=1):
c = constraint
if not c or c.type != "TRANSFORM":
return
@@ -293,14 +282,14 @@ class FnObject:
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None:
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
assert isinstance(mesh_object.data, bpy.types.Mesh)
key: Key = shape_key.id_data
key: bpy.types.Key = shape_key.id_data
assert key == mesh_object.data.shape_keys
if mesh_object.animation_data is not None:
fc_curve: FCurve
fc_curve: bpy.types.FCurve
for fc_curve in mesh_object.animation_data.drivers:
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
continue
@@ -324,35 +313,43 @@ class FnContext:
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def ensure_context(context: Optional[Context] = None) -> Context:
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
return context or bpy.context
@staticmethod
def get_active_object(context: Context) -> Optional[Object]:
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
# Added defensive programming for get methods
# Related to: https://github.com/MMD-Blender/blender_mmd_tools_local/issues/176
if context is None or not hasattr(context, "active_object"):
return None
return context.active_object
@staticmethod
def set_active_object(context: Context, obj: Object) -> Object:
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 set_active_and_select_single_object(context: Context, obj: Object) -> Object:
def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
@staticmethod
def get_scene_objects(context: Context) -> bpy.types.SceneObjects:
def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects:
# Added defensive programming for get methods
# Added for consistency with get_active_object
if context is None or not hasattr(context, "scene") or not hasattr(context.scene, "objects"):
return []
return context.scene.objects
@staticmethod
def ensure_selectable(context: Context, obj: Object) -> Object:
def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
obj.hide_viewport = False
obj.hide_select = False
obj.hide_set(False)
if obj not in context.selectable_objects:
def __layer_check(layer_collection: LayerCollection) -> bool:
def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool:
for lc in layer_collection.children:
if __layer_check(lc):
lc.hide_viewport = False
@@ -374,44 +371,44 @@ class FnContext:
return obj
@staticmethod
def select_object(context: Context, obj: Object) -> Object:
def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
FnContext.ensure_selectable(context, obj).select_set(True)
return obj
@staticmethod
def select_objects(context: Context, *objects: Object) -> List[Object]:
def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]:
return [FnContext.select_object(context, obj) for obj in objects]
@staticmethod
def select_single_object(context: Context, obj: Object) -> Object:
def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
for i in context.selected_objects:
if i != obj:
i.select_set(False)
return FnContext.select_object(context, obj)
@staticmethod
def link_object(context: Context, obj: Object) -> Object:
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: Context, name: str, object_data: Optional[ID]) -> Object:
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: Context, object_to_duplicate: Object, target_count: int) -> List[Object]:
def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
"""
Duplicate object.
This function duplicates the given object and returns a list of duplicated objects.
Args:
context (Context): The context in which the duplication is performed.
object_to_duplicate (Object): The object to be duplicated.
context (bpy.types.Context): The context in which the duplication is performed.
object_to_duplicate (bpy.types.Object): The object to be duplicated.
target_count (int): The desired count of duplicated objects.
Returns:
List[Object]: A list of duplicated objects.
List[bpy.types.Object]: A list of duplicated objects.
Raises:
AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated.
@@ -435,28 +432,27 @@ class FnContext:
last_selected_objects[i].select_set(True)
last_selected_objects = context.selected_objects
assert len(result_objects) == target_count
logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects")
return result_objects
@staticmethod
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[LayerCollection]:
def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
"""
Finds the layer collection that contains the given target_object in the user's collections.
Find the layer collection that contains the given target_object in the user's collections.
Args:
context (Context): The Blender context.
target_object (Object): The target object to find the layer collection for.
context (bpy.types.Context): The Blender context.
target_object (bpy.types.Object): The target object to find the layer collection for.
Returns:
Optional[LayerCollection]: The layer collection that contains the target_object, or None if not found.
Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found.
"""
scene_layer_collection: LayerCollection = context.view_layer.layer_collection
scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection
def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]:
def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
if layer_collection.name == name:
return layer_collection
child_layer_collection: LayerCollection
child_layer_collection: bpy.types.LayerCollection
for child_layer_collection in layer_collection.children:
found = find_layer_collection_by_name(child_layer_collection, name)
if found is not None:
@@ -464,7 +460,7 @@ class FnContext:
return None
user_collection: Collection
user_collection: bpy.types.Collection
for user_collection in target_object.users_collection:
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
if found is not None:
@@ -474,7 +470,7 @@ class FnContext:
@staticmethod
@contextlib.contextmanager
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
"""
Context manager to temporarily override the active_layer_collection that contains the target object.
@@ -482,11 +478,11 @@ class FnContext:
It ensures that the original active_layer_collection is restored after the context is exited.
Args:
context (Context): The context in which the active_layer_collection will be overridden.
target_object (Object): The target object whose layer collection will be set as the active_layer_collection.
context (bpy.types.Context): The context in which the active_layer_collection will be overridden.
target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection.
Yields:
Context: The modified context with the active_layer_collection overridden.
bpy.types.Context: The modified context with the active_layer_collection overridden.
Example:
with FnContext.temp_override_active_layer_collection(context, target_object):
@@ -507,24 +503,24 @@ class FnContext:
context.view_layer.active_layer_collection = original_layer_collection
@staticmethod
def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]:
addon: Addon = context.preferences.addons.get(__package__, None)
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
addon: bpy.types.Addon = context.preferences.addons.get(__package__, None)
return addon.preferences if addon else None
@staticmethod
def get_addon_preferences_attribute(context: Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
@staticmethod
def temp_override_objects(
context: Context,
window: Optional[Window] = None,
area: Optional[Area] = None,
region: Optional[Region] = None,
active_object: Optional[Object] = None,
selected_objects: Optional[List[Object]] = None,
**keywords: Any,
) -> Generator[Context, None, None]:
context: bpy.types.Context,
window: Optional[bpy.types.Window] = None,
area: Optional[bpy.types.Area] = None,
region: Optional[bpy.types.Region] = None,
active_object: Optional[bpy.types.Object] = None,
selected_objects: Optional[List[bpy.types.Object]] = None,
**keywords,
) -> Generator[bpy.types.Context, None, None]:
if active_object is not None:
keywords["active_object"] = active_object
keywords["object"] = active_object