Files
Avatar-Toolkit/core/mmd/bpyutils.py
T
2025-04-10 23:40:51 +01:00

522 lines
20 KiB
Python

# -*- 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.
import contextlib
from typing import Generator, List, Optional, TypeVar
import bpy
class Props: # For API changes of only name changed properties
show_in_front = "show_in_front"
display_type = "display_type"
display_size = "display_size"
empty_display_type = "empty_display_type"
empty_display_size = "empty_display_size"
class __EditMode:
def __init__(self, obj):
if not isinstance(obj, bpy.types.Object):
raise ValueError
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:
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:
pass
contenxt = FnContext.ensure_context()
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.__hides: List[bool] = []
for i in self.__selected_objects:
self.__hides.append(i.hide_get())
FnContext.select_object(contenxt, i)
FnContext.set_active_object(contenxt, active_object)
def __enter__(self) -> bpy.types.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, parent):
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
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]
bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False)
bpy.ops.object.mode_set(mode="OBJECT")
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.
with edit_object:
some functions...
"""
return __EditMode(obj)
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.
This function can select "hidden" objects safely.
with select_object(obj):
some functions...
"""
# TODO: Reimplement with bpy.context.temp_override (If it ain't broke, don't fix it.)
return __SelectObjects(obj, objects)
def duplicateObject(obj, total_len):
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
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=8, ring_count=5, radius=1.0, target_object=None):
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=(1, 1, 1), target_object=None):
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=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
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*i/ring_count
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:
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
@staticmethod
def create(constraints, name, map_type):
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, name_id=""):
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, influence=1):
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:
def __init__(self):
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
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:
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
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)
ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = TypeVar("ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE")
class FnContext:
def __init__(self):
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
return context or bpy.context
@staticmethod
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
return context.active_object
@staticmethod
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: 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: bpy.types.Context) -> bpy.types.SceneObjects:
return context.scene.objects
@staticmethod
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: 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: 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: 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: 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: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
context.collection.objects.link(obj)
return obj
@staticmethod
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: 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 (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[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.
"""
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: 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.
Args:
context (bpy.types.Context): The Blender context.
target_object (bpy.types.Object): The target object to find the layer collection for.
Returns:
Optional[bpy.types.LayerCollection]: The layer collection that contains 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
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:
return found
return None
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:
return found
return None
@staticmethod
@contextlib.contextmanager
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.
This context manager allows you to temporarily change the active_layer_collection in the given context to the one that contains the target object.
It ensures that the original active_layer_collection is restored after the context is exited.
Args:
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:
bpy.types.Context: The modified context with the active_layer_collection overridden.
Example:
with FnContext.temp_override_active_layer_collection(context, target_object):
# Perform operations with the modified context
bpy.ops.object.select_all(action='DESELECT')
target_object.select_set(True)
bpy.ops.object.delete()
"""
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
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: 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: 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
if selected_objects is not None:
keywords["selected_objects"] = selected_objects
keywords["selected_editable_objects"] = selected_objects
return context.temp_override(window=window, area=area, region=region, **keywords)