diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index fc6874e2dd..069ada8f13 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -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" diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index 87bc9e607d..62f4765de6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -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", ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/validate_type.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/validate_type.py new file mode 100644 index 0000000000..6be8686732 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/validate_type.py @@ -0,0 +1,87 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Dion Moult +# +# 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 . + +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 diff --git a/src/ifcopenshell-python/test/api/geometry/test_validate_type.py b/src/ifcopenshell-python/test/api/geometry/test_validate_type.py new file mode 100644 index 0000000000..6eed24efcb --- /dev/null +++ b/src/ifcopenshell-python/test/api/geometry/test_validate_type.py @@ -0,0 +1,105 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult +# +# 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 . + +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