Basic support for mesh booleans

This commit is contained in:
Dion Moult
2022-10-13 17:04:53 +11:00
parent c354de7b83
commit 8be1a913ec
5 changed files with 123 additions and 29 deletions
@@ -20,11 +20,13 @@ import bpy
import gpu import gpu
import bgl import bgl
import bmesh import bmesh
import logging
import blenderbim.bim.handler import blenderbim.bim.handler
import ifcopenshell import ifcopenshell
import ifcopenshell.util.representation import ifcopenshell.util.representation
import blenderbim.tool as tool import blenderbim.tool as tool
import blenderbim.core.geometry import blenderbim.core.geometry
import blenderbim.bim.import_ifc as import_ifc
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from math import pi from math import pi
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
@@ -327,12 +329,17 @@ class AddBoolean(Operator, tool.Ifc.Operator):
return {"FINISHED"} return {"FINISHED"}
representation = tool.Ifc.get().by_id(obj1.data.BIMMeshProperties.ifc_definition_id) 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( ifcopenshell.api.run(
"geometry.add_boolean", "geometry.add_boolean",
tool.Ifc.get(), tool.Ifc.get(),
representation=representation, representation=representation,
operator="DIFFERENCE", operator="DIFFERENCE",
matrix=obj1.matrix_world.inverted() @ obj2.matrix_world, **mesh_data
) )
tool.Model.clear_scene_openings() tool.Model.clear_scene_openings()
@@ -369,15 +376,42 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
booleans = [] booleans = []
for item in representation.Items: for item in representation.Items:
booleans.extend(self.get_booleans(item)) booleans.extend(self.get_booleans(item))
props = bpy.context.scene.BIMModelProperties
tool.Model.clear_scene_openings()
for boolean in booleans: for boolean in booleans:
boolean_obj = None
if boolean.is_a() == "IfcHalfSpaceSolid": if boolean.is_a() == "IfcHalfSpaceSolid":
if boolean.BaseSurface.is_a("IfcPlane"): if boolean.BaseSurface.is_a("IfcPlane"):
boolean_obj = self.create_half_space_solid() boolean_obj = self.create_half_space_solid()
position = boolean.BaseSurface.Position position = boolean.BaseSurface.Position
position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist()) position = Matrix(ifcopenshell.util.placement.get_axis2placement(position).tolist())
boolean_obj.matrix_world = obj.matrix_world @ position boolean_obj.matrix_world = obj.matrix_world @ position
boolean_obj.data.BIMMeshProperties.ifc_boolean_id = boolean.id() else:
boolean_obj.data.BIMMeshProperties.obj = obj 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"} return {"FINISHED"}
def get_booleans(self, item): def get_booleans(self, item):
@@ -388,7 +422,6 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
return results return results
def create_half_space_solid(self): def create_half_space_solid(self):
props = bpy.context.scene.BIMModelProperties
bm = bmesh.new() bm = bmesh.new()
bmesh.ops.create_grid(bm, size=0.5) bmesh.ops.create_grid(bm, size=0.5)
bm.verts.ensure_lookup_table() bm.verts.ensure_lookup_table()
@@ -399,13 +432,6 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper):
bm.free() bm.free()
obj = object_data_add(bpy.context, mesh, operator=self) obj = object_data_add(bpy.context, mesh, operator=self)
obj.name = "HalfSpaceSolid" obj.name = "HalfSpaceSolid"
tool.Model.clear_scene_openings()
new = props.openings.add()
new.obj = obj
DecorationsHandler.install(bpy.context)
return obj return obj
@@ -971,8 +971,6 @@ class DumbWallJoiner:
if clipping["operand_type"] == "IfcHalfSpaceSolid": if clipping["operand_type"] == "IfcHalfSpaceSolid":
clipping["matrix"] = new_matrix @ clipping["matrix"] clipping["matrix"] = new_matrix @ clipping["matrix"]
self.clippings.extend(tool.Model.get_manual_booleans(element))
length = (self.body[1] - self.body[0]).length length = (self.body[1] - self.body[0]).length
if self.axis_context: if self.axis_context:
@@ -1001,6 +999,7 @@ class DumbWallJoiner:
offset=layers["offset"], offset=layers["offset"],
thickness=layers["thickness"], thickness=layers["thickness"],
clippings=self.clippings, clippings=self.clippings,
booleans=tool.Model.get_manual_booleans(element),
) )
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
+3 -12
View File
@@ -88,25 +88,16 @@ class Model(blenderbim.core.tool.Model):
@classmethod @classmethod
def get_manual_booleans(cls, element): def get_manual_booleans(cls, element):
results = []
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not body: if not body:
return [] return []
clippings = [] booleans = []
items = list(body.Items) items = list(body.Items)
while items: while items:
item = items.pop() item = items.pop()
if item.is_a() == "IfcBooleanResult": if item.is_a() == "IfcBooleanResult":
clippings.append(item.SecondOperand) booleans.append(item)
items.append(item.FirstOperand) items.append(item.FirstOperand)
elif item.is_a("IfcBooleanClippingResult"): elif item.is_a("IfcBooleanClippingResult"):
items.append(item.FirstOperand) items.append(item.FirstOperand)
for clipping in clippings: return booleans
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
@@ -25,8 +25,14 @@ class Usecase:
self.settings = { self.settings = {
"representation": None, "representation": None,
"operator": "DIFFERENCE", "operator": "DIFFERENCE",
# IfcHalfSpaceSolid, Mesh
"type": "IfcHalfSpaceSolid",
# The XY plane is the clipping boundary and +Z is removed. # 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(): for key, value in settings.items():
self.settings[key] = value self.settings[key] = value
@@ -34,7 +40,11 @@ class Usecase:
def execute(self): def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["representation"].RepresentationType = "Clipping" 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 = [] items = []
for item in self.settings["representation"].Items: for item in self.settings["representation"].Items:
items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result)) items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result))
@@ -59,5 +69,63 @@ class Usecase:
False, 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): def convert_si_to_unit(self, co):
return co / self.settings["unit_scale"] return co / self.settings["unit_scale"]
@@ -31,6 +31,7 @@ class Usecase:
# Planes are defined as a matrix. The XY plane is the clipping boundary and +Z is removed. # Planes are defined as a matrix. The XY plane is the clipping boundary and +Z is removed.
# [{"type": "IfcBooleanClippingResult", "operand_type": "IfcHalfSpaceSolid", "matrix": [...]}, {...}] # [{"type": "IfcBooleanClippingResult", "operand_type": "IfcHalfSpaceSolid", "matrix": [...]}, {...}]
"clippings": [], # A list of planes that define clipping half space solids "clippings": [], # A list of planes that define clipping half space solids
"booleans": [], # Any existing IfcBooleanResults
} }
for key, value in settings.items(): for key, value in settings.items():
self.settings[key] = value self.settings[key] = value
@@ -40,7 +41,7 @@ class Usecase:
return self.file.createIfcShapeRepresentation( return self.file.createIfcShapeRepresentation(
self.settings["context"], self.settings["context"],
self.settings["context"].ContextIdentifier, 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()], [self.create_item()],
) )
@@ -68,10 +69,19 @@ class Usecase:
self.file.createIfcDirection((0.0, 0.0, 1.0)), self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.convert_si_to_unit(self.settings["height"]), self.convert_si_to_unit(self.settings["height"]),
) )
if self.settings["booleans"]:
extrusion = self.apply_booleans(extrusion)
if self.settings["clippings"]: if self.settings["clippings"]:
return self.apply_clippings(extrusion) extrusion = self.apply_clippings(extrusion)
return 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): def apply_clippings(self, first_operand):
while self.settings["clippings"]: while self.settings["clippings"]:
clipping = self.settings["clippings"].pop() clipping = self.settings["clippings"].pop()