Bringing files in-line with Avatar Toolkit

- Adding better typing
- Update to use Avatar Toolkit's logging system.
- Removed some files which were in the wrong location (From my first attempt).
This commit is contained in:
Yusarina
2025-04-12 00:17:11 +01:00
parent d25c95fc73
commit bb5a314796
5 changed files with 153 additions and 429 deletions
+82 -67
View File
@@ -6,9 +6,13 @@
# 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
from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union
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
class Props: # For API changes of only name changed properties
@@ -20,7 +24,7 @@ class Props: # For API changes of only name changed properties
class __EditMode:
def __init__(self, obj):
def __init__(self, obj: Object):
if not isinstance(obj, bpy.types.Object):
raise ValueError
self.__prevMode = obj.mode
@@ -30,10 +34,10 @@ class __EditMode:
if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
def __enter__(self):
def __enter__(self) -> Any:
return self.__obj.data
def __exit__(self, type, value, traceback):
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
if self.__prevMode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
bpy.ops.object.mode_set(mode=self.__prevMode)
@@ -41,17 +45,18 @@ class __EditMode:
class __SelectObjects:
def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
def __init__(self, active_object: Object, selected_objects: Optional[List[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
contenxt = FnContext.ensure_context()
context = FnContext.ensure_context()
for i in contenxt.selected_objects:
for i in context.selected_objects:
i.select_set(False)
self.__active_object = active_object
@@ -60,23 +65,23 @@ class __SelectObjects:
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)
FnContext.select_object(context, i)
FnContext.set_active_object(context, active_object)
def __enter__(self) -> bpy.types.Object:
def __enter__(self) -> Object:
return self.__active_object
def __exit__(self, type, value, traceback):
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 setParent(obj, parent):
def setParent(obj: Object, parent: Object) -> None:
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):
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name]
@@ -84,7 +89,7 @@ def setParentToBone(obj, parent, bone_name):
bpy.ops.object.mode_set(mode="OBJECT")
def edit_object(obj):
def edit_object(obj: Object) -> __EditMode:
"""Set the object interaction mode to 'EDIT'
It is recommended to use 'edit_object' with 'with' statement like the following code.
@@ -95,7 +100,7 @@ def edit_object(obj):
return __EditMode(obj)
def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects:
"""Select objects.
It is recommended to use 'select_object' with 'with' statement like the following code.
@@ -108,20 +113,23 @@ def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object
return __SelectObjects(obj, objects)
def duplicateObject(obj, total_len):
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
def createObject(name="Object", object_data=None, target_scene=None):
def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object:
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):
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
import bmesh
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 = target_object.data
bm = bmesh.new()
@@ -138,12 +146,15 @@ def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
return target_object
def makeBox(size=(1, 1, 1), target_object=None):
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
import bmesh
from mathutils import Matrix
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 = target_object.data
bm = bmesh.new()
@@ -159,13 +170,16 @@ def makeBox(size=(1, 1, 1), target_object=None):
return target_object
def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
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
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}")
height = max(height, 1e-3)
mesh = target_object.data
@@ -224,10 +238,10 @@ def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=N
class TransformConstraintOp:
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
__MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"}
@staticmethod
def create(constraints, name, map_type):
def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint:
c = constraints.get(name, None)
if c and c.type != "TRANSFORM":
constraints.remove(c)
@@ -245,7 +259,7 @@ class TransformConstraintOp:
return c
@classmethod
def min_max_attributes(cls, map_type, name_id=""):
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
key = (map_type, name_id)
ret = cls.__MIN_MAX_MAP.get(key, None)
if ret is None:
@@ -255,7 +269,7 @@ class TransformConstraintOp:
return ret
@classmethod
def update_min_max(cls, constraint, value, influence=1):
def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None:
c = constraint
if not c or c.type != "TRANSFORM":
return
@@ -279,14 +293,14 @@ class FnObject:
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):
def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None:
assert isinstance(mesh_object.data, bpy.types.Mesh)
key: bpy.types.Key = shape_key.id_data
key: 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
fc_curve: FCurve
for fc_curve in mesh_object.animation_data.drivers:
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
continue
@@ -310,35 +324,35 @@ class FnContext:
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
def ensure_context(context: Optional[Context] = None) -> Context:
return context or bpy.context
@staticmethod
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
def get_active_object(context: Context) -> Optional[Object]:
return context.active_object
@staticmethod
def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def set_active_object(context: Context, obj: Object) -> 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:
def set_active_and_select_single_object(context: Context, obj: Object) -> 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:
def get_scene_objects(context: Context) -> bpy.types.SceneObjects:
return context.scene.objects
@staticmethod
def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def ensure_selectable(context: Context, obj: Object) -> 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:
def __layer_check(layer_collection: LayerCollection) -> bool:
for lc in layer_collection.children:
if __layer_check(lc):
lc.hide_viewport = False
@@ -360,44 +374,44 @@ class FnContext:
return obj
@staticmethod
def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def select_object(context: Context, obj: Object) -> 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]:
def select_objects(context: Context, *objects: Object) -> List[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:
def select_single_object(context: Context, obj: Object) -> 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:
def link_object(context: Context, obj: Object) -> 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:
def new_and_link_object(context: Context, name: str, object_data: Optional[ID]) -> 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]:
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[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.
context (Context): The context in which the duplication is performed.
object_to_duplicate (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.
List[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.
@@ -421,27 +435,28 @@ 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: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[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.
context (Context): The Blender context.
target_object (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.
Optional[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
scene_layer_collection: LayerCollection = context.view_layer.layer_collection
def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]:
if layer_collection.name == name:
return layer_collection
child_layer_collection: bpy.types.LayerCollection
child_layer_collection: LayerCollection
for child_layer_collection in layer_collection.children:
found = find_layer_collection_by_name(child_layer_collection, name)
if found is not None:
@@ -449,7 +464,7 @@ class FnContext:
return None
user_collection: bpy.types.Collection
user_collection: 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:
@@ -459,7 +474,7 @@ class FnContext:
@staticmethod
@contextlib.contextmanager
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
"""
Context manager to temporarily override the active_layer_collection that contains the target object.
@@ -467,11 +482,11 @@ class FnContext:
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.
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.
Yields:
bpy.types.Context: The modified context with the active_layer_collection overridden.
Context: The modified context with the active_layer_collection overridden.
Example:
with FnContext.temp_override_active_layer_collection(context, target_object):
@@ -492,24 +507,24 @@ class FnContext:
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)
def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]:
addon: 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:
def get_addon_preferences_attribute(context: 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]:
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]:
if active_object is not None:
keywords["active_object"] = active_object
keywords["object"] = active_object