diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index dfa0a30946..14db0ad3cf 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -27,6 +27,7 @@ import ifcopenshell.util.representation import ifcopenshell.api import blenderbim.core.geometry as core import blenderbim.core.style +import blenderbim.core.root import blenderbim.tool as tool import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore @@ -421,6 +422,7 @@ class OverrideDuplicateMove(bpy.types.Operator): obj.select_set(False) new_obj.select_set(True) # This is the only difference - bpy.ops.bim.copy_class(obj=new_obj.name) + blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=new_obj) bpy.ops.transform.translate("INVOKE_DEFAULT") + blenderbim.bim.handler.purge_module_data() return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index d3185b680e..14c903225e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -27,6 +27,7 @@ import ifcopenshell.util.representation import mathutils.geometry import blenderbim.bim.handler import blenderbim.core.type +import blenderbim.core.root import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.pset.data import Data as PsetData @@ -212,7 +213,7 @@ class DumbWallSplitter: def duplicate_wall(self): new = self.wall.copy() self.wall.users_collection[0].objects.link(new) - bpy.ops.bim.copy_class(obj=new.name) + blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=new) return new def snap_end_face_to_point(self, wall, which_end): diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index 252369dcc6..7d0a320fd8 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -27,11 +27,19 @@ import blenderbim.core.material import blenderbim.core.spatial import blenderbim.core.style import blenderbim.core.type +import blenderbim.core.root as core import blenderbim.tool as tool from ifcopenshell.api.void.data import Data as VoidData from blenderbim.bim.ifc import IfcStore +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + blenderbim.bim.handler.refresh_ui_data() + return {"FINISHED"} + + class EnableReassignClass(bpy.types.Operator): bl_idname = "bim.enable_reassign_class" bl_label = "Enable Reassign IFC Class" @@ -338,58 +346,14 @@ class UnlinkObject(bpy.types.Operator): return {"FINISHED"} -class CopyClass(bpy.types.Operator): +class CopyClass(bpy.types.Operator, Operator): bl_idname = "bim.copy_class" bl_label = "Copy Class" bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): - self.file = IfcStore.get_file() - if self.obj: - objects = [bpy.data.objects.get(self.obj)] - else: - objects = context.selected_objects + objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects for obj in objects: - if not obj.BIMObjectProperties.ifc_definition_id: - continue - old_element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - result = ifcopenshell.api.run("root.copy_class", self.file, **{"product": old_element}) - IfcStore.link_element(result, obj) - relating_type = ifcopenshell.util.element.get_type(result) - if relating_type and relating_type.RepresentationMaps: - blenderbim.core.type.assign_type(tool.Ifc, tool.Geometry, tool.Type, element=result, type=relating_type) - else: - blenderbim.core.geometry.add_representation( - tool.Ifc, - tool.Geometry, - tool.Style, - tool.Surveyor, - obj=obj, - context=tool.Ifc.get().by_id(int(context.scene.BIMProperties.contexts)), - ifc_representation_class=None, - profile_set_usage=None, - ) - if result.is_a("IfcSpatialElement") or result.is_a("IfcSpatialStructureElement"): - tool.Collector.assign(obj) - elif result.is_a("IfcOpeningElement"): - self.add_opening_modifiers(result, obj) + core.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=obj) blenderbim.bim.handler.purge_module_data() - return {"FINISHED"} - - def add_opening_modifiers(self, result, obj): - for rel in result.VoidsElements: - building_obj = IfcStore.get_element(rel.RelatingBuildingElement.id()) - try: - modifier = next(m for m in obj.modifiers if m.type == "BOOLEAN" and m.object == obj) - except StopIteration: - modifier = building_obj.modifiers.new("IfcOpeningElement", "BOOLEAN") - modifier.object = obj - finally: - modifier.operation = "DIFFERENCE" - modifier.solver = "EXACT" - modifier.use_self = True - modifier.operand_type = "OBJECT" diff --git a/src/blenderbim/blenderbim/bim/module/spatial/operator.py b/src/blenderbim/blenderbim/bim/module/spatial/operator.py index 250d5cea34..c63efc9d1e 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/operator.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/operator.py @@ -21,6 +21,7 @@ import ifcopenshell.api import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.core.spatial as core +import blenderbim.core.root import blenderbim.bim.handler from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.spatial.data import SpatialData @@ -122,9 +123,10 @@ class CopyToContainer(bpy.types.Operator): new_obj = obj.copy() new_obj.data = obj.data.copy() new_obj.matrix_world = container_obj.matrix_world @ local_position - bpy.ops.bim.copy_class(obj=new_obj.name) + blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=new_obj) core.assign_container( tool.Ifc, tool.Collector, tool.Container, structure_obj=container_obj, element_obj=new_obj ) + blenderbim.bim.handler.purge_module_data() obj.BIMObjectSpatialProperties.is_editing = False return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/core/root.py b/src/blenderbim/blenderbim/core/root.py new file mode 100644 index 0000000000..a363881cde --- /dev/null +++ b/src/blenderbim/blenderbim/core/root.py @@ -0,0 +1,33 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + + +def copy_class(ifc, collector, root, obj=None): + element = ifc.get_entity(obj) + if not element: + return + element = ifc.run("root.copy_class", product=element) + ifc.link(element, obj) + relating_type = root.get_element_type(element) + if relating_type and root.does_type_have_representations(relating_type): + ifc.run("type.assign_type", related_object=element, relating_type=relating_type) + else: + root.run_geometry_add_representation(obj=obj, context=root.get_object_context(obj)) + collector.assign(obj) + if root.is_opening_element(element): + root.add_dynamic_opening_voids(element, obj) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 09d753e976..3dcd76a49f 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -143,6 +143,16 @@ class Owner: def set_user(cls, user): pass +@interface +class Root: + def add_dynamic_opening_voids(cls, element, obj): pass + def does_type_have_representations(cls, element): pass + def get_element_type(cls, element): pass + def get_object_context(cls, obj): pass + def is_opening_element(cls, element): pass + def run_geometry_add_representation(cls, obj=None, context=None): pass + + @interface class Selector: def set_active(cls, obj): pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index 0b40a66c56..05c17e05e1 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -26,6 +26,7 @@ from blenderbim.tool.ifc import Ifc from blenderbim.tool.material import Material from blenderbim.tool.misc import Misc from blenderbim.tool.owner import Owner +from blenderbim.tool.root import Root from blenderbim.tool.style import Style from blenderbim.tool.surveyor import Surveyor from blenderbim.tool.type import Type diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py new file mode 100644 index 0000000000..afae8f18d6 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/root.py @@ -0,0 +1,64 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import blenderbim.core.tool +import blenderbim.core.geometry +import blenderbim.tool as tool +from mathutils import Vector + + +class Root(blenderbim.core.tool.Root): + @classmethod + def add_dynamic_opening_voids(cls, element, obj): + for rel in element.VoidsElements: + building_obj = tool.Ifc.get_object(rel.RelatingBuildingElement) + try: + modifier = next(m for m in obj.modifiers if m.type == "BOOLEAN" and m.object == obj) + except StopIteration: + modifier = building_obj.modifiers.new("IfcOpeningElement", "BOOLEAN") + modifier.object = obj + finally: + modifier.operation = "DIFFERENCE" + modifier.solver = "EXACT" + modifier.use_self = True + modifier.operand_type = "OBJECT" + + @classmethod + def does_type_have_representations(cls, element): + return bool(element.RepresentationMaps) + + @classmethod + def get_element_type(cls, element): + return ifcopenshell.util.element.get_type(element) + + @classmethod + def get_object_context(cls, obj): + if obj.data and obj.data.BIMMeshProperties.ifc_definition_id: + return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id).ContextOfItems + + @classmethod + def is_opening_element(cls, element): + return element.is_a("IfcOpeningElement") + + @classmethod + def run_geometry_add_representation(cls, obj=None, context=None): + return blenderbim.core.geometry.add_representation( + tool.Ifc, tool.Geometry, tool.Style, tool.Surveyor, obj=obj, context=context + ) diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index d1a36ddf36..805900acf1 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -42,6 +42,20 @@ def aggregate(): prophet.verify() +@pytest.fixture +def collector(): + prophet = Prophecy(blenderbim.core.tool.Collector) + yield prophet + prophet.verify() + + +@pytest.fixture +def container(): + prophet = Prophecy(blenderbim.core.tool.Container) + yield prophet + prophet.verify() + + @pytest.fixture def context(): prophet = Prophecy(blenderbim.core.tool.Context) @@ -78,15 +92,8 @@ def owner(): @pytest.fixture -def type(): - prophet = Prophecy(blenderbim.core.tool.Type) - yield prophet - prophet.verify() - - -@pytest.fixture -def voider(): - prophet = Prophecy(blenderbim.core.tool.Voider) +def root(): + prophet = Prophecy(blenderbim.core.tool.Root) yield prophet prophet.verify() @@ -113,15 +120,15 @@ def surveyor(): @pytest.fixture -def collector(): - prophet = Prophecy(blenderbim.core.tool.Collector) +def type(): + prophet = Prophecy(blenderbim.core.tool.Type) yield prophet prophet.verify() @pytest.fixture -def container(): - prophet = Prophecy(blenderbim.core.tool.Container) +def voider(): + prophet = Prophecy(blenderbim.core.tool.Voider) yield prophet prophet.verify() diff --git a/src/blenderbim/test/core/test_root.py b/src/blenderbim/test/core/test_root.py new file mode 100644 index 0000000000..f9c715ad01 --- /dev/null +++ b/src/blenderbim/test/core/test_root.py @@ -0,0 +1,72 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import blenderbim.core.root as subject +import test.core.test_geometry +from test.core.bootstrap import ifc, collector, root + + +class TestCopyClass: + def test_doing_nothing_if_not_an_ifc_element(self, ifc, collector, root): + ifc.get_entity("obj").should_be_called().will_return(None) + subject.copy_class(ifc, collector, root, obj="obj") + + 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") + 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(True) + ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + collector.assign("obj").should_be_called() + root.is_opening_element("element").should_be_called().will_return(False) + subject.copy_class(ifc, collector, root, obj="obj") + + def test_copy_with_new_geometry_added_afresh_for_speed( + self, + ifc, + collector, + root, + ): + ifc.get_entity("obj").should_be_called().will_return("original_element") + 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) + root.get_object_context("obj").should_be_called().will_return("context") + root.run_geometry_add_representation(obj="obj", context="context").should_be_called() + collector.assign("obj").should_be_called() + root.is_opening_element("element").should_be_called().will_return(False) + subject.copy_class(ifc, collector, root, obj="obj") + + def test_copied_openings_have_dynamic_voids_added( + self, + ifc, + collector, + root, + ): + ifc.get_entity("obj").should_be_called().will_return("original_element") + 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(True) + ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() + collector.assign("obj").should_be_called() + root.is_opening_element("element").should_be_called().will_return(True) + root.add_dynamic_opening_voids("element", "obj").should_be_called() + subject.copy_class(ifc, collector, root, obj="obj") diff --git a/src/blenderbim/test/tool/test_root.py b/src/blenderbim/test/tool/test_root.py new file mode 100644 index 0000000000..61e7fcded0 --- /dev/null +++ b/src/blenderbim/test/tool/test_root.py @@ -0,0 +1,97 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + +import bpy +import ifcopenshell +import blenderbim.core.tool +import blenderbim.tool as tool +from test.bim.bootstrap import NewFile +from blenderbim.tool.root import Root as subject + + +class TestImplementsTool(NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.Root) + + +class TestAddDynamicOpeningVoids(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + element = ifc.createIfcOpeningElement() + tool.Ifc.link(element, obj) + + wall_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + wall_element = ifc.createIfcOpeningElement() + tool.Ifc.link(wall_element, wall_obj) + + ifcopenshell.api.run("void.add_opening", ifc, opening=element, element=wall_element) + + subject.add_dynamic_opening_voids(element, obj) + + modifier = wall_obj.modifiers[0] + assert modifier.type == "BOOLEAN" + assert modifier.name == "IfcOpeningElement" + assert modifier.operation == "DIFFERENCE" + assert modifier.object == obj + assert modifier.solver == "EXACT" + assert modifier.use_self is True + + +class TestDoesTypeHaveRepresentations(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + element = ifc.createIfcWallType() + assert subject.does_type_have_representations(element) is False + element.RepresentationMaps = [ifc.createIfcRepresentationMap()] + assert subject.does_type_have_representations(element) is True + + +class TestGetElementType(NewFile): + def test_run(self): + bpy.ops.bim.create_project() + ifc = tool.Ifc.get() + element = ifc.createIfcWall() + type = ifc.createIfcWallType() + ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=type) + assert subject.get_element_type(element) == type + + +class TestGetElementType(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + context = ifc.createIfcGeometricRepresentationContext() + representation = ifc.createIfcShapeRepresentation(ContextOfItems=context) + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + obj.data.BIMMeshProperties.ifc_definition_id = representation.id() + assert subject.get_object_context(obj) == context + + +class TestIsOpeningElement(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 + + +class TestRunGeometryAddRepresntation(NewFile): + def test_nothing(self): + pass