refactor: use generic type in EnumPropHelper

- use typing.Generic in EnumPropHelper and its child classes.
- change Doxygen docstring into reStructedText docstring.
This commit is contained in:
2025-07-30 10:56:24 +08:00
parent fc34b19a42
commit a9a889a8fd
5 changed files with 173 additions and 117 deletions

View File

@ -572,13 +572,13 @@ _g_Helper_MtlPreset: UTIL_functions.EnumPropHelper = UTIL_functions.EnumPropHelp
#region Fix Material #region Fix Material
def fix_material(mtl: bpy.types.Material) -> bool: def fix_material(mtl: bpy.types.Material) -> bool:
"""! """
Fix single Blender material. Fix single Blender material.
@remark The implementation of this function is copied from BallanceVirtoolsHelper/bvh/features/mapping/bmfile_fix_texture.cpp The implementation of this function is copied from `BallanceVirtoolsHelper/bvh/features/mapping/bmfile_fix_texture.cpp`
@param mtl[in] The blender material need to be processed. :param mtl: The blender material need to be processed.
@return True if we do a fix, otherwise return False. :return: True if we do a fix, otherwise return False.
""" """
# prepare return value first # prepare return value first
ret: bool = False ret: bool = False

View File

@ -64,12 +64,10 @@ def set_raw_virtools_texture(img: bpy.types.Image, rawdata: RawVirtoolsTexture)
#region Virtools Texture Drawer #region Virtools Texture Drawer
"""! # YYC MARK:
@remark # Because Image do not have its unique properties window,
Because Image do not have its unique properties window # so we only can draw Virtools Texture properties in other window.
so we only can draw virtools texture properties in other window # We provide various functions to help draw properties.
we provide various function to help draw property.
"""
def draw_virtools_texture(img: bpy.types.Image, layout: bpy.types.UILayout): def draw_virtools_texture(img: bpy.types.Image, layout: bpy.types.UILayout):
props: BBP_PG_virtools_texture = get_virtools_texture(img) props: BBP_PG_virtools_texture = get_virtools_texture(img)

View File

@ -187,15 +187,14 @@ class PrototypeShowcaseCfgDescriptor():
def get_default(self) -> typing.Any: def get_default(self) -> typing.Any:
return _eval_showcase_cfgs_default(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DEFAULT]) return _eval_showcase_cfgs_default(self.__mRawCfg[TOKEN_SHOWCASE_CFGS_DEFAULT])
class EnumPropHelper(UTIL_functions.EnumPropHelper): class EnumPropHelper(UTIL_functions.EnumPropHelper[str]):
""" """
The BME specialized Blender EnumProperty helper. The BME specialized Blender EnumProperty helper.
""" """
def __init__(self): def __init__(self):
# init parent class # init parent class
UTIL_functions.EnumPropHelper.__init__( super().__init__(
self,
self.get_bme_identifiers(), self.get_bme_identifiers(),
lambda x: x, lambda x: x,
lambda x: x, lambda x: x,

View File

@ -2,37 +2,35 @@ import bpy, mathutils
import math, typing, enum, sys import math, typing, enum, sys
class BBPException(Exception): class BBPException(Exception):
""" """ The exception thrown by Ballance Blender Plugin"""
The exception thrown by Ballance Blender Plugin
"""
pass pass
def clamp_float(v: float, min_val: float, max_val: float) -> float: def clamp_float(v: float, min_val: float, max_val: float) -> float:
"""! """
@brief Clamp a float value Clamp a float value
@param v[in] The value need to be clamp. :param v: The value need to be clamp.
@param min_val[in] The allowed minium value, including self. :param min_val: The allowed minium value (inclusive).
@param max_val[in] The allowed maxium value, including self. :param max_val: The allowed maxium value (inclusive).
@return Clamped value. :return: Clamped value.
""" """
if (max_val < min_val): raise BBPException("Invalid range of clamp_float().") if (max_val < min_val): raise BBPException("Invalid range of clamp_float().")
if (v < min_val): return min_val if (v < min_val): return min_val
elif (v > max_val): return max_val elif (v > max_val): return max_val
else: return v else: return v
def clamp_int(v: int, min_val: int, max_val: int) -> int: def clamp_int(v: int, min_val: int, max_val: int) -> int:
"""! """
@brief Clamp a int value Clamp a int value
@param v[in] The value need to be clamp. :param v: The value need to be clamp.
@param min_val[in] The allowed minium value, including self. :param min_val: The allowed minium value (inclusive).
@param max_val[in] The allowed maxium value, including self. :param max_val: The allowed maxium value (inclusive).
@return Clamped value. :return: Clamped value.
""" """
if (max_val < min_val): raise BBPException("Invalid range of clamp_int().") if (max_val < min_val): raise BBPException("Invalid range of clamp_int().")
if (v < min_val): return min_val if (v < min_val): return min_val
elif (v > max_val): return max_val elif (v > max_val): return max_val
else: return v else: return v
@ -41,41 +39,65 @@ def message_box(message: tuple[str, ...], title: str, icon: str):
""" """
Show a message box in Blender. Non-block mode. Show a message box in Blender. Non-block mode.
@param message[in] The text this message box displayed. Each item in this param will show as a single line. :param message: The text this message box displayed. Each item in this param will show as a single line.
@param title[in] Message box title text. :param title: Message box title text.
@param icon[in] The icon this message box displayed. :param icon: The icon this message box displayed.
""" """
def draw(self, context: bpy.types.Context): def draw(self, context: bpy.types.Context):
layout = self.layout layout = self.layout
for item in message: for item in message:
layout.label(text=item, translate=False) layout.label(text=item, translate=False)
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def add_into_scene(obj: bpy.types.Object): 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 view_layer = bpy.context.view_layer
collection = view_layer.active_layer_collection.collection collection = view_layer.active_layer_collection.collection
collection.objects.link(obj) collection.objects.link(obj)
def move_to_cursor(obj: bpy.types.Object): def move_to_cursor(obj: bpy.types.Object):
# use obj.matrix_world to move, not obj.location because this bug: """
# https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation Move given object to the position of cursor.
# the update of matrix_world after setting location is not immediately.
# and calling update() function for view_layer for the translation of each object is not suit for too much objects.
: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.location = bpy.context.scene.cursor.location
obj.matrix_world = obj.matrix_world @ mathutils.Matrix.Translation(bpy.context.scene.cursor.location - obj.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): 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) add_into_scene(obj)
move_to_cursor(obj) move_to_cursor(obj)
def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None: 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 # deselect all objects first
bpy.ops.object.select_all(action = 'DESELECT') bpy.ops.object.select_all(action = 'DESELECT')
# if no objects, return # if no objects, return
if len(objs) == 0: return if len(objs) == 0: return
# set selection for each object # set selection for each object
for obj in objs: for obj in objs:
obj.select_set(True) obj.select_set(True)
@ -83,66 +105,73 @@ def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None:
bpy.context.view_layer.objects.active = objs[0] bpy.context.view_layer.objects.active = objs[0]
def is_in_object_mode() -> bool: 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 # get active object from context
obj = bpy.context.active_object obj = bpy.context.active_object
# if there is no active object, we think it is in object mode # if there is no active object, we think it is in object mode
if obj is None: return True if obj is None: return True
# simply check active object mode # simply check active object mode
return obj.mode == 'OBJECT' return obj.mode == 'OBJECT'
class EnumPropHelper(): #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`, 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. parsing data from EnumProperty string value and getting EnumProperty acceptable string format from data.
""" """
# define some type hint __mCollections: typing.Iterable[_TRawEnum]
_TFctToStr = typing.Callable[[typing.Any], str]
_TFctFromStr = typing.Callable[[str], typing.Any]
_TFctName = typing.Callable[[typing.Any], str]
_TFctDesc = typing.Callable[[typing.Any], str]
_TFctIcon = typing.Callable[[typing.Any], str | int]
# define class member
__mCollections: typing.Iterable[typing.Any]
__mFctToStr: _TFctToStr __mFctToStr: _TFctToStr
__mFctFromStr: _TFctFromStr __mFctFromStr: _TFctFromStr
__mFctName: _TFctName __mFctName: _TFctName
__mFctDesc: _TFctDesc __mFctDesc: _TFctDesc
__mFctIcon: _TFctIcon __mFctIcon: _TFctIcon
def __init__( def __init__(self, collections: typing.Iterable[typing.Any],
self, fct_to_str: _TFctToStr, fct_from_str: _TFctFromStr,
collections_: typing.Iterable[typing.Any], fct_name: _TFctName, fct_desc: _TFctDesc,
fct_to_str: _TFctToStr, fct_icon: _TFctIcon):
fct_from_str: _TFctFromStr,
fct_name: _TFctName,
fct_desc: _TFctDesc,
fct_icon: _TFctIcon):
""" """
Initialize a EnumProperty helper. Initialize an EnumProperty helper.
@param collections_ [in] The collection all available enum property entries contained. :param collections: The collection containing all available enum property entries.
It can be enum.Enum or a simple list/tuple/dict. It can be `enum.Enum` or a simple list/tuple.
@param fct_to_str [in] A function pointer converting data collection member to its string format. :param fct_to_str: A function pointer converting data collection member to its string format.
For enum.IntEnum, it can be simply `lambda x: str(x.value)` You must make sure that each members built name is unique in collection!
@param fct_from_str [in] A function pointer getting data collection member from its string format. For `enum.IntEnum`, it can be simple `lambda x: str(x.value)`
For enum.IntEnum, it can be simply `lambda x: TEnum(int(x))` :param fct_from_str: A function pointer getting data collection member from its string format.
@param fct_name [in] A function pointer converting data collection member to its display name. This class promise that given string must can be parsed.
@param fct_desc [in] Same as `fct_name` but return description instead. Return empty string, not None if no description. For `enum.IntEnum`, it can be simple `lambda x: TEnum(int(x))`
@param fct_icon [in] Same as `fct_name` but return the used icon instead. Return empty string if no icon. :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 # assign member
self.__mCollections = collections_ self.__mCollections = collections
self.__mFctToStr = fct_to_str self.__mFctToStr = fct_to_str
self.__mFctFromStr = fct_from_str self.__mFctFromStr = fct_from_str
self.__mFctName = fct_name self.__mFctName = fct_name
self.__mFctDesc = fct_desc self.__mFctDesc = fct_desc
self.__mFctIcon = fct_icon self.__mFctIcon = fct_icon
def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]: def generate_items(self) -> tuple[tuple[str, str, str, int | str, int], ...]:
""" """
Generate a tuple which can be applied to Blender EnumProperty's "items". Generate a tuple which can be applied to Blender EnumProperty's "items".
@ -152,27 +181,29 @@ class EnumPropHelper():
return tuple( return tuple(
( (
self.__mFctToStr(member), # call to_str as its token. self.__mFctToStr(member), # call to_str as its token.
self.__mFctName(member), self.__mFctName(member),
self.__mFctDesc(member), self.__mFctDesc(member),
self.__mFctIcon(member), self.__mFctIcon(member),
idx # use hardcode index, not the collection member self. idx # use hardcode index, not the collection member self.
) for idx, member in enumerate(self.__mCollections) ) for idx, member in enumerate(self.__mCollections)
) )
def get_selection(self, prop: str) -> typing.Any: def get_selection(self, prop: str) -> _TRawEnum:
""" """
Return collection member from given Blender EnumProp string data. Return collection member from given Blender EnumProp string data.
""" """
# call from_str fct ptr # call from_str fct ptr
return self.__mFctFromStr(prop) return self.__mFctFromStr(prop)
def to_selection(self, val: typing.Any) -> str: def to_selection(self, val: _TRawEnum) -> str:
""" """
Parse collection member to Blender EnumProp acceptable string format. Parse collection member to Blender EnumProp acceptable string format.
""" """
# call to_str fct ptr # call to_str fct ptr
return self.__mFctToStr(val) return self.__mFctToStr(val)
#endregion
#region Blender Collection Visitor #region Blender Collection Visitor
_TPropertyGroup = typing.TypeVar('_TPropertyGroup', bound = bpy.types.PropertyGroup) _TPropertyGroup = typing.TypeVar('_TPropertyGroup', bound = bpy.types.PropertyGroup)
@ -183,40 +214,43 @@ class CollectionVisitor(typing.Generic[_TPropertyGroup]):
Blender collcetion property lack essential type hint and document. 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. So I create a wrapper for my personal use to reduce type hint errors raised by my linter.
""" """
__mSrcProp: bpy.types.CollectionProperty __mSrcProp: bpy.types.CollectionProperty
def __init__(self, src_prop: bpy.types.CollectionProperty): def __init__(self, src_prop: bpy.types.CollectionProperty):
self.__mSrcProp = src_prop self.__mSrcProp = src_prop
def add(self) -> _TPropertyGroup: def add(self) -> _TPropertyGroup:
"""! """
@brief Adds a new item to the collection. Adds a new item to the collection.
@return The instance of newly created item.
:return: The instance of newly created item.
""" """
return self.__mSrcProp.add() return self.__mSrcProp.add()
def remove(self, index: int) -> None: def remove(self, index: int) -> None:
"""! """
@brief Removes the item at the specified index from the collection. Removes the item at the specified index from the collection.
@param[in] index The index of the item to remove.
:param index: The index of the item to remove.
""" """
self.__mSrcProp.remove(index) self.__mSrcProp.remove(index)
def move(self, from_index: int, to_index: int) -> None: def move(self, from_index: int, to_index: int) -> None:
"""! """
@brief Moves an item from one index to another within the collection. Moves an item from one index to another within the collection.
@param[in] from_index The current index of the item to move.
@param[in] to_index The target index where the item should be moved. :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) self.__mSrcProp.move(from_index, to_index)
def clear(self) -> None: def clear(self) -> None:
"""! """
@brief Clears all items from the collection. Clears all items from the collection.
""" """
self.__mSrcProp.clear() self.__mSrcProp.clear()
def __len__(self) -> int: def __len__(self) -> int:
return self.__mSrcProp.__len__() return self.__mSrcProp.__len__()
def __getitem__(self, index: int | str) -> _TPropertyGroup: def __getitem__(self, index: int | str) -> _TPropertyGroup:
@ -238,32 +272,50 @@ _TMutexObject = typing.TypeVar('_TMutexObject')
class TinyMutex(typing.Generic[_TMutexObject]): class TinyMutex(typing.Generic[_TMutexObject]):
""" """
In this plugin, some class have "with" context feature. In this plugin, some classes have "with" context feature.
However, it is essential to block any futher visiting if some "with" context are operating on some object. 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. This is the reason why this tiny mutex is designed.
Please note this class is not a real MUTEX. 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. 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. So it doesn't matter that we do not use lock before operating something.
""" """
__mProtectedObjects: set[_TMutexObject] __mProtectedObjects: set[_TMutexObject]
def __init__(self): def __init__(self):
self.__mProtectedObjects = set() self.__mProtectedObjects = set()
def lock(self, obj: _TMutexObject) -> None: 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: if obj in self.__mProtectedObjects:
raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.') raise BBPException('It is not allowed that operate multiple "with" contexts on a single object.')
self.__mProtectedObjects.add(obj) self.__mProtectedObjects.add(obj)
def try_lock(self, obj: _TMutexObject) -> bool: 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: if obj in self.__mProtectedObjects:
return False return False
self.__mProtectedObjects.add(obj) self.__mProtectedObjects.add(obj)
return True return True
def unlock(self, obj: _TMutexObject) -> None: 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: if obj not in self.__mProtectedObjects:
raise BBPException('It is not allowed that unlock an non-existent object.') raise BBPException('It is not allowed that unlock an non-existent object.')
self.__mProtectedObjects.remove(obj) self.__mProtectedObjects.remove(obj)

View File

@ -235,21 +235,28 @@ _g_Annotation: dict[type, dict[int, EnumAnnotation]] = {
} }
} }
class EnumPropHelper(UTIL_functions.EnumPropHelper): _TRawEnum = typing.TypeVar('_TRawEnum', bound = enum.Enum)
class EnumPropHelper(UTIL_functions.EnumPropHelper[_TRawEnum]):
""" """
Virtools type specified Blender EnumProp helper. Virtools type specified Blender EnumProp helper.
""" """
__mAnnotationDict: dict[int, EnumAnnotation] __mAnnotationDict: dict[int, EnumAnnotation]
__mEnumTy: type[enum.Enum] __mEnumTy: type[_TRawEnum]
def __init__(self, ty: type[enum.Enum]): def __init__(self, ty: type[_TRawEnum]):
# set enum type and annotation ref first # set enum type and annotation ref first
self.__mEnumTy = ty self.__mEnumTy = ty
self.__mAnnotationDict = _g_Annotation[ty] self.__mAnnotationDict = _g_Annotation[ty]
# init parent data
UTIL_functions.EnumPropHelper.__init__( # YYC MARK:
self, # It seems that Pylance has bad generic analyse ability in there.
self.__mEnumTy, # enum.Enum it self is iterable # It can not deduce the correct generic type in lambda.
# I gave up.
# Init parent data
super().__init__(
self.__mEnumTy, # enum.Enum its self is iterable
lambda x: str(x.value), # convert enum.Enum's value to string lambda x: str(x.value), # convert enum.Enum's value to string
lambda x: self.__mEnumTy(int(x)), # use stored enum type and int() to get enum member lambda x: self.__mEnumTy(int(x)), # use stored enum type and int() to get enum member
lambda x: self.__mAnnotationDict[x.value].mDisplayName, lambda x: self.__mAnnotationDict[x.value].mDisplayName,
@ -265,11 +272,11 @@ def virtools_name_regulator(name: str | None) -> str:
if name: return name if name: return name
else: return bpy.app.translations.pgettext_data('annoymous', 'BME/UTIL_virtools_types.virtools_name_regulator()') else: return bpy.app.translations.pgettext_data('annoymous', 'BME/UTIL_virtools_types.virtools_name_regulator()')
## Default Encoding for PyBMap # YYC MARK:
# Use semicolon split each encodings. Support Western European and Simplified Chinese in default. # There are default encodings for PyBMap. We support Western European and Simplified Chinese in default.
# Since LibCmo 0.2, the encoding name of LibCmo become universal encoding which is platfoorm independent. # Since LibCmo 0.2, the encoding name of LibCmo become universal encoding which is platfoorm independent.
# So no need set it according to different platform. # So no need set it according to different platform.
# Use universal encoding name (like Python). # Use universal encoding name (like Python).
g_PyBMapDefaultEncodings: tuple[str, ...] = ( g_PyBMapDefaultEncodings: tuple[str, ...] = (
'cp1252', 'cp1252',
'gbk' 'gbk'