mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 02:23:34 +00:00
Merge branch 'v0.8.0' of github.com:IfcOpenShell/IfcOpenShell into v0.8.0
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user