From 83d97d7e9588a6c8a930c5262fa93967f5ee7515 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 30 Jan 2026 17:18:02 +1100 Subject: [PATCH] Fix #7616. Make regenerate array an operator instead of an array preference Sync children was a bit odd because it's not actually an "array parameter" per se, just a way to regenerate. It's now an operator. There was a deeper issue I encountered where the way arrays work is that they duplicate the parent element. (first encountered in e51d2d ) However, the duplication code has special array handling too. To avoid issues with this cyclical coupling the previous solution was to reimplement object duplication (with all sorts of pitfalls that has). Now, I've tried to decouple it further by clearing all array psets prior to any change, and readding the pset after everything has been regenerated. This can be improved upon but I don't feel confident until there is more comprehensive test coverage for the duplicate operator. --- .../bonsai/bim/module/model/__init__.py | 3 +- src/bonsai/bonsai/bim/module/model/array.py | 59 ++++++++++++------- src/bonsai/bonsai/bim/module/model/prop.py | 5 -- src/bonsai/bonsai/bim/module/model/ui.py | 2 +- src/bonsai/bonsai/tool/model.py | 41 ++++++------- 5 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 57b4b561d0..9fbd631003 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -50,9 +50,10 @@ classes = ( array.EditArray, array.EnableEditingArray, array.ApplyArray, + array.RegenerateArray, array.RemoveArray, - array.SelectArrayParent, array.SelectAllArrayObjects, + array.SelectArrayParent, array.Input3DCursorXArray, array.Input3DCursorYArray, array.Input3DCursorZArray, diff --git a/src/bonsai/bonsai/bim/module/model/array.py b/src/bonsai/bonsai/bim/module/model/array.py index 3fb9360256..b1c13dbe75 100644 --- a/src/bonsai/bonsai/bim/module/model/array.py +++ b/src/bonsai/bonsai/bim/module/model/array.py @@ -61,7 +61,6 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator): "y": 0.0, "z": 0.0, "use_local_space": True, - "sync_children": False, "method": "OFFSET", } @@ -80,28 +79,27 @@ class AddArray(bpy.types.Operator, tool.Ifc.Operator): pset=pset, properties={"Parent": element.GlobalId, "Data": ifc_file.create_entity("IfcText", json.dumps(data))}, ) - return {"FINISHED"} -class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator): +class DisableEditingArray(bpy.types.Operator): bl_idname = "bim.disable_editing_array" bl_label = "Disable Editing Array" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): obj = context.active_object assert obj tool.Model.get_array_props(obj).is_editing = -1 return {"FINISHED"} -class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingArray(bpy.types.Operator): bl_idname = "bim.enable_editing_array" bl_label = "Enable Editing Array" bl_options = {"REGISTER", "UNDO"} item: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): obj = context.active_object assert obj element = tool.Ifc.get_entity(obj) @@ -122,11 +120,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): props.y = data["y"] * si_conversion props.z = data["z"] * si_conversion props.use_local_space = data.get("use_local_space", False) - props.sync_children = data.get("sync_children", False) props.method = data.get("method", "OFFSET") props.is_editing = self.item - return {"FINISHED"} @@ -151,31 +147,25 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator): "y": props.y / si_conversion, "z": props.z / si_conversion, "use_local_space": props.use_local_space, - "sync_children": props.sync_children, "method": props.method, } props.is_editing = -1 try: - parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"])) + parent_element = tool.Ifc.get().by_guid(pset["Parent"]) + parent = tool.Ifc.get_object(parent_element) except: return {"FINISHED"} + tool.Blender.Modifier.Array.remove_constraints(parent_element) tool.Model.regenerate_array(parent, data) - - pset = tool.Ifc.get().by_id(pset["id"]) - data = tool.Ifc.get().createIfcText(json.dumps(data)) - ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": data}) - tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True) tool.Blender.Modifier.Array.constrain_children_to_parent(element) # clears the relating_array_object so it doesn't show again next time props.relating_array_object = None - return {"FINISHED"} - class ApplyArray(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.apply_array" @@ -192,6 +182,33 @@ class ApplyArray(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} +class RegenerateArray(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.regenerate_array" + bl_label = "Regenerate Array" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + obj = context.active_object + element = tool.Ifc.get_entity(obj) + pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") + try: + parent_element = tool.Ifc.get().by_guid(pset["Parent"]) + parent = tool.Ifc.get_object(parent_element) + except: + return {"FINISHED"} + pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array") + arrays = json.loads(pset["Data"]) + pset = tool.Ifc.get().by_id(pset["id"]) + for array in arrays: + for child in set(array["children"]): + if child_obj := tool.Ifc.get_object(tool.Ifc.get().by_guid(child)): + tool.Geometry.delete_ifc_object(child_obj) + array["children"].clear() + print('cleared array', arrays) + tool.Model.regenerate_array(obj, arrays) + tool.Blender.Modifier.Array.constrain_children_to_parent(element) + + class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_array" bl_label = "Remove Array" @@ -216,7 +233,8 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): props.is_editing = -1 try: - parent = tool.Ifc.get_object(tool.Ifc.get().by_guid(pset["Parent"])) + parent_element = tool.Ifc.get().by_guid(pset["Parent"]) + parent = tool.Ifc.get_object(parent_element) except: return {"FINISHED"} @@ -226,9 +244,10 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): if not self.keep_objs: data[self.item]["count"] = 1 + tool.Blender.Modifier.Array.remove_constraints(parent_element) tool.Model.regenerate_array(parent, data, array_layers_to_apply=[self.item] if self.keep_objs else []) - pset = tool.Ifc.get().by_id(pset["id"]) + pset = tool.Pset.get_element_pset(element, "BBIM_Array") if len(data) == 1: ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset) else: @@ -237,8 +256,6 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": data}) tool.Blender.Modifier.Array.constrain_children_to_parent(element) - return {"FINISHED"} - class SelectArrayParent(bpy.types.Operator): bl_idname = "bim.select_array_parent" diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 9f2664190d..ec79a49196 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -387,11 +387,6 @@ class BIMArrayProperties(PropertyGroup): name="Method", default="OFFSET", ) - sync_children: bpy.props.BoolProperty( - name="Sync Children", - description="Regenerate all children based on the parent object", - default=False, - ) relating_array_object: bpy.props.PointerProperty( type=bpy.types.Object, name="Copy Array Properties", diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 2c931f4f45..d8a297433b 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -229,6 +229,7 @@ class BIM_PT_array(bpy.types.Panel): if ArrayData.data["parameters"]: row = self.layout.row(align=True) row.label(text=ArrayData.data["parameters"]["parent_name"], icon="CON_CHILDOF") + row.operator("bim.regenerate_array", icon="FILE_REFRESH", text="") row.operator("bim.select_array_parent", icon="OBJECT_DATA", text="") row.operator("bim.select_all_array_objects", icon="RESTRICT_SELECT_OFF", text="") @@ -246,7 +247,6 @@ class BIM_PT_array(bpy.types.Panel): row.prop(props, "method") row = box.row(align=True) row.prop(props, "use_local_space") - row.prop(props, "sync_children") col = box.column() row = col.row(align=True) row.prop(props, "x") diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 08a0a0fd85..636ec9bc73 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1051,6 +1051,7 @@ class Model(bonsai.core.tool.Model): tool.Model.regenerate_array(obj, array_data) + array_pset = tool.Pset.get_element_pset(element, "BBIM_Array") json_data = tool.Ifc.get().createIfcText(json.dumps(array_data)) ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=array_pset, properties={"Data": json_data}) @@ -1063,22 +1064,15 @@ class Model(bonsai.core.tool.Model): cls, parent_obj: bpy.types.Object, data: list[dict[str, Any]], array_layers_to_apply: Iterable[int] = tuple() ) -> None: """`array_layers_to_apply` - list of array layer indices to apply""" - tool.Blender.Modifier.Array.remove_constraints(tool.Ifc.get_entity(parent_obj)) + parent_element = tool.Ifc.get_entity(parent_obj) + + if pset := ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array"): + ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=parent_element, pset=tool.Ifc.get().by_id(pset["id"])) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) obj_stack = [parent_obj] for array_i, array in enumerate(data): - # for `sync_children` we remove all previously generated children to regenerate them again - # to assure they are in complete sync (psets, etc) with the array parent - if array["sync_children"]: - removed_children = set(array["children"]) - for removed_child in removed_children: - element = tool.Ifc.get().by_guid(removed_child) - if obj := tool.Ifc.get_object(element): - tool.Geometry.delete_ifc_object(obj) - array["children"].clear() - child_i = 0 existing_children = set(array["children"]) total_existing_children = len(array["children"]) @@ -1104,22 +1098,19 @@ class Model(bonsai.core.tool.Model): child_obj = tool.Ifc.get_object(child_element) assert child_obj except: - old_to_new, _ = tool.Geometry.duplicate_ifc_objects([obj]) - # TODO Is this correct to assume one child? I really - # don't understand the linked aggregates and array - # behaviour. + old_to_new, _ = tool.Geometry.duplicate_ifc_objects([parent_obj]) child_element = next(iter(old_to_new.values()))[0] child_obj = tool.Ifc.get_object(child_element) # add child pset - child_pset = tool.Pset.get_element_pset(child_element, "BBIM_Array") - if child_pset: - ifcopenshell.api.pset.edit_pset( - tool.Ifc.get(), - pset=child_pset, - properties={"Data": None}, - should_purge=False, - ) + if not (child_pset := tool.Pset.get_element_pset(child_element, "BBIM_Array")): + child_pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=child_element, name="BBIM_Array") + ifcopenshell.api.pset.edit_pset( + tool.Ifc.get(), + pset=child_pset, + properties={"Data": None, "Parent": parent_element.GlobalId}, + should_purge=False, + ) # set child object position new_matrix = obj.matrix_world.copy() @@ -1155,6 +1146,10 @@ class Model(bonsai.core.tool.Model): bpy.context.view_layer.update() + pset = ifcopenshell.api.pset.add_pset(tool.Ifc.get(), product=parent_element, name="BBIM_Array") + json_data = tool.Ifc.get().createIfcText(json.dumps(data)) + ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": json_data, "Parent": parent_element.GlobalId}) + @classmethod def replace_object_ifc_representation( cls,