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.
This commit is contained in:
yyc12345 2025-01-06 15:12:14 +08:00
parent 6ae8899912
commit f4d3e48be2
7 changed files with 182 additions and 101 deletions

View File

@ -96,27 +96,29 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
# init data collection # 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 # clear first
self.bme_struct_cfgs.clear() adder_cfgs_visitor.clear()
# create enough entries specified by gotten cfgs # create enough entries specified by gotten cfgs
for _ in range(max(counter_int, counter_float, counter_bool)): for _ in range(max(counter_int, counter_float, counter_bool)):
self.bme_struct_cfgs.add() adder_cfgs_visitor.add()
# assign default value # assign default value
for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: for (cfg, cfg_index) in self.bme_struct_cfg_index_cache:
# show prop differently by cfg type # show prop differently by cfg type
match(cfg.get_type()): match(cfg.get_type()):
case UTIL_bme.PrototypeShowcaseCfgsTypes.Integer: 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: 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: 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: case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
# face is just 6 bool # face is just 6 bool
default_values: tuple[bool, ...] = cfg.get_default() default_values: tuple[bool, ...] = cfg.get_default()
for i in range(6): 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 # reset outdated flag
self.outdated_flag = False self.outdated_flag = False
@ -182,20 +184,23 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
# call internal updator # call internal updator
self.__internal_update_bme_struct_type() 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 # collect cfgs data
cfgs: dict[str, typing.Any] = {} cfgs: dict[str, typing.Any] = {}
for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: for (cfg, cfg_index) in self.bme_struct_cfg_index_cache:
match(cfg.get_type()): match(cfg.get_type()):
case UTIL_bme.PrototypeShowcaseCfgsTypes.Integer: 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: 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: 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: case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
# face is just 6 bool tuple # face is just 6 bool tuple
cfgs[cfg.get_field()] = 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 # call general creator
@ -225,6 +230,9 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
# show type # show type
layout.prop(self, 'bme_struct_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 # visit cfgs cache list to show cfg
layout.label(text = "Prototype Configurations:") layout.label(text = "Prototype Configurations:")
for (cfg, cfg_index) in self.bme_struct_cfg_index_cache: for (cfg, cfg_index) in self.bme_struct_cfg_index_cache:
@ -238,24 +246,24 @@ class BBP_OT_add_bme_struct(bpy.types.Operator):
# show prop differently by cfg type # show prop differently by cfg type
match(cfg.get_type()): match(cfg.get_type()):
case UTIL_bme.PrototypeShowcaseCfgsTypes.Integer: 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: 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: 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: case UTIL_bme.PrototypeShowcaseCfgsTypes.Face:
# face will show a special layout (grid view) # face will show a special layout (grid view)
grids = box_layout.grid_flow( grids = box_layout.grid_flow(
row_major=True, columns=3, even_columns=True, even_rows=True, align=True) row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
grids.alignment = 'CENTER' grids.alignment = 'CENTER'
grids.separator() grids.separator()
grids.prop(self.bme_struct_cfgs[cfg_index + 0], 'prop_bool', text = 'Top') # top grids.prop(adder_cfgs_visitor[cfg_index + 0], 'prop_bool', text = 'Top') # top
grids.prop(self.bme_struct_cfgs[cfg_index + 2], 'prop_bool', text = 'Front') # front grids.prop(adder_cfgs_visitor[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 + 4], 'prop_bool', text = 'Left') # left
grids.label(text = '', icon = 'CUBE') # show a 3d cube as icon 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(adder_cfgs_visitor[cfg_index + 5], 'prop_bool', text = 'Right') # right
grids.prop(self.bme_struct_cfgs[cfg_index + 3], 'prop_bool', text = 'Back') # back grids.prop(adder_cfgs_visitor[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 + 1], 'prop_bool', text = 'Bottom') # bottom
grids.separator() grids.separator()
# show extra transform props # show extra transform props

View File

@ -84,11 +84,13 @@ class BBP_OT_legacy_align(bpy.types.Operator):
# check whether add new entry # check whether add new entry
# if no selected axis, this alignment is invalid # 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: if entry.align_x == True or entry.align_y == True or entry.align_z == True:
# valid one # valid one
# add a new entry in history # add a new entry in history
self.align_history.add() histories.add()
else: else:
# invalid one # invalid one
# reset all data to default # reset all data to default
@ -127,9 +129,11 @@ class BBP_OT_legacy_align(bpy.types.Operator):
return _check_align_requirement() return _check_align_requirement()
def invoke(self, context, event): 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 # clear history and add 1 entry for following functions
self.align_history.clear() histories.clear()
self.align_history.add() histories.add()
# run execute() function # run execute() function
return self.execute(context) return self.execute(context)
@ -145,8 +149,9 @@ class BBP_OT_legacy_align(bpy.types.Operator):
# If you place it at the end of this function, it doesn't work. # If you place it at the end of this function, it doesn't work.
context.view_layer.update() context.view_layer.update()
# iterate history to align objects # iterate history to align objects
entry: BBP_PG_legacy_align_history histories: UTIL_functions.CollectionVisitor[BBP_PG_legacy_align_history]
for entry in self.align_history: histories = UTIL_functions.CollectionVisitor(self.align_history)
for entry in histories:
_align_objects( _align_objects(
current_obj, target_objs, current_obj, target_objs,
entry.align_x, entry.align_y, entry.align_z, entry.align_x, entry.align_y, entry.align_z,
@ -157,7 +162,9 @@ class BBP_OT_legacy_align(bpy.types.Operator):
def draw(self, context): def draw(self, context):
# get last entry in history to show # 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 layout = self.layout
col = layout.column() col = layout.column()
@ -183,7 +190,7 @@ class BBP_OT_legacy_align(bpy.types.Operator):
conditional_disable_area.enabled = entry.align_x == True or entry.align_y == True or entry.align_z == True conditional_disable_area.enabled = entry.align_x == True or entry.align_y == True or entry.align_z == True
# show apply and counter # show apply and counter
conditional_disable_area.prop(self, 'apply_flag', text = 'Apply', icon = 'CHECKMARK', toggle = 1) 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 #region Core Functions

View File

@ -27,15 +27,10 @@ class BBP_OT_snoop_group_then_to_mesh(bpy.types.Operator):
if bevel_obj is None: continue if bevel_obj is None: continue
# copy bevel object group info into current object # 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: with PROP_virtools_group.VirtoolsGroupsHelper(obj) as this_gp:
this_gp.clear_groups() 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 # convert all selected object to mesh
# no matter the success of copying virtools group infos and whether selected object is curve # no matter the success of copying virtools group infos and whether selected object is curve

View File

@ -104,8 +104,8 @@ class BBP_PG_ballance_element(bpy.types.PropertyGroup):
type = bpy.types.Mesh type = bpy.types.Mesh
) # type: ignore ) # type: ignore
def get_ballance_elements(scene: bpy.types.Scene) -> bpy.types.CollectionProperty: def get_ballance_elements(scene: bpy.types.Scene) -> UTIL_functions.CollectionVisitor[BBP_PG_ballance_element]:
return scene.ballance_elements return UTIL_functions.CollectionVisitor(scene.ballance_elements)
#endregion #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 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. 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 __mIsValid: bool
__mAssocScene: bpy.types.Scene __mAssocScene: bpy.types.Scene
__mElementMap: dict[BallanceElementType, bpy.types.Mesh] __mElementMap: dict[BallanceElementType, bpy.types.Mesh]
@ -232,14 +232,11 @@ class BallanceElementsHelper():
def __init__(self, assoc: bpy.types.Scene): def __init__(self, assoc: bpy.types.Scene):
self.__mElementMap = {} self.__mElementMap = {}
self.__mAssocScene = assoc self.__mAssocScene = assoc
self.__mIsValid = False
# check singleton # check singleton
if BallanceElementsHelper.__mSingletonMutex: BallanceElementsHelper.__mSingletonMutex.lock(self.__mAssocScene)
self.__mIsValid = False
raise UTIL_functions.BBPException('BallanceElementsHelper is mutex.')
# set validation and read ballance elements property # set validation and read ballance elements property
BallanceElementsHelper.__mSingletonMutex = True
self.__mIsValid = True self.__mIsValid = True
self.__read_from_ballance_element() self.__read_from_ballance_element()
@ -257,7 +254,7 @@ class BallanceElementsHelper():
# write to ballance elements property and reset validation # write to ballance elements property and reset validation
self.__write_to_ballance_elements() self.__write_to_ballance_elements()
self.__mIsValid = False self.__mIsValid = False
BallanceElementsHelper.__mSingletonMutex = False BallanceElementsHelper.__mSingletonMutex.unlock(self.__mAssocScene)
def get_element(self, element_type: BallanceElementType) -> bpy.types.Mesh: def get_element(self, element_type: BallanceElementType) -> bpy.types.Mesh:
if not self.is_valid(): if not self.is_valid():
@ -276,8 +273,16 @@ class BallanceElementsHelper():
self.__mElementMap[element_type] = new_mesh self.__mElementMap[element_type] = new_mesh
return 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: def __write_to_ballance_elements(self) -> None:
elements: bpy.types.CollectionProperty = get_ballance_elements(self.__mAssocScene) elements = get_ballance_elements(self.__mAssocScene)
elements.clear() elements.clear()
for elety, elemesh in self.__mElementMap.items(): for elety, elemesh in self.__mElementMap.items():
@ -286,10 +291,9 @@ class BallanceElementsHelper():
item.mesh_ptr = elemesh item.mesh_ptr = elemesh
def __read_from_ballance_element(self) -> None: 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() self.__mElementMap.clear()
item: BBP_PG_ballance_element
for item in elements: for item in elements:
# check requirements # check requirements
if item.mesh_ptr is None: continue if item.mesh_ptr is None: continue
@ -299,30 +303,6 @@ class BallanceElementsHelper():
# add into map # add into map
self.__mElementMap[element_type] = item.mesh_ptr 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 #endregion
#region Ballance Elements Representation #region Ballance Elements Representation
@ -348,7 +328,8 @@ class BBP_OT_reset_ballance_elements(bpy.types.Operator):
return context.scene is not None return context.scene is not None
def execute(self, context): 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 # show a window to let user know, not silence
UTIL_functions.message_box( UTIL_functions.message_box(
('Reset OK.', ), ('Reset OK.', ),

View File

@ -85,8 +85,8 @@ class BBP_PG_bme_material(bpy.types.PropertyGroup):
type = bpy.types.Material type = bpy.types.Material
) # type: ignore ) # type: ignore
def get_bme_materials(scene: bpy.types.Scene) -> bpy.types.CollectionProperty: def get_bme_materials(scene: bpy.types.Scene) -> UTIL_functions.CollectionVisitor[BBP_PG_bme_material]:
return scene.bme_materials return UTIL_functions.CollectionVisitor(scene.bme_materials)
#endregion #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 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. 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 __mIsValid: bool
__mAssocScene: bpy.types.Scene __mAssocScene: bpy.types.Scene
__mMaterialMap: dict[str, bpy.types.Material] __mMaterialMap: dict[str, bpy.types.Material]
@ -134,14 +134,11 @@ class BMEMaterialsHelper():
def __init__(self, assoc: bpy.types.Scene): def __init__(self, assoc: bpy.types.Scene):
self.__mMaterialMap = {} self.__mMaterialMap = {}
self.__mAssocScene = assoc self.__mAssocScene = assoc
self.__mIsValid = False
# check singleton # check singleton
if BMEMaterialsHelper.__mSingletonMutex: BMEMaterialsHelper.__mSingletonMutex.lock(self.__mAssocScene)
self.__mIsValid = False
raise UTIL_functions.BBPException('BMEMaterialsHelper is mutex.')
# set validation and read ballance elements property # set validation and read ballance elements property
BMEMaterialsHelper.__mSingletonMutex = True
self.__mIsValid = True self.__mIsValid = True
self.__read_from_bme_materials() self.__read_from_bme_materials()
@ -159,7 +156,7 @@ class BMEMaterialsHelper():
# write to ballance elements property and reset validation # write to ballance elements property and reset validation
self.__write_to_bme_materials() self.__write_to_bme_materials()
self.__mIsValid = False self.__mIsValid = False
BMEMaterialsHelper.__mSingletonMutex = False BMEMaterialsHelper.__mSingletonMutex.unlock(self.__mAssocScene)
def get_material(self, preset_name: str) -> bpy.types.Material: def get_material(self, preset_name: str) -> bpy.types.Material:
if not self.is_valid(): if not self.is_valid():
@ -179,7 +176,7 @@ class BMEMaterialsHelper():
return new_mtl return new_mtl
def __write_to_bme_materials(self) -> None: def __write_to_bme_materials(self) -> None:
mtls: bpy.types.CollectionProperty = get_bme_materials(self.__mAssocScene) mtls = get_bme_materials(self.__mAssocScene)
mtls.clear() mtls.clear()
for preset_name, mtl in self.__mMaterialMap.items(): for preset_name, mtl in self.__mMaterialMap.items():
@ -188,10 +185,9 @@ class BMEMaterialsHelper():
item.material_ptr = mtl item.material_ptr = mtl
def __read_from_bme_materials(self) -> None: 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() self.__mMaterialMap.clear()
item: BBP_PG_bme_material
for item in mtls: for item in mtls:
# check requirements # check requirements
if item.material_ptr is None: continue if item.material_ptr is None: continue
@ -200,7 +196,7 @@ class BMEMaterialsHelper():
def reset_bme_materials(scene: bpy.types.Scene) -> None: def reset_bme_materials(scene: bpy.types.Scene) -> None:
invalid_idx: list[int] = [] invalid_idx: list[int] = []
mtls: bpy.types.CollectionProperty = get_bme_materials(scene) mtls = get_bme_materials(scene)
# re-load all elements # re-load all elements
index: int = 0 index: int = 0

View File

@ -10,8 +10,8 @@ class BBP_PG_virtools_group(bpy.types.PropertyGroup):
default = "" default = ""
) # type: ignore ) # type: ignore
def get_virtools_groups(obj: bpy.types.Object) -> bpy.types.CollectionProperty: def get_virtools_groups(obj: bpy.types.Object) -> UTIL_functions.CollectionVisitor[BBP_PG_virtools_group]:
return obj.virtools_groups return UTIL_functions.CollectionVisitor(obj.virtools_groups)
def get_active_virtools_groups(obj: bpy.types.Object) -> int: def get_active_virtools_groups(obj: bpy.types.Object) -> int:
return obj.active_virtools_groups return obj.active_virtools_groups
@ -26,7 +26,7 @@ class VirtoolsGroupsHelper():
All Virtools group operations should be done by this class. All Virtools group operations should be done by this class.
Do NOT manipulate object's Virtools group properties directly. 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 __mIsValid: bool
__mNoChange: bool ##< A bool indicate whether any change happended during lifetime. If no change, skip the writing when exiting. __mNoChange: bool ##< A bool indicate whether any change happended during lifetime. If no change, skip the writing when exiting.
__mAssocObj: bpy.types.Object __mAssocObj: bpy.types.Object
@ -36,14 +36,11 @@ class VirtoolsGroupsHelper():
self.__mGroupsSet = set() self.__mGroupsSet = set()
self.__mAssocObj = assoc self.__mAssocObj = assoc
self.__mNoChange = True self.__mNoChange = True
self.__mIsValid = False
# check singleton # check singleton
if VirtoolsGroupsHelper.__mSingletonMutex: VirtoolsGroupsHelper.__mSingletonMutex.lock(self.__mAssocObj)
self.__mIsValid = False
raise UTIL_functions.BBPException('VirtoolsGroupsHelper is mutex.')
# set validation and read ballance elements property # set validation and read ballance elements property
VirtoolsGroupsHelper.__mSingletonMutex = True
self.__mIsValid = True self.__mIsValid = True
self.__read_from_virtools_groups() self.__read_from_virtools_groups()
@ -63,7 +60,7 @@ class VirtoolsGroupsHelper():
if not self.__mNoChange: if not self.__mNoChange:
self.__write_to_virtools_groups() self.__write_to_virtools_groups()
self.__mIsValid = False self.__mIsValid = False
VirtoolsGroupsHelper.__mSingletonMutex = False VirtoolsGroupsHelper.__mSingletonMutex.unlock(self.__mAssocObj)
def __check_valid(self) -> None: def __check_valid(self) -> None:
if not self.is_valid(): if not self.is_valid():
@ -127,7 +124,7 @@ class VirtoolsGroupsHelper():
return len(self.__mGroupsSet) return len(self.__mGroupsSet)
def __write_to_virtools_groups(self) -> None: 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) sel: int = get_active_virtools_groups(self.__mAssocObj)
groups.clear() groups.clear()
@ -143,10 +140,9 @@ class VirtoolsGroupsHelper():
set_active_virtools_groups(self.__mAssocObj, sel) set_active_virtools_groups(self.__mAssocObj, sel)
def __read_from_virtools_groups(self) -> None: 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() self.__mGroupsSet.clear()
item: BBP_PG_virtools_group
for item in groups: for item in groups:
self.__mGroupsSet.add(item.group_name) self.__mGroupsSet.add(item.group_name)
@ -306,7 +302,8 @@ class BBP_OT_add_virtools_group(bpy.types.Operator, SharedGroupNameInputProperti
def execute(self, context): def execute(self, context):
# add group # 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()) hlp.add_group(self.general_get_group_name())
return {'FINISHED'} return {'FINISHED'}
@ -335,8 +332,9 @@ class BBP_OT_rm_virtools_group(bpy.types.Operator):
def execute(self, context): def execute(self, context):
# get selected group name first # get selected group name first
obj = context.object obj = typing.cast(bpy.types.Object, context.object)
item: BBP_PG_virtools_group = get_virtools_groups(obj)[get_active_virtools_groups(obj)] groups = get_virtools_groups(obj)
item = groups[get_active_virtools_groups(obj)]
gname: str = item.group_name gname: str = item.group_name
# then delete it # then delete it
with VirtoolsGroupsHelper(obj) as hlp: with VirtoolsGroupsHelper(obj) as hlp:
@ -359,7 +357,8 @@ class BBP_OT_clear_virtools_groups(bpy.types.Operator):
return wm.invoke_confirm(self, event) return wm.invoke_confirm(self, event)
def execute(self, context): 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() hlp.clear_groups()
return {'FINISHED'} return {'FINISHED'}
@ -384,7 +383,6 @@ class BBP_PT_virtools_groups(bpy.types.Panel):
layout.label(text = 'Virtools Group is invalid on non-mesh object!', icon = 'ERROR') layout.label(text = 'Virtools Group is invalid on non-mesh object!', icon = 'ERROR')
# draw main body # draw main body
row = layout.row() row = layout.row()
row.template_list( row.template_list(
"BBP_UL_virtools_groups", "", "BBP_UL_virtools_groups", "",

View File

@ -173,3 +173,99 @@ class EnumPropHelper():
# call to_str fct ptr # call to_str fct ptr
return self.__mFctToStr(val) 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