diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 70b7a893e7..eac368e6e7 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -108,37 +108,15 @@ class RemoveRepresentation(bpy.types.Operator, Operator): bl_idname = "bim.remove_representation" bl_label = "Remove Representation" bl_options = {"REGISTER", "UNDO"} - obj: bpy.props.StringProperty() representation_id: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() - representation = self.file.by_id(self.representation_id) - obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - is_mapped_representation = representation.RepresentationType == "MappedRepresentation" - if is_mapped_representation: - mesh_name = "{}/{}".format( - representation.ContextOfItems.id(), representation.Items[0].MappingSource.MappedRepresentation.id() - ) - else: - mesh_name = "{}/{}".format(representation.ContextOfItems.id(), representation.id()) - mesh = bpy.data.meshes.get(mesh_name) - if mesh: - if obj.data == mesh: - # TODO we can do better than this - void_mesh = bpy.data.meshes.get("Void") - if not void_mesh: - void_mesh = bpy.data.meshes.new("Void") - obj.data = void_mesh - if not is_mapped_representation: - bpy.data.meshes.remove(mesh) - product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - ifcopenshell.api.run( - "geometry.unassign_representation", self.file, **{"product": product, "representation": representation} + core.remove_representation( + tool.Ifc, + tool.Geometry, + obj=context.active_object, + representation=tool.Ifc.get().by_id(self.representation_id), ) - ifcopenshell.api.run("geometry.remove_representation", self.file, **{"representation": representation}) - Data.load(self.file, product.id()) - return {"FINISHED"} class UpdateRepresentation(bpy.types.Operator): @@ -236,7 +214,7 @@ class UpdateRepresentation(bpy.types.Operator): obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" - bpy.ops.bim.remove_representation(representation_id=old_representation.id(), obj=obj.name) + core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation) Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id) if obj.data.BIMMeshProperties.ifc_parameters: bpy.ops.bim.get_representation_ifc_parameters() diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index 4331bf68f6..a64a9d3ee9 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -255,7 +255,7 @@ def generate_box(usecase_path, ifc_file, settings): old_box = ifcopenshell.util.representation.get_representation(product, "Model", "Box", "MODEL_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": if old_box: - bpy.ops.bim.remove_representation(representation_id=old_box.id(), obj=obj.name) + blenderbim.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_box) new_settings = settings.copy() new_settings["context"] = box_context diff --git a/src/blenderbim/blenderbim/bim/module/model/slab.py b/src/blenderbim/blenderbim/bim/module/model/slab.py index ec9978975b..517a7cf0ec 100644 --- a/src/blenderbim/blenderbim/bim/module/model/slab.py +++ b/src/blenderbim/blenderbim/bim/module/model/slab.py @@ -26,6 +26,7 @@ import ifcopenshell.util.element import mathutils.geometry import blenderbim.bim.handler import blenderbim.core.type +import blenderbim.core.geometry import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from math import pi, degrees @@ -112,7 +113,9 @@ def generate_footprint(usecase_path, ifc_file, settings): old_footprint = ifcopenshell.util.representation.get_representation(product, "Plan", "FootPrint", "SKETCH_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": if old_footprint: - bpy.ops.bim.remove_representation(representation_id=old_footprint.id(), obj=obj.name) + blenderbim.core.geometry.remove_representation( + tool.Ifc, tool.Geometry, obj=obj, representation=old_footprint + ) helper = Helper(ifc_file) indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(settings["geometry"]) diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 6b3aff5844..513d929a8f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -28,6 +28,7 @@ import mathutils.geometry import blenderbim.bim.handler import blenderbim.core.type import blenderbim.core.root +import blenderbim.core.geometry import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.pset.data import Data as PsetData @@ -828,7 +829,7 @@ def generate_axis(usecase_path, ifc_file, settings): old_axis = ifcopenshell.util.representation.get_representation(product, "Model", "Axis", "GRAPH_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": if old_axis: - bpy.ops.bim.remove_representation(representation_id=old_axis.id(), obj=obj.name) + blenderbim.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_axis) new_settings = settings.copy() new_settings["context"] = axis_context diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 62b79fc503..3cdbc526fb 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -76,6 +76,14 @@ class Geometry(blenderbim.core.tool.Geometry): ) ) + @classmethod + def get_element_type(cls, element): + return ifcopenshell.util.element.get_type(element) + + @classmethod + def get_elements_of_type(cls, type): + return ifcopenshell.util.element.get_types(type) + @classmethod def get_ifc_representation_class(cls, element, representation): material = ifcopenshell.util.element.get_material(element) @@ -124,6 +132,10 @@ class Geometry(blenderbim.core.tool.Geometry): def get_total_representation_items(cls, obj): return max(1, len(obj.material_slots)) + @classmethod + def has_data_users(cls, data): + return data.users != 0 + @classmethod def import_representation(cls, obj, representation, enable_dynamic_voids=False): logger = logging.getLogger("ImportIFC") @@ -152,6 +164,14 @@ class Geometry(blenderbim.core.tool.Geometry): def is_body_representation(cls, representation): return representation.ContextOfItems.ContextIdentifier == "Body" + @classmethod + def is_mapped_representation(cls, representation): + return representation.RepresentationType == "MappedRepresentation" + + @classmethod + def is_type_product(cls, element): + return element.is_a("IfcTypeProduct") + @classmethod def link(cls, element, obj): obj.BIMMeshProperties.ifc_definition_id = element.id() @@ -160,6 +180,20 @@ class Geometry(blenderbim.core.tool.Geometry): def rename_object(cls, obj, name): obj.name = name + @classmethod + def replace_object_with_empty(cls, obj): + element = tool.Ifc.get_entity(obj) + name = obj.name + tool.Ifc.unlink(obj) + obj.name = ifcopenshell.guid.new() + new_obj = bpy.data.objects.new(name, None) + if element: + tool.Ifc.link(element, new_obj) + for collection in obj.users_collection: + collection.objects.link(new_obj) + new_obj.matrix_world = obj.matrix_world + bpy.data.objects.remove(obj) + @classmethod def resolve_mapped_representation(cls, representation): if representation.RepresentationType == "MappedRepresentation": diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index a59f91a831..0aa5efee59 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -84,6 +84,66 @@ Scenario: Switch representation - existing Blender modifiers must be purged And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation})" Then the object "IfcWall/Cube" has no modifiers +Scenario: Remove representation - remove an active representation + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I add an array modifier + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" + And I press "bim.remove_representation(representation_id={representation})" + Then the object "IfcWall/Cube" has no data + +Scenario: Remove representation - remove an unloaded representation + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I add an array modifier + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[1].id()" + And I press "bim.remove_representation(representation_id={representation})" + Then the object "IfcWall/Cube" has data which is an IFC representation + +Scenario: Remove representation - remove an instanced representation from an active type object + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" + And I press "bim.assign_class" + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" + And I set "scene.BIMModelProperties.relating_type" to "{cube}" + And I press "bim.add_type_instance" + And I press "bim.add_type_instance" + And the object "IfcWallType/Cube" is selected + When the variable "representation" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[1].MappedRepresentation.id()" + And I press "bim.remove_representation(representation_id={representation})" + Then the object "IfcWallType/Cube" has no data + Then the object "IfcWall/Instance" has no data + Then the object "IfcWall/Instance.001" has no data + +Scenario: Remove representation - remove an instanced representation from an active instance object + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" + And I press "bim.assign_class" + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" + And I set "scene.BIMModelProperties.relating_type" to "{cube}" + And I press "bim.add_type_instance" + And I press "bim.add_type_instance" + And the object "IfcWall/Instance" is selected + When the variable "representation" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[1].id()" + And I press "bim.remove_representation(representation_id={representation})" + Then the object "IfcWallType/Cube" has no data + Then the object "IfcWall/Instance" has no data + Then the object "IfcWall/Instance.001" has no data + Scenario: Update representation - updating a tessellation Given an empty IFC project And I add a cube diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 60c1360a83..2918e78ec2 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -356,6 +356,11 @@ def the_object_name_is_not_an_ifc_element(name): assert id == 0, f"The ID is {id}" +@then(parsers.parse('the object "{name}" has no data')) +def the_object_name_has_no_data(name): + assert the_object_name_exists(name).data is None + + @then(parsers.parse('the object "{name}" has data which is an IFC representation')) def the_object_name_is_not_an_ifc_element(name): id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py index 6005642b0f..97b7ee96f3 100644 --- a/src/blenderbim/test/tool/test_geometry.py +++ b/src/blenderbim/test/tool/test_geometry.py @@ -184,6 +184,26 @@ class TestGetCartesianPointCoordinateOffset(NewFile): assert subject.get_cartesian_point_coordinate_offset(obj) is None +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 TestGetElementsOfType(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_elements_of_type(type) == (element,) + + class TestGetTotalRepresentationItems(NewFile): def test_run(self): material1 = bpy.data.materials.new("Material") @@ -194,6 +214,14 @@ class TestGetTotalRepresentationItems(NewFile): assert subject.get_total_representation_items(obj) == 2 +class TestHasDataUsers(NewFile): + def test_run(self): + data = bpy.data.meshes.new("Mesh") + assert subject.has_data_users(data) is False + bpy.data.objects.new("Object", data) + assert subject.has_data_users(data) is True + + class TestImportRepresentation(NewFile): def test_importing_a_normal_shape(self): ifc = ifcopenshell.open("test/files/basic.ifc") @@ -232,6 +260,22 @@ class TestIsBodyRepresentation(NewFile): assert subject.is_body_representation(representation) is False +class TestIsMappedRepresentation(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + representation = ifc.createIfcShapeRepresentation() + assert subject.is_mapped_representation(representation) is False + representation.RepresentationType = "MappedRepresentation" + assert subject.is_mapped_representation(representation) is True + + +class TestIsTypeProduct(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + assert subject.is_type_product(ifc.createIfcWall()) is False + assert subject.is_type_product(ifc.createIfcWallType()) is True + + class TestLink(NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -241,13 +285,31 @@ class TestLink(NewFile): assert obj.BIMMeshProperties.ifc_definition_id == element.id() -class TestRenameObjectData(NewFile): +class TestRenameObject(NewFile): def test_run(self): obj = bpy.data.meshes.new("Mesh") subject.rename_object(obj, "name") assert obj.name == "name" +class TestReplaceObjectWithEmpty(NewFile): + def test_run(self): + 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.replace_object_with_empty(obj) + 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 + + class TestResolveMappedRepresentation(NewFile): def test_run(self): ifc = ifcopenshell.file()