diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py index 8bfdb7d574..97948f75c7 100644 --- a/src/bonsai/bonsai/bim/module/geometry/data.py +++ b/src/bonsai/bonsai/bim/module/geometry/data.py @@ -46,18 +46,51 @@ class ViewportData: @classmethod def mode(cls): + obj_mode = ("OBJECT", "IFC Object Mode", "View and move the placements of objects", "OBJECT_DATAMODE", 0) + item_mode = ("ITEM", "IFC Item Mode", "View individual representation items", "MESH_DATA", 1) + edit_mode = ("EDIT", "IFC Edit Mode", "Edit representation items", "EDITMODE_HLT", 2) + obj = bpy.context.active_object - modes = [ - ("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0), - ("ITEM", "IFC Item Mode", "", "MESH_DATA", 1), - ] - if ( - not obj - or not tool.Blender.is_editable(obj) - or ((element := tool.Ifc.get_entity(obj)) and tool.Geometry.is_locked(element)) - ): + print("checking", obj) + element = tool.Ifc.get_entity(obj) + + modes = [obj_mode] + + if bpy.context.scene.BIMGeometryProperties.representation_obj: + modes.append(item_mode) + + if not obj: return modes - modes.append(("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 2)) + + if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + pass + elif element: + if (usage := tool.Model.get_usage_type(element)) and usage in ("LAYER1", "LAYER2"): + pass + elif tool.Geometry.is_locked(element): + pass + elif usage == "PROFILE": + modes.append(edit_mode) + elif usage == "LAYER3": + modes.append(edit_mode) + elif obj.data and tool.Geometry.is_profile_based(obj.data): + modes.append(edit_mode) + elif element.is_a("IfcRelSpaceBoundary"): + modes.append(edit_mode) + elif element.is_a("IfcGridAxis"): + modes.append(edit_mode) + elif tool.Blender.Modifier.is_editing_parameters(obj): + # This should go BEFORE the modifiers + pass + elif tool.Blender.Modifier.is_roof(element): + modes.append(edit_mode) + elif tool.Blender.Modifier.is_railing(element): + modes.append(edit_mode) + elif item_mode not in modes: + modes.append(item_mode) + elif tool.Geometry.is_representation_item(obj): + modes.append(edit_mode) + return modes diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index d204754d8a..6d545926fd 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1576,135 +1576,91 @@ class OverridePasteBuffer(bpy.types.Operator): class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): - bl_description = "Switch from Object mode to Edit mode" + bl_description = "Switch from Object to Item to Edit mode" bl_idname = "bim.override_mode_set_edit" bl_label = "IFC Mode Set Edit" bl_options = {"REGISTER", "UNDO"} + def invoke(self, context, event): + return IfcStore.execute_ifc_operator(self, context, is_invoke=True) + + def _invoke(self, context, event): + if not tool.Ifc.get(): + return tool.Blender.toggle_edit_mode(context) + return self.execute(context) + def _execute(self, context): - selected_objs = tool.Blender.get_selected_objects() - active_obj = context.active_object + selected_objs = context.selected_objects # Purposely exclude active object - if context.active_object: - context.active_object.select_set(True) + if len(selected_objs) == 1 and context.active_object: + self.handle_single_object(context, context.active_object) + elif len(selected_objs) == 0: + tool.Geometry.disable_item_mode() + elif len(selected_objs) > 1: + self.handle_multiple_selected_objects(context) - element = tool.Ifc.get_entity(context.active_object) - if element and element.is_a("IfcRelSpaceBoundary"): - return bpy.ops.bim.enable_editing_boundary_geometry() - - for obj in selected_objs: - if obj == context.scene.BIMGeometryProperties.representation_obj: - self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly.") - obj.select_set(False) - continue - if tool.Geometry.is_representation_item(obj): - return self.enable_editing_representation_item(context, obj) - if not tool.Blender.is_editable(obj): - self.report({"INFO"}, f"Object cannot be edited: {obj.name}") - obj.select_set(False) - continue - element = tool.Ifc.get_entity(obj) - if not element: - continue - if tool.Geometry.is_locked(element): - self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be edited.") - obj.select_set(False) - continue - representation = tool.Geometry.get_active_representation(obj) - if not representation: - continue - if tool.Blender.Modifier.is_modifier_with_non_editable_path(element): - obj.select_set(False) - continue - - usage_type = tool.Model.get_usage_type(element) - is_profile = True - representation_class = tool.Geometry.get_ifc_representation_class(element, representation) - if usage_type == "PROFILE": - operator = lambda: bpy.ops.bim.hotkey(hotkey="A_E") - elif representation_class and "IfcCircleProfileDef" in representation_class: - self.report({"INFO"}, "Can't edit Circle Profile Extrusion") - obj.select_set(False) - continue - elif ( - tool.Geometry.is_profile_based(obj.data) - or usage_type == "LAYER3" - or tool.Geometry.is_swept_profile(representation) - ): - operator = lambda: bpy.ops.bim.hotkey(hotkey="S_E") + def handle_single_object(self, context, obj): + element = tool.Ifc.get_entity(obj) + if obj == context.scene.BIMGeometryProperties.representation_obj: + self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly") + elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + self.report({"ERROR"}, "Clipping planes cannot be edited") + elif element: + if (usage := tool.Model.get_usage_type(element)) and usage in ("LAYER1", "LAYER2"): + self.report({"INFO"}, f"Parametric {usage} elements cannot be edited directly") + elif tool.Geometry.is_locked(element): + self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be edited") + elif usage == "PROFILE": + bpy.ops.bim.hotkey(hotkey="A_E") + elif usage == "LAYER3": + bpy.ops.bim.hotkey(hotkey="S_E") + elif obj.data and tool.Geometry.is_profile_based(obj.data): + bpy.ops.bim.hotkey(hotkey="S_E") + elif element.is_a("IfcRelSpaceBoundary"): + bpy.ops.bim.enable_editing_boundary_geometry() + elif element.is_a("IfcGridAxis"): + self.enable_edit_mode(context) elif tool.Blender.Modifier.is_editing_parameters(obj): # This should go BEFORE the modifiers - self.report({"INFO"}, "Can't edit path while the modifier parameters are being modified") - obj.select_set(False) - continue + self.report({"INFO"}, "Can't edit while modifier parameters are being modified") elif tool.Blender.Modifier.is_roof(element): - operator = lambda: bpy.ops.bim.enable_editing_roof_path() + bpy.ops.bim.enable_editing_roof_path() elif tool.Blender.Modifier.is_railing(element): - operator = lambda: bpy.ops.bim.enable_editing_railing_path() + bpy.ops.bim.enable_editing_railing_path() else: - is_profile = False - if is_profile: - if len(context.selected_objects) == 1 and context.active_object == context.selected_objects[0]: + bpy.ops.bim.import_representation_items() + elif tool.Geometry.is_representation_item(obj): + self.enable_editing_representation_item(context, obj) + + def handle_multiple_selected_objects(self, context): + obj = context.active_object + if obj and (tool.Ifc.get_entity(obj) or tool.Geometry.is_representation_item(obj)): + tool.Blender.select_and_activate_single_object(context, obj) + self.handle_single_object(context, obj) + else: + blender_objs = [] + ifc_objs = [] + for selected_obj in tool.Blender.get_selected_objects(): + if tool.Ifc.get_entity(selected_obj) or tool.Geometry.is_representation_item(selected_obj): + ifc_objs.append(selected_obj) + else: + blender_objs.append(selected_obj) + + if blender_objs: + for obj in ifc_objs: + obj.select_set(False) + context.view_layer.objects.active = blender_objs[0] + blender_objs[0].select_set(True) + return tool.Blender.toggle_edit_mode(context) + elif ifc_objs: + obj = ifc_objs[0] tool.Blender.select_and_activate_single_object(context, obj) - operator() - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "EDIT": - context.scene.BIMGeometryProperties.mode = "EDIT" - context.scene.BIMGeometryProperties.is_changing_mode = False - return {"FINISHED"} - self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.") - obj.select_set(False) - continue - - if tool.Geometry.is_meshlike(representation): - tool.Ifc.edit(obj) - if getattr(element, "HasOpenings", None): - # 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, - ) - if isinstance(obj.data, bpy.types.Mesh): - tool.Geometry.apply_item_ids_as_vertex_groups(obj) - tool.Geometry.dissolve_triangulated_edges(obj) - obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data) - else: - obj.select_set(False) - continue - if len(selected_objs) > 1 and ( - not context.selected_objects or len(context.selected_objects) != len(selected_objs) - ): - # We are trying to edit at least one non-mesh-like object : Display a hint to the user - self.report({"INFO"}, "Only mesh-compatible representations may be edited concurrently in edit mode.") - - if context.active_object not in context.selected_objects: - # The active object is non-mesh-like. Set a valid object (or None) as active - context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None - if context.active_object: - if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}: - return {"CANCELLED"} - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "EDIT": - context.scene.BIMGeometryProperties.mode = "EDIT" - context.scene.BIMGeometryProperties.is_changing_mode = False - return {"FINISHED"} - - # Restore the selection if nothing worked - for obj in selected_objs: - obj.select_set(True) - context.view_layer.objects.active = active_obj - return {"FINISHED"} + self.handle_single_object(obj) def enable_editing_representation_item(self, context, obj): item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) if tool.Geometry.is_meshlike_item(item): + tool.Geometry.dissolve_triangulated_edges(obj) tool.Blender.select_and_activate_single_object(context, obj) obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data) self.enable_edit_mode(context) @@ -1727,16 +1683,9 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): context.scene.BIMGeometryProperties.mode = "EDIT" context.scene.BIMGeometryProperties.is_changing_mode = False - def invoke(self, context, event): - return IfcStore.execute_ifc_operator(self, context, is_invoke=True) - - def _invoke(self, context, event): - if not tool.Ifc.get(): - return tool.Blender.toggle_edit_mode(context) - return self.execute(context) - class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): + bl_description = "Switch from Edit to Item or Object mode" bl_idname = "bim.override_mode_set_object" bl_label = "IFC Mode Set Object" bl_options = {"REGISTER", "UNDO"} @@ -1746,15 +1695,14 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): return IfcStore.execute_ifc_operator(self, context, is_invoke=True) def _invoke(self, context, event): + if not tool.Ifc.get(): + return tool.Blender.toggle_edit_mode(context) + if not context.active_object: return {"FINISHED"} self.is_valid = True - if context.mode != "OBJECT": - bpy.ops.object.mode_set(mode="OBJECT") - - if not tool.Ifc.get(): - return {"FINISHED"} + tool.Blender.toggle_edit_mode(context) context.scene.BIMGeometryProperties.is_changing_mode = True if context.scene.BIMGeometryProperties.representation_obj: @@ -1864,10 +1812,8 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) - return else: tool.Geometry.import_item(obj) - return elif item.is_a("IfcSweptAreaSolid"): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) ProfileDecorator.uninstall() @@ -2344,10 +2290,9 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - tool.Geometry.apply_item_ids_as_vertex_groups(obj) - tool.Geometry.dissolve_triangulated_edges(obj) props = context.scene.BIMGeometryProperties props.representation_obj = obj + obj.hide_set(True) tool.Geometry.lock_object(obj) context.scene.BIMGeometryProperties.is_changing_mode = True @@ -2355,16 +2300,11 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): context.scene.BIMGeometryProperties.mode = "ITEM" context.scene.BIMGeometryProperties.is_changing_mode = False - item_ids = set() - for vgroup in obj.vertex_groups: - if vgroup.name.startswith("ios_item_id_"): - item_ids.add(int(vgroup.name.split("_")[-1])) - tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) tool.Loader.settings.context_settings = tool.Loader.create_settings() tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) - for item_id in item_ids: + for item_id in set(obj.data["ios_item_ids"]): item = tool.Ifc.get().by_id(item_id) item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}") item_mesh.BIMMeshProperties.ifc_definition_id = item_id @@ -2436,6 +2376,7 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator): bm.free() mesh.update() obj.matrix_world = props.representation_obj.matrix_world + obj.show_in_front = True new = props.item_objs.add() new.obj = obj @@ -2481,6 +2422,7 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator): matrix.translation = context.scene.cursor.location local_matrix = props.representation_obj.matrix_world.inverted() @ matrix + obj.show_in_front = True obj.matrix_world = matrix tool.Geometry.record_object_position(obj) diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 01b55f68e8..53a7f84d87 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -42,16 +42,22 @@ def get_contexts(self, context): def update_mode(self, context): if self.is_changing_mode: return - if self.mode == "OBJECT": - if self.representation_obj: - tool.Geometry.unlock_object(self.representation_obj) - tool.Geometry.sync_item_positions() - self.representation_obj = None - bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT") - elif self.mode == "ITEM": - bpy.ops.bim.import_representation_items() - elif self.mode == "EDIT": - bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT") + if context.mode.startswith("EDIT"): + if self.mode == "OBJECT": + bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT") + tool.Geometry.disable_item_mode() + elif self.mode == "ITEM": + bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT") + elif self.mode == "EDIT": + pass + else: + if self.mode == "OBJECT": + tool.Geometry.disable_item_mode() + elif self.mode == "ITEM": + if not self.representation_obj: + bpy.ops.bim.import_representation_items() + elif self.mode == "EDIT": + bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT") def update_representation_obj(self, context): diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 161130b877..e1cc04b1b9 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -111,6 +111,8 @@ def update_grid_is_locked(self, context): tool.Geometry.lock_object(obj) else: tool.Geometry.unlock_object(obj) + # Need to update ViewportData.mode. + bonsai.bim.handler.refresh_ui_data() def update_spatial_is_locked(self, context): diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index caca0843c0..fcbd7da8a6 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1472,3 +1472,16 @@ class Geometry(bonsai.core.tool.Geometry): obj.matrix_world = props.representation_obj.matrix_world @ position for vert in obj.data.vertices: vert.co = position_i @ vert.co + + @classmethod + def disable_item_mode(cls): + props = bpy.context.scene.BIMGeometryProperties + if props.representation_obj: + props.representation_obj.hide_set(False) + cls.unlock_object(props.representation_obj) + cls.sync_item_positions() + props.is_changing_mode = True + if props.mode != "OBJECT": + props.mode = "OBJECT" + props.is_changing_mode = False + props.representation_obj = None