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.
This commit is contained in:
Dion Moult
2026-01-30 17:18:02 +11:00
parent c1aa1dedba
commit 83d97d7e95
5 changed files with 59 additions and 51 deletions
@@ -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,
+38 -21
View File
@@ -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"
@@ -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",
+1 -1
View File
@@ -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")
+18 -23
View File
@@ -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,