From e933ff08f888e02c1187ca32108d799ab43896fc Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Fri, 3 Jan 2025 12:22:08 +0100 Subject: [PATCH] Use relevant tool.Geometry functions for querying openings relations --- src/bonsai/bonsai/bim/module/model/opening.py | 108 +++++++++--------- src/bonsai/bonsai/bim/module/void/operator.py | 2 +- src/bonsai/bonsai/core/tool.py | 2 +- src/bonsai/bonsai/tool/geometry.py | 4 +- src/bonsai/bonsai/tool/model.py | 16 +-- 5 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index afc46ee792..02c6b2ce1f 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -357,7 +357,7 @@ class FilledOpeningGenerator: return shape_builder.get_representation(context, [extrusion]) def has_visible_openings(self, element): - for opening in [r.RelatedOpeningElement for r in element.HasOpenings]: + for opening in [r.RelatedOpeningElement for r in tool.Geometry.get_openings(element)]: if tool.Ifc.get_object(opening): return True return False @@ -496,7 +496,7 @@ class AddPotentialOpening(Operator, AddObjectHelper): if new_matrix: obj.matrix_world = new_matrix - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() new = props.openings.add() new.obj = obj @@ -523,7 +523,7 @@ class AddPotentialHalfSpaceSolid(Operator, AddObjectHelper): obj = object_data_add(context, mesh, operator=self) obj.name = "HalfSpaceSolid" - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() new = props.openings.add() new.obj = obj @@ -574,7 +574,7 @@ class AddBoolean(Operator, tool.Ifc.Operator): ) tool.Model.mark_manual_booleans(element1, booleans) - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() bonsai.core.geometry.switch_representation( tool.Ifc, @@ -628,7 +628,7 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): booleans.extend(self.get_booleans(item)) props = bpy.context.scene.BIMModelProperties - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() existing_booleans = { boolean_obj.data.BIMMeshProperties.ifc_boolean_id: boolean_obj @@ -763,7 +763,7 @@ class HideBooleans(Operator, tool.Ifc.Operator): objects_to_remove.add(obj) tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True) - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() if set_active_obj and set_selected_objs is not None: tool.Blender.set_objects_selection(context, set_active_obj, set_selected_objs) @@ -831,38 +831,40 @@ class ShowOpenings(Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = bpy.context.scene.BIMModelProperties objs = list(context.selected_objects) - self.update_with_aggregates(objs) - - for obj in objs: + # If several parts of an aggregation are selected, the aggregate openings may be queried more than once + objects_element_map = set() + while objs: + obj = objs.pop(0) element = tool.Ifc.get_entity(obj) - if not element or not getattr(element, "HasOpenings", None): - continue - if tool.Ifc.is_moved(obj): - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - openings = tool.Model.load_openings( - [ - r.RelatedOpeningElement - for r in element.HasOpenings - if not tool.Ifc.get_object(r.RelatedOpeningElement) - ], - ) - for opening in openings: - new = props.openings.add() - new.obj = opening - opening.display_type = "WIRE" + if element and getattr(element, "Decomposes", None): + # Select aggregate recursively + if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject): + aggregate_obj = tool.Ifc.get_object(aggregate) + objs.append(aggregate_obj) + objects_element_map.add((obj, element)) + + for obj, element in objects_element_map: + self.show_object_openings(obj, element) DecorationsHandler.install(bpy.context) bpy.ops.bim.update_openings_focus() return {"FINISHED"} - def update_with_aggregates(self, objs): - for obj in objs: - element = tool.Ifc.get_entity(obj) - if element and getattr(element, "Decomposes", None): - if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject): - aggregate_obj = tool.Ifc.get_object(aggregate) - objs.append(aggregate_obj) + def show_object_openings(self, obj, element): + openings_elements = [rel.RelatedOpeningElement for rel in tool.Geometry.get_openings(element)] + if not openings_elements: + return + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + openings_elements_to_load = [o for o in openings_elements if not tool.Ifc.get_object(o)] + openings_objects = tool.Model.load_openings(openings_elements_to_load) + for opening in openings_objects: + self.on_new_opening_obj(opening) + + def on_new_opening_obj(self, opening_obj): + new = bpy.context.scene.BIMModelProperties.openings.add() + new.obj = opening_obj + opening_obj.display_type = "WIRE" class UpdateOpeningsFocus(Operator, tool.Ifc.Operator): @@ -908,24 +910,25 @@ def hide_openings(context, objects): objects_to_remove = set() for opening_prop in context.scene.BIMModelProperties.openings: opening_obj = opening_prop.obj - if opening_obj: - opening_element = tool.Ifc.get_entity(opening_obj) - if opening_element: - if not opening_element.is_a("IfcOpeningElement"): - # This opening has been assigned to another ifc class. Remove it from the openings pool. See #3854 - opening_prop.obj = None - continue - building_element = opening_element.VoidsElements[0].RelatingBuildingElement - if building_element: - building_obj = tool.Ifc.get_object(building_element) - if building_obj in objects: - tool.Ifc.unlink(element=opening_element) - objects_to_remove.add(opening_obj) - if opening_obj in objects: - objects_to_remove.add(opening_obj) + if not opening_obj: + continue + opening_element = tool.Ifc.get_entity(opening_obj) + if opening_element: + if not opening_element.is_a("IfcOpeningElement"): + # This opening has been assigned to another ifc class. Remove it from the openings pool. See #3854 + opening_prop.obj = None + continue + building_element = opening_element.VoidsElements[0].RelatingBuildingElement + if building_element: + building_obj = tool.Ifc.get_object(building_element) + if building_obj in objects: + tool.Ifc.unlink(element=opening_element) + objects_to_remove.add(opening_obj) + if opening_obj in objects: + objects_to_remove.add(opening_obj) tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True) - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() bpy.ops.bim.update_openings_focus() @@ -962,7 +965,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): building_objs, opening_elements = self.get_buildings_and_openings(context) self.edit_openings(building_objs, opening_elements) - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() tool.Model.reload_body_representation(building_objs) bpy.ops.bim.update_openings_focus() return {"FINISHED"} @@ -979,7 +982,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): continue opening_element = tool.Ifc.get_entity(opening_obj) if opening_element is None: - objects_to_remove.add(opening_element) + objects_to_remove.add(opening_obj) continue opening_elements.add(opening_element) building_element = opening_element.VoidsElements[0].RelatingBuildingElement @@ -995,9 +998,8 @@ class EditOpenings(Operator, tool.Ifc.Operator): building_obj = tool.Ifc.get_object(building_element) if building_obj: building_objs.add(building_obj) - elif hasattr(element, "HasOpenings"): - building_element = element - for relation in building_element.HasOpenings: + else: + for relation in tool.Geometry.get_openings(element): opening_element = relation.RelatedOpeningElement if tool.Ifc.get_object(opening_element): opening_elements.add(opening_element) diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index 09f3326aaf..548ad53f73 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -157,7 +157,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc.unlink(element=element2) bpy.data.objects.remove(obj2) - tool.Model.clear_scene_openings() + tool.Model.purge_scene_openings() context.view_layer.objects.active = obj1 return {"FINISHED"} diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index b660226e93..291e0b2890 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -560,7 +560,7 @@ class Model: def import_curve(cls, curve, obj=None, position=None): pass def import_rectangle(cls, obj, position, profile): pass def load_openings(cls, openings): pass - def clear_scene_openings(cls): pass + def purge_scene_openings(cls): pass def get_usage_type(cls, element): pass def get_material_layer_parameters(cls, element): pass def get_manual_booleans(cls, element): pass diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 897e92f961..d98786a6ba 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1526,8 +1526,8 @@ class Geometry(bonsai.core.tool.Geometry): Use `.RelatedOpeningElement` to get the opening element. """ - for element in getattr(element, "HasOpenings", ()): - yield element + for element_rel in getattr(element, "HasOpenings", ()): + yield element_rel if aggregate := ifcopenshell.util.element.get_aggregate(element): yield from cls.get_openings(aggregate) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index e0bda5fb07..2eb5be5aba 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -494,16 +494,12 @@ class Model(bonsai.core.tool.Model): return ifc_importer.added_data.values() @classmethod - def clear_scene_openings(cls) -> None: - """Clear removed scene openings.""" - props = bpy.context.scene.BIMModelProperties - has_deleted_opening = True - while has_deleted_opening: - has_deleted_opening = False - for i, opening in enumerate(list(props.openings)): - if not opening.obj: - props.openings.remove(i) - has_deleted_opening = True + def purge_scene_openings(cls) -> None: + """Purge removed scene openings.""" + openings = bpy.context.scene.BIMModelProperties.openings + for i in range(len(openings) - 1, -1, -1): + if not openings[i].obj: + openings.remove(i) @classmethod def get_material_layer_parameters(cls, element: ifcopenshell.entity_instance) -> dict[str, Any]: