diff --git a/src/blenderbim/blenderbim/bim/module/boundary/operator.py b/src/blenderbim/blenderbim/bim/module/boundary/operator.py index 6373bca5d9..23db0e03e2 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/operator.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/operator.py @@ -491,8 +491,7 @@ class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator): element = tool.Ifc.get_entity(obj) for rel in element.BoundedBy or []: boundary_obj = loader.load_boundary(rel, obj) - new = props.boundaries.add() - new.obj = boundary_obj + tool.Boundary.decorate_boundary(boundary_obj) BoundaryDecorator.install(bpy.context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 94839bae39..b9b3063e29 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -527,6 +527,8 @@ class OverrideDuplicateMove(bpy.types.Operator): array_pset = tool.Ifc.get().by_id(array_pset["id"]) ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new, pset=array_pset) old_to_new[tool.Ifc.get_entity(obj)] = [new] + if new.is_a("IfcRelSpaceBoundary"): + tool.Boundary.decorate_boundary(new_obj) # Recreate decompositions tool.Root.recreate_decompositions(relationships, old_to_new) blenderbim.bim.handler.purge_module_data() diff --git a/src/blenderbim/blenderbim/core/root.py b/src/blenderbim/blenderbim/core/root.py index f558b8b57e..3f36c39adb 100644 --- a/src/blenderbim/blenderbim/core/root.py +++ b/src/blenderbim/blenderbim/core/root.py @@ -21,6 +21,10 @@ def copy_class(ifc, collector, geometry, root, obj=None): element = ifc.get_entity(obj) if not element: return + if root.is_element_a(element, "IfcRelSpaceBoundary"): + new = ifc.run("boundary.copy_boundary", boundary=element) + ifc.link(new, obj) + return new representation = root.get_object_representation(obj) new = ifc.run("root.copy_class", product=element) ifc.link(new, obj) @@ -38,7 +42,7 @@ def copy_class(ifc, collector, geometry, root, obj=None): geometry.link(new_representation, data) root.assign_body_styles(new, obj) collector.assign(obj) - if root.is_opening_element(new): + if root.is_element_a(new, "IfcOpeningElement"): root.add_tracked_opening(obj) return new diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index ae7ffd9662..6e32e35b84 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -606,7 +606,7 @@ class Root: def get_object_name(cls, obj): pass def get_object_representation(cls, obj): pass def get_representation_context(cls, representation): pass - def is_opening_element(cls, element): pass + def is_element_a(cls, element, ifc_class): pass def link_object_data(cls, source_obj, destination_obj): pass def recreate_decompositions(cls, relationships, old_to_new): pass def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass diff --git a/src/blenderbim/blenderbim/tool/boundary.py b/src/blenderbim/blenderbim/tool/boundary.py index d31ba8d99b..c922a23462 100644 --- a/src/blenderbim/blenderbim/tool/boundary.py +++ b/src/blenderbim/blenderbim/tool/boundary.py @@ -70,3 +70,9 @@ class Boundary(blenderbim.core.tool.Boundary): translation = obj.matrix_world.translation - space.matrix_world.translation obj.data.transform(mathutils.Matrix.Translation(translation)) obj.matrix_world = space.matrix_world + + @classmethod + def decorate_boundary(cls, obj): + new = bpy.context.scene.BIMBoundaryProperties.boundaries.add() + new.obj = obj + obj.show_in_front = True diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index 3bb9b4f42d..2640e7d1b3 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -119,8 +119,8 @@ class Root(blenderbim.core.tool.Root): return representation.ContextOfItems @classmethod - def is_opening_element(cls, element): - return element.is_a("IfcOpeningElement") + def is_element_a(cls, element, ifc_class): + return element.is_a(ifc_class) @classmethod def link_object_data(cls, source_obj, destination_obj): diff --git a/src/blenderbim/test/core/test_root.py b/src/blenderbim/test/core/test_root.py index d1a69d58f1..65a99d626d 100644 --- a/src/blenderbim/test/core/test_root.py +++ b/src/blenderbim/test/core/test_root.py @@ -28,6 +28,7 @@ class TestCopyClass: def test_copy_with_new_geometry_derived_from_the_type(self, ifc, collector, root): ifc.get_entity("obj").should_be_called().will_return("original_element") + root.is_element_a("original_element", "IfcRelSpaceBoundary").should_be_called().will_return(False) root.get_object_representation("obj").should_be_called().will_return("representation") ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element") ifc.link("element", "obj").should_be_called() @@ -37,7 +38,7 @@ class TestCopyClass: ifc.get_object("type").should_be_called().will_return("type_obj") root.link_object_data("type_obj", "obj").should_be_called() collector.assign("obj").should_be_called() - root.is_opening_element("element").should_be_called().will_return(False) + root.is_element_a("element", "IfcOpeningElement").should_be_called().will_return(False) subject.copy_class(ifc, collector, geometry, root, obj="obj") def test_copy_with_new_geometry_copied_from_the_old(self, ifc, collector, geometry, root): @@ -45,6 +46,7 @@ class TestCopyClass: # copied. This was faster (though I cannot recreate it now) but had the # bigger problem of not preserving non-mesh geometry and openings. ifc.get_entity("obj").should_be_called().will_return("original_element") + root.is_element_a("original_element", "IfcRelSpaceBoundary").should_be_called().will_return(False) root.get_object_representation("obj").should_be_called().will_return("representation") ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element") ifc.link("element", "obj").should_be_called() @@ -60,22 +62,24 @@ class TestCopyClass: root.assign_body_styles("element", "obj").should_be_called() geometry.duplicate_object_data("obj").should_be_called().will_return("data") collector.assign("obj").should_be_called() - root.is_opening_element("element").should_be_called().will_return(False) + root.is_element_a("element", "IfcOpeningElement").should_be_called().will_return(False) subject.copy_class(ifc, collector, geometry, root, obj="obj") def test_copy_with_no_new_geometry(self, ifc, collector, geometry, root): ifc.get_entity("obj").should_be_called().will_return("original_element") + root.is_element_a("original_element", "IfcRelSpaceBoundary").should_be_called().will_return(False) root.get_object_representation("obj").should_be_called().will_return(None) ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element") ifc.link("element", "obj").should_be_called() root.get_element_type("element").should_be_called().will_return("type") root.does_type_have_representations("type").should_be_called().will_return(False) collector.assign("obj").should_be_called() - root.is_opening_element("element").should_be_called().will_return(False) + root.is_element_a("element", "IfcOpeningElement").should_be_called().will_return(False) subject.copy_class(ifc, collector, geometry, root, obj="obj") - def test_copied_openings_are_tracked_for_special_visualiation(self, ifc, collector, root): + def test_copied_openings_are_tracked_for_special_visualiation(self, ifc, collector, geometry, root): ifc.get_entity("obj").should_be_called().will_return("original_element") + root.is_element_a("original_element", "IfcRelSpaceBoundary").should_be_called().will_return(False) root.get_object_representation("obj").should_be_called().will_return(None) ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element") ifc.link("element", "obj").should_be_called() @@ -85,10 +89,17 @@ class TestCopyClass: ifc.get_object("type").should_be_called().will_return("type_obj") root.link_object_data("type_obj", "obj").should_be_called() collector.assign("obj").should_be_called() - root.is_opening_element("element").should_be_called().will_return(True) + root.is_element_a("element", "IfcOpeningElement").should_be_called().will_return(True) root.add_tracked_opening("obj").should_be_called() subject.copy_class(ifc, collector, geometry, root, obj="obj") + def test_copying_boundaries_are_dealt_with_specially(self, ifc, collector, geometry, root): + ifc.get_entity("obj").should_be_called().will_return("original_element") + root.is_element_a("original_element", "IfcRelSpaceBoundary").should_be_called().will_return(True) + ifc.run("boundary.copy_boundary", boundary="original_element").should_be_called().will_return("element") + ifc.link("element", "obj").should_be_called() + assert subject.copy_class(ifc, collector, geometry, root, obj="obj") == "element" + class TestAssignClass: def test_do_nothing_if_already_assigned(self, ifc, collector, root): diff --git a/src/blenderbim/test/tool/test_root.py b/src/blenderbim/test/tool/test_root.py index b4f798444c..15ddf8a115 100644 --- a/src/blenderbim/test/tool/test_root.py +++ b/src/blenderbim/test/tool/test_root.py @@ -132,11 +132,11 @@ class TestGetRepresentationContext(NewFile): assert subject.get_representation_context(representation) == context -class TestIsOpeningElement(NewFile): +class TestIsElementA(NewFile): def test_run(self): ifc = ifcopenshell.file() - assert subject.is_opening_element(ifc.createIfcWall()) is False - assert subject.is_opening_element(ifc.createIfcOpeningElement()) is True + assert subject.is_a(ifc.createIfcWall(), "IfcSlab") is False + assert subject.is_a(ifc.createIfcOpeningElement(), "IfcOpeningElement") is True class TestLinkObjectData(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py new file mode 100644 index 0000000000..af78438421 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py @@ -0,0 +1,47 @@ +# 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 ifcopenshell.util.element + + +class Usecase: + def __init__(self, file, boundary=None): + """Copies a space boundary + + :param boundary: The IfcRelSpaceBoundary you want to copy. + :type boundary: ifcopenshell.entity_instance.entity_instance + :return: None + :rtype: None + + Example: + + # A boring boundary with no geometry. Note that this boundary is + # invalid and does not relate to any space or building element. + boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary") + + # And now we have two + boundary_copy = ifcopenshell.api.run("boundary.copy_boundary", model, boundary=boundary) + """ + self.file = file + self.settings = {"boundary": boundary} + + def execute(self): + result = ifcopenshell.util.element.copy(self.file, self.settings["boundary"]) + if result.ConnectionGeometry: + result.ConnectionGeometry = ifcopenshell.util.element.copy_deep(self.file, result.ConnectionGeometry) + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py index cdbbd37586..dc82c5ea84 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py @@ -38,7 +38,7 @@ class Usecase: boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary") # Let's remove it! - ifcopenshell.api.run("grid.remove_grid_axis", model, axis=axis_2) + ifcopenshell.api.run("boundary.remove_boundary", model, boundary=boundary) """ self.file = file self.settings = {"boundary": boundary} diff --git a/src/ifcopenshell-python/test/api/boundary/__init__.py b/src/ifcopenshell-python/test/api/boundary/__init__.py new file mode 100644 index 0000000000..8bed51a56f --- /dev/null +++ b/src/ifcopenshell-python/test/api/boundary/__init__.py @@ -0,0 +1,17 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 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 . diff --git a/src/ifcopenshell-python/test/api/boundary/test_copy_boundary.py b/src/ifcopenshell-python/test/api/boundary/test_copy_boundary.py new file mode 100644 index 0000000000..b044d32eac --- /dev/null +++ b/src/ifcopenshell-python/test/api/boundary/test_copy_boundary.py @@ -0,0 +1,36 @@ +# 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 + + +class TestCopyBoundary(test.bootstrap.IFC4): + def test_run(self): + boundary = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelSpaceBoundary") + boundary2 = ifcopenshell.api.run("boundary.copy_boundary", self.file, boundary=boundary) + assert boundary2.is_a("IfcRelSpaceBoundary") + assert boundary2.GlobalId != boundary.GlobalId + + def test_copying_connection_geometry(self): + geometry = self.file.createIfcConnectionSurfaceGeometry() + boundary = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelSpaceBoundary") + boundary.ConnectionGeometry = geometry + boundary2 = ifcopenshell.api.run("boundary.copy_boundary", self.file, boundary=boundary) + assert boundary2.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry") + assert boundary2.ConnectionGeometry != boundary.ConnectionGeometry