Merge branch 'v0.8.0' of github.com:IfcOpenShell/IfcOpenShell into v0.8.0

This commit is contained in:
Bruno Postle
2025-01-18 19:18:15 +00:00
22 changed files with 517 additions and 77 deletions
+6 -2
View File
@@ -33,6 +33,7 @@ from bonsai.bim.module.model.workspace import LIST_OF_TOOLS, TOOLS_TO_CLASSES_MA
from bonsai.bim.module.aggregate.decorator import AggregateDecorator
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator
from bonsai.bim.module.nest.decorator import NestDecorator
from mathutils import Vector
from math import cos, degrees
from typing import Union, Callable
@@ -323,11 +324,14 @@ def load_post(scene):
# Bonsai overlays
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = bpy.context.scene.BIMModelProperties
if aggregate_props.aggregate_decorator:
AggregateDecorator.install(bpy.context)
if georeference_props.should_visualise:
GeoreferenceDecorator.install(bpy.context)
if aggregate_props.aggregate_decorator:
AggregateDecorator.install(bpy.context)
if nest_props.nest_decorator:
NestDecorator.install(bpy.context)
if model_props.show_wall_axis:
WallAxisDecorator.install(bpy.context)
if model_props.show_slab_direction:
@@ -167,6 +167,8 @@ class AggregateDecorator:
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
@@ -3037,6 +3037,23 @@ class OverrideMove(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
# Get filling objects
selection = []
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
selection.append(obj)
for rel in getattr(element, "HasOpenings", []) or []:
opening = rel.RelatedOpeningElement
for rel2 in getattr(opening, "HasFillings", []) or []:
filling = rel2.RelatedBuildingElement
selection.append(tool.Ifc.get_object(filling))
for obj in selection:
obj.select_set(True)
self.new_active_obj = obj
# Get aggregates
props = context.scene.BIMAggregateProperties
not_editing_objs = [o.obj for o in props.not_editing_objects]
@@ -3046,6 +3063,8 @@ class OverrideMove(bpy.types.Operator):
if obj in not_editing_objs:
obj.select_set(False)
continue
if obj == props.editing_aggregate:
continue
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
continue
@@ -3079,6 +3098,8 @@ class OverrideMove(bpy.types.Operator):
if obj in not_editing_objs:
obj.select_set(False)
continue
if obj == props.editing_nest:
continue
element = tool.Ifc.get_entity(obj)
if not element or not element.is_a("IfcElement"):
continue
@@ -88,6 +88,7 @@ classes = (
opening.HideOpenings,
opening.PurgeUnusedOpenings,
opening.RecalculateFill,
opening.RemoveBoolean,
opening.RemoveBooleans,
opening.ShowBooleans,
opening.ShowOpenings,
@@ -1144,6 +1144,27 @@ class PurgeUnusedOpenings(Operator, tool.Ifc.Operator):
return {"FINISHED"}
class RemoveBoolean(Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_boolean"
bl_label = "Remove Boolean"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Removes the actively selected boolean"
@classmethod
def poll(cls, context):
props = context.scene.BIMBooleanProperties
return props.active_boolean
def _execute(self, context):
props = context.scene.BIMBooleanProperties
ifcopenshell.api.geometry.remove_boolean(
tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)
)
bpy.ops.bim.enable_editing_booleans()
rep_obj = bpy.context.scene.BIMGeometryProperties.representation_obj
tool.Geometry.reload_representation(rep_obj)
# TODO: merge with ProfileDecorator?
class DecorationsHandler:
installed = None
@@ -590,6 +590,11 @@ class EditObjectUI:
row = cls.layout.row(align=True)
op = row.operator("bim.disable_aggregate_mode", text="", icon="X")
op = row.operator("bim.toggle_aggregate_mode_local_view", text="", icon="ZOOM_SELECTED")
if context.scene.BIMNestProperties.in_nest_mode:
layout.label(text=f"Nest Mode", icon="EMPTY_AXIS")
row = cls.layout.row(align=True)
op = row.operator("bim.disable_nest_mode", text="", icon="X")
op = row.operator("bim.toggle_nest_mode_local_view", text="", icon="ZOOM_SELECTED")
text = format_ifc_camel_case(AuthoringData.data["active_class"])
layout.label(text=f"{text} Edit Tools:", icon="RESTRICT_SELECT_OFF")
@@ -26,6 +26,8 @@ classes = (
operator.BIM_OT_select_components,
operator.BIM_OT_select_nest,
operator.BIM_OT_nest_unassign_object,
operator.BIM_OT_disable_nest_mode,
operator.BIM_OT_toggle_nest_mode_local_view,
prop.BIMObjectNestProperties,
prop.Objects,
prop.BIMNestProperties,
+32 -26
View File
@@ -86,7 +86,7 @@ class NestDecorator:
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
@@ -153,8 +153,8 @@ class NestDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_aggregate(self, context):
if context.scene.BIMAggregateProperties.in_aggregate_mode:
def draw_nest(self, context):
if context.scene.BIMNestProperties.in_nest_mode:
return
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
@@ -165,11 +165,13 @@ class NestDecorator:
theme = context.preferences.themes.items()[0][1]
selected_object_color = (*theme.view_3d.object_active, 1)
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind()
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
aggregates = []
nests = []
if not (selected_objects := context.selected_objects):
return
for obj in selected_objects:
@@ -177,35 +179,39 @@ class NestDecorator:
if not element or not element.is_a("IfcElement"):
return
parts = ifcopenshell.util.element.get_parts(element)
if parts:
aggregates.append(obj)
components = ifcopenshell.util.element.get_components(element)
if components:
nests.append(obj)
continue
aggregate = ifcopenshell.util.element.get_aggregate(element)
if aggregate:
aggregates.append(tool.Ifc.get_object(aggregate))
nest = ifcopenshell.util.element.get_nest(element)
if nest:
nests.append(tool.Ifc.get_object(nest))
aggregates = set(aggregates)
for aggregate in aggregates:
nests = set(nests)
for nest in nests:
self.line_shader.uniform_float("lineWidth", 1.0)
color = decorator_color_unselected
if aggregate in selected_objects:
if nest in selected_objects:
color = selected_object_color
size = aggregate.empty_display_size
location = aggregate.location
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
if context.scene.BIMAggregateProperties.in_aggregate_mode:
return
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
parts_objs = [tool.Ifc.get_object(p) for p in parts]
size = nest.empty_display_size
location = nest.location
if nest.type == "EMPTY":
line_x = (location - Vector((size, 0.0, 0.0)), location + Vector((size, 0.0, 0.0)))
self.draw_batch("LINES", line_x, color, [(0, 1)])
line_y = (location - Vector((0.0, size, 0.0)), location + Vector((0.0, size, 0.0)))
self.draw_batch("LINES", line_y, color, [(0, 1)])
line_z = (location - Vector((0.0, 0.0, size)), location + Vector((0.0, 0.0, size)))
self.draw_batch("LINES", line_z, color, [(0, 1)])
else:
self.draw_batch("POINTS", [location], color)
# if context.scene.BIMNestProperties.in_aggregate_mode:
# return
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(nest))
components_objs = [tool.Ifc.get_object(p) for p in components]
components_objs.append(nest)
indices, edges = create_bounding_box(parts_objs)
indices, edges = create_bounding_box(components_objs)
self.line_shader.uniform_float("lineWidth", 0.5)
self.draw_batch("LINES", indices, color, edges)
line = (Vector(indices[0]), location)
@@ -140,3 +140,33 @@ class BIM_OT_select_nest(bpy.types.Operator):
nest_obj.select_set(True)
bpy.context.view_layer.objects.active = nest_obj
return {"FINISHED"}
class BIM_OT_disable_nest_mode(bpy.types.Operator):
bl_idname = "bim.disable_nest_mode"
bl_label = "Disable Nest Mode"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.object.select_all(action="DESELECT")
core.disable_nest_mode(tool.Nest)
return {"FINISHED"}
class BIM_OT_toggle_nest_mode_local_view(bpy.types.Operator):
bl_idname = "bim.toggle_nest_mode_local_view"
bl_label = "Toggle Nest Mode Local View"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMNestProperties
objs = [o.obj for o in props.editing_objects]
if props.in_nest_mode:
if context.space_data.local_view:
bpy.ops.view3d.localview()
else:
for obj in objs:
obj.select_set(True)
bpy.ops.view3d.localview()
return {"FINISHED"}
@@ -45,3 +45,8 @@ class BIMBooleanProperties(PropertyGroup):
name="Operator",
default="DIFFERENCE",
)
@property
def active_boolean(self):
if self.booleans and self.active_boolean_index < len(self.booleans):
return self.booleans[self.active_boolean_index]
+4
View File
@@ -176,6 +176,10 @@ class BIM_PT_booleans(Panel):
row.prop(props, "operator", text="")
row.operator("bim.add_boolean", text="", icon="ADD")
row = layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.remove_boolean", text="", icon="X")
self.layout.template_list("BIM_UL_booleans", "", props, "booleans", props, "active_boolean_index")
+8 -5
View File
@@ -1209,8 +1209,9 @@ class BIM_PT_decorators_overlay(Panel):
view = context.space_data
overlay = view.overlay
geo_props = bpy.context.scene.BIMGeoreferenceProperties
agg_props = bpy.context.scene.BIMAggregateProperties
georeference_props = bpy.context.scene.BIMGeoreferenceProperties
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = bpy.context.scene.BIMModelProperties
display_all = overlay.show_overlays
@@ -1218,10 +1219,12 @@ class BIM_PT_decorators_overlay(Panel):
col.active = display_all
row = col.row(align=True)
row.prop(geo_props, "should_visualise", text="Georeference")
row.prop(geo_props, "visualization_scale", text="Size", slider=True)
row.prop(georeference_props, "should_visualise", text="Georeference")
row.prop(georeference_props, "visualization_scale", text="Size", slider=True)
row = col.row(align=True)
row.prop(agg_props, "aggregate_decorator", text="Aggregate")
row.prop(aggregate_props, "aggregate_decorator", text="Aggregate")
row = col.row(align=True)
row.prop(nest_props, "nest_decorator", text="Nest")
row = col.row(align=True)
row.prop(model_props, "show_wall_axis", text="Wall Axis")
row = col.row(align=True)
+1
View File
@@ -145,3 +145,4 @@ class Aggregate(bonsai.core.tool.Aggregate):
props.in_aggregate_mode = False
props.not_editing_objects.clear()
props.editing_objects.clear()
props.editing_aggregate = None
+2
View File
@@ -1742,6 +1742,8 @@ class Geometry(bonsai.core.tool.Geometry):
cls.unlock_object(props.representation_obj)
tool.Blender.set_active_object(props.representation_obj)
cls.sync_item_positions()
representation = cls.get_active_representation(props.representation_obj)
ifcopenshell.api.geometry.validate_type(tool.Ifc.get(), representation)
props.is_changing_mode = True
if props.mode != "OBJECT":
props.mode = "OBJECT"
+1
View File
@@ -120,3 +120,4 @@ class Nest(bonsai.core.tool.Nest):
props.in_nest_mode = False
props.not_editing_objects.clear()
props.editing_objects.clear()
props.editing_nest = None
@@ -62,6 +62,7 @@ from .map_representation import map_representation
from .remove_boolean import remove_boolean
from .remove_representation import remove_representation
from .unassign_representation import unassign_representation
from .validate_type import validate_type
wrap_usecases(__path__, __name__)
@@ -88,4 +89,5 @@ __all__ = [
"remove_boolean",
"remove_representation",
"unassign_representation",
"validate_type",
]
@@ -18,17 +18,60 @@
from __future__ import annotations
import ifcopenshell.util.element
from typing import Literal
def add_boolean(
file: ifcopenshell.file,
first_item: ifcopenshell.entity_instance,
second_items: list[ifcopenshell.entity_instance],
operator: str = "DIFFERENCE",
) -> set[ifcopenshell.entity_instance]:
operator: Literal["DIFFERENCE", "INTERSECTION", "UNION"] = "DIFFERENCE",
) -> list[ifcopenshell.entity_instance]:
"""Adds a boolean operation to two or more representation items
If an IfcBooleanOperand is part of the top level items in an
IfcShapeRepresentation, it will be removed from that level whilst being
added to the IfcBooleanResult. This is because it is generally intuitive
that an item is either participating in a boolean operation, or being an
item in its own right, but not both.
However, if an IfcBooleanOperand is part of another boolean operation
already, it will not be removed from the existing operation. A new
operation will be created, and therefore it will participate in two
operations.
This function protects against recursive booleans.
After a boolean operation is made, since the items of
IfcShapeRepresentation may be modified, it is not guaranteed that the
RepresentationType is still valid. After performing all your booleans, it
is recommended to run :func:`ifcopenshell.api.geometry.validate_csg` to
ensure correctness.
:param first_item: The IfcBooleanOperand that the operation is performed upon
:param second_items: The IfcBooleanOperands that the operation will be
performed with, in the order given of the list.
:param operator: The type of boolean operation to perform
:return: A list of newly created IfcBooleanResult in the order of boolean
operations (based on the order of second items). If nothing was
created, the list will be empty.
"""
def is_operand(item):
return (
item.is_a("IfcBooleanResult")
or item.is_a("IfcCsgPrimitive3D")
or item.is_a("IfcHalfSpaceSolid")
or item.is_a("IfcSolidModel")
or item.is_a("IfcTessellatedFaceSet")
)
if not is_operand(first_item):
return []
original_first_item = first_item
if first_item in second_items:
second_items.remove(first_item)
second_items = [i for i in second_items if i != first_item and is_operand(i)]
while True:
is_part_of_boolean = False
@@ -45,20 +88,16 @@ def add_boolean(
break
if not second_items:
return
return []
# Don't replace style or aspect relationships.
to_replace = set(
[
i
for i in file.get_inverse(first_item)
if i.is_a("IfcShapeRepresentation") or i.is_a("IfcBooleanResult")
]
[i for i in file.get_inverse(first_item) if i.is_a("IfcShapeRepresentation") or i.is_a("IfcBooleanResult")]
)
first = first_item
booleans = set()
booleans = []
for second_item in second_items:
for inverse in file.get_inverse(second_item):
if inverse.is_a("IfcShapeRepresentation"):
@@ -67,8 +106,19 @@ def add_boolean(
first.Closed = True # For now, trust the user to do the right thing.
if second_item.is_a("IfcTesselatedFaceSet"):
second_item.Closed = True # For now, trust the user to do the right thing.
first = file.create_entity("IfcBooleanResult", operator, first, second_item)
booleans.add(first)
if (
operator == "DIFFERENCE"
and second_item.is_a("IfcHalfSpaceSolid")
and (
first.is_a("IfcSweptAreaSolid")
or first.is_a("IfcSweptDiskSolid")
or first.is_a("IfcBooleanClippingResult")
)
):
first = file.create_entity("IfcBooleanClippingResult", operator, first, second_item)
else:
first = file.create_entity("IfcBooleanResult", operator, first, second_item)
booleans.append(first)
for inverse in to_replace:
ifcopenshell.util.element.replace_attribute(inverse, first_item, first)
@@ -20,40 +20,42 @@ import ifcopenshell.util.element
def remove_boolean(file: ifcopenshell.file, item: ifcopenshell.entity_instance) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"item": item}
return usecase.execute()
"""Removes a boolean operation without deleting the operands
The first operand will replace the boolean result itself, and the second
operand will be reset as a top level representation item.
class Usecase:
def execute(self):
item = None
for inverse in self.file.get_inverse(self.settings["item"]):
This may affect the Items of IfcShapeRepresentation, so it is recommended
to run :func:`ifcopenshell.api.geometry.validate_type` after all boolean
modifications are complete.
:param item: This may either be an IfcBooleanResult or an
IfcRepresentationItem that is participating in one or more boolean
results (in which case all are removed).
"""
if not item.is_a("IfcBooleanResult"):
for inverse in file.get_inverse(item):
if inverse.is_a("IfcBooleanResult"):
item = inverse
break
remove_boolean(file, inverse)
return
representation = self.get_representation(item)
representations = []
queue = list(file.get_inverse(item))
while queue:
inverse = queue.pop()
if inverse.is_a("IfcShapeRepresentation"):
representations.append(inverse)
elif inverse.is_a("IfcBooleanResult"):
queue.extend(file.get_inverse(inverse))
elif inverse.is_a("IfcCsgSolid"):
queue.extend(file.get_inverse(inverse))
first_operand = item.FirstOperand
second_operand = item.SecondOperand
for inverse in self.file.get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, first_operand)
self.file.remove(item)
ifcopenshell.util.element.remove_deep2(self.file, second_operand)
first = item.FirstOperand
second = item.SecondOperand
for inverse in file.get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, first)
item_classes = {i.is_a() for i in representation.Items}
if "IfcBooleanResult" in item_classes:
representation.RepresentationType = "CSG"
elif "IfcBooleanClippingResult" in item_classes:
representation.RepresentationType = "Clipping"
else:
representation.RepresentationType = "SweptSolid"
for representation in set(representations):
representation.Items = list(representation.Items) + [second]
def get_representation(self, item):
for inverse in self.file.get_inverse(item):
if inverse.is_a("IfcShapeRepresentation"):
return inverse
elif inverse.is_a("IfcRepresentationItem"):
return self.get_representation(inverse)
file.remove(item)
@@ -0,0 +1,87 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api.geometry
import ifcopenshell.util.representation
def validate_type(
file: ifcopenshell.file,
representation: ifcopenshell.entity_instance,
preferred_item: ifcopenshell.entity_instance | None = None,
) -> bool:
"""Validates the RepresentationType of an IfcShapeRepresentation
A shape representation has to identify its geometry using the
RepresentationType attribute. For example, if it holds tessellated
geometry, it should store "Tessellation" as its RepresentationType.
This function checks whether or not the RepresentationType is valid. This
is a wrapper around :func:`ifcopenshell.util.representation.guess_type`. It
will then set RepresentationType to the most appropriate value, or return
False otherwise. In addition, it also attempts to reconcile otherwise
invalid CSG geometry by unioning all remaining top level items to existing
boolean results.
:param representation: The IfcShapeRepresentation with Items
:param preferred_item: If the type is expected to be a CSG, this will be
the preferred item to union all remaining items to. If no preferred
item is provided, the first boolean result will be chosen.
:return: True if the representation type was set and it is a valid
combination, or False otherwise.
"""
def is_operand(item):
return (
item.is_a("IfcBooleanResult")
or item.is_a("IfcCsgPrimitive3D")
or item.is_a("IfcHalfSpaceSolid")
or item.is_a("IfcSolidModel")
or item.is_a("IfcTessellatedFaceSet")
)
has_boolean = False
remaining_items = []
for item in representation.Items:
if item.is_a("IfcBooleanResult"):
has_boolean = True
if item != preferred_item and is_operand(item):
remaining_items.append(item)
if not has_boolean:
result = ifcopenshell.util.representation.guess_type(representation.Items)
if result:
representation.RepresentationType = result
return True
return False
if not preferred_item:
# Prioritise an existing boolean result
for i in remaining_items:
if i.is_a("IfcBooleanResult"):
preferred_item = i
break
if not preferred_item and remaining_items:
preferred_item = remaining_items[0]
if remaining_items:
ifcopenshell.api.geometry.add_boolean(file, preferred_item, remaining_items, "UNION")
representation.RepresentationType = ifcopenshell.util.representation.guess_type(representation.Items)
if representation.RepresentationType == "CSG":
return True
return False
@@ -59,7 +59,6 @@ class TestAddBoolean(test.bootstrap.IFC4):
assert len(booleans) == 2
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
@@ -0,0 +1,87 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api.root
import ifcopenshell.api.context
import ifcopenshell.api.geometry
class TestRemoveBoolean(test.bootstrap.IFC4):
def test_removing_a_single_top_level_boolean(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])
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second])
ifcopenshell.api.geometry.remove_boolean(self.file, booleans[0])
assert set(rep.Items) == {first, second}
assert not self.file.by_type("IfcBooleanResult")
def test_removing_a_top_level_nested_boolean(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])
ifcopenshell.api.geometry.add_boolean(self.file, first, [second1, second2])
ifcopenshell.api.geometry.remove_boolean(self.file, second2)
assert len(rep.Items) == 2
assert second2 in rep.Items
boolean = self.file.by_type("IfcBooleanResult")[0]
assert boolean in rep.Items
assert boolean.FirstOperand == first
assert boolean.SecondOperand == second1
def test_removing_a_nested_boolean(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])
ifcopenshell.api.geometry.add_boolean(self.file, first, [second1, second2])
ifcopenshell.api.geometry.remove_boolean(self.file, second1)
assert len(rep.Items) == 2
assert second1 in rep.Items
boolean = self.file.by_type("IfcBooleanResult")[0]
assert boolean in rep.Items
assert boolean.FirstOperand == first
assert boolean.SecondOperand == second2
class TestRemoveBooleanIFC2X3(test.bootstrap.IFC2X3, TestRemoveBoolean):
pass
@@ -0,0 +1,105 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api.root
import ifcopenshell.api.context
import ifcopenshell.api.geometry
class TestValidateType(test.bootstrap.IFC4):
def test_validating_a_non_csg_representation(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)
rep = builder.get_representation(body, [builder.rectangle()])
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is True
assert rep.RepresentationType == "Curve2D"
def test_failing_a_non_csg_representation(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)
rep = builder.get_representation(body, [builder.rectangle(), builder.block()])
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is False
assert rep.RepresentationType is None
def test_validating_a_correct_representation(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])
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is True
assert rep.RepresentationType == "CSG"
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
)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
first = builder.sphere()
second1 = builder.block()
second2 = builder.block()
second3 = builder.block()
rep = builder.get_representation(body, [first, second1, second2, second3])
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1])
assert len(booleans) == 1
assert len(rep.Items) == 3
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is True
assert len(rep.Items) == 1
assert rep.RepresentationType == "CSG"
assert rep.Items[0].Operator == "UNION"
def test_failing_validation_on_unreconcilable_types(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.rectangle()
rep = builder.get_representation(body, [first, second1, second2])
booleans = ifcopenshell.api.geometry.add_boolean(self.file, first, [second1])
assert len(booleans) == 1
assert len(rep.Items) == 2
assert ifcopenshell.api.geometry.validate_type(self.file, rep) is False
assert len(rep.Items) == 2
assert rep.RepresentationType is None
class TestValidateTypeIFC2X3(test.bootstrap.IFC2X3, TestValidateType):
pass