From 8e215382f71352dd7d24c2905c8784de9c6eb1dc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 6 Nov 2021 20:56:53 +1100 Subject: [PATCH] Fix bug where assigning a type did not use existing representation data in the scene if the context had changed --- src/blenderbim/blenderbim/core/tool.py | 3 +- src/blenderbim/blenderbim/core/type.py | 24 +++++------ src/blenderbim/blenderbim/tool/geometry.py | 1 - src/blenderbim/blenderbim/tool/type.py | 18 +++++--- .../test/bim/feature/geometry.feature | 12 +++--- src/blenderbim/test/bim/feature/model.feature | 21 +++++++++ src/blenderbim/test/bim/test_feature.py | 14 ++++++ src/blenderbim/test/core/test_type.py | 29 +++---------- src/blenderbim/test/tool/test_type.py | 43 ++++++++++++------- 9 files changed, 99 insertions(+), 66 deletions(-) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 4127734018..f9d7b38de8 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -223,11 +223,12 @@ class Surveyor: @interface class Type: + def change_object_data(cls, obj, data, is_global=False): pass def disable_editing(cls, obj): pass - def get_any_representation(cls, element): pass def get_body_context(cls): pass def get_body_representation(cls, element): pass def get_ifc_representation_class(cls, element): pass + def get_object_data(cls, obj): pass def get_profile_set_usage(cls, element): pass def get_representation_context(cls, representation): pass def has_dynamic_voids(cls, obj): pass diff --git a/src/blenderbim/blenderbim/core/type.py b/src/blenderbim/blenderbim/core/type.py index 5076ee5e93..c4b3038fe3 100644 --- a/src/blenderbim/blenderbim/core/type.py +++ b/src/blenderbim/blenderbim/core/type.py @@ -36,18 +36,16 @@ def assign_type(ifc, type_tool, element=None, type=None): ifc_representation_class=type_tool.get_ifc_representation_class(element), profile_set_usage=type_tool.get_profile_set_usage(element), ) - should_reload = True + if representation: + type_tool.run_geometry_switch_representation( + obj=obj, + representation=representation, + should_reload=True, + enable_dynamic_voids=type_tool.has_dynamic_voids(obj), + is_global=False, + ) else: - representation = type_tool.get_body_representation(element) - if not representation: - representation = type_tool.get_any_representation(element) - should_reload = False - if representation: - type_tool.run_geometry_switch_representation( - obj=obj, - representation=representation, - should_reload=should_reload, - enable_dynamic_voids=type_tool.has_dynamic_voids(obj), - is_global=False, - ) + type_data = type_tool.get_object_data(ifc.get_object(type)) + if type_data: + type_tool.change_object_data(obj, type_data, is_global=False) type_tool.disable_editing(obj) diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index bdba057ec7..62b79fc503 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -52,7 +52,6 @@ class Geometry(blenderbim.core.tool.Geometry): modifier.solver = "EXACT" modifier.use_self = True - @classmethod def delete_data(cls, data): bpy.data.meshes.remove(data) diff --git a/src/blenderbim/blenderbim/tool/type.py b/src/blenderbim/blenderbim/tool/type.py index 3798fcf389..6aaab74026 100644 --- a/src/blenderbim/blenderbim/tool/type.py +++ b/src/blenderbim/blenderbim/tool/type.py @@ -25,15 +25,15 @@ import blenderbim.bim.helper class Type(blenderbim.core.tool.Type): @classmethod - def disable_editing(cls, obj): - obj.BIMTypeProperties.is_editing_type = False + def change_object_data(cls, obj, data, is_global=False): + if is_global: + obj.data.user_remap(data) + else: + obj.data = data @classmethod - def get_any_representation(cls, element): - if element.is_a("IfcProduct") and element.Representation and element.Representation.Representations: - return element.Representation.Representations[0] - elif element.is_a("IfcTypeProduct") and element.RepresentationMaps: - return element.RepresentationMaps[0].MappedRepresentation + def disable_editing(cls, obj): + obj.BIMTypeProperties.is_editing_type = False @classmethod def get_body_context(cls): @@ -59,6 +59,10 @@ class Type(blenderbim.core.tool.Type): elif material.is_a("IfcMaterialLayerSetUsage"): return "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" + @classmethod + def get_object_data(cls, obj): + return obj.data + @classmethod def get_profile_set_usage(cls, element): material = ifcopenshell.util.element.get_material(element) diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index c09f1339c2..f72c5e4597 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -18,11 +18,11 @@ Scenario: Add representation And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the object "IfcWall/Cube" is selected - Then the object "IfcWall/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" + Then the object "IfcWall/Cube" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" When the variable "context" is "{ifc}.by_type('IfcGeometricRepresentationSubContext')[-1].id()" And I set "scene.BIMProperties.contexts" to "{context}" And I press "bim.add_representation" - Then the object "IfcWall/Cube" has a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" + Then the object "IfcWall/Cube" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" Scenario: Add representation - add a new representation to a typed instance Given an empty IFC project @@ -33,14 +33,14 @@ Scenario: Add representation - add a new representation to a typed instance And I press "bim.assign_class" And I press "bim.add_type_instance" And I press "bim.add_type_instance" - Then the object "IfcWall/Instance" has a "MappedRepresentation" representation of "Model/Body/MODEL_VIEW" - And the object "IfcWall/Instance.001" has a "MappedRepresentation" representation of "Model/Body/MODEL_VIEW" + Then the object "IfcWall/Instance" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" + And the object "IfcWall/Instance.001" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" When the object "IfcWall/Instance" is selected And the variable "context" is "{ifc}.by_type('IfcGeometricRepresentationSubContext')[-1].id()" And I set "scene.BIMProperties.contexts" to "{context}" And I press "bim.add_representation" - Then the object "IfcWall/Instance" has a "MappedRepresentation" representation of "Plan/Annotation/PLAN_VIEW" - And the object "IfcWall/Instance.001" has a "MappedRepresentation" representation of "Plan/Annotation/PLAN_VIEW" + Then the object "IfcWall/Instance" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" + And the object "IfcWall/Instance.001" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" Scenario: Switch representation Given an empty IFC project diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index 032b0791f1..c6ab626fdb 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -27,6 +27,27 @@ Scenario: Add type instance - add from an empty When I press "bim.add_type_instance" Then the object "IfcWall/Instance" exists +Scenario: Add type instance - add a mesh where existing instances have changed context + 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.BIMTypeProperties.ifc_class" to "IfcWallType" + And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" + And I set "scene.BIMTypeProperties.relating_type" to "{cube}" + And I press "bim.add_type_instance" + And the object "IfcWall/Instance" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" + And the object "IfcWall/Instance" is selected + And the variable "context" is "{ifc}.by_type('IfcGeometricRepresentationSubContext')[-1].id()" + And I set "scene.BIMProperties.contexts" to "{context}" + And I press "bim.add_representation" + And the object "IfcWall/Instance" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" + When I press "bim.add_type_instance" + Then the object "IfcWall/Instance" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" + And the object "IfcWall/Instance.001" data is a "Annotation2D" representation of "Plan/Annotation/PLAN_VIEW" + Scenario: Add grid Given an empty IFC project When I press "mesh.add_grid" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index decf07405e..b5844574f2 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -228,6 +228,7 @@ def the_object_name_has_a_body_of_value(name, value): assert the_object_name_exists(name).data.body == value +@given(parsers.parse('the object "{name}" has a "{type}" representation of "{context}"')) @then(parsers.parse('the object "{name}" has a "{type}" representation of "{context}"')) def the_object_name_has_a_representation_type_of_context(name, type, context): ifc = an_ifc_file_exists() @@ -238,6 +239,19 @@ def the_object_name_has_a_representation_type_of_context(name, type, context): assert rep.RepresentationType == type +@given(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"')) +@then(parsers.parse('the object "{name}" data is a "{type}" representation of "{context}"')) +def the_object_name_has_a_representation_type_of_context(name, type, context): + ifc = an_ifc_file_exists() + context, subcontext, target_view = context.split("/") + rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id) + assert rep + assert rep.RepresentationType == type + assert rep.ContextOfItems.ContextType == context + assert rep.ContextOfItems.ContextIdentifier == subcontext + assert rep.ContextOfItems.TargetView == target_view + + @then(parsers.parse('the material "{name}" exists')) def the_material_name_exists(name) -> bpy.types.Material: obj = bpy.data.materials.get(name) diff --git a/src/blenderbim/test/core/test_type.py b/src/blenderbim/test/core/test_type.py index d117fb67fd..396917817f 100644 --- a/src/blenderbim/test/core/test_type.py +++ b/src/blenderbim/test/core/test_type.py @@ -21,36 +21,21 @@ from test.core.bootstrap import ifc, type class TestAssignType: - def test_assigning_and_switching_preferably_to_a_body_representation(self, ifc, type): + def test_assigning_and_switching_to_an_existing_type_data(self, ifc, type): ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() type.has_material_usage("element").should_be_called().will_return(False) - type.get_body_representation("element").should_be_called().will_return("mapped_rep") + ifc.get_object("type").should_be_called().will_return("type_obj") + type.get_object_data("type_obj").should_be_called().will_return("type_obj_data") + type.change_object_data("obj", "type_obj_data", is_global=False).should_be_called() ifc.get_object("element").should_be_called().will_return("obj") - type.has_dynamic_voids("obj").should_be_called().will_return(False) - type.run_geometry_switch_representation( - obj="obj", representation="mapped_rep", should_reload=False, enable_dynamic_voids=False, is_global=False - ).should_be_called() type.disable_editing("obj").should_be_called() subject.assign_type(ifc, type, element="element", type="type") - def test_assigning_and_switching_to_any_representation_as_a_fallback(self, ifc, type): + def test_assigning_and_not_changing_data_if_the_type_has_no_data(self, ifc, type): ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() type.has_material_usage("element").should_be_called().will_return(False) - type.get_body_representation("element").should_be_called().will_return(None) - type.get_any_representation("element").should_be_called().will_return("mapped_rep") - ifc.get_object("element").should_be_called().will_return("obj") - type.has_dynamic_voids("obj").should_be_called().will_return(False) - type.run_geometry_switch_representation( - obj="obj", representation="mapped_rep", should_reload=False, enable_dynamic_voids=False, is_global=False - ).should_be_called() - type.disable_editing("obj").should_be_called() - subject.assign_type(ifc, type, element="element", type="type") - - def test_assigning_and_not_changing_representation_if_there_is_no_representation_to_change_to(self, ifc, type): - ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called() - type.has_material_usage("element").should_be_called().will_return(False) - type.get_body_representation("element").should_be_called().will_return(None) - type.get_any_representation("element").should_be_called().will_return(None) + ifc.get_object("type").should_be_called().will_return("type_obj") + type.get_object_data("type_obj").should_be_called().will_return(None) ifc.get_object("element").should_be_called().will_return("obj") type.disable_editing("obj").should_be_called() subject.assign_type(ifc, type, element="element", type="type") diff --git a/src/blenderbim/test/tool/test_type.py b/src/blenderbim/test/tool/test_type.py index dc3783d211..689866682e 100644 --- a/src/blenderbim/test/tool/test_type.py +++ b/src/blenderbim/test/tool/test_type.py @@ -29,6 +29,26 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), blenderbim.core.tool.Type) +class TestChangeObjectData(NewFile): + def test_change_single_object_data(self): + data1 = bpy.data.meshes.new("Mesh") + data2 = bpy.data.meshes.new("Mesh") + obj1 = bpy.data.objects.new("Object", data1) + obj2 = bpy.data.objects.new("Object", data1) + subject.change_object_data(obj1, data2, is_global=False) + assert obj1.data == data2 + assert obj2.data == data1 + + def test_change_object_data_globally(self): + data1 = bpy.data.meshes.new("Mesh") + data2 = bpy.data.meshes.new("Mesh") + obj1 = bpy.data.objects.new("Object", data1) + obj2 = bpy.data.objects.new("Object", data1) + subject.change_object_data(obj1, data2, is_global=True) + assert obj1.data == data2 + assert obj2.data == data2 + + class TestDisableEditing(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) @@ -37,22 +57,6 @@ class TestDisableEditing(NewFile): assert obj.BIMTypeProperties.is_editing_type is False -class TestGetAnyRepresentation(NewFile): - def test_get_product_representation(self): - ifc = ifcopenshell.file() - representation = ifc.createIfcShapeRepresentation() - element = ifc.createIfcWall(Representation=ifc.createIfcProductRepresentation(Representations=[representation])) - assert subject.get_any_representation(element) == representation - - def test_get_type_product_representation(self): - ifc = ifcopenshell.file() - representation = ifc.createIfcShapeRepresentation() - element = ifc.createIfcWallType( - RepresentationMaps=[ifc.createIfcRepresentationMap(MappedRepresentation=representation)] - ) - assert subject.get_any_representation(element) == representation - - class TestGetBodyContext(NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -110,6 +114,13 @@ class TestGetIfcRepresentationClass(NewFile): assert subject.get_ifc_representation_class(ifc.createIfcColumn()) is None +class TestGetObjectData(NewFile): + def test_run(self): + data = bpy.data.meshes.new("Mesh") + obj = bpy.data.objects.new("Object", data) + assert subject.get_object_data(obj) == obj.data + + class TestGetProfileSetUsage(NewFile): def test_getting_a_profile_set_usage(self): ifc = ifcopenshell.file()