diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index ef4a8660a0..708580b9ca 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -20,11 +20,13 @@ import bpy import gpu import bgl import bmesh +import logging import blenderbim.bim.handler import ifcopenshell import ifcopenshell.util.representation import blenderbim.tool as tool import blenderbim.core.geometry +import blenderbim.bim.import_ifc as import_ifc from blenderbim.bim.ifc import IfcStore from math import pi from mathutils import Vector, Matrix @@ -327,12 +329,17 @@ class AddBoolean(Operator, tool.Ifc.Operator): return {"FINISHED"} representation = tool.Ifc.get().by_id(obj1.data.BIMMeshProperties.ifc_definition_id) + if not obj2.data or len(obj2.data.polygons) <= 4: # It takes 4 faces to create a closed solid + mesh_data = {"type": "IfcHalfSpaceSolid", "matrix": obj1.matrix_world.inverted() @ obj2.matrix_world} + elif obj2.data: + mesh_data = {"type": "Mesh", "blender_obj": obj1, "blender_void": obj2} + ifcopenshell.api.run( "geometry.add_boolean", tool.Ifc.get(), representation=representation, operator="DIFFERENCE", - matrix=obj1.matrix_world.inverted() @ obj2.matrix_world, + **mesh_data ) tool.Model.clear_scene_openings() @@ -369,15 +376,42 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): booleans = [] for item in representation.Items: booleans.extend(self.get_booleans(item)) + + props = bpy.context.scene.BIMModelProperties + tool.Model.clear_scene_openings() + for boolean in booleans: + boolean_obj = None + if boolean.is_a() == "IfcHalfSpaceSolid": if boolean.BaseSurface.is_a("IfcPlane"): boolean_obj = self.create_half_space_solid() position = boolean.BaseSurface.Position position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist()) boolean_obj.matrix_world = obj.matrix_world @ position - boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean.id() - boolean_obj.data.BIMMeshProperties.obj = obj + else: + settings = ifcopenshell.geom.settings() + logger = logging.getLogger("ImportIFC") + ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) + shape = ifcopenshell.geom.create_shape(settings, boolean) + if shape: + ifc_importer = import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = tool.Ifc.get() + mesh = ifc_importer.create_mesh(boolean, shape) + else: + mesh = None + boolean_obj = object_data_add(bpy.context, mesh, operator=self) + boolean_obj.name = "BooleanMesh" + boolean_obj.matrix_world = obj.matrix_world + + if boolean_obj: + boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean.id() + boolean_obj.data.BIMMeshProperties.obj = obj + new = props.openings.add() + new.obj = boolean_obj + + if booleans: + DecorationsHandler.install(bpy.context) return {"FINISHED"} def get_booleans(self, item): @@ -388,7 +422,6 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): return results def create_half_space_solid(self): - props = bpy.context.scene.BIMModelProperties bm = bmesh.new() bmesh.ops.create_grid(bm, size=0.5) bm.verts.ensure_lookup_table() @@ -399,13 +432,6 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): bm.free() obj = object_data_add(bpy.context, mesh, operator=self) obj.name = "HalfSpaceSolid" - - tool.Model.clear_scene_openings() - - new = props.openings.add() - new.obj = obj - - DecorationsHandler.install(bpy.context) return obj diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 78e8bcc746..a62488c75c 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -971,8 +971,6 @@ class DumbWallJoiner: if clipping["operand_type"] == "IfcHalfSpaceSolid": clipping["matrix"] = new_matrix @ clipping["matrix"] - self.clippings.extend(tool.Model.get_manual_booleans(element)) - length = (self.body[1] - self.body[0]).length if self.axis_context: @@ -1001,6 +999,7 @@ class DumbWallJoiner: offset=layers["offset"], thickness=layers["thickness"], clippings=self.clippings, + booleans=tool.Model.get_manual_booleans(element), ) old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 4132d52d8f..a6766f4601 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -88,25 +88,16 @@ class Model(blenderbim.core.tool.Model): @classmethod def get_manual_booleans(cls, element): - results = [] body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not body: return [] - clippings = [] + booleans = [] items = list(body.Items) while items: item = items.pop() if item.is_a() == "IfcBooleanResult": - clippings.append(item.SecondOperand) + booleans.append(item) items.append(item.FirstOperand) elif item.is_a("IfcBooleanClippingResult"): items.append(item.FirstOperand) - for clipping in clippings: - if clipping.is_a("IfcHalfSpaceSolid") and clipping.BaseSurface.is_a("IfcPlane"): - placement = Matrix(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement).tolist()) - position = clipping.BaseSurface.Position - position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist()) - results.append( - {"type": "IfcBooleanResult", "operand_type": "IfcHalfSpaceSolid", "matrix": position} - ) - return results + return booleans diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py index 53fbd82640..43014ca5aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py @@ -25,8 +25,14 @@ class Usecase: self.settings = { "representation": None, "operator": "DIFFERENCE", + # IfcHalfSpaceSolid, Mesh + "type": "IfcHalfSpaceSolid", # The XY plane is the clipping boundary and +Z is removed. - "matrix": None, # A matrix to define a clipping half space solid. + "matrix": None, # A matrix to define a clipping Ifchalfspacesolid. + "blender_obj": None, # A Blender OBJ to define the voided OBJ for a "Mesh" type + "blender_void": None, # A Blender OBJ to define the void OBJ for a "Mesh" type + "should_force_faceted_brep": False, + "should_force_triangulation": False, } for key, value in settings.items(): self.settings[key] = value @@ -34,7 +40,11 @@ class Usecase: def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.settings["representation"].RepresentationType = "Clipping" - result = self.create_half_space_solid() + if self.settings["type"] == "IfcHalfSpaceSolid": + result = self.create_half_space_solid() + elif self.settings["type"] == "Mesh": + if self.settings["blender_obj"]: + result = self.create_blender_mesh() items = [] for item in self.settings["representation"].Items: items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result)) @@ -59,5 +69,63 @@ class Usecase: False, ) + def create_blender_mesh(self): + self.ifc_vertices = [] + if self.file.schema == "IFC2X3" or self.settings["should_force_faceted_brep"]: + return self.create_faceted_brep() + if self.settings["should_force_triangulation"]: + return self.create_triangulated_face_set() + return self.create_polygonal_face_set() + + def create_faceted_brep(self): + self.create_vertices() + faces = [] + for polygon in self.settings["blender_void"].data.polygons: + faces.append( + self.file.createIfcFace( + [ + self.file.createIfcFaceOuterBound( + self.file.createIfcPolyLoop([self.ifc_vertices[vertice] for vertice in polygon.vertices]), + True, + ) + ] + ) + ) + # TODO: May not actually be a closed shell, but who checks anyway? + return self.file.createIfcFacetedBrep(self.file.createIfcClosedShell(faces)) + + def create_triangulated_face_set(self): + faces = [] + for polygon in self.settings["blender_void"].data.polygons: + faces.append([v + 1 for v in polygon.vertices]) + + mat1 = self.settings["blender_void"].matrix_world + mat2 = self.settings["blender_obj"].matrix_world.inverted() + coordinates = self.file.createIfcCartesianPointList3D( + [self.convert_si_to_unit(mat2 @ mat1 @ v.co) for v in self.settings["blender_void"].data.vertices] + ) + return self.file.createIfcTriangulatedFaceSet(coordinates, None, None, faces) + + def create_polygonal_face_set(self): + faces = [] + for polygon in self.settings["blender_void"].data.polygons: + faces.append(self.file.createIfcIndexedPolygonalFace([v + 1 for v in polygon.vertices])) + mat1 = self.settings["blender_void"].matrix_world + mat2 = self.settings["blender_obj"].matrix_world.inverted() + coordinates = self.file.createIfcCartesianPointList3D( + [self.convert_si_to_unit(mat2 @ mat1 @ v.co) for v in self.settings["blender_void"].data.vertices] + ) + return self.file.createIfcPolygonalFaceSet(coordinates, None, faces) + + def create_vertices(self): + mat1 = self.settings["blender_void"].matrix_world + mat2 = self.settings["blender_obj"].matrix_world.inverted() + self.ifc_vertices.extend( + [ + self.file.createIfcCartesianPoint(self.convert_si_to_unit(mat2 @ mat1 @ v.co)) + for v in self.settings["blender_void"].data.vertices + ] + ) + def convert_si_to_unit(self, co): return co / self.settings["unit_scale"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py index 141910645a..c42ac95a86 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py @@ -31,6 +31,7 @@ class Usecase: # Planes are defined as a matrix. The XY plane is the clipping boundary and +Z is removed. # [{"type": "IfcBooleanClippingResult", "operand_type": "IfcHalfSpaceSolid", "matrix": [...]}, {...}] "clippings": [], # A list of planes that define clipping half space solids + "booleans": [], # Any existing IfcBooleanResults } for key, value in settings.items(): self.settings[key] = value @@ -40,7 +41,7 @@ class Usecase: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, - "Clipping" if self.settings["clippings"] else "SweptSolid", + "Clipping" if self.settings["clippings"] or self.settings["booleans"] else "SweptSolid", [self.create_item()], ) @@ -68,10 +69,19 @@ class Usecase: self.file.createIfcDirection((0.0, 0.0, 1.0)), self.convert_si_to_unit(self.settings["height"]), ) + if self.settings["booleans"]: + extrusion = self.apply_booleans(extrusion) if self.settings["clippings"]: - return self.apply_clippings(extrusion) + extrusion = self.apply_clippings(extrusion) return extrusion + def apply_booleans(self, first_operand): + while self.settings["booleans"]: + boolean = self.settings["booleans"].pop() + boolean.FirstOperand = first_operand + first_operand = boolean + return first_operand + def apply_clippings(self, first_operand): while self.settings["clippings"]: clipping = self.settings["clippings"].pop()