Files
BallanceBlenderHelper/bbp_ng/UTIL_functions.py
yyc12345 4ba3ff9e5a fix: fix the aftermath of changing EnumPropHelper.
- fix the generic error of EnumPropHelper.
- use EnumPropHelper in UTIL_ioport_shared.ExportParams field instead of raw Blender string.
- remove useless type hint in various modules.
2025-07-30 13:35:36 +08:00

330 lines
12 KiB
Python

import bpy, mathutils
import math, typing, enum, sys
class BBPException(Exception):
""" The exception thrown by Ballance Blender Plugin"""
pass
def clamp_float(v: float, min_val: float, max_val: float) -> float:
"""
Clamp a float value
:param v: The value need to be clamp.
:param min_val: The allowed minium value (inclusive).
:param max_val: The allowed maxium value (inclusive).
:return: Clamped value.
"""
if (max_val < min_val): raise BBPException("Invalid range of clamp_float().")
if (v < min_val): return min_val
elif (v > max_val): return max_val
else: return v
def clamp_int(v: int, min_val: int, max_val: int) -> int:
"""
Clamp a int value
:param v: The value need to be clamp.
:param min_val: The allowed minium value (inclusive).
:param max_val: The allowed maxium value (inclusive).
:return: Clamped value.
"""
if (max_val < min_val): raise BBPException("Invalid range of clamp_int().")
if (v < min_val): return min_val
elif (v > max_val): return max_val
else: return v
def message_box(message: tuple[str, ...], title: str, icon: str):
"""
Show a message box in Blender. Non-block mode.
:param message: The text this message box displayed. Each item in this param will show as a single line.
:param title: Message box title text.
:param icon: The icon this message box displayed.
"""
def draw(self, context: bpy.types.Context):
layout = self.layout
for item in message:
layout.label(text=item, translate=False)
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def add_into_scene(obj: bpy.types.Object):
"""
Add given object into active scene.
:param obj: The 3d object to be added.
"""
view_layer = bpy.context.view_layer
collection = view_layer.active_layer_collection.collection
collection.objects.link(obj)
def move_to_cursor(obj: bpy.types.Object):
"""
Move given object to the position of cursor.
:param obj: The 3d object to be moved.
"""
# YYC MARK:
# Use `obj.matrix_world` to move, not `obj.location`, because this bug:
# https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation
# The update of `matrix_world` after setting `location` is not immediately.
# And it is inviable that calling `update()` function for `view_layer` to update these fields,
# because it involve too much objects and cost too much time.
# obj.location = bpy.context.scene.cursor.location
obj.matrix_world = obj.matrix_world @ mathutils.Matrix.Translation(bpy.context.scene.cursor.location - obj.location)
def add_into_scene_and_move_to_cursor(obj: bpy.types.Object):
"""
Add given object into active scene and move it to cursor position.
This function is just a simple combination of previous functions.
:param obj: The 3d object to be processed.
"""
add_into_scene(obj)
move_to_cursor(obj)
def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
"""
Deselect all objects and then select given 3d objects.
:param objs: The tuple of 3d objects to be selected.
"""
# deselect all objects first
bpy.ops.object.select_all(action = 'DESELECT')
# if no objects, return
if len(objs) == 0: return
# set selection for each object
for obj in objs:
obj.select_set(True)
# select first object as active object
bpy.context.view_layer.objects.active = objs[0]
def is_in_object_mode() -> bool:
"""
Check whether we are in Blender Object Mode.
:return: True if we are in object mode which suit for exporting something.
"""
# get active object from context
obj = bpy.context.active_object
# if there is no active object, we think it is in object mode
if obj is None: return True
# simply check active object mode
return obj.mode == 'OBJECT'
#region Blender Enum Property Helper
_TRawEnum = typing.TypeVar('_TRawEnum')
_TFctToStr = typing.Callable[[_TRawEnum], str]
_TFctFromStr = typing.Callable[[str], _TRawEnum]
_TFctName = typing.Callable[[_TRawEnum], str]
_TFctDesc = typing.Callable[[_TRawEnum], str]
_TFctIcon = typing.Callable[[_TRawEnum], str | int]
class EnumPropHelper(typing.Generic[_TRawEnum]):
"""
These class contain all functions related to EnumProperty, including generating `items`,
parsing data from EnumProperty string value and getting EnumProperty acceptable string format from data.
"""
# YYC MARK:
# I don't know why I can have subscripting for a `typing.Callable` object.
# It was not introduced in any document and I just know it from AI.
# If I am not doing this, the type hint will crash into Unknown type.
# But it works now I don't want to touch it anymore.
__mCollections: typing.Iterable[_TRawEnum]
__mFctToStr: _TFctToStr[_TRawEnum]
__mFctFromStr: _TFctFromStr[_TRawEnum]
__mFctName: _TFctName[_TRawEnum]
__mFctDesc: _TFctDesc[_TRawEnum]
__mFctIcon: _TFctIcon[_TRawEnum]
def __init__(self, collections: typing.Iterable[_TRawEnum],
fct_to_str: _TFctToStr[_TRawEnum], fct_from_str: _TFctFromStr[_TRawEnum],
fct_name: _TFctName[_TRawEnum], fct_desc: _TFctDesc[_TRawEnum],
fct_icon: _TFctIcon[_TRawEnum]):
"""
Initialize an EnumProperty helper.
:param collections: The collection containing all available enum property entries.
It can be `enum.Enum` or a simple list/tuple.
:param fct_to_str: A function pointer converting data collection member to its string format.
You must make sure that each members built name is unique in collection!
For `enum.IntEnum`, it can be simple `lambda x: str(x.value)`
:param fct_from_str: A function pointer getting data collection member from its string format.
This class promise that given string must can be parsed.
For `enum.IntEnum`, it can be simple `lambda x: TEnum(int(x))`
:param fct_name: A function pointer converting data collection member to its display name which shown in Blender.
:param fct_desc: Same as `fct_name` but return description instead which shown in Blender
If no description, return empty string, not None.
:param fct_icon: Same as `fct_name` but return the used icon instead which shown in Blender.
It can be a Blender builtin icon string, or any loaded icon integer ID.
If no icon, return empty string.
"""
# assign member
self.__mCollections = collections
self.__mFctToStr = fct_to_str
self.__mFctFromStr = fct_from_str
self.__mFctName = fct_name
self.__mFctDesc = fct_desc
self.__mFctIcon = fct_icon
def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]:
"""
Generate a tuple which can be applied to Blender EnumProperty's "items".
"""
# blender enum prop item format:
# (token, display name, descriptions, icon, index)
return tuple(
(
self.__mFctToStr(member), # call to_str as its token.
self.__mFctName(member),
self.__mFctDesc(member),
self.__mFctIcon(member),
idx # use hardcode index, not the collection member self.
) for idx, member in enumerate(self.__mCollections)
)
def get_selection(self, prop: str) -> _TRawEnum:
"""
Return collection member from given Blender EnumProp string data.
"""
# call from_str fct ptr
return self.__mFctFromStr(prop)
def to_selection(self, val: _TRawEnum) -> str:
"""
Parse collection member to Blender EnumProp acceptable string format.
"""
# call to_str fct ptr
return self.__mFctToStr(val)
#endregion
#region Blender Collection Visitor
_TPropertyGroup = typing.TypeVar('_TPropertyGroup', bound = bpy.types.PropertyGroup)
class CollectionVisitor(typing.Generic[_TPropertyGroup]):
"""
This is a patch class for Blender collection property.
Blender collcetion property lack essential type hint and document.
So I create a wrapper for my personal use to reduce type hint errors raised by my linter.
"""
__mSrcProp: bpy.types.CollectionProperty
def __init__(self, src_prop: bpy.types.CollectionProperty):
self.__mSrcProp = src_prop
def add(self) -> _TPropertyGroup:
"""
Adds a new item to the collection.
:return: The instance of newly created item.
"""
return self.__mSrcProp.add()
def remove(self, index: int) -> None:
"""
Removes the item at the specified index from the collection.
:param index: The index of the item to remove.
"""
self.__mSrcProp.remove(index)
def move(self, from_index: int, to_index: int) -> None:
"""
Moves an item from one index to another within the collection.
:param from_index: The current index of the item to move.
:param to_index: The target index where the item should be moved.
"""
self.__mSrcProp.move(from_index, to_index)
def clear(self) -> None:
"""
Clears all items from the collection.
"""
self.__mSrcProp.clear()
def __len__(self) -> int:
return self.__mSrcProp.__len__()
def __getitem__(self, index: int | str) -> _TPropertyGroup:
return self.__mSrcProp.__getitem__(index)
def __setitem__(self, index: int | str, value: _TPropertyGroup) -> None:
self.__mSrcProp.__setitem__(index, value)
def __delitem__(self, index: int | str) -> None:
self.__mSrcProp.__delitem__(index)
def __iter__(self) -> typing.Iterator[_TPropertyGroup]:
return self.__mSrcProp.__iter__()
def __contains__(self, item: _TPropertyGroup) -> bool:
return self.__mSrcProp.__contains__(item)
#endregion
#region Tiny Mutex for With Context
_TMutexObject = typing.TypeVar('_TMutexObject')
class TinyMutex(typing.Generic[_TMutexObject]):
"""
In this plugin, some classes have "with" context feature.
However, in some cases, it is essential to block any futher visiting if some "with" context are operating on some object.
This is the reason why this tiny mutex is designed.
Please note this class is not a real MUTEX.
We just want to make sure the resources only can be visited by one "with" context.
So it doesn't matter that we do not use lock before operating something.
"""
__mProtectedObjects: set[_TMutexObject]
def __init__(self):
self.__mProtectedObjects = set()
def lock(self, obj: _TMutexObject) -> None:
"""
Lock given object.
:raise BBPException: Raised if given object has been locked.
:param obj: The resource to be locked.
"""
if obj in self.__mProtectedObjects:
raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.')
self.__mProtectedObjects.add(obj)
def try_lock(self, obj: _TMutexObject) -> bool:
"""
Try lock given object.
:param obj: The resource to be locked.
:return: True if we successfully lock it, otherwise false.
"""
if obj in self.__mProtectedObjects:
return False
self.__mProtectedObjects.add(obj)
return True
def unlock(self, obj: _TMutexObject) -> None:
"""
Unlock given object.
:raise BBPException: Raised if given object is not locked.
:param obj: The resource to be unlocked.
"""
if obj not in self.__mProtectedObjects:
raise BBPException('It is not allowed that unlock an non-existent object.')
self.__mProtectedObjects.remove(obj)
#endregion