diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 5469cebce8..8ab359af23 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -211,7 +211,7 @@ class AddRepresentation(bpy.types.Operator, Operator): if original_data: tool.Geometry.change_object_data(obj, data, is_global=True) else: - obj = tool.Geometry.recreate_object_with_data(obj, data) + obj = tool.Geometry.recreate_object_with_data(obj, data, is_global=True) try: core.add_representation( diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index c35b59ef11..a829c25ddf 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -690,7 +690,9 @@ class Geometry(blenderbim.core.tool.Geometry): obj.name = name @classmethod - def recreate_object_with_data(cls, obj: bpy.types.Object, data: Union[bpy.types.ID, None]) -> bpy.types.Object: + def recreate_object_with_data( + cls, obj: bpy.types.Object, data: Union[bpy.types.ID, None], is_global: bool = False + ) -> bpy.types.Object: """Recreate a Blender object with the provided `data`. This method is useful when an object should no longer have associated @@ -703,11 +705,18 @@ class Geometry(blenderbim.core.tool.Geometry): Original `obj` is deleted and becomes invalid and should be replaced with an object returned by this method. + :param is_global: Whether all `obj` occurrences should also be recreated + with the provided `data`. Works only if `obj` is an IfcTypeProduct. :return: The newly recreated object. """ element = tool.Ifc.get_entity(obj) name = obj.name if element: + if is_global and element.is_a("IfcTypeProduct"): + ocurrences = ifcopenshell.util.element.get_types(element) + for occurrence in ocurrences: + cls.recreate_object_with_data(tool.Ifc.get_object(occurrence), data) + tool.Ifc.unlink(element=element) obj.name = ifcopenshell.guid.new() diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py index 834ccd8687..f3cfea1c79 100644 --- a/src/blenderbim/test/tool/test_geometry.py +++ b/src/blenderbim/test/tool/test_geometry.py @@ -20,11 +20,13 @@ import bpy import math import numpy as np import ifcopenshell +import ifcopenshell.api.type import blenderbim.core.tool import blenderbim.tool as tool from mathutils import Vector from test.bim.bootstrap import NewFile from blenderbim.tool.geometry import Geometry as subject +from typing import Union class TestImplementsTool(NewFile): @@ -375,23 +377,75 @@ class TestRenameObject(NewFile): assert obj.name == "name" -# TODO: add a test class TestReplaceObjectWithEmpty(NewFile): - def test_run(self): + def validate( + self, + element: ifcopenshell.entity_instance, + obj: bpy.types.Object, + new_obj: bpy.types.Object, + new_data: Union[bpy.types.Mesh, None], + ) -> None: + assert not tool.Blender.is_valid_data_block(obj) + assert new_obj.users_collection[0] == bpy.context.scene.collection + assert tool.Ifc.get_entity(new_obj) == element + assert tool.Ifc.get_object(element) == new_obj + assert new_obj.data is new_data + assert new_obj.matrix_world.translation.x == 1 + + def create_object( + self, name: str, data: Union[bpy.types.Mesh, None], ifc_class: str + ) -> tuple[bpy.types.Object, ifcopenshell.entity_instance]: + ifc = tool.Ifc.get() + obj = bpy.data.objects.new(name, data) + obj.matrix_world.translation.x = 1 + bpy.context.scene.collection.objects.link(obj) + element = ifc.create_entity(ifc_class) + tool.Ifc.link(element, obj) + return obj, element + + def run_test_replace_non_global( + self, from_data: Union[bpy.types.Mesh, None], to_data: Union[bpy.types.Mesh, None] + ) -> None: ifc = ifcopenshell.file() tool.Ifc.set(ifc) - obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) - obj.matrix_world[0][3] = 1 - bpy.context.scene.collection.objects.link(obj) - element = ifc.createIfcWall() - tool.Ifc.link(element, obj) - subject.recreate_object_with_data(obj, None) - obj = bpy.data.objects.get("Object") - assert obj.users_collection[0] == bpy.context.scene.collection - assert tool.Ifc.get_entity(obj) == element - assert tool.Ifc.get_object(element) == obj - assert obj.data is None - assert obj.matrix_world[0][3] == 1 + + obj, element = self.create_object("Object", from_data, "IfcWall") + new_obj = subject.recreate_object_with_data(obj, to_data) + if from_data: + assert tool.Blender.is_valid_data_block(from_data) + self.validate(element, obj, new_obj, to_data) + + def test_replace_mesh_with_empty(self) -> None: + self.run_test_replace_non_global(bpy.data.meshes.new("Mesh"), None) + + def test_replace_empty_with_mesh(self) -> None: + self.run_test_replace_non_global(None, bpy.data.meshes.new("New Mesh")) + + def run_test_for_global_argument( + self, from_data: Union[bpy.types.Mesh, None], to_data: Union[bpy.types.Mesh, None] + ) -> None: + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + + type_obj, element_type = self.create_object("IfcWallType", from_data, "IfcWallType") + obj1, element1 = self.create_object("IfcWall1", from_data, "IfcWall") + obj2, element2 = self.create_object("IfcWall2", from_data, "IfcWall") + ifcopenshell.api.type.assign_type(ifc, related_objects=[element1, element2], relating_type=element_type) + + to_data = to_data + new_obj = subject.recreate_object_with_data(type_obj, to_data, is_global=True) + if from_data: + assert tool.Blender.is_valid_data_block(from_data) + + self.validate(element_type, type_obj, new_obj, to_data) + self.validate(element1, obj1, bpy.data.objects["IfcWall1"], to_data) + self.validate(element2, obj2, bpy.data.objects["IfcWall2"], to_data) + + def test_replace_mesh_with_empty_global(self) -> None: + self.run_test_for_global_argument(bpy.data.meshes.new("Mesh"), None) + + def test_replace_empty_with_mesh_global(self) -> None: + self.run_test_for_global_argument(None, bpy.data.meshes.new("New Mesh")) class TestResolveMappedRepresentation(NewFile):