mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +00:00
See #5972. Add tests for adding boolean and check against recursive booleans and invalid boolean items.
This commit is contained in:
@@ -533,11 +533,11 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
first_obj = tool.Blender.get_active_object()
|
first_obj = tool.Blender.get_active_object()
|
||||||
if not tool.Geometry.is_representation_item(first_obj):
|
if not first_obj or not tool.Geometry.is_boolean_operand(first_obj):
|
||||||
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
|
self.report({"INFO"}, "At least two valid objects must be selected to add a boolean.")
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
second_objs = [
|
second_objs = [
|
||||||
o for o in tool.Blender.get_selected_objects() if o != first_obj and tool.Geometry.is_representation_item(o)
|
o for o in tool.Blender.get_selected_objects() if o != first_obj and tool.Geometry.is_boolean_operand(o)
|
||||||
]
|
]
|
||||||
if not second_objs:
|
if not second_objs:
|
||||||
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
|
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
|
||||||
@@ -546,38 +546,8 @@ class AddBoolean(Operator, tool.Ifc.Operator):
|
|||||||
props = context.scene.BIMBooleanProperties
|
props = context.scene.BIMBooleanProperties
|
||||||
|
|
||||||
first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id)
|
first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id)
|
||||||
|
second_items = [tool.Ifc.get().by_id(o.data.BIMMeshProperties.ifc_definition_id) for o in second_objs]
|
||||||
while True:
|
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
|
||||||
is_part_of_boolean = False
|
|
||||||
for inverse in tool.Ifc.get().get_inverse(first_item):
|
|
||||||
if inverse.is_a("IfcBooleanResult"):
|
|
||||||
is_part_of_boolean = True
|
|
||||||
first_item = inverse
|
|
||||||
if not is_part_of_boolean:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Don't replace style or aspect relationships.
|
|
||||||
to_replace = set(
|
|
||||||
[
|
|
||||||
i
|
|
||||||
for i in tool.Ifc.get().get_inverse(first_item)
|
|
||||||
if i.is_a("IfcShapeRepresentation") or i.is_a("IfcBooleanResult")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
first = first_item
|
|
||||||
|
|
||||||
booleans = set()
|
|
||||||
for second_obj in second_objs:
|
|
||||||
second = tool.Ifc.get().by_id(second_obj.data.BIMMeshProperties.ifc_definition_id)
|
|
||||||
for inverse in tool.Ifc.get().get_inverse(second):
|
|
||||||
if inverse.is_a("IfcShapeRepresentation"):
|
|
||||||
inverse.Items = list(set(inverse.Items) - {second})
|
|
||||||
first = tool.Ifc.get().create_entity("IfcBooleanResult", props.operator, first, second)
|
|
||||||
booleans.add(first)
|
|
||||||
|
|
||||||
for inverse in to_replace:
|
|
||||||
ifcopenshell.util.element.replace_attribute(inverse, first_item, first)
|
|
||||||
|
|
||||||
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
|
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
|
||||||
rep_element = tool.Ifc.get_entity(rep_obj)
|
rep_element = tool.Ifc.get_entity(rep_obj)
|
||||||
|
|||||||
@@ -978,6 +978,22 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
and tool.Ifc.get().by_id(ifc_id).is_a("IfcRepresentationItem")
|
and tool.Ifc.get().by_id(ifc_id).is_a("IfcRepresentationItem")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_boolean_operand(cls, obj: bpy.types.Object) -> bool:
|
||||||
|
return bool(
|
||||||
|
(data := obj.data)
|
||||||
|
and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES)
|
||||||
|
and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
|
||||||
|
and (item := tool.Ifc.get().by_id(ifc_id))
|
||||||
|
and (
|
||||||
|
item.is_a("IfcBooleanResult")
|
||||||
|
or item.is_a("IfcCsgPrimitive3D")
|
||||||
|
or item.is_a("IfcHalfSpaceSolid")
|
||||||
|
or item.is_a("IfcSolidModel")
|
||||||
|
or item.is_a("IfcTessellatedFaceSet")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_text_literal(cls, representation: ifcopenshell.entity_instance) -> bool:
|
def is_text_literal(cls, representation: ifcopenshell.entity_instance) -> bool:
|
||||||
items = ifcopenshell.util.representation.resolve_items(representation)
|
items = ifcopenshell.util.representation.resolve_items(representation)
|
||||||
|
|||||||
@@ -17,147 +17,60 @@
|
|||||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import ifcopenshell.util.unit
|
import ifcopenshell.util.element
|
||||||
import numpy as np
|
|
||||||
import numpy.typing as npt
|
|
||||||
from typing import Optional, TYPE_CHECKING, Literal
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
import bpy.types
|
|
||||||
|
|
||||||
|
|
||||||
NPArrayOfFloats = npt.NDArray[np.float64]
|
|
||||||
|
|
||||||
|
|
||||||
def add_boolean(
|
def add_boolean(
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
representation: ifcopenshell.entity_instance,
|
first_item: ifcopenshell.entity_instance,
|
||||||
# A matrix to define a clipping Ifchalfspacesolid.
|
second_items: list[ifcopenshell.entity_instance],
|
||||||
# The XY plane is the clipping boundary and +Z is removed.
|
|
||||||
operator: str = "DIFFERENCE",
|
operator: str = "DIFFERENCE",
|
||||||
# IfcHalfSpaceSolid, Mesh
|
) -> set[ifcopenshell.entity_instance]:
|
||||||
type: Literal["IfcHalfSpaceSolid", "Mesh"] = "IfcHalfSpaceSolid",
|
original_first_item = first_item
|
||||||
matrix: Optional[NPArrayOfFloats] = None,
|
if first_item in second_items:
|
||||||
# A Blender OBJ to define the voided OBJ for a "Mesh" type
|
second_items.remove(first_item)
|
||||||
blender_obj: Optional[bpy.types.Object] = None,
|
|
||||||
# A Blender OBJ to define the void OBJ for a "Mesh" type
|
|
||||||
blender_void: Optional[bpy.types.Object] = None,
|
|
||||||
should_force_faceted_brep: bool = False,
|
|
||||||
should_force_triangulation: bool = False,
|
|
||||||
) -> list[ifcopenshell.entity_instance]:
|
|
||||||
"""For `type` values:
|
|
||||||
- "IfcHalfSpaceSolid" - `matrix` is not optional.
|
|
||||||
- "Mesh" - `blender_obj` and `blender_void` are not optional
|
|
||||||
"""
|
|
||||||
usecase = Usecase()
|
|
||||||
usecase.file = file
|
|
||||||
usecase.settings = {
|
|
||||||
"representation": representation,
|
|
||||||
"operator": operator,
|
|
||||||
"type": type,
|
|
||||||
"matrix": matrix,
|
|
||||||
"blender_obj": blender_obj,
|
|
||||||
"blender_void": blender_void,
|
|
||||||
"should_force_faceted_brep": should_force_faceted_brep,
|
|
||||||
"should_force_triangulation": should_force_triangulation,
|
|
||||||
}
|
|
||||||
return usecase.execute()
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
is_part_of_boolean = False
|
||||||
|
for inverse in file.get_inverse(first_item):
|
||||||
|
if inverse.is_a("IfcBooleanResult"):
|
||||||
|
is_part_of_boolean = True
|
||||||
|
first_item = inverse
|
||||||
|
if inverse.FirstOperand == original_first_item and inverse.SecondOperand in second_items:
|
||||||
|
second_items.remove(inverse.SecondOperand)
|
||||||
|
elif inverse.SecondOperand == original_first_item and inverse.FirstOperand in second_items:
|
||||||
|
second_items.remove(inverse.FirstOperand)
|
||||||
|
break
|
||||||
|
if not is_part_of_boolean:
|
||||||
|
break
|
||||||
|
|
||||||
class Usecase:
|
if not second_items:
|
||||||
def execute(self):
|
return
|
||||||
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
|
||||||
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:
|
|
||||||
if (
|
|
||||||
self.settings["operator"] == "DIFFERENCE"
|
|
||||||
and result.is_a("IfcHalfSpaceSolid")
|
|
||||||
and (
|
|
||||||
item.is_a("IfcSweptAreaSolid")
|
|
||||||
or item.is_a("IfcSweptDiskSolid")
|
|
||||||
or item.is_a("IfcBooleanClippingResult")
|
|
||||||
)
|
|
||||||
):
|
|
||||||
items.append(self.file.createIfcBooleanClippingResult(self.settings["operator"], item, result))
|
|
||||||
representation_type = "Clipping"
|
|
||||||
else:
|
|
||||||
items.append(self.file.createIfcBooleanResult(self.settings["operator"], item, result))
|
|
||||||
representation_type = "CSG"
|
|
||||||
self.settings["representation"].RepresentationType = representation_type
|
|
||||||
self.settings["representation"].Items = items
|
|
||||||
return items
|
|
||||||
|
|
||||||
def create_half_space_solid(self):
|
# Don't replace style or aspect relationships.
|
||||||
clipping = np.array(self.settings["matrix"])[:3]
|
to_replace = set(
|
||||||
local_z = self.file.createIfcDirection(clipping[:, 2].tolist())
|
[
|
||||||
local_x = self.file.createIfcDirection(clipping[:, 0].tolist())
|
i
|
||||||
point = self.file.createIfcCartesianPoint(self.convert_si_to_unit(clipping[:, 3]).tolist())
|
for i in file.get_inverse(first_item)
|
||||||
placement = self.file.createIfcAxis2Placement3D(point, local_z, local_x)
|
if i.is_a("IfcShapeRepresentation") or i.is_a("IfcBooleanResult")
|
||||||
plane = self.file.createIfcPlane(placement)
|
]
|
||||||
return self.file.createIfcHalfSpaceSolid(plane, AgreementFlag=False)
|
)
|
||||||
|
|
||||||
def create_blender_mesh(self):
|
first = first_item
|
||||||
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):
|
booleans = set()
|
||||||
self.create_vertices()
|
for second_item in second_items:
|
||||||
faces = []
|
for inverse in file.get_inverse(second_item):
|
||||||
for polygon in self.settings["blender_void"].data.polygons:
|
if inverse.is_a("IfcShapeRepresentation"):
|
||||||
faces.append(
|
inverse.Items = list(set(inverse.Items) - {second_item})
|
||||||
self.file.createIfcFace(
|
if first.is_a("IfcTesselatedFaceSet"):
|
||||||
[
|
first.Closed = True # For now, trust the user to do the right thing.
|
||||||
self.file.createIfcFaceOuterBound(
|
if second_item.is_a("IfcTesselatedFaceSet"):
|
||||||
self.file.createIfcPolyLoop([self.ifc_vertices[vertice] for vertice in polygon.vertices]),
|
second_item.Closed = True # For now, trust the user to do the right thing.
|
||||||
True,
|
first = file.create_entity("IfcBooleanResult", operator, first, second_item)
|
||||||
)
|
booleans.add(first)
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# 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):
|
for inverse in to_replace:
|
||||||
faces = []
|
ifcopenshell.util.element.replace_attribute(inverse, first_item, first)
|
||||||
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
|
return booleans
|
||||||
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"]
|
|
||||||
|
|||||||
@@ -20,38 +20,127 @@ import test.bootstrap
|
|||||||
import ifcopenshell.api.root
|
import ifcopenshell.api.root
|
||||||
import ifcopenshell.api.context
|
import ifcopenshell.api.context
|
||||||
import ifcopenshell.api.geometry
|
import ifcopenshell.api.geometry
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
class TestAddBoolean(test.bootstrap.IFC4):
|
class TestAddBoolean(test.bootstrap.IFC4):
|
||||||
def test_returning_ifc_boolean_clipping_result(self):
|
def test_adding_a_boolean_from_two_top_level_items(self):
|
||||||
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||||
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
|
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
|
||||||
body = ifcopenshell.api.context.add_context(
|
body = ifcopenshell.api.context.add_context(
|
||||||
self.file,
|
self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||||
context_type="Model",
|
|
||||||
context_identifier="Body",
|
|
||||||
target_view="MODEL_VIEW",
|
|
||||||
parent=model,
|
|
||||||
)
|
)
|
||||||
wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
|
||||||
|
first = builder.sphere()
|
||||||
|
second = builder.block()
|
||||||
|
rep = builder.get_representation(body, [first, second])
|
||||||
|
|
||||||
profile = self.file.create_entity(
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second])
|
||||||
"IfcIShapeProfileDef",
|
assert len(booleans) == 1
|
||||||
ProfileName="HEA100",
|
boolean = list(booleans)[0]
|
||||||
ProfileType="AREA",
|
assert boolean.is_a("IfcBooleanResult")
|
||||||
OverallWidth=100,
|
assert boolean.FirstOperand == first
|
||||||
OverallDepth=96,
|
assert boolean.SecondOperand == second
|
||||||
WebThickness=5,
|
assert boolean.Operator == "DIFFERENCE"
|
||||||
FlangeThickness=8,
|
assert set(rep.Items) == {boolean}
|
||||||
FilletRadius=12,
|
|
||||||
|
def test_adding_multiple_booleans_from_three_top_level_items(self):
|
||||||
|
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||||
|
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
|
||||||
|
body = ifcopenshell.api.context.add_context(
|
||||||
|
self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||||
)
|
)
|
||||||
rep = ifcopenshell.api.geometry.add_profile_representation(self.file, context=body, profile=profile, depth=5)
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
|
||||||
ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep)
|
first = builder.sphere()
|
||||||
|
second1 = builder.block()
|
||||||
|
second2 = builder.block()
|
||||||
|
rep = builder.get_representation(body, [first, second1, second2])
|
||||||
|
|
||||||
ifcopenshell.api.geometry.add_boolean(self.file, representation=rep, matrix=np.eye(4))
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1, second2])
|
||||||
assert rep.Items[0].is_a() == "IfcBooleanClippingResult"
|
assert len(booleans) == 2
|
||||||
assert rep.RepresentationType == "Clipping"
|
assert len(rep.Items) == 1
|
||||||
|
assert rep.Items[0].FirstOperand.is_a("IfcBooleanResult")
|
||||||
|
assert rep.Items[0].FirstOperand.is_a("IfcBooleanResult")
|
||||||
|
assert rep.Items[0].SecondOperand == second2
|
||||||
|
assert rep.Items[0].Operator == "DIFFERENCE"
|
||||||
|
assert rep.Items[0].FirstOperand.FirstOperand == first
|
||||||
|
assert rep.Items[0].FirstOperand.SecondOperand == second1
|
||||||
|
assert rep.Items[0].FirstOperand.Operator == "DIFFERENCE"
|
||||||
|
|
||||||
|
def test_adding_a_boolean_to_an_existing_operand_from_a_top_level_item(self):
|
||||||
|
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||||
|
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
|
||||||
|
body = ifcopenshell.api.context.add_context(
|
||||||
|
self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||||
|
)
|
||||||
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
|
||||||
|
first = builder.sphere()
|
||||||
|
second1 = builder.block()
|
||||||
|
second2 = builder.block()
|
||||||
|
rep = builder.get_representation(body, [first, second1])
|
||||||
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1])
|
||||||
|
rep.Items = list(rep.Items) + [second2]
|
||||||
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second2])
|
||||||
|
assert len(booleans) == 1
|
||||||
|
assert len(rep.Items) == 1
|
||||||
|
assert rep.Items[0].FirstOperand.is_a("IfcBooleanResult")
|
||||||
|
assert rep.Items[0].SecondOperand == second2
|
||||||
|
assert rep.Items[0].FirstOperand.FirstOperand == first
|
||||||
|
assert rep.Items[0].FirstOperand.SecondOperand == second1
|
||||||
|
|
||||||
|
def test_adding_a_boolean_to_an_existing_operand_from_another_operand(self):
|
||||||
|
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||||
|
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
|
||||||
|
body = ifcopenshell.api.context.add_context(
|
||||||
|
self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||||
|
)
|
||||||
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
|
||||||
|
first1 = builder.sphere()
|
||||||
|
second1 = builder.block()
|
||||||
|
first2 = builder.sphere()
|
||||||
|
second2 = builder.block()
|
||||||
|
rep = builder.get_representation(body, [first1, first2, second1, second2])
|
||||||
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first1, [second1])
|
||||||
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first2, [second2])
|
||||||
|
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first1, [second2])
|
||||||
|
|
||||||
|
assert len(booleans) == 1
|
||||||
|
assert len(rep.Items) == 2
|
||||||
|
|
||||||
|
assert len(self.file.get_inverse(first1)) == 1
|
||||||
|
result = list(self.file.get_inverse(first1))[0]
|
||||||
|
assert result.FirstOperand == first1
|
||||||
|
assert result.SecondOperand == second1
|
||||||
|
result2 = list(self.file.get_inverse(result))[0]
|
||||||
|
assert result2.FirstOperand == result
|
||||||
|
# Second2 is now used twice. Reusing is OK (albeit confusing), so long as things don't get recursive.
|
||||||
|
assert result2.SecondOperand == second2
|
||||||
|
|
||||||
|
assert len(self.file.get_inverse(first2)) == 1
|
||||||
|
result3 = list(self.file.get_inverse(first2))[0]
|
||||||
|
assert result3.FirstOperand == first2
|
||||||
|
assert result3.SecondOperand == second2
|
||||||
|
|
||||||
|
def test_preventing_recursive_booleans(self):
|
||||||
|
ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject")
|
||||||
|
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
|
||||||
|
body = ifcopenshell.api.context.add_context(
|
||||||
|
self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||||
|
)
|
||||||
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
|
||||||
|
first = builder.sphere()
|
||||||
|
second = builder.block()
|
||||||
|
rep = builder.get_representation(body, [first, second])
|
||||||
|
ifcopenshell.api.geometry.add_boolean(self.file, first, [second])
|
||||||
|
ifcopenshell.api.geometry.add_boolean(self.file, first, [second])
|
||||||
|
assert len(rep.Items) == 1
|
||||||
|
assert rep.Items[0].FirstOperand == first
|
||||||
|
assert rep.Items[0].SecondOperand == second
|
||||||
|
ifcopenshell.api.geometry.add_boolean(self.file, second, [second])
|
||||||
|
ifcopenshell.api.geometry.add_boolean(self.file, second, [first])
|
||||||
|
assert len(rep.Items) == 1
|
||||||
|
assert rep.Items[0].FirstOperand == first
|
||||||
|
assert rep.Items[0].SecondOperand == second
|
||||||
|
assert len(self.file.by_type("IfcBooleanResult")) == 1
|
||||||
|
|
||||||
|
|
||||||
class TestAddBooleanIFC2X3(test.bootstrap.IFC2X3, TestAddBoolean):
|
class TestAddBooleanIFC2X3(test.bootstrap.IFC2X3, TestAddBoolean):
|
||||||
|
|||||||
Reference in New Issue
Block a user