diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 9fbd631003..6a7e7ee631 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -104,6 +104,7 @@ classes = ( profile.ExtendProfile, profile.RecalculateProfile, profile.Rotate90, + profile.SplitProfile, profile.PatchNonParametricMepSegment, roof.GenerateHippedRoof, slab.DisableEditingExtrusionProfile, diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 1369ad3cb6..04677c167a 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -849,6 +849,57 @@ class DumbProfileJoiner: def create_matrix(self, p: Vector, x: Vector, y: Vector, z: Vector) -> Matrix: return Matrix([x, y, z, p]).to_4x4().transposed() + def split(self, profile1: bpy.types.Object, target: Vector) -> None: + element1 = tool.Ifc.get_entity(profile1) + if not element1: + return + + if tool.Ifc.is_moved(profile1): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=profile1) + + axis1 = self.get_profile_axis(profile1) + intersect, cut_percentage = mathutils.geometry.intersect_point_line(target, *axis1) + if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)): + return + + # Duplicate the profile element + profile2 = profile1.copy() + profile2.data = profile2.data.copy() + for collection in profile1.users_collection: + collection.objects.link(profile2) + bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=profile2) + element2 = tool.Ifc.get_entity(profile2) + + # Transfer ATEND connection from element1 to element2 + relating_element = None + relating_connection = None + description = None + for conn in list(element1.ConnectedTo): + if conn.is_a("IfcRelConnectsPathElements") and conn.RelatingConnectionType == "ATEND": + relating_element = conn.RelatedElement + relating_connection = conn.RelatedConnectionType + description = conn.Description + bonsai.core.geometry.remove_connection(tool.Geometry, connection=conn) + for conn in list(element1.ConnectedFrom): + if conn.is_a("IfcRelConnectsPathElements") and conn.RelatedConnectionType == "ATEND": + relating_element = conn.RelatingElement + relating_connection = conn.RelatingConnectionType + description = conn.Description + bonsai.core.geometry.remove_connection(tool.Geometry, connection=conn) + if relating_element: + ifcopenshell.api.geometry.connect_path( + tool.Ifc.get(), + relating_element=relating_element, + related_element=element2, + relating_connection=relating_connection, + related_connection="ATEND", + description=description, + ) + + # Recreate both profiles with split axes + self.recreate_profile(element1, profile1, [axis1[0], intersect], [axis1[0], intersect]) + self.recreate_profile(element2, profile2, [intersect, axis1[1]], [intersect, axis1[1]]) + def get_profile_axis(self, obj: bpy.types.Object) -> list[Vector]: z_values = [v[2] for v in obj.bound_box] return [ @@ -857,6 +908,29 @@ class DumbProfileJoiner: ] +class SplitProfile(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.split_profile" + bl_label = "Split Profile" + bl_options = {"REGISTER", "UNDO"} + bl_description = ( + "Split selected profile element into two elements at the Blender cursor location. " + "The cursor must be positioned on the element's axis." + ) + + @classmethod + def poll(cls, context): + if not tool.Model.has_selected_ifc_objects(): + cls.poll_message_set("No IFC objects selected.") + return False + return True + + def _execute(self, context): + selected_objs = tool.Model.get_selected_mesh_objects() + for obj in selected_objs: + DumbProfileJoiner().split(obj, context.scene.cursor.location) + return {"FINISHED"} + + class RecalculateProfile(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.recalculate_profile" bl_label = "Recalculate Profile" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 828fc2c21f..58d6058eb6 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -937,6 +937,14 @@ class EditObjectUI: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Mitre", "S_Y", "", ui_context) row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row + add_layout_hotkey_operator( + row, + "Split", + "S_K", + "Split selected Element into two Elements at the cursor location\n\nHotkey: ⇧ K", + ui_context, + ) + row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context) else: @@ -1361,6 +1369,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": bpy.ops.bim.split_wall() + elif self.active_material_usage == "PROFILE": + bpy.ops.bim.split_profile() def hotkey_S_T(self): if not bpy.context.selected_objects: