From 3a7488d54b285e1cd47ef823ad5701c966f785f0 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Fri, 3 Jan 2025 17:37:46 +0100 Subject: [PATCH] Fix #3261 : You can now purge unused openings that do not intersect with their related building element. It's a naive implementation and very inefficient. Moreover there may be false positives because it's testing overlapping the evaluated mesh element with all openings applied. --- .../bonsai/bim/module/model/__init__.py | 1 + src/bonsai/bonsai/bim/module/model/opening.py | 56 ++++++++++++++++--- src/bonsai/bonsai/bim/module/project/ui.py | 1 + src/bonsai/bonsai/tool/geometry.py | 9 +++ .../ifcopenshell/util/element.py | 2 +- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 479199fe03..d4c539b8f7 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -87,6 +87,7 @@ classes = ( opening.HideBooleans, opening.HideAllOpenings, opening.HideOpenings, + opening.PurgeUnusedOpenings, opening.RecalculateFill, opening.RemoveBooleans, opening.ShowBooleans, diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 93b7d10261..85e0eaac7d 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -574,7 +574,6 @@ class AddBoolean(Operator, tool.Ifc.Operator): ) tool.Model.mark_manual_booleans(element1, booleans) - tool.Model.purge_scene_openings() bonsai.core.geometry.switch_representation( tool.Ifc, @@ -587,6 +586,7 @@ class AddBoolean(Operator, tool.Ifc.Operator): ) tool.Blender.remove_data_blocks([obj2], remove_unused_data=True) + tool.Model.purge_scene_openings() return {"FINISHED"} @@ -837,12 +837,13 @@ class ShowOpenings(Operator, tool.Ifc.Operator): while objs: obj = objs.pop(0) element = tool.Ifc.get_entity(obj) - 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)) + if element: + if 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) @@ -1085,6 +1086,47 @@ class CloneOpening(Operator, tool.Ifc.Operator): return {"FINISHED"} +class PurgeUnusedOpenings(Operator, tool.Ifc.Operator): + bl_idname = "bim.purge_unused_openings" + bl_label = "Purge Unused Openings" + bl_description = "Purge Openings that do not intersect with their related building element" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return any( + [tool.Geometry.has_openings(element)] + for element in [tool.Ifc.get_entity(obj) for obj in context.selected_objects] + if element + ) + + def _execute(self, context): + bpy.ops.bim.show_openings() + objects = context.selected_objects[:] + [o.select_set(False) for o in objects] + active_object = context.active_object + purged = 0 + for obj in objects: + element = tool.Ifc.get_entity(obj) + if not element or not tool.Geometry.has_openings(element): + continue + obj_bvh_tree = tool.Geometry.get_bvh_tree(obj) + for opening_rel in tool.Geometry.get_openings(element): + opening_elt = opening_rel.RelatedOpeningElement + opening_obj = tool.Ifc.get_object(opening_elt) + opening_bvh_tree = tool.Geometry.get_bvh_tree(opening_obj) + if not opening_bvh_tree.overlap(obj_bvh_tree): + opening_obj.select_set(True) + purged += 1 + if context.selected_objects: + bpy.ops.bim.override_object_delete(is_batch=False) + bpy.ops.bim.edit_openings(apply_all=True) + [o.select_set(True) for o in objects] + context.view_layer.objects.active = active_object + self.report({"INFO"}, f"{purged} unused openings were purged.") + return {"FINISHED"} + + # TODO: merge with ProfileDecorator? class DecorationsHandler: installed = None diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 4af58993ba..e8ba4fbe29 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -537,6 +537,7 @@ class BIM_PT_purge(Panel): layout = self.layout layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE" layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE" + layout.operator("bim.purge_unused_openings", text="Purge Unused Openings in Selected Objects") row = layout.row(align=True) row.label(text="Materials: ") row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "MATERIAL" diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 02b26fe40c..5a49c948cd 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -48,6 +48,7 @@ import bonsai.bim.import_ifc from collections import defaultdict from math import radians, pi from mathutils import Vector, Matrix +from mathutils.bvhtree import BVHTree from bonsai.bim.ifc import IfcStore from typing import Union, Iterable, Optional, Literal, Iterator, List, TYPE_CHECKING, get_args, Generator from typing_extensions import TypeIs @@ -1792,3 +1793,11 @@ class Geometry(bonsai.core.tool.Geometry): if not edge.link_faces: return True return False + + @classmethod + def get_bvh_tree(cls, obj:bpy.types.Object) -> BVHTree: + bm = tool.Blender.get_bmesh_for_mesh(obj.data) + bm.transform(obj.matrix_world) + return BVHTree.FromBMesh(bm) + + \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 9f297ff673..1a7ea489e5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1132,8 +1132,8 @@ def get_aggregate(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.e element = file.by_type("IfcBeam")[0] aggregate = ifcopenshell.util.element.get_aggregate(element) """ - is_not_ifc2x3 = element.file.schema != "IFC2X3" if decomposes := getattr(element, "Decomposes", None): + is_not_ifc2x3 = element.file.schema != "IFC2X3" if is_not_ifc2x3 or decomposes[0].is_a("IfcRelAggregates"): return decomposes[0].RelatingObject