diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index a1ff930408..7d2907f11f 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -125,15 +125,25 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator): "Will unassign element from a type if type has a representation." ) bl_options = {"REGISTER", "UNDO"} + mode: bpy.props.StringProperty() @classmethod def poll(cls, context): - return context.selected_objects and tool.Ifc.get() + return context.selected_objects def _execute(self, context): cutter = context.active_object objs = [o for o in context.selected_objects if o != cutter] + if not tool.Ifc.get(): + if self.mode == "BOOLEAN": + tool.Misc.boolean_objects_with_cutter(objs, cutter) + elif self.mode == "BISECT": + tool.Misc.bisect_objects_with_cutter(objs, cutter) + for obj in objs: + bpy.data.objects.remove(obj) + return + objs_to_cut = [] # Splitting only works on meshes for obj in objs: @@ -169,7 +179,11 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator): objs_to_cut.append(obj) - new_objs = tool.Misc.split_objects_with_cutter(objs_to_cut, cutter) + if self.mode == "BOOLEAN": + new_objs = tool.Misc.boolean_objects_with_cutter(objs_to_cut, cutter) + elif self.mode == "BISECT": + new_objs = tool.Misc.bisect_objects_with_cutter(objs_to_cut, cutter) + for obj in new_objs: bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj) bpy.ops.bim.update_representation(obj=obj.name) diff --git a/src/bonsai/bonsai/bim/module/misc/ui.py b/src/bonsai/bonsai/bim/module/misc/ui.py index 7e33017619..c009b720e4 100644 --- a/src/bonsai/bonsai/bim/module/misc/ui.py +++ b/src/bonsai/bonsai/bim/module/misc/ui.py @@ -39,8 +39,9 @@ class BIM_PT_misc_utilities(bpy.types.Panel): row = layout.split(factor=0.2, align=True) row.prop(props, "total_storeys", text="") row.operator("bim.resize_to_storey").total_storeys = props.total_storeys - row = layout.row() - row.operator("bim.split_along_edge") + row = layout.row(align=True) + row.operator("bim.split_along_edge", text="Split Along Edge").mode = "BOOLEAN" + row.operator("bim.split_along_edge", text="Bisect At Faces").mode = "BISECT" row = layout.row() row.operator("bim.get_connected_system_elements") row = layout.row() diff --git a/src/bonsai/bonsai/tool/misc.py b/src/bonsai/bonsai/tool/misc.py index db43a14c5d..db99dd89d2 100644 --- a/src/bonsai/bonsai/tool/misc.py +++ b/src/bonsai/bonsai/tool/misc.py @@ -99,7 +99,7 @@ class Misc(bonsai.core.tool.Misc): bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) @classmethod - def split_objects_with_cutter( + def boolean_objects_with_cutter( cls, objs: list[bpy.types.Object], cutter: bpy.types.Object ) -> list[bpy.types.Object]: cutter_mesh = cutter.data @@ -144,3 +144,74 @@ class Misc(bonsai.core.tool.Misc): bm.free() bm_flipped.free() return new_objs + + @classmethod + def bisect_objects_with_cutter( + cls, objs: list[bpy.types.Object], cutter: bpy.types.Object + ) -> list[bpy.types.Object]: + cutter_mesh = cutter.data + assert isinstance(cutter_mesh, bpy.types.Mesh) + + bm = bmesh.new() + bm.from_mesh(cutter_mesh) + bm.faces.ensure_lookup_table() + + planes = [] + for face in bm.faces: + no = face.normal.to_4d() + no.w = 0.0 + no = cutter.matrix_world @ no + co = cutter.matrix_world @ face.verts[0].co + planes.append((co, no)) + + bm.free() + + new_objs = [] + for obj in objs: + if not isinstance(obj.data, bpy.types.Mesh) or obj == cutter: + continue + matrix_i = obj.matrix_world.inverted() + bm_obj = bmesh.new() + bm_obj.from_mesh(obj.data) + + bms = [bm_obj.copy()] + for co, no in planes: + co = matrix_i @ co + no = (matrix_i @ no).to_3d() + new_bms = [] + for bm in bms: + bm1 = bm.copy() + bm2 = bm.copy() + geom = bm1.verts[:] + bm1.edges[:] + bm1.faces[:] + bisect1 = bmesh.ops.bisect_plane( + bm1, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_inner=True + ) + if not bisect1["geom"] or not [g for g in bisect1["geom"] if isinstance(g, bmesh.types.BMFace)]: + new_bms.append(bm) + continue + edges = [g for g in bisect1["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + bmesh.ops.triangle_fill(bm1, use_dissolve=True, edges=edges) + geom = bm2.verts[:] + bm2.edges[:] + bm2.faces[:] + bisect2 = bmesh.ops.bisect_plane( + bm2, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True + ) + if not bisect2["geom"] or not [g for g in bisect2["geom"] if isinstance(g, bmesh.types.BMFace)]: + new_bms.append(bm) + continue + edges = [g for g in bisect2["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + bmesh.ops.triangle_fill(bm2, use_dissolve=True, edges=edges) + + new_bms.append(bm1) + new_bms.append(bm2) + bms = new_bms + + for bm in bms: + mesh = obj.data + new_obj = obj.copy() + new_obj.data = mesh.copy() + bm.to_mesh(new_obj.data) + for collection in obj.users_collection: + collection.objects.link(new_obj) + new_objs.append(new_obj) + + return new_objs