diff --git a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py index a23e123354..665a8ee6ad 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/__init__.py @@ -27,6 +27,7 @@ classes = ( operator.OverrideDelete, operator.OverrideDuplicateMove, operator.OverrideDuplicateMoveLinked, + operator.OverrideModeSet, operator.OverrideOutlinerDelete, operator.OverridePasteBuffer, operator.RemoveConnection, @@ -59,10 +60,14 @@ def register(): kmi = km.keymap_items.new("bim.override_object_duplicate_move", "D", "PRESS", shift=True) kmi = km.keymap_items.new("bim.override_object_duplicate_move_linked", "D", "PRESS", alt=True) kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True) + kmi = km.keymap_items.new("bim.override_mode_set", "TAB", "PRESS") kmi = km.keymap_items.new("bim.override_object_delete", "X", "PRESS") kmi = km.keymap_items.new("bim.override_object_delete", "DEL", "PRESS") kmi.properties.confirm = False + km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY") + kmi = km.keymap_items.new("bim.override_mode_set", "TAB", "PRESS") + km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER") kmi = km.keymap_items.new("bim.override_paste_buffer", "V", "PRESS", ctrl=True) kmi = km.keymap_items.new("bim.override_outliner_delete", "X", "PRESS") diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 726ce584a1..7429285524 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -18,6 +18,8 @@ import bpy import bmesh +import struct +import hashlib import logging import numpy as np import ifcopenshell @@ -620,3 +622,126 @@ class OverridePasteBuffer(bpy.types.Operator): for obj in context.selected_objects: blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj) return {"FINISHED"} + + +class OverrideModeSet(bpy.types.Operator): + bl_idname = "bim.override_mode_set" + bl_label = "IFC Mode Set" + bl_options = {"REGISTER", "UNDO"} + should_save: bpy.props.BoolProperty(name="Should Save", default=True) + + @classmethod + def poll(cls, context): + return context.active_object + + def execute(self, context): + objs = context.selected_objects or [context.active_object] + context.active_object.select_set(True) + edited_objs = [] + for obj in objs: + element = tool.Ifc.get_entity(obj) + if not element: + continue + + if self.active_mode == "EDIT": + # We are switching from EDIT to OBJECT mode. + if obj.data.BIMMeshProperties.ifc_definition_id: + representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if representation.RepresentationType in ("Tessellation", "Brep"): + edited_objs.append(obj) + else: + # We are switching from OBJECT to EDIT mode. + usage_type = tool.Model.get_usage_type(element) + if usage_type: + # Parametric objects shall not be edited as meshes as they + # can be modified to be incompatible with the parametric + # constraints. + obj.select_set(False) + continue + + if obj.data.BIMMeshProperties.ifc_definition_id: + representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if representation.RepresentationType in ("Tessellation", "Brep"): + if element.HasOpenings: + # Mesh elements with openings must disable openings + # so that you can edit the original topology. + core.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + apply_openings=False, + ) + obj.data.BIMMeshProperties.mesh_checksum = self.get_mesh_checksum(obj.data) + if not context.selected_objects: + return {"FINISHED"} + bpy.ops.object.mode_set(mode="EDIT", toggle=True) + for obj in edited_objs: + if self.should_save and obj.data.BIMMeshProperties.mesh_checksum != self.get_mesh_checksum(obj.data): + bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="") + if element.HasOpenings: + representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + # Mesh elements with openings must disable openings + # so that you can edit the original topology. + core.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + apply_openings=True, + ) + else: + representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + core.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + apply_openings=True, + ) + + return {"FINISHED"} + + def draw(self, context): + row = self.layout.row(align=True) + if self.active_mode == "EDIT": + row.prop(self, "should_save") + + def invoke(self, context, event): + if not tool.Ifc.get(): + return bpy.ops.object.mode_set(mode="EDIT", toggle=True) + objs = context.selected_objects or [context.active_object] + obj = objs[0] + self.active_mode = obj.mode + if self.active_mode == "EDIT": + return context.window_manager.invoke_props_dialog(self) + return self.execute(context) + + def get_mesh_checksum(self, mesh): + # Get mesh data + vertices = mesh.vertices[:] + edges = mesh.edges[:] + faces = mesh.polygons[:] + + # Convert mesh data to bytes + data_bytes = b'' + for v in vertices: + data_bytes += struct.pack('3f', *v.co) + for e in edges: + data_bytes += struct.pack('2i', *e.vertices) + for f in faces: + data_bytes += struct.pack('%di' % len(f.vertices), *f.vertices) + + # Generate hash of mesh data + hasher = hashlib.sha1() + hasher.update(data_bytes) + return hasher.hexdigest() diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 3fdbbb8ac5..0fe6ebd8df 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -426,3 +426,4 @@ class BIMMeshProperties(PropertyGroup): ifc_definition: StringProperty(name="IFC Definition") ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) material_checksum: StringProperty(name="Material Checksum", default="[]") + mesh_checksum: StringProperty(name="Mesh Checksum", default="") diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 4a28f11ce3..bc7cbc60ff 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -86,6 +86,7 @@ def switch_representation( should_reload=True, is_global=True, should_sync_changes_first=False, + apply_openings=True, ): """Function can switch to representation that wasn't yet assigned to that object. See #2766.""" if should_sync_changes_first and geometry.is_edited(obj) and not geometry.is_box_representation(representation): @@ -98,7 +99,7 @@ def switch_representation( existing_data = geometry.get_representation_data(representation) if should_reload or not existing_data: - data = geometry.import_representation(obj, representation) + data = geometry.import_representation(obj, representation, apply_openings=apply_openings) geometry.rename_object(data, geometry.get_representation_name(representation)) geometry.link(representation, data) else: diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 8b9744e7ec..fca0b0dc24 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -169,7 +169,7 @@ class Geometry(blenderbim.core.tool.Geometry): return data.users != 0 @classmethod - def import_representation(cls, obj, representation): + def import_representation(cls, obj, representation, apply_openings=True): logger = logging.getLogger("ImportIFC") ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger) element = tool.Ifc.get_entity(obj) @@ -178,7 +178,7 @@ class Geometry(blenderbim.core.tool.Geometry): context = representation.ContextOfItems if context.ContextIdentifier == "Body" and context.TargetView == "MODEL_VIEW": - if element.is_a("IfcTypeProduct"): + if element.is_a("IfcTypeProduct") or not apply_openings: shape = ifcopenshell.geom.create_shape(settings, representation) else: shape = ifcopenshell.geom.create_shape(settings, element) diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 75c856e3f2..54381e95fe 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -20,7 +20,7 @@ Scenario: Add representation And the object "IfcWall/Cube" is selected Then the object "IfcWall/Cube" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" When the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()" - And I set "scene.BIMRootProperties.contexts" to "{context}" + And I set "active_object.BIMGeometryProperties.contexts" to "{context}" And I press "bim.add_representation" Then the object "IfcWall/Cube" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" @@ -37,7 +37,7 @@ Scenario: Add representation - add a new representation to a typed instance And the object "IfcWall/Wall.001" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" When the object "IfcWall/Wall" is selected And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()" - And I set "scene.BIMRootProperties.contexts" to "{context}" + And I set "active_object.BIMGeometryProperties.contexts" to "{context}" And I press "bim.add_representation" Then the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" And the object "IfcWall/Wall.001" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" @@ -80,7 +80,7 @@ Scenario: Switch representation - current edited representation is updated prior And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" And I press "bim.assign_class" And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier=='Annotation'][0].id()" - And I set "scene.BIMRootProperties.contexts" to "{context}" + And I set "active_object.BIMGeometryProperties.contexts" to "{context}" And I press "bim.add_representation" When the object "IfcWall/Cube" is scaled to "2" And the variable "representation" is "[r for r in {ifc}.by_type('IfcShapeRepresentation') if r.RepresentationType=='Tessellation'][0].id()" diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index b48b723e66..7544f8762a 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -41,7 +41,7 @@ Scenario: Add type instance - add a mesh where existing instances have changed c And the object "IfcWall/Wall" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Wall" is selected And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()" - And I set "scene.BIMRootProperties.contexts" to "{context}" + And I set "active_object.BIMGeometryProperties.contexts" to "{context}" And I press "bim.add_representation" And the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" When I press "bim.add_constr_type_instance" @@ -524,4 +524,4 @@ Scenario: Create a door, undo and create a new door And I undo And I press "mesh.add_door()" Then nothing happens - And the object "IfcDoor/IfcDoor" exists \ No newline at end of file + And the object "IfcDoor/IfcDoor" exists diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py index 4d28c32f8d..0f69eac15a 100644 --- a/src/blenderbim/test/core/test_geometry.py +++ b/src/blenderbim/test/core/test_geometry.py @@ -194,7 +194,7 @@ class TestSwitchRepresentation: 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("obj", "representation").should_be_called().will_return("new_data") + geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return("new_data") geometry.get_representation_name("representation").should_be_called().will_return("name") geometry.rename_object("new_data", "name").should_be_called() geometry.link("representation", "new_data").should_be_called() @@ -210,13 +210,14 @@ class TestSwitchRepresentation: should_reload=True, is_global=True, should_sync_changes_first=True, + apply_openings=True, ) def test_switching_to_a_reloaded_representation_and_deleting_the_existing_data(self, ifc, 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("obj", "representation").should_be_called().will_return("new_data") + geometry.import_representation("obj", "representation", apply_openings=True).should_be_called().will_return("new_data") geometry.get_representation_name("representation").should_be_called().will_return("name") geometry.rename_object("new_data", "name").should_be_called() geometry.link("representation", "new_data").should_be_called() @@ -233,6 +234,7 @@ class TestSwitchRepresentation: should_reload=True, is_global=True, should_sync_changes_first=True, + apply_openings=True, ) def test_switching_to_an_existing_representation(self, ifc, geometry):