mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 15:08:51 +00:00
Basic support for mesh booleans
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user