Initial MMD Importer Commit
- This is the initial commit I spent several hours trying to get it up two Avatar Toolkit standard, it does not work yet because there are files missing but I been doing this since 6am and it is 4pm almost, i need food. - I have also removed as much legacy code as i could, MMD Tools contains so much of it even though there have a 4.2+ only version there have not removed any of the legacy code for pre 4.2.... this is going to take a while. God I hope this works fine once I am done.
This commit is contained in:
@@ -0,0 +1,533 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2013 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
|
||||
# All credit goes to the original authors.
|
||||
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
|
||||
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
|
||||
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
|
||||
|
||||
import contextlib
|
||||
from typing import Generator, List, Optional, TypeVar, Dict, Any, Set, Tuple, Type
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Material, Context
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ...logging_setup import logger
|
||||
from ...addon_preferences import get_preference, save_preference
|
||||
|
||||
|
||||
class __EditMode:
|
||||
"""Context manager for edit mode operations"""
|
||||
def __init__(self, obj: Object):
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
raise ValueError("Expected a Blender Object")
|
||||
self.__prevMode = obj.mode
|
||||
self.__obj = obj
|
||||
self.__obj_select = obj.select_get()
|
||||
with select_object(obj):
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
def __enter__(self):
|
||||
return self.__obj.data
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self.__prevMode == "EDIT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
|
||||
bpy.ops.object.mode_set(mode=self.__prevMode)
|
||||
self.__obj.select_set(self.__obj_select)
|
||||
|
||||
|
||||
class __SelectObjects:
|
||||
"""Context manager for object selection operations"""
|
||||
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
|
||||
if not isinstance(active_object, bpy.types.Object):
|
||||
raise ValueError("Expected a Blender Object")
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = FnContext.ensure_context()
|
||||
|
||||
for i in context.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.__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)
|
||||
|
||||
def __enter__(self) -> Object:
|
||||
return self.__active_object
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
for i, j in zip(self.__selected_objects, self.__hides):
|
||||
i.hide_set(j)
|
||||
|
||||
|
||||
def setParent(obj: Object, parent: Object) -> None:
|
||||
"""Set parent relationship between objects"""
|
||||
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:
|
||||
"""Set parent relationship to a specific bone"""
|
||||
with select_object(parent, objects=[parent, obj]):
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
parent.data.bones.active = parent.data.bones[bone_name]
|
||||
bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def edit_object(obj: Object):
|
||||
"""Set the object interaction mode to 'EDIT'
|
||||
|
||||
It is recommended to use 'edit_object' with 'with' statement like the following code.
|
||||
|
||||
with edit_object:
|
||||
some functions...
|
||||
"""
|
||||
return __EditMode(obj)
|
||||
|
||||
|
||||
def select_object(obj: Object, objects: Optional[List[Object]] = None):
|
||||
"""Select objects.
|
||||
|
||||
It is recommended to use 'select_object' with 'with' statement like the following code.
|
||||
This function can select "hidden" objects safely.
|
||||
|
||||
with select_object(obj):
|
||||
some functions...
|
||||
"""
|
||||
return __SelectObjects(obj, objects)
|
||||
|
||||
|
||||
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
|
||||
"""Duplicate an object multiple times"""
|
||||
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
|
||||
|
||||
|
||||
def createObject(name: str = "Object", object_data: Optional[Any] = None, target_scene: Optional[Any] = None) -> Object:
|
||||
"""Create a new object and link it to the scene"""
|
||||
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:
|
||||
"""Create a sphere mesh object"""
|
||||
import bmesh
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Sphere")
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_uvsphere(
|
||||
bm,
|
||||
u_segments=segment,
|
||||
v_segments=ring_count,
|
||||
radius=radius,
|
||||
)
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
return target_object
|
||||
|
||||
|
||||
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
|
||||
"""Create a box mesh object"""
|
||||
import bmesh
|
||||
from mathutils import Matrix
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Box")
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_cube(
|
||||
bm,
|
||||
size=2,
|
||||
matrix=Matrix([[size[0], 0, 0, 0], [0, size[1], 0, 0], [0, 0, size[2], 0], [0, 0, 0, 1]]),
|
||||
)
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
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:
|
||||
"""Create a capsule mesh object"""
|
||||
import math
|
||||
import bmesh
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Capsule")
|
||||
height = max(height, 1e-3)
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
verts = bm.verts
|
||||
top = (0, 0, height / 2 + radius)
|
||||
verts.new(top)
|
||||
|
||||
f = lambda i: 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)
|
||||
for j in range(segment):
|
||||
theta = 2 * math.pi / segment * j
|
||||
x = t * math.sin(-theta)
|
||||
y = t * math.cos(-theta)
|
||||
verts.new((x, y, z + height / 2))
|
||||
|
||||
for i in range(ring_count):
|
||||
z = -f(i)
|
||||
t = math.sqrt(radius**2 - z**2)
|
||||
for j in range(segment):
|
||||
theta = 2 * math.pi / segment * j
|
||||
x = t * math.sin(-theta)
|
||||
y = t * math.cos(-theta)
|
||||
verts.new((x, y, z - height / 2))
|
||||
|
||||
bottom = (0, 0, -(height / 2 + radius))
|
||||
verts.new(bottom)
|
||||
if hasattr(verts, "ensure_lookup_table"):
|
||||
verts.ensure_lookup_table()
|
||||
|
||||
faces = bm.faces
|
||||
for i in range(1, segment):
|
||||
faces.new([verts[x] for x in (0, i, i + 1)])
|
||||
faces.new([verts[x] for x in (0, segment, 1)])
|
||||
offset = segment + 1
|
||||
for i in range(ring_count * 2 - 1):
|
||||
for j in range(segment - 1):
|
||||
t = offset + j
|
||||
faces.new([verts[x] for x in (t - segment, t, t + 1, t - segment + 1)])
|
||||
faces.new([verts[x] for x in (offset - 1, offset + segment - 1, offset, offset - segment)])
|
||||
offset += segment
|
||||
for i in range(segment - 1):
|
||||
t = offset + i
|
||||
faces.new([verts[x] for x in (t - segment, offset, t - segment + 1)])
|
||||
faces.new([verts[x] for x in (offset - 1, offset, offset - segment)])
|
||||
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
bm.normal_update()
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
return target_object
|
||||
|
||||
|
||||
class TransformConstraintOp:
|
||||
"""Helper class for transform constraints"""
|
||||
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
|
||||
|
||||
@staticmethod
|
||||
def create(constraints, name: str, map_type: str):
|
||||
"""Create a transform constraint"""
|
||||
c = constraints.get(name, None)
|
||||
if c and c.type != "TRANSFORM":
|
||||
constraints.remove(c)
|
||||
c = None
|
||||
if c is None:
|
||||
c = constraints.new("TRANSFORM")
|
||||
c.name = name
|
||||
c.use_motion_extrapolate = True
|
||||
c.target_space = c.owner_space = "LOCAL"
|
||||
c.map_from = c.map_to = map_type
|
||||
c.map_to_x_from = "X"
|
||||
c.map_to_y_from = "Y"
|
||||
c.map_to_z_from = "Z"
|
||||
c.influence = 1
|
||||
return c
|
||||
|
||||
@classmethod
|
||||
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
|
||||
"""Get min/max attribute names for a constraint type"""
|
||||
key = (map_type, name_id)
|
||||
ret = cls.__MIN_MAX_MAP.get(key, None)
|
||||
if ret is None:
|
||||
defaults = (i + j + k for i in ("from_", "to_") for j in ("min_", "max_") for k in "xyz")
|
||||
extension = cls.__MIN_MAX_MAP.get(map_type, "")
|
||||
ret = cls.__MIN_MAX_MAP[key] = tuple(n + extension for n in defaults if name_id in n)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def update_min_max(cls, constraint, value: float, influence: Optional[float] = 1):
|
||||
"""Update min/max values for a constraint"""
|
||||
c = constraint
|
||||
if not c or c.type != "TRANSFORM":
|
||||
return
|
||||
|
||||
for attr in cls.min_max_attributes(c.map_from, "from_min"):
|
||||
setattr(c, attr, -value)
|
||||
for attr in cls.min_max_attributes(c.map_from, "from_max"):
|
||||
setattr(c, attr, value)
|
||||
|
||||
if influence is None:
|
||||
return
|
||||
|
||||
for attr in cls.min_max_attributes(c.map_to, "to_min"):
|
||||
setattr(c, attr, -value * influence)
|
||||
for attr in cls.min_max_attributes(c.map_to, "to_max"):
|
||||
setattr(c, attr, value * influence)
|
||||
|
||||
|
||||
class FnObject:
|
||||
"""Function collection for object operations"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def mesh_remove_shape_key(mesh_object: Object, shape_key: bpy.types.ShapeKey) -> None:
|
||||
"""Remove a shape key from a mesh object, cleaning up drivers"""
|
||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||
|
||||
key: bpy.types.Key = shape_key.id_data
|
||||
assert key == mesh_object.data.shape_keys
|
||||
|
||||
if mesh_object.animation_data is not None:
|
||||
for fc_curve in mesh_object.animation_data.drivers:
|
||||
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
|
||||
continue
|
||||
mesh_object.driver_remove(fc_curve.data_path)
|
||||
|
||||
key_blocks = key.key_blocks
|
||||
|
||||
last_index = mesh_object.active_shape_key_index or 0
|
||||
if last_index >= key_blocks.find(shape_key.name):
|
||||
last_index = max(0, last_index - 1)
|
||||
|
||||
mesh_object.shape_key_remove(shape_key)
|
||||
mesh_object.active_shape_key_index = min(last_index, len(key_blocks) - 1)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class FnContext:
|
||||
"""Function collection for context operations"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def ensure_context(context: Optional[Context] = None) -> Context:
|
||||
"""Get a valid context, using bpy.context if none provided"""
|
||||
return context or bpy.context
|
||||
|
||||
@staticmethod
|
||||
def get_active_object(context: Context) -> Optional[Object]:
|
||||
"""Get the active object from context safely"""
|
||||
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:
|
||||
"""Set the active object in context"""
|
||||
context.view_layer.objects.active = obj
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def set_active_and_select_single_object(context: Context, obj: Object) -> Object:
|
||||
"""Set an object as active and the only selected object"""
|
||||
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
|
||||
|
||||
@staticmethod
|
||||
def get_scene_objects(context: Context) -> List[Object]:
|
||||
"""Get all objects in the scene safely"""
|
||||
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:
|
||||
"""Make sure an object is selectable by unhiding it and its collections"""
|
||||
obj.hide_viewport = False
|
||||
obj.hide_select = False
|
||||
obj.hide_set(False)
|
||||
|
||||
if obj not in context.selectable_objects:
|
||||
def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool:
|
||||
for lc in layer_collection.children:
|
||||
if __layer_check(lc):
|
||||
lc.hide_viewport = False
|
||||
lc.collection.hide_viewport = False
|
||||
lc.collection.hide_select = False
|
||||
return True
|
||||
if obj in layer_collection.collection.objects.values():
|
||||
if layer_collection.exclude:
|
||||
layer_collection.exclude = False
|
||||
return True
|
||||
return False
|
||||
|
||||
selected_objects = set(context.selected_objects)
|
||||
__layer_check(context.view_layer.layer_collection)
|
||||
if len(context.selected_objects) != len(selected_objects):
|
||||
for i in context.selected_objects:
|
||||
if i not in selected_objects:
|
||||
i.select_set(False)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def select_object(context: Context, obj: Object) -> Object:
|
||||
"""Select an object in the context"""
|
||||
FnContext.ensure_selectable(context, obj).select_set(True)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def select_objects(context: Context, *objects: Object) -> List[Object]:
|
||||
"""Select multiple objects in the context"""
|
||||
return [FnContext.select_object(context, obj) for obj in objects]
|
||||
|
||||
@staticmethod
|
||||
def select_single_object(context: Context, obj: Object) -> Object:
|
||||
"""Select only the specified object, deselecting all others"""
|
||||
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:
|
||||
"""Link an object to the active collection"""
|
||||
context.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def new_and_link_object(context: Context, name: str, object_data: Optional[Any]) -> Object:
|
||||
"""Create a new object and link it to the active collection"""
|
||||
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]:
|
||||
"""
|
||||
Duplicate an object to reach the target count.
|
||||
|
||||
Args:
|
||||
context: The context in which the duplication is performed
|
||||
object_to_duplicate: The object to be duplicated
|
||||
target_count: The desired count of duplicated objects
|
||||
|
||||
Returns:
|
||||
A list of duplicated objects
|
||||
"""
|
||||
for o in context.selected_objects:
|
||||
o.select_set(False)
|
||||
object_to_duplicate.select_set(True)
|
||||
assert len(context.selected_objects) == 1
|
||||
assert context.selected_objects[0] == object_to_duplicate
|
||||
last_selected_objects = result_objects = [object_to_duplicate]
|
||||
while len(result_objects) < target_count:
|
||||
bpy.ops.object.duplicate()
|
||||
result_objects.extend(context.selected_objects)
|
||||
remain = target_count - len(result_objects) - len(context.selected_objects)
|
||||
if remain < 0:
|
||||
last_selected_objects = context.selected_objects
|
||||
for i in range(-remain):
|
||||
last_selected_objects[i].select_set(False)
|
||||
else:
|
||||
for i in range(min(remain, len(last_selected_objects))):
|
||||
last_selected_objects[i].select_set(True)
|
||||
last_selected_objects = context.selected_objects
|
||||
assert len(result_objects) == target_count
|
||||
return result_objects
|
||||
|
||||
@staticmethod
|
||||
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[bpy.types.LayerCollection]:
|
||||
"""
|
||||
Find the layer collection containing the target object.
|
||||
|
||||
Args:
|
||||
context: The Blender context
|
||||
target_object: The target object to find the layer collection for
|
||||
|
||||
Returns:
|
||||
The layer collection containing the target object, or None if not found
|
||||
"""
|
||||
scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection
|
||||
|
||||
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
|
||||
|
||||
for child_layer_collection in layer_collection.children:
|
||||
found = find_layer_collection_by_name(child_layer_collection, name)
|
||||
if found is not None:
|
||||
return found
|
||||
|
||||
return None
|
||||
|
||||
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:
|
||||
return found
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
|
||||
"""
|
||||
Temporarily override the active layer collection to the one containing the target object.
|
||||
|
||||
Args:
|
||||
context: The context to modify
|
||||
target_object: The object whose collection should become active
|
||||
|
||||
Yields:
|
||||
The modified context
|
||||
"""
|
||||
original_layer_collection = context.view_layer.active_layer_collection
|
||||
target_layer_collection = FnContext.find_user_layer_collection_by_object(context, target_object)
|
||||
if target_layer_collection is not None:
|
||||
context.view_layer.active_layer_collection = target_layer_collection
|
||||
try:
|
||||
yield context
|
||||
finally:
|
||||
if context.view_layer.active_layer_collection.name != original_layer_collection.name:
|
||||
context.view_layer.active_layer_collection = original_layer_collection
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def temp_override_objects(
|
||||
context: Context,
|
||||
active_object: Optional[Object] = None,
|
||||
selected_objects: Optional[List[Object]] = None,
|
||||
**keywords
|
||||
) -> Generator[Context, None, None]:
|
||||
"""Create a temporary context override for object operations using Blender 4.4+ temp_override."""
|
||||
override_dict = {}
|
||||
|
||||
if active_object is not None:
|
||||
override_dict["active_object"] = active_object
|
||||
override_dict["object"] = active_object
|
||||
|
||||
if selected_objects is not None:
|
||||
override_dict["selected_objects"] = selected_objects
|
||||
override_dict["selected_editable_objects"] = selected_objects
|
||||
|
||||
override_dict.update(keywords)
|
||||
|
||||
with context.temp_override(**override_dict) as override_context:
|
||||
yield override_context
|
||||
|
||||
@staticmethod
|
||||
def get_preference(key: str, default: T = None) -> T:
|
||||
"""
|
||||
Get a preference value using Avatar Toolkit's preference system."""
|
||||
return get_preference(key, default)
|
||||
|
||||
@staticmethod
|
||||
def save_preference(key: str, value: Any) -> None:
|
||||
"""Save a preference value using Avatar Toolkit's preference system."""
|
||||
save_preference(key, value)
|
||||
@@ -0,0 +1,296 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2013 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
|
||||
# All credit goes to the original authors.
|
||||
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
|
||||
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
|
||||
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Callable, Optional, Set, List, Dict, Any
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Context, Bone, PoseBone
|
||||
|
||||
from ...logging_setup import logger
|
||||
from .bpyutils import FnContext
|
||||
|
||||
|
||||
def selectAObject(obj: Object) -> None:
|
||||
"""Select a single object and make it active"""
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
logger.debug(f"Failed to set object mode for {obj.name}")
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
FnContext.select_object(FnContext.ensure_context(), obj)
|
||||
FnContext.set_active_object(FnContext.ensure_context(), obj)
|
||||
|
||||
|
||||
def enterEditMode(obj: Object) -> None:
|
||||
"""Enter edit mode for the specified object"""
|
||||
selectAObject(obj)
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
|
||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
||||
"""Set an object's parent to a specific bone"""
|
||||
selectAObject(obj)
|
||||
FnContext.set_active_object(FnContext.ensure_context(), parent)
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
parent.data.bones.active = parent.data.bones[bone_name]
|
||||
bpy.ops.object.parent_set(type="BONE", keep_transform=False)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def selectSingleBone(context: Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
|
||||
"""Select a single bone in an armature"""
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
logger.debug(f"Failed to set object mode for bone selection: {bone_name}")
|
||||
|
||||
for i in context.selected_objects:
|
||||
i.select_set(False)
|
||||
|
||||
FnContext.set_active_object(context, armature)
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
if reset_pose:
|
||||
for p_bone in armature.pose.bones:
|
||||
p_bone.matrix_basis.identity()
|
||||
|
||||
armature_bones = armature.data.bones
|
||||
for bone in armature_bones:
|
||||
bone.select = bone.name == bone_name
|
||||
bone.select_head = bone.select_tail = bone.select
|
||||
if bone.select:
|
||||
armature_bones.active = bone
|
||||
bone.hide = False
|
||||
|
||||
|
||||
# Regular expressions for name conversion
|
||||
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
|
||||
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
|
||||
|
||||
|
||||
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
|
||||
"""Convert Japanese left/right naming to Blender's L/R convention"""
|
||||
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
|
||||
delimiter = "_" if use_underscore else "."
|
||||
if m:
|
||||
name = m.group(1) + m.group(2) + delimiter + "L"
|
||||
m = __CONVERT_NAME_TO_R_REGEXP.match(name)
|
||||
if m:
|
||||
name = m.group(1) + m.group(2) + delimiter + "R"
|
||||
return name
|
||||
|
||||
|
||||
__CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<after>($|(?P=separator)))")
|
||||
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
|
||||
|
||||
|
||||
def convertLRToName(name: str) -> str:
|
||||
"""Convert Blender's L/R convention to Japanese left/right naming"""
|
||||
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
|
||||
if match:
|
||||
return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
||||
|
||||
match = __CONVERT_R_TO_NAME_REGEXP.search(name)
|
||||
if match:
|
||||
return f"右{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
|
||||
"""Merge weights from source vertex group to destination vertex group"""
|
||||
mesh = meshObj.data
|
||||
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
|
||||
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
|
||||
|
||||
vtxIndex = src_vertex_group.index
|
||||
for v in mesh.vertices:
|
||||
try:
|
||||
gi = [i.group for i in v.groups].index(vtxIndex)
|
||||
dest_vertex_group.add([v.index], v.groups[gi].weight, "ADD")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def separateByMaterials(meshObj: Object) -> None:
|
||||
"""Separate a mesh object by materials"""
|
||||
if len(meshObj.data.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)
|
||||
bpy.context.collection.objects.link(dummy_parent)
|
||||
|
||||
meshObj.parent = dummy_parent
|
||||
meshObj.active_shape_key_index = 0
|
||||
|
||||
try:
|
||||
enterEditMode(meshObj)
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
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
|
||||
|
||||
bpy.data.objects.remove(dummy_parent)
|
||||
|
||||
|
||||
def clearUnusedMeshes() -> None:
|
||||
"""Remove unused mesh data blocks"""
|
||||
meshes_to_delete = []
|
||||
for mesh in bpy.data.meshes:
|
||||
if mesh.users == 0:
|
||||
meshes_to_delete.append(mesh)
|
||||
|
||||
for mesh in meshes_to_delete:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
|
||||
"""Create a mapping from bone names to pose bones"""
|
||||
return {(i.mmd_bone.name_j or i.name): i for i in armObj.pose.bones}
|
||||
|
||||
|
||||
__REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")
|
||||
|
||||
|
||||
def unique_name(name: str, used_names: Set[str]) -> str:
|
||||
"""Create a unique name that doesn't exist in the used_names set
|
||||
|
||||
Args:
|
||||
name (str): The name to make unique
|
||||
used_names (Set[str]): A set of names that are already used
|
||||
|
||||
Returns:
|
||||
str: The unique name, formatted as "{name}.{number:03d}"
|
||||
"""
|
||||
if name not in used_names:
|
||||
return name
|
||||
|
||||
count = 1
|
||||
new_name = orig_name = __REMOVE_PREFIX_DIGITS_REGEXP.sub("", name)
|
||||
|
||||
while new_name in used_names:
|
||||
new_name = f"{orig_name}.{count:03d}"
|
||||
count += 1
|
||||
|
||||
return new_name
|
||||
|
||||
|
||||
def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
|
||||
"""Safely get a relative path, handling different drive issues on Windows
|
||||
|
||||
Strategies:
|
||||
- inside: returns the basename of the path
|
||||
- outside: prepends '..' to the basename if on different drive
|
||||
- absolute: returns the absolute path
|
||||
"""
|
||||
if strategy == "inside":
|
||||
return os.path.basename(path)
|
||||
|
||||
if strategy == "absolute":
|
||||
return os.path.abspath(path)
|
||||
|
||||
if strategy == "outside" and os.name == "nt":
|
||||
d1, _ = os.path.splitdrive(path)
|
||||
d2, _ = os.path.splitdrive(start)
|
||||
if d1 != d2:
|
||||
return ".." + os.sep + os.path.basename(path)
|
||||
|
||||
return os.path.relpath(path, start)
|
||||
|
||||
|
||||
class ItemOp:
|
||||
"""Operations for managing collections of items"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_index(items: List[Any], index: int) -> Optional[Any]:
|
||||
"""Get an item by index with bounds checking"""
|
||||
if 0 <= index < len(items):
|
||||
return items[index]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
|
||||
"""Resize a collection to the specified length"""
|
||||
count = length - len(items)
|
||||
if count > 0:
|
||||
for i in range(count):
|
||||
items.add()
|
||||
elif count < 0:
|
||||
for i in range(-count):
|
||||
items.remove(length)
|
||||
|
||||
@staticmethod
|
||||
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> tuple:
|
||||
"""Add a new item after the specified index"""
|
||||
index_end = len(items)
|
||||
index = max(0, min(index_end, index + 1))
|
||||
items.add()
|
||||
items.move(index_end, index)
|
||||
return items[index], index
|
||||
|
||||
|
||||
class ItemMoveOp:
|
||||
"""Operations for moving items in collections"""
|
||||
|
||||
@staticmethod
|
||||
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
|
||||
index_min: int = 0, index_max: Optional[int] = None) -> int:
|
||||
"""Move an item in a collection
|
||||
|
||||
Args:
|
||||
items: The collection to modify
|
||||
index: Current index of the item
|
||||
move_type: Type of move ('UP', 'DOWN', 'TOP', 'BOTTOM')
|
||||
index_min: Minimum allowed index
|
||||
index_max: Maximum allowed index
|
||||
|
||||
Returns:
|
||||
int: The new index after moving
|
||||
"""
|
||||
if index_max is None:
|
||||
index_max = len(items) - 1
|
||||
else:
|
||||
index_max = min(index_max, len(items) - 1)
|
||||
|
||||
index_min = min(index_min, index_max)
|
||||
|
||||
if index < index_min:
|
||||
items.move(index, index_min)
|
||||
return index_min
|
||||
elif index > index_max:
|
||||
items.move(index, index_max)
|
||||
return index_max
|
||||
|
||||
index_new = index
|
||||
if move_type == "UP":
|
||||
index_new = max(index_min, index - 1)
|
||||
elif move_type == "DOWN":
|
||||
index_new = min(index + 1, index_max)
|
||||
elif move_type == "TOP":
|
||||
index_new = index_min
|
||||
elif move_type == "BOTTOM":
|
||||
index_new = index_max
|
||||
|
||||
if index_new != index:
|
||||
items.move(index, index_new)
|
||||
|
||||
return index_new
|
||||
Reference in New Issue
Block a user