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.
This commit is contained in:
Gorgious56
2025-01-03 17:37:46 +01:00
parent 7bfa926e6d
commit 3a7488d54b
5 changed files with 61 additions and 8 deletions
@@ -87,6 +87,7 @@ classes = (
opening.HideBooleans, opening.HideBooleans,
opening.HideAllOpenings, opening.HideAllOpenings,
opening.HideOpenings, opening.HideOpenings,
opening.PurgeUnusedOpenings,
opening.RecalculateFill, opening.RecalculateFill,
opening.RemoveBooleans, opening.RemoveBooleans,
opening.ShowBooleans, opening.ShowBooleans,
+49 -7
View File
@@ -574,7 +574,6 @@ class AddBoolean(Operator, tool.Ifc.Operator):
) )
tool.Model.mark_manual_booleans(element1, booleans) tool.Model.mark_manual_booleans(element1, booleans)
tool.Model.purge_scene_openings()
bonsai.core.geometry.switch_representation( bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Ifc,
@@ -587,6 +586,7 @@ class AddBoolean(Operator, tool.Ifc.Operator):
) )
tool.Blender.remove_data_blocks([obj2], remove_unused_data=True) tool.Blender.remove_data_blocks([obj2], remove_unused_data=True)
tool.Model.purge_scene_openings()
return {"FINISHED"} return {"FINISHED"}
@@ -837,12 +837,13 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
while objs: while objs:
obj = objs.pop(0) obj = objs.pop(0)
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if element and getattr(element, "Decomposes", None): if element:
# Select aggregate recursively if getattr(element, "Decomposes", None):
if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject): # Select aggregate recursively
aggregate_obj = tool.Ifc.get_object(aggregate) if element.Decomposes and (aggregate := element.Decomposes[0].RelatingObject):
objs.append(aggregate_obj) aggregate_obj = tool.Ifc.get_object(aggregate)
objects_element_map.add((obj, element)) objs.append(aggregate_obj)
objects_element_map.add((obj, element))
for obj, element in objects_element_map: for obj, element in objects_element_map:
self.show_object_openings(obj, element) self.show_object_openings(obj, element)
@@ -1085,6 +1086,47 @@ class CloneOpening(Operator, tool.Ifc.Operator):
return {"FINISHED"} 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? # TODO: merge with ProfileDecorator?
class DecorationsHandler: class DecorationsHandler:
installed = None installed = None
@@ -537,6 +537,7 @@ class BIM_PT_purge(Panel):
layout = self.layout 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 Profiles").object_type = "PROFILE"
layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE" 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 = layout.row(align=True)
row.label(text="Materials: ") row.label(text="Materials: ")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "MATERIAL" row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "MATERIAL"
+9
View File
@@ -48,6 +48,7 @@ import bonsai.bim.import_ifc
from collections import defaultdict from collections import defaultdict
from math import radians, pi from math import radians, pi
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from mathutils.bvhtree import BVHTree
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from typing import Union, Iterable, Optional, Literal, Iterator, List, TYPE_CHECKING, get_args, Generator from typing import Union, Iterable, Optional, Literal, Iterator, List, TYPE_CHECKING, get_args, Generator
from typing_extensions import TypeIs from typing_extensions import TypeIs
@@ -1792,3 +1793,11 @@ class Geometry(bonsai.core.tool.Geometry):
if not edge.link_faces: if not edge.link_faces:
return True return True
return False 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)
@@ -1132,8 +1132,8 @@ def get_aggregate(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.e
element = file.by_type("IfcBeam")[0] element = file.by_type("IfcBeam")[0]
aggregate = ifcopenshell.util.element.get_aggregate(element) aggregate = ifcopenshell.util.element.get_aggregate(element)
""" """
is_not_ifc2x3 = element.file.schema != "IFC2X3"
if decomposes := getattr(element, "Decomposes", None): if decomposes := getattr(element, "Decomposes", None):
is_not_ifc2x3 = element.file.schema != "IFC2X3"
if is_not_ifc2x3 or decomposes[0].is_a("IfcRelAggregates"): if is_not_ifc2x3 or decomposes[0].is_a("IfcRelAggregates"):
return decomposes[0].RelatingObject return decomposes[0].RelatingObject