From f4d3e48be27aa07ceeec564106aac8321eb58df0 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 6 Jan 2025 15:12:14 +0800 Subject: [PATCH] refactor: add 2 classes to refactor old code which raise too much erros by linter. - add TinyMutex to resolve the issue that we can not operate the virtools group infos of 2 individual objects. * use this new class for all class need resource mutex, such as ballance element, bme materials and etc. - add CollectionVisitor to resolve that blender have bad document and type hint for runtime bpy.types.CollectionProperty. * now all visit to CollectionProperty are delegated by this class. --- bbp_ng/OP_ADDS_bme.py | 46 ++++++---- bbp_ng/OP_OBJECT_legacy_align.py | 23 +++-- bbp_ng/OP_OBJECT_snoop_group_then_to_mesh.py | 9 +- bbp_ng/PROP_ballance_element.py | 55 ++++------- bbp_ng/PROP_bme_material.py | 22 ++--- bbp_ng/PROP_virtools_group.py | 32 +++---- bbp_ng/UTIL_functions.py | 96 ++++++++++++++++++++ 7 files changed, 182 insertions(+), 101 deletions(-) diff --git a/bbp_ng/OP_ADDS_bme.py b/bbp_ng/OP_ADDS_bme.py index dea5c0d..d7c0239 100644 --- a/bbp_ng/OP_ADDS_bme.py +++ b/bbp_ng/OP_ADDS_bme.py @@ -96,27 +96,29 @@ def __internal_update_bme_struct_type(self) -> None: # init data collection + adder_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs] + adder_cfgs_visitor = UTIL_functions.CollectionVisitor(self.bme_struct_cfgs) # clear first - self.bme_struct_cfgs.clear() + adder_cfgs_visitor.clear() # create enough entries specified by gotten cfgs for _ in range(max(counter_int, counter_float, counter_bool)): - self.bme_struct_cfgs.add() + adder_cfgs_visitor.add() # assign default value for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: # show prop differently by cfg type match(cfg.get_type()): case UTIL_bme.PrototypeShowcaseCfgsTypes.Integer: - self.bme_struct_cfgs[cfg_index].prop_int = cfg.get_default() + adder_cfgs_visitor[cfg_index].prop_int = cfg.get_default() case UTIL_bme.PrototypeShowcaseCfgsTypes.Float: - self.bme_struct_cfgs[cfg_index].prop_float = cfg.get_default() + adder_cfgs_visitor[cfg_index].prop_float = cfg.get_default() case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean: - self.bme_struct_cfgs[cfg_index].prop_bool = cfg.get_default() + adder_cfgs_visitor[cfg_index].prop_bool = cfg.get_default() case UTIL_bme.PrototypeShowcaseCfgsTypes.Face: # face is just 6 bool default_values: tuple[bool, ...] = cfg.get_default() for i in range(6): - self.bme_struct_cfgs[cfg_index + i].prop_bool = default_values[i] + adder_cfgs_visitor[cfg_index + i].prop_bool = default_values[i] # reset outdated flag self.outdated_flag = False @@ -182,20 +184,23 @@ def execute(self, context): # call internal updator self.__internal_update_bme_struct_type() + # create cfg visitor + adder_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs] + adder_cfgs_visitor = UTIL_functions.CollectionVisitor(self.bme_struct_cfgs) # collect cfgs data cfgs: dict[str, typing.Any] = {} for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: match(cfg.get_type()): case UTIL_bme.PrototypeShowcaseCfgsTypes.Integer: - cfgs[cfg.get_field()] = self.bme_struct_cfgs[cfg_index].prop_int + cfgs[cfg.get_field()] = adder_cfgs_visitor[cfg_index].prop_int case UTIL_bme.PrototypeShowcaseCfgsTypes.Float: - cfgs[cfg.get_field()] = self.bme_struct_cfgs[cfg_index].prop_float + cfgs[cfg.get_field()] = adder_cfgs_visitor[cfg_index].prop_float case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean: - cfgs[cfg.get_field()] = self.bme_struct_cfgs[cfg_index].prop_bool + cfgs[cfg.get_field()] = adder_cfgs_visitor[cfg_index].prop_bool case UTIL_bme.PrototypeShowcaseCfgsTypes.Face: # face is just 6 bool tuple cfgs[cfg.get_field()] = tuple( - self.bme_struct_cfgs[cfg_index + i].prop_bool for i in range(6) + adder_cfgs_visitor[cfg_index + i].prop_bool for i in range(6) ) # call general creator @@ -225,6 +230,9 @@ def draw(self, context): # show type layout.prop(self, 'bme_struct_type') + # create cfg visitor + adder_cfgs_visitor: UTIL_functions.CollectionVisitor[BBP_PG_bme_adder_cfgs] + adder_cfgs_visitor = UTIL_functions.CollectionVisitor(self.bme_struct_cfgs) # visit cfgs cache list to show cfg layout.label(text = "Prototype Configurations:") for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: @@ -238,24 +246,24 @@ def draw(self, context): # show prop differently by cfg type match(cfg.get_type()): case UTIL_bme.PrototypeShowcaseCfgsTypes.Integer: - box_layout.prop(self.bme_struct_cfgs[cfg_index], 'prop_int', text = '') + box_layout.prop(adder_cfgs_visitor[cfg_index], 'prop_int', text = '') case UTIL_bme.PrototypeShowcaseCfgsTypes.Float: - box_layout.prop(self.bme_struct_cfgs[cfg_index], 'prop_float', text = '') + box_layout.prop(adder_cfgs_visitor[cfg_index], 'prop_float', text = '') case UTIL_bme.PrototypeShowcaseCfgsTypes.Boolean: - box_layout.prop(self.bme_struct_cfgs[cfg_index], 'prop_bool', text = '') + box_layout.prop(adder_cfgs_visitor[cfg_index], 'prop_bool', text = '') case UTIL_bme.PrototypeShowcaseCfgsTypes.Face: # face will show a special layout (grid view) grids = box_layout.grid_flow( row_major=True, columns=3, even_columns=True, even_rows=True, align=True) grids.alignment = 'CENTER' grids.separator() - grids.prop(self.bme_struct_cfgs[cfg_index + 0], 'prop_bool', text = 'Top') # top - grids.prop(self.bme_struct_cfgs[cfg_index + 2], 'prop_bool', text = 'Front') # front - grids.prop(self.bme_struct_cfgs[cfg_index + 4], 'prop_bool', text = 'Left') # left + grids.prop(adder_cfgs_visitor[cfg_index + 0], 'prop_bool', text = 'Top') # top + grids.prop(adder_cfgs_visitor[cfg_index + 2], 'prop_bool', text = 'Front') # front + grids.prop(adder_cfgs_visitor[cfg_index + 4], 'prop_bool', text = 'Left') # left grids.label(text = '', icon = 'CUBE') # show a 3d cube as icon - grids.prop(self.bme_struct_cfgs[cfg_index + 5], 'prop_bool', text = 'Right') # right - grids.prop(self.bme_struct_cfgs[cfg_index + 3], 'prop_bool', text = 'Back') # back - grids.prop(self.bme_struct_cfgs[cfg_index + 1], 'prop_bool', text = 'Bottom') # bottom + grids.prop(adder_cfgs_visitor[cfg_index + 5], 'prop_bool', text = 'Right') # right + grids.prop(adder_cfgs_visitor[cfg_index + 3], 'prop_bool', text = 'Back') # back + grids.prop(adder_cfgs_visitor[cfg_index + 1], 'prop_bool', text = 'Bottom') # bottom grids.separator() # show extra transform props diff --git a/bbp_ng/OP_OBJECT_legacy_align.py b/bbp_ng/OP_OBJECT_legacy_align.py index 6979cd1..852f4ea 100644 --- a/bbp_ng/OP_OBJECT_legacy_align.py +++ b/bbp_ng/OP_OBJECT_legacy_align.py @@ -84,11 +84,13 @@ def apply_flag_updated(self, context): # check whether add new entry # if no selected axis, this alignment is invalid - entry: BBP_PG_legacy_align_history = self.align_history[-1] + histories: UTIL_functions.CollectionVisitor[BBP_PG_legacy_align_history] + histories = UTIL_functions.CollectionVisitor(self.align_history) + entry: BBP_PG_legacy_align_history = histories[-1] if entry.align_x == True or entry.align_y == True or entry.align_z == True: # valid one # add a new entry in history - self.align_history.add() + histories.add() else: # invalid one # reset all data to default @@ -127,9 +129,11 @@ def poll(cls, context): return _check_align_requirement() def invoke(self, context, event): + histories: UTIL_functions.CollectionVisitor[BBP_PG_legacy_align_history] + histories = UTIL_functions.CollectionVisitor(self.align_history) # clear history and add 1 entry for following functions - self.align_history.clear() - self.align_history.add() + histories.clear() + histories.add() # run execute() function return self.execute(context) @@ -145,8 +149,9 @@ def execute(self, context): # If you place it at the end of this function, it doesn't work. context.view_layer.update() # iterate history to align objects - entry: BBP_PG_legacy_align_history - for entry in self.align_history: + histories: UTIL_functions.CollectionVisitor[BBP_PG_legacy_align_history] + histories = UTIL_functions.CollectionVisitor(self.align_history) + for entry in histories: _align_objects( current_obj, target_objs, entry.align_x, entry.align_y, entry.align_z, @@ -157,7 +162,9 @@ def execute(self, context): def draw(self, context): # get last entry in history to show - entry: BBP_PG_legacy_align_history = self.align_history[-1] + histories: UTIL_functions.CollectionVisitor[BBP_PG_legacy_align_history] + histories = UTIL_functions.CollectionVisitor(self.align_history) + entry: BBP_PG_legacy_align_history = histories[-1] layout = self.layout col = layout.column() @@ -183,7 +190,7 @@ def draw(self, context): conditional_disable_area.enabled = entry.align_x == True or entry.align_y == True or entry.align_z == True # show apply and counter conditional_disable_area.prop(self, 'apply_flag', text = 'Apply', icon = 'CHECKMARK', toggle = 1) - conditional_disable_area.label(text = f'Total {len(self.align_history) - 1} applied alignments') + conditional_disable_area.label(text = f'Total {len(histories) - 1} applied alignments') #region Core Functions diff --git a/bbp_ng/OP_OBJECT_snoop_group_then_to_mesh.py b/bbp_ng/OP_OBJECT_snoop_group_then_to_mesh.py index e6e2245..0cc5c4e 100644 --- a/bbp_ng/OP_OBJECT_snoop_group_then_to_mesh.py +++ b/bbp_ng/OP_OBJECT_snoop_group_then_to_mesh.py @@ -27,15 +27,10 @@ def execute(self, context): if bevel_obj is None: continue # copy bevel object group info into current object - # MARK: VirtoolsGroupsHelper is self-mutex. - # we only can operate one VirtoolsGroupsHelper at the same time. - # so we extract bevel object group infos then apply to target later. - group_infos: tuple[str, ...] - with PROP_virtools_group.VirtoolsGroupsHelper(bevel_obj) as bevel_gp: - group_infos = tuple(bevel_gp.iterate_groups()) with PROP_virtools_group.VirtoolsGroupsHelper(obj) as this_gp: this_gp.clear_groups() - this_gp.add_groups(group_infos) + with PROP_virtools_group.VirtoolsGroupsHelper(bevel_obj) as bevel_gp: + this_gp.add_groups(bevel_gp.iterate_groups()) # convert all selected object to mesh # no matter the success of copying virtools group infos and whether selected object is curve diff --git a/bbp_ng/PROP_ballance_element.py b/bbp_ng/PROP_ballance_element.py index dbc216e..3efc140 100644 --- a/bbp_ng/PROP_ballance_element.py +++ b/bbp_ng/PROP_ballance_element.py @@ -104,8 +104,8 @@ class BBP_PG_ballance_element(bpy.types.PropertyGroup): type = bpy.types.Mesh ) # type: ignore -def get_ballance_elements(scene: bpy.types.Scene) -> bpy.types.CollectionProperty: - return scene.ballance_elements +def get_ballance_elements(scene: bpy.types.Scene) -> UTIL_functions.CollectionVisitor[BBP_PG_ballance_element]: + return UTIL_functions.CollectionVisitor(scene.ballance_elements) #endregion @@ -224,7 +224,7 @@ class BallanceElementsHelper(): This class should only have 1 instance at the same time. This class support `with` syntax to achieve this. This class frequently used in importing stage to create element placeholder. """ - __mSingletonMutex: typing.ClassVar[bool] = False + __mSingletonMutex: typing.ClassVar[UTIL_functions.TinyMutex[bpy.types.Scene]] = UTIL_functions.TinyMutex() __mIsValid: bool __mAssocScene: bpy.types.Scene __mElementMap: dict[BallanceElementType, bpy.types.Mesh] @@ -232,14 +232,11 @@ class BallanceElementsHelper(): def __init__(self, assoc: bpy.types.Scene): self.__mElementMap = {} self.__mAssocScene = assoc + self.__mIsValid = False # check singleton - if BallanceElementsHelper.__mSingletonMutex: - self.__mIsValid = False - raise UTIL_functions.BBPException('BallanceElementsHelper is mutex.') - + BallanceElementsHelper.__mSingletonMutex.lock(self.__mAssocScene) # set validation and read ballance elements property - BallanceElementsHelper.__mSingletonMutex = True self.__mIsValid = True self.__read_from_ballance_element() @@ -257,7 +254,7 @@ def dispose(self) -> None: # write to ballance elements property and reset validation self.__write_to_ballance_elements() self.__mIsValid = False - BallanceElementsHelper.__mSingletonMutex = False + BallanceElementsHelper.__mSingletonMutex.unlock(self.__mAssocScene) def get_element(self, element_type: BallanceElementType) -> bpy.types.Mesh: if not self.is_valid(): @@ -276,8 +273,16 @@ def get_element(self, element_type: BallanceElementType) -> bpy.types.Mesh: self.__mElementMap[element_type] = new_mesh return new_mesh + def reset_elements(self) -> None: + if not self.is_valid(): + raise UTIL_functions.BBPException('calling invalid BallanceElementsHelper') + + # reload all items + for elety, elemesh in self.__mElementMap.items(): + _load_element(elemesh, elety) + def __write_to_ballance_elements(self) -> None: - elements: bpy.types.CollectionProperty = get_ballance_elements(self.__mAssocScene) + elements = get_ballance_elements(self.__mAssocScene) elements.clear() for elety, elemesh in self.__mElementMap.items(): @@ -286,10 +291,9 @@ def __write_to_ballance_elements(self) -> None: item.mesh_ptr = elemesh def __read_from_ballance_element(self) -> None: - elements: bpy.types.CollectionProperty = get_ballance_elements(self.__mAssocScene) + elements = get_ballance_elements(self.__mAssocScene) self.__mElementMap.clear() - item: BBP_PG_ballance_element for item in elements: # check requirements if item.mesh_ptr is None: continue @@ -299,30 +303,6 @@ def __read_from_ballance_element(self) -> None: # add into map self.__mElementMap[element_type] = item.mesh_ptr -def reset_ballance_elements(scene: bpy.types.Scene) -> None: - invalid_idx: list[int] = [] - elements: bpy.types.CollectionProperty = get_ballance_elements(scene) - - # re-load all elements - index: int = 0 - item: BBP_PG_ballance_element - for item in elements: - elety: BallanceElementType | None = get_ballance_element_type_from_id(item.element_id) - - # load or record invalid entry - if elety is None or item.mesh_ptr is None: - invalid_idx.append(index) - else: - _load_element(item.mesh_ptr, elety) - - # inc counter - index += 1 - - # remove invalid one with reversed order - invalid_idx.reverse() - for idx in invalid_idx: - elements.remove(idx) - #endregion #region Ballance Elements Representation @@ -348,7 +328,8 @@ def poll(cls, context): return context.scene is not None def execute(self, context): - reset_ballance_elements(context.scene) + with BallanceElementsHelper(context.scene) as helper: + helper.reset_elements() # show a window to let user know, not silence UTIL_functions.message_box( ('Reset OK.', ), diff --git a/bbp_ng/PROP_bme_material.py b/bbp_ng/PROP_bme_material.py index 48280c9..d0a51e4 100644 --- a/bbp_ng/PROP_bme_material.py +++ b/bbp_ng/PROP_bme_material.py @@ -85,8 +85,8 @@ class BBP_PG_bme_material(bpy.types.PropertyGroup): type = bpy.types.Material ) # type: ignore -def get_bme_materials(scene: bpy.types.Scene) -> bpy.types.CollectionProperty: - return scene.bme_materials +def get_bme_materials(scene: bpy.types.Scene) -> UTIL_functions.CollectionVisitor[BBP_PG_bme_material]: + return UTIL_functions.CollectionVisitor(scene.bme_materials) #endregion @@ -126,7 +126,7 @@ class BMEMaterialsHelper(): This class should only have 1 instance at the same time. This class support `with` syntax to achieve this. This class frequently used in creating BME meshes. """ - __mSingletonMutex: typing.ClassVar[bool] = False + __mSingletonMutex: typing.ClassVar[UTIL_functions.TinyMutex[bpy.types.Scene]] = UTIL_functions.TinyMutex() __mIsValid: bool __mAssocScene: bpy.types.Scene __mMaterialMap: dict[str, bpy.types.Material] @@ -134,14 +134,11 @@ class BMEMaterialsHelper(): def __init__(self, assoc: bpy.types.Scene): self.__mMaterialMap = {} self.__mAssocScene = assoc + self.__mIsValid = False # check singleton - if BMEMaterialsHelper.__mSingletonMutex: - self.__mIsValid = False - raise UTIL_functions.BBPException('BMEMaterialsHelper is mutex.') - + BMEMaterialsHelper.__mSingletonMutex.lock(self.__mAssocScene) # set validation and read ballance elements property - BMEMaterialsHelper.__mSingletonMutex = True self.__mIsValid = True self.__read_from_bme_materials() @@ -159,7 +156,7 @@ def dispose(self) -> None: # write to ballance elements property and reset validation self.__write_to_bme_materials() self.__mIsValid = False - BMEMaterialsHelper.__mSingletonMutex = False + BMEMaterialsHelper.__mSingletonMutex.unlock(self.__mAssocScene) def get_material(self, preset_name: str) -> bpy.types.Material: if not self.is_valid(): @@ -179,7 +176,7 @@ def get_material(self, preset_name: str) -> bpy.types.Material: return new_mtl def __write_to_bme_materials(self) -> None: - mtls: bpy.types.CollectionProperty = get_bme_materials(self.__mAssocScene) + mtls = get_bme_materials(self.__mAssocScene) mtls.clear() for preset_name, mtl in self.__mMaterialMap.items(): @@ -188,10 +185,9 @@ def __write_to_bme_materials(self) -> None: item.material_ptr = mtl def __read_from_bme_materials(self) -> None: - mtls: bpy.types.CollectionProperty = get_bme_materials(self.__mAssocScene) + mtls = get_bme_materials(self.__mAssocScene) self.__mMaterialMap.clear() - item: BBP_PG_bme_material for item in mtls: # check requirements if item.material_ptr is None: continue @@ -200,7 +196,7 @@ def __read_from_bme_materials(self) -> None: def reset_bme_materials(scene: bpy.types.Scene) -> None: invalid_idx: list[int] = [] - mtls: bpy.types.CollectionProperty = get_bme_materials(scene) + mtls = get_bme_materials(scene) # re-load all elements index: int = 0 diff --git a/bbp_ng/PROP_virtools_group.py b/bbp_ng/PROP_virtools_group.py index 9fe5240..287231b 100644 --- a/bbp_ng/PROP_virtools_group.py +++ b/bbp_ng/PROP_virtools_group.py @@ -10,8 +10,8 @@ class BBP_PG_virtools_group(bpy.types.PropertyGroup): default = "" ) # type: ignore -def get_virtools_groups(obj: bpy.types.Object) -> bpy.types.CollectionProperty: - return obj.virtools_groups +def get_virtools_groups(obj: bpy.types.Object) -> UTIL_functions.CollectionVisitor[BBP_PG_virtools_group]: + return UTIL_functions.CollectionVisitor(obj.virtools_groups) def get_active_virtools_groups(obj: bpy.types.Object) -> int: return obj.active_virtools_groups @@ -26,7 +26,7 @@ class VirtoolsGroupsHelper(): All Virtools group operations should be done by this class. Do NOT manipulate object's Virtools group properties directly. """ - __mSingletonMutex: typing.ClassVar[bool] = False + __mSingletonMutex: typing.ClassVar[UTIL_functions.TinyMutex[bpy.types.Object]] = UTIL_functions.TinyMutex() __mIsValid: bool __mNoChange: bool ##< A bool indicate whether any change happended during lifetime. If no change, skip the writing when exiting. __mAssocObj: bpy.types.Object @@ -36,14 +36,11 @@ def __init__(self, assoc: bpy.types.Object): self.__mGroupsSet = set() self.__mAssocObj = assoc self.__mNoChange = True + self.__mIsValid = False # check singleton - if VirtoolsGroupsHelper.__mSingletonMutex: - self.__mIsValid = False - raise UTIL_functions.BBPException('VirtoolsGroupsHelper is mutex.') - + VirtoolsGroupsHelper.__mSingletonMutex.lock(self.__mAssocObj) # set validation and read ballance elements property - VirtoolsGroupsHelper.__mSingletonMutex = True self.__mIsValid = True self.__read_from_virtools_groups() @@ -63,7 +60,7 @@ def dispose(self) -> None: if not self.__mNoChange: self.__write_to_virtools_groups() self.__mIsValid = False - VirtoolsGroupsHelper.__mSingletonMutex = False + VirtoolsGroupsHelper.__mSingletonMutex.unlock(self.__mAssocObj) def __check_valid(self) -> None: if not self.is_valid(): @@ -127,7 +124,7 @@ def get_count(self) -> int: return len(self.__mGroupsSet) def __write_to_virtools_groups(self) -> None: - groups: bpy.types.CollectionProperty = get_virtools_groups(self.__mAssocObj) + groups = get_virtools_groups(self.__mAssocObj) sel: int = get_active_virtools_groups(self.__mAssocObj) groups.clear() @@ -143,10 +140,9 @@ def __write_to_virtools_groups(self) -> None: set_active_virtools_groups(self.__mAssocObj, sel) def __read_from_virtools_groups(self) -> None: - groups: bpy.types.CollectionProperty = get_virtools_groups(self.__mAssocObj) + groups = get_virtools_groups(self.__mAssocObj) self.__mGroupsSet.clear() - item: BBP_PG_virtools_group for item in groups: self.__mGroupsSet.add(item.group_name) @@ -306,7 +302,8 @@ def invoke(self, context, event): def execute(self, context): # add group - with VirtoolsGroupsHelper(context.object) as hlp: + obj = typing.cast(bpy.types.Object, context.object) + with VirtoolsGroupsHelper(obj) as hlp: hlp.add_group(self.general_get_group_name()) return {'FINISHED'} @@ -335,8 +332,9 @@ def poll(cls, context: bpy.types.Context): def execute(self, context): # get selected group name first - obj = context.object - item: BBP_PG_virtools_group = get_virtools_groups(obj)[get_active_virtools_groups(obj)] + obj = typing.cast(bpy.types.Object, context.object) + groups = get_virtools_groups(obj) + item = groups[get_active_virtools_groups(obj)] gname: str = item.group_name # then delete it with VirtoolsGroupsHelper(obj) as hlp: @@ -359,7 +357,8 @@ def invoke(self, context, event): return wm.invoke_confirm(self, event) def execute(self, context): - with VirtoolsGroupsHelper(context.object) as hlp: + obj = typing.cast(bpy.types.Object, context.object) + with VirtoolsGroupsHelper(obj) as hlp: hlp.clear_groups() return {'FINISHED'} @@ -384,7 +383,6 @@ def draw(self, context): layout.label(text = 'Virtools Group is invalid on non-mesh object!', icon = 'ERROR') # draw main body - row = layout.row() row.template_list( "BBP_UL_virtools_groups", "", diff --git a/bbp_ng/UTIL_functions.py b/bbp_ng/UTIL_functions.py index cac581c..02b7a7f 100644 --- a/bbp_ng/UTIL_functions.py +++ b/bbp_ng/UTIL_functions.py @@ -173,3 +173,99 @@ def to_selection(self, val: typing.Any) -> str: # call to_str fct ptr return self.__mFctToStr(val) +#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: + """! + @brief Adds a new item to the collection. + @return The instance of newly created item. + """ + return self.__mSrcProp.add() + + def remove(self, index: int) -> None: + """! + @brief Removes the item at the specified index from the collection. + @param[in] index The index of the item to remove. + """ + self.__mSrcProp.remove(index) + + def move(self, from_index: int, to_index: int) -> None: + """! + @brief 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. + """ + self.__mSrcProp.move(from_index, to_index) + + def clear(self) -> None: + """! + @brief 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 class have "with" context feature. + However, 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: + 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: + if obj in self.__mProtectedObjects: + return False + self.__mProtectedObjects.add(obj) + return True + + def unlock(self, obj: _TMutexObject) -> None: + if obj not in self.__mProtectedObjects: + raise BBPException('It is not allowed that unlock an non-existent object.') + self.__mProtectedObjects.remove(obj) + +#endregion