a929f68ad4
- Truly fixes PMX Import lol, i messed up completely - Updated MMD Tools to use Cats One
533 lines
20 KiB
Python
533 lines
20 KiB
Python
# Copyright 2013 MMD Tools authors
|
|
# This file is part of MMD Tools.
|
|
|
|
import contextlib
|
|
import math
|
|
from typing import Generator, List, Optional, TypeVar
|
|
|
|
import bmesh
|
|
import bpy
|
|
from mathutils import Matrix
|
|
|
|
|
|
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, 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)
|
|
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) | {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, 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, 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: 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, 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):
|
|
if target_object is None:
|
|
mesh_data = bpy.data.meshes.new("Sphere")
|
|
target_object = createObject(name="Sphere", object_data=mesh_data)
|
|
|
|
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):
|
|
if target_object is None:
|
|
mesh_data = bpy.data.meshes.new("Box")
|
|
target_object = createObject(name="Box", object_data=mesh_data)
|
|
|
|
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):
|
|
if target_object is None:
|
|
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
|
|
bm = bmesh.new()
|
|
verts = bm.verts
|
|
top = (0, 0, height / 2 + radius)
|
|
verts.new(top)
|
|
|
|
# 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)
|
|
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]:
|
|
# 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: 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:
|
|
# 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: 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]:
|
|
"""
|
|
Find 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)
|