How can I split mesh by edges?

I’m guessing that you copied the script into a GHPython component, right?
If that’s the case, make sure that you set the type hint for input M to Mesh and leave it set to Item Access. Then set input L to List Access and the type hint to Polyline.

You can also get rid of the Combine&Clean component. It’s superfluous if you’re using the second, improved version of my script. It can handle input meshes with unwelded egdes and even transfers the latter to the split meshes, it outputs.

Since, I’ve written the script in Rhino 8 and thus Python 3, I can’t guarantee that it’ll work in IronPython, but @laurent_delrieu seems to have had success in getting it running in Rhino 7.
I got rid of my Rhino 7 installation about a week ago, so I can’t test it.

Thank you! Got it.
I tested it, it works fine in Rhino 7 too.

You’re welcome.

I once wrote something similar, though with a different/slightly simpler method for getting the topology edges to unweld. It might be relevant to this great topic you are having (this is Rhino 7):


231230_SplitMeshAtPolyline_00.gh (12.5 KB)

Happy holidays to all :partying_face:

Thank you very much!
Happy New Year to all!

The only downside to this approach is that unwelded edges are used in Rhino to display hard-surface models, like the low-poly looking Bulbasaur, which when completely welded looks a bit weird and soft. This is why I came up with a more convoluted script that remembers the initial, unwelded mesh edges and transfers them to the split model, and the low-poly look of the model thus gets preserved.

Best wishes and good health for 2024 y’all! :clinking_glasses:

Ah yes totally, a more topologically explicit algorithm is probably also more appropriate/applicable in general. As I recall, the code I wrote was just a minimal method for introducing obstacles into navigation meshes. Afraid I can’t check your code, as I’m still on Rhino 7. But it did inspire me to extend my code to also track/reintroduce the original unwelded edges:


230104_SplitMeshAtPolyline_00.gh (17.9 KB)

Haha, you’re code is nice and concise and faster than mine. The trick with the mid points is neat!

Here’s what I did. To be honest, I implemented it quickly without much thought for performance.

"""Splits a mesh along an edge loop."""

__version__ = "0.25 (2023-12-29)"
__author__ = "diff-arch (diff-arch.xyz)"

import Rhino.Geometry as rg
import scriptcontext as sc
import copy

MTOL = sc.doc.ModelAbsoluteTolerance


def get_welding_data(mesh):
    """Returns a dictionary mapping mesh topology edge indices to a boolean,
        indicating whether the edge at index is welded or not."""
    welds = dict()
    for i in range(mesh.TopologyEdges.Count):
        welds[i] = not mesh.TopologyEdges.IsEdgeUnwelded(i)
    return welds

def get_welded(mesh):
    """Returns a completely welded copy of the input mesh."""
    new_mesh = rg.Mesh()
    new_mesh.CopyFrom(mesh)

    rtree = rg.RTree()
    for i in range(new_mesh.Vertices.Count):
        rtree.Insert(rg.Point3d(new_mesh.Vertices[i]), i)

    deleted = []
    for i in range(new_mesh.Vertices.Count):
        if i in deleted:
            continue
        
        vtx = new_mesh.Vertices[i]
        pt = rg.Point3d(vtx.X, vtx.Y, vtx.Z)
        search_sphere = rg.Sphere(pt, MTOL)

        closest = []
        rtree.Search(
            search_sphere,
            lambda sender, args: closest.append(args.Id) if i < args.Id else None
        )
        
        if len(closest) < 1:
            continue
            
        for j in closest:
            faces = new_mesh.Vertices.GetVertexFaces(j)
            edited = False
            for k in faces:
                face = new_mesh.Faces.GetFace(k)
                for f in range(4):
                    if face[f] == j:
                        face[f] = i
                        edited = True
                new_mesh.Faces.SetFace(k, face.A, face.B, face.C, face.D)
            if edited:
                deleted.append(j)
                rtree.Remove(rg.Point3d(new_mesh.Vertices[j]), j)
    
    new_mesh.Compact()
    new_mesh.RebuildNormals()
    return new_mesh


def get_topo_vertex_indices_rec(mesh, edge, __vindices=[], __count=0):
    """Returns exactly two indices of topology vertices of the mesh
        that are closest to the end points of the edge."""
    if len(__vindices) == 2 or __count > 1:
        return __vindices
        
    curr_indices = copy.deepcopy(__vindices)
    end_pts = [edge.From, edge.To]

    for i in range(M.TopologyVertices.Count):
        pt = rg.Point3d(M.TopologyVertices[i])
        if end_pts[len(curr_indices)].DistanceToSquared(pt) < MTOL**2:
            curr_indices.append(i)
            return get_topo_vertex_indices_rec(
                mesh, edge, curr_indices, __count=__count+1
            )


if __name__ == "__main__":
    welding_data = get_welding_data(M)
    welded_mesh = get_welded(M)

    edge_indices = []
    segment_count = 0
    for edge_loop in L:
        loop_segments = edge_loop.GetSegments()
        segment_count += len(loop_segments)
        for segment in loop_segments:
            vertex_indices = get_topo_vertex_indices_rec(welded_mesh, segment)
            if len(vertex_indices) != 2:
                continue

            v1, v2 = vertex_indices
            edge_idx = welded_mesh.TopologyEdges.GetEdgeIndex(v1, v2)
            edge_indices.append(edge_idx)

    if len(edge_indices) != segment_count:
        raise RuntimeError("Unable to detect all loop edges.")

    welded_mesh.UnweldEdge(edge_indices, True)
    
    prev_unwelded = [k for k, v in welding_data.items() if not v]
    fragments = welded_mesh.ExplodeAtUnweldedEdges()

    for mesh in fragments:
        edge_indices = []
        rtree = rg.RTree()
        for i in range(mesh.TopologyVertices.Count):
            rtree.Insert(rg.Point3d(mesh.TopologyVertices[i]), i)
        
        for ei in prev_unwelded:
            indices_pair = M.TopologyEdges.GetTopologyVertices(ei)  # original edge vertex pair indices
            closest = []
            for vi in indices_pair:
                vtx = rg.Point3d(M.TopologyVertices[vi])
                search_sphere = rg.Sphere(vtx, MTOL)
            
                rtree.Search(
                    search_sphere,
                    lambda sender, args: closest.append(args.Id)
                )
            
            if len(closest) < 2:
                continue

            edge_idx = mesh.TopologyEdges.GetEdgeIndex(closest[0], closest[1])
            if not mesh.TopologyEdges.IsEdgeUnwelded(edge_idx):
                edge_indices.append(edge_idx)

        mesh.UnweldEdge(edge_indices, True)
        mesh.UnifyNormals(False)

    F =  fragments

This should work in Rhino 7 as well and can handle multiple edge loops to split the mesh.

Well … I can barely imagine why not using FF+FE Conn plus an elementary Recursive grow on that puzzle (see my answer to Laurent)

Ah nice, it’s fun to see how many approaches there is to this seemingly simple problem. Thanks for sharing.

I totally overlooked that requirement, quick update:


230104_SplitMeshAtPolyline_01.gh (18.1 KB)

Hi All, I found this thread looking for advice on something else. I know it’s solved but I wanted to give a big shout out to the Leopard component library for its really efficient interactive mesh edge picker. If you ever need to combine parametric / automated mesh unweld / explode methods with manual adjustment to the processing (like I do for a current project), Leopard really knocks it out of the park.