diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index eac368e6e7..7dbe97b83b 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -101,6 +101,7 @@ class SwitchRepresentation(bpy.types.Operator, Operator): should_reload=self.should_reload, enable_dynamic_voids=self.disable_opening_subtractions, is_global=self.should_switch_all_meshes, + should_sync_changes_first=True, ) @@ -217,7 +218,7 @@ class UpdateRepresentation(bpy.types.Operator): 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() + core.get_representation_ifc_parameters(tool.Geometry, obj=obj) class UpdateParametricRepresentation(bpy.types.Operator): @@ -244,35 +245,20 @@ class UpdateParametricRepresentation(bpy.types.Operator): should_reload=True, enable_dynamic_voids=False, is_global=True, + should_sync_changes_first=False, ) if show_representation_parameters: - bpy.ops.bim.get_representation_ifc_parameters() + core.get_representation_ifc_parameters(tool.Geometry, obj=obj) return {"FINISHED"} -class GetRepresentationIfcParameters(bpy.types.Operator): +class GetRepresentationIfcParameters(bpy.types.Operator, Operator): bl_idname = "bim.get_representation_ifc_parameters" bl_label = "Get Representation IFC Parameters" bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - self.file = IfcStore.get_file() - obj = context.active_object - props = obj.data.BIMMeshProperties - elements = self.file.traverse(self.file.by_id(props.ifc_definition_id)) - props.ifc_parameters.clear() - for element in elements: - if element.is_a("IfcRepresentationItem") or element.is_a("IfcParameterizedProfileDef"): - for i in range(0, len(element)): - if element.attribute_type(i) == "DOUBLE": - new = props.ifc_parameters.add() - new.name = "{}/{}".format(element.is_a(), element.attribute_name(i)) - new.step_id = element.id() - new.type = element.attribute_type(i) - new.index = i - if element[i]: - new.value = element[i] - return {"FINISHED"} + def _execute(self, context): + core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object) class CopyRepresentation(bpy.types.Operator, Operator): diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 5cddf5583d..a2e994ac84 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -66,6 +66,7 @@ def mode_callback(obj, data): should_reload=True, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=False, ) IfcStore.edited_objs.add(obj) bm = bmesh.from_edit_mesh(obj.data) diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index d52d2b3fdc..4d9c8c6abd 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -238,6 +238,7 @@ class DynamicallyVoidProduct(bpy.types.Operator): should_reload=True, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=False, ) if was_edit_mode: bpy.ops.object.mode_set(mode="EDIT") @@ -296,6 +297,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings): should_reload=True, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=False, ) diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index 9f3c9f74ff..fb3ef464fd 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -220,6 +220,7 @@ class DumbProfileRegenerator: should_reload=True, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=False, ) def sync_object(self, element): diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 656e6c2e52..da7379cca9 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -1087,7 +1087,7 @@ class ImportP6(bpy.types.Operator, ImportHelper): def poll(cls, context): ifc_file = IfcStore.get_file() return ifc_file is not None - + def execute(self, context): from ifc4d.p62ifc import P62Ifc @@ -1114,7 +1114,7 @@ class ImportP6XER(bpy.types.Operator, ImportHelper): def poll(cls, context): ifc_file = IfcStore.get_file() return ifc_file is not None - + def execute(self, context): from ifc4d.p6xer2ifc import P6XER2Ifc @@ -1141,7 +1141,7 @@ class ImportPP(bpy.types.Operator, ImportHelper): def poll(cls, context): ifc_file = IfcStore.get_file() return ifc_file is not None - + def execute(self, context): from ifc4d.pp2ifc import PP2Ifc @@ -1168,7 +1168,7 @@ class ImportMSP(bpy.types.Operator, ImportHelper): def poll(cls, context): ifc_file = IfcStore.get_file() return ifc_file is not None - + def execute(self, context): from ifc4d.msp2ifc import MSP2Ifc diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index 589ec6cd5d..7e5edb1d38 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -129,6 +129,7 @@ class RemoveOpening(bpy.types.Operator): should_reload=True, enable_dynamic_voids=False, is_global=True, + should_sync_changes_first=False, ) Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index d0689bb4fd..9e123651f5 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -75,8 +75,17 @@ def add_representation( def switch_representation( - geometry, obj=None, representation=None, should_reload=True, enable_dynamic_voids=True, is_global=True + geometry, + obj=None, + representation=None, + should_reload=True, + enable_dynamic_voids=True, + is_global=True, + should_sync_changes_first=False, ): + if should_sync_changes_first and geometry.is_edited(obj) and not geometry.is_box_representation(representation): + geometry.run_geometry_update_representation(obj=obj) + representation = geometry.resolve_mapped_representation(representation) existing_data = geometry.get_representation_data(representation) @@ -98,6 +107,10 @@ def switch_representation( geometry.create_dynamic_voids(obj) +def get_representation_ifc_parameters(geometry, obj=None, should_sync_changes_first=False): + geometry.import_representation_parameters(geometry.get_object_data(obj)) + + def remove_representation(ifc, geometry, obj=None, representation=None): element = ifc.get_entity(obj) if geometry.is_mapped_representation(representation) or geometry.is_type_product(element): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 8ab8a3c730..1ab667844c 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -89,13 +89,17 @@ class Geometry: def get_total_representation_items(cls, obj): pass def has_data_users(cls, data): pass def import_representation(cls, obj, representation, enable_dynamic_voids=False): pass + def import_representation_parameters(cls, data): pass def is_body_representation(cls, representation): pass + def is_box_representation(cls, representation): pass + def is_edited(cls, obj): pass def is_mapped_representation(cls, representation): pass def is_type_product(cls, element): pass def link(cls, element, obj): pass def rename_object(cls, obj, name): pass def replace_object_with_empty(cls, obj): pass def resolve_mapped_representation(cls, representation): pass + def run_geometry_update_representation(cls, obj=None): pass def should_force_faceted_brep(cls): pass def should_force_triangulation(cls): pass def should_use_presentation_style_assignment(cls): pass diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 3cdbc526fb..170d2158fe 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -160,10 +160,35 @@ class Geometry(blenderbim.core.tool.Geometry): ifc_importer.material_creator.create(element, obj, mesh) return mesh + @classmethod + def import_representation_parameters(cls, data): + props = data.BIMMeshProperties + elements = tool.Ifc.get().traverse(tool.Ifc.get().by_id(props.ifc_definition_id)) + props.ifc_parameters.clear() + for element in elements: + if element.is_a("IfcRepresentationItem") or element.is_a("IfcParameterizedProfileDef"): + for i in range(0, len(element)): + if element.attribute_type(i) == "DOUBLE": + new = props.ifc_parameters.add() + new.name = "{}/{}".format(element.is_a(), element.attribute_name(i)) + new.step_id = element.id() + new.type = element.attribute_type(i) + new.index = i + if element[i]: + new.value = element[i] + @classmethod def is_body_representation(cls, representation): return representation.ContextOfItems.ContextIdentifier == "Body" + @classmethod + def is_box_representation(cls, representation): + return representation.ContextOfItems.ContextIdentifier == "Box" + + @classmethod + def is_edited(cls, obj): + return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs + @classmethod def is_mapped_representation(cls, representation): return representation.RepresentationType == "MappedRepresentation" @@ -200,6 +225,10 @@ class Geometry(blenderbim.core.tool.Geometry): return cls.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation) return representation + @classmethod + def run_geometry_update_representation(cls, obj=None): + bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="") + @classmethod def should_force_faceted_brep(cls): return bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep diff --git a/src/blenderbim/blenderbim/tool/type.py b/src/blenderbim/blenderbim/tool/type.py index 6aaab74026..16730d13c0 100644 --- a/src/blenderbim/blenderbim/tool/type.py +++ b/src/blenderbim/blenderbim/tool/type.py @@ -114,4 +114,5 @@ class Type(blenderbim.core.tool.Type): should_reload=should_reload, enable_dynamic_voids=enable_dynamic_voids, is_global=is_global, + should_sync_changes_first=False, ) diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index edbf8de1df..3ca93317b7 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -73,6 +73,37 @@ Scenario: Switch representation And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation})" Then nothing happens +Scenario: Switch representation - current edited representation is updated prior to switch + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + 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" + When the object "IfcWall/Cube" is scaled to "2" + And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" + And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" + And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[-1].id()" + And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" + When I press "export_ifc.bim(filepath='{cwd}/test/files/export.ifc')" + Then the object "IfcWall/Cube" dimensions are "4,4,0" + +Scenario: Switch representation - current edited representation is discarded if switching to a box + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + When the object "IfcWall/Cube" is scaled to "2" + And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[-1].id()" + And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" + And the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" + And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation}, should_reload=True)" + When I press "export_ifc.bim(filepath='{cwd}/test/files/export.ifc')" + Then the object "IfcWall/Cube" dimensions are "2,2,2" + Scenario: Switch representation - existing Blender modifiers must be purged Given an empty IFC project And I add a cube @@ -210,6 +241,16 @@ Scenario: Update representation - updating a profiled extrusion When I press "bim.update_representation(obj='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" +Scenario: Get representation IFC parameters + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I press "bim.update_representation(ifc_representation_class='IfcExtrudedAreaSolid/IfcRectangleProfileDef')" + When I press "bim.get_representation_ifc_parameters" + Then nothing happens + Scenario: Copy representation Given an empty IFC project And I add a cube diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py index db39002b8f..128b2c3b08 100644 --- a/src/blenderbim/test/core/test_geometry.py +++ b/src/blenderbim/test/core/test_geometry.py @@ -182,6 +182,7 @@ class TestAddRepresentation: class TestSwitchRepresentation: def test_switching_to_a_freshly_loaded_representation(self, geometry): + geometry.is_edited("obj").should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return(None) geometry.import_representation( @@ -201,9 +202,11 @@ class TestSwitchRepresentation: should_reload=True, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=True, ) def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, geometry): + geometry.is_edited("obj").should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return("existing_data") geometry.import_representation( @@ -224,9 +227,11 @@ class TestSwitchRepresentation: should_reload=True, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=True, ) def test_switching_to_an_existing_representation(self, geometry): + geometry.is_edited("obj").should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return("data") geometry.change_object_data("obj", "data", is_global=True).should_be_called() @@ -240,9 +245,11 @@ class TestSwitchRepresentation: should_reload=False, enable_dynamic_voids=True, is_global=True, + should_sync_changes_first=True, ) def test_switching_to_non_dynamic_baked_voids(self, geometry): + geometry.is_edited("obj").should_be_called().will_return(False) geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") geometry.get_representation_data("representation").should_be_called().will_return("data") geometry.change_object_data("obj", "data", is_global=False).should_be_called() @@ -254,8 +261,34 @@ class TestSwitchRepresentation: should_reload=False, enable_dynamic_voids=False, is_global=False, + should_sync_changes_first=True, ) + def test_updating_a_representation_if_the_blender_object_has_been_edited_prior_to_switching(self, geometry): + geometry.is_edited("obj").should_be_called().will_return(True) + geometry.is_box_representation("mapped_rep").should_be_called().will_return(False) + geometry.run_geometry_update_representation(obj="obj").should_be_called() + geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") + geometry.get_representation_data("representation").should_be_called().will_return("data") + geometry.change_object_data("obj", "data", is_global=False).should_be_called() + geometry.clear_modifiers("obj").should_be_called() + subject.switch_representation( + geometry, + obj="obj", + representation="mapped_rep", + should_reload=False, + enable_dynamic_voids=False, + is_global=False, + should_sync_changes_first=True, + ) + + +class TestGetRepresentationIfcParameters: + def test_run(self, geometry): + geometry.get_object_data("obj").should_be_called().will_return("data") + geometry.import_representation_parameters("data").should_be_called() + subject.get_representation_ifc_parameters(geometry, obj="obj", should_sync_changes_first=False) + class TestRemoveRepresentation: def test_removing_an_actively_used_mapped_representation_by_remapping_usages_to_an_empty(self, ifc, geometry): diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py index 97b7ee96f3..4e948a31d0 100644 --- a/src/blenderbim/test/tool/test_geometry.py +++ b/src/blenderbim/test/tool/test_geometry.py @@ -248,6 +248,24 @@ class TestImportRepresentation(NewFile): assert len(mesh.edges) == 4 +class TestImportRepresentationParameters(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + swept_area = ifc.createIfcCircleProfileDef(Radius=1) + item = ifc.createIfcExtrudedAreaSolid(SweptArea=swept_area, Depth=2) + representation = ifc.createIfcShapeRepresentation(Items=[item]) + data = bpy.data.meshes.new("Mesh") + data.BIMMeshProperties.ifc_definition_id = representation.id() + subject.import_representation_parameters(data) + assert data.BIMMeshProperties.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth" + assert data.BIMMeshProperties.ifc_parameters[0].step_id == item.id() + assert data.BIMMeshProperties.ifc_parameters[0].index == 3 + assert data.BIMMeshProperties.ifc_parameters[1].name == "IfcCircleProfileDef/Radius" + assert data.BIMMeshProperties.ifc_parameters[1].step_id == swept_area.id() + assert data.BIMMeshProperties.ifc_parameters[1].index == 3 + + class TestIsBodyRepresentation(NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -260,6 +278,30 @@ class TestIsBodyRepresentation(NewFile): assert subject.is_body_representation(representation) is False +class TestIsBoxRepresentation(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + context = ifc.createIfcGeometricRepresentationContext() + representation = ifc.createIfcShapeRepresentation() + representation.ContextOfItems = context + context.ContextIdentifier = "Box" + assert subject.is_box_representation(representation) is True + context.ContextIdentifier = "Clearance" + assert subject.is_box_representation(representation) is False + + +class TestIsEdited(NewFile): + def test_run(self): + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + assert subject.is_edited(obj) is False + obj.scale[0] = 2 + assert subject.is_edited(obj) is True + obj.scale[0] = 1 + assert subject.is_edited(obj) is False + IfcStore.edited_objs.add(obj) + assert subject.is_edited(obj) is True + + class TestIsMappedRepresentation(NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -327,6 +369,11 @@ class TestResolveMappedRepresentation(NewFile): assert subject.resolve_mapped_representation(representation) == representation +class TestRunGeometryUpdateRepresentation(NewFile): + def test_nothing(self): + pass + + class TestShouldForceFacetedBrep(NewFile): def test_run(self): result = bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep