SplitMeshWithCurve Triangulates Mesh - Why?

I have a quadmesh and a closed polyline I want to use to split it.
The edit points of the polyline match the vertices of the mesh.
I run SplitMeshWithCurve and I get a triangulated mesh.

Why does this happen? How can i split the mesh and keep the faces as they are?


MeshSplit is even worse, creates bad results…

Hi Bogdan -

That seems to be mesh-specific.

Could you post that 3dm file?

Have you tried ExtractMeshFaces, and then, perhaps, SelBrush?
-wim

Hi @wim
I attached the file for you, with a sample mesh and a curve.

SplitMeshWithCurve triangulates mesh 250711.3dm (2.3 MB)

ExtractMeshFaces with SelBrush works, but in this case I really need to define regions and use SplitMeshWithCurve. Otherwise I do the work twice: draw the polylines and ExtractMeshFaces…

(the actual model is around 5k+ quads)

Any extra feedback on this issue @wim ? Thanks!


#Extract Mesh Faces By ClosedPolyline
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
import math
from collections import deque


# =========================
# SETTINGS
# =========================
SAMPLES_PER_SEGMENT = 12
EDGE_TOL_FACTOR = 2.0
INCLUDE_BOUNDARY = True   # True = output inside + boundary, False = inside only


# =========================
# GEOMETRY HELPERS
# =========================
def point_segment_distance(pt, a, b):
    ax, ay, az = a.X, a.Y, a.Z
    bx, by, bz = b.X, b.Y, b.Z
    px, py, pz = pt.X, pt.Y, pt.Z

    abx = bx - ax
    aby = by - ay
    abz = bz - az

    apx = px - ax
    apy = py - ay
    apz = pz - az

    ab_len_sq = abx * abx + aby * aby + abz * abz
    if ab_len_sq <= 1e-20:
        dx = px - ax
        dy = py - ay
        dz = pz - az
        return math.sqrt(dx * dx + dy * dy + dz * dz)

    t = (apx * abx + apy * aby + apz * abz) / ab_len_sq
    if t < 0.0:
        t = 0.0
    elif t > 1.0:
        t = 1.0

    cx = ax + t * abx
    cy = ay + t * aby
    cz = az + t * abz

    dx = px - cx
    dy = py - cy
    dz = pz - cz

    return math.sqrt(dx * dx + dy * dy + dz * dz)


def face_center(mesh, fi):
    f = mesh.Faces[fi]
    if f.IsTriangle:
        ids = [f.A, f.B, f.C]
    else:
        ids = [f.A, f.B, f.C, f.D]

    sx = sy = sz = 0.0
    for vid in ids:
        v = mesh.Vertices[vid]
        sx += v.X
        sy += v.Y
        sz += v.Z

    n = float(len(ids))
    return Rhino.Geometry.Point3d(sx / n, sy / n, sz / n)


# =========================
# PREVIEW / OUTPUT
# =========================
def add_faces_to_new_mesh(mesh, face_ids):
    out_mesh = Rhino.Geometry.Mesh()
    vmap = {}

    for fi in face_ids:
        f = mesh.Faces[fi]

        if f.IsTriangle:
            src = [f.A, f.B, f.C]
        else:
            src = [f.A, f.B, f.C, f.D]

        dst = []
        for vid in src:
            if vid not in vmap:
                v = mesh.Vertices[vid]
                new_id = out_mesh.Vertices.Add(v.X, v.Y, v.Z)
                vmap[vid] = new_id
            dst.append(vmap[vid])

        if f.IsTriangle:
            out_mesh.Faces.AddFace(dst[0], dst[1], dst[2])
        else:
            out_mesh.Faces.AddFace(dst[0], dst[1], dst[2], dst[3])

    out_mesh.Normals.ComputeNormals()
    out_mesh.Compact()
    return out_mesh


# =========================
# STEP 1: BOUNDARY FACES
# =========================
def collect_faces_from_point_on_edge(mesh, face_index, pt, tol):
    result = set([face_index])

    top_edges = mesh.TopologyEdges
    face_edge_ids = top_edges.GetEdgesForFace(face_index)
    if not face_edge_ids:
        return result

    for ei in face_edge_ids:
        tv = top_edges.GetTopologyVertices(ei)
        a = mesh.TopologyVertices[tv.I]
        b = mesh.TopologyVertices[tv.J]

        d = point_segment_distance(pt, a, b)
        if d <= tol:
            connected = top_edges.GetConnectedFaces(ei)
            for fi in connected:
                result.add(fi)

    return result


def get_boundary_faces_by_polyline_sampling(mesh, curve, samples_per_segment=10, edge_tol=None):
    ok, pl = curve.TryGetPolyline()
    if not ok:
        return None, "Curve must be a polyline."

    pts = list(pl)
    if len(pts) < 2:
        return None, "Invalid polyline."

    if edge_tol is None:
        edge_tol = sc.doc.ModelAbsoluteTolerance * 2.0

    closed = pts[0].DistanceTo(pts[-1]) <= sc.doc.ModelAbsoluteTolerance
    if not closed:
        return None, "Polyline must be closed."

    face_ids = set()
    count = len(pts) - 1

    for i in range(count):
        a = pts[i]
        b = pts[i + 1]

        for j in range(samples_per_segment + 1):
            t = float(j) / float(samples_per_segment)
            p = Rhino.Geometry.Point3d(
                a.X + (b.X - a.X) * t,
                a.Y + (b.Y - a.Y) * t,
                a.Z + (b.Z - a.Z) * t
            )

            mp = mesh.ClosestMeshPoint(p, 0.0)
            if not mp:
                continue

            found = collect_faces_from_point_on_edge(mesh, mp.FaceIndex, p, edge_tol)
            face_ids.update(found)

    return sorted(face_ids), None


# =========================
# STEP 2: FACE ADJACENCY
# =========================
def build_face_adjacency(mesh):
    face_count = mesh.Faces.Count
    adjacency = [set() for _ in range(face_count)]

    top_edges = mesh.TopologyEdges
    for ei in range(top_edges.Count):
        connected = top_edges.GetConnectedFaces(ei)
        if connected and len(connected) >= 2:
            for i in range(len(connected)):
                for j in range(i + 1, len(connected)):
                    a = connected[i]
                    b = connected[j]
                    adjacency[a].add(b)
                    adjacency[b].add(a)

    return adjacency


def connected_components_excluding_boundary(mesh, boundary_faces):
    adjacency = build_face_adjacency(mesh)
    face_count = mesh.Faces.Count
    boundary = set(boundary_faces)

    visited = set()
    components = []

    for start in range(face_count):
        if start in visited or start in boundary:
            continue

        comp = []
        q = deque([start])
        visited.add(start)

        while q:
            f = q.popleft()
            comp.append(f)

            for nb in adjacency[f]:
                if nb in visited or nb in boundary:
                    continue
                visited.add(nb)
                q.append(nb)

        components.append(comp)

    return components, adjacency


# =========================
# STEP 3: CHOOSE INSIDE
# =========================
def choose_inside_component(mesh, boundary_faces, components, adjacency):
    """
    Heuristic:
    pick the smallest component that touches the boundary.
    """
    boundary = set(boundary_faces)
    touching = []

    for comp in components:
        comp_set = set(comp)
        touches = False

        for fi in comp:
            for nb in adjacency[fi]:
                if nb in boundary:
                    touches = True
                    break
            if touches:
                break

        if touches:
            touching.append(comp)

    if not touching:
        return None, "No inside region found."

    touching.sort(key=len)
    return touching[0], None


# =========================
# MAIN
# =========================
def main():
    tol = sc.doc.ModelAbsoluteTolerance

    mesh_id = rs.GetObject("Select mesh", rs.filter.mesh, preselect=True)
    if not mesh_id:
        return

    crv_id = rs.GetObject("Select closed polyline on mesh", rs.filter.curve, preselect=True)
    if not crv_id:
        return

    mesh = rs.coercemesh(mesh_id)
    curve = rs.coercecurve(crv_id)

    if not mesh:
        print("Invalid mesh.")
        return

    if not curve:
        print("Invalid curve.")
        return

    if not curve.IsClosed:
        print("Curve must be closed.")
        return

    boundary_faces, err = get_boundary_faces_by_polyline_sampling(
        mesh,
        curve,
        samples_per_segment=SAMPLES_PER_SEGMENT,
        edge_tol=tol * EDGE_TOL_FACTOR
    )
    if err:
        print(err)
        return

    if not boundary_faces:
        print("No boundary faces found.")
        return

    components, adjacency = connected_components_excluding_boundary(mesh, boundary_faces)
    inside_faces, err = choose_inside_component(mesh, boundary_faces, components, adjacency)
    if err:
        print(err)
        return

    if INCLUDE_BOUNDARY:
        final_faces = sorted(set(boundary_faces).union(set(inside_faces)))
    else:
        final_faces = sorted(set(inside_faces))

    out_mesh = add_faces_to_new_mesh(mesh, final_faces)
    sc.doc.Objects.AddMesh(out_mesh)
    sc.doc.Views.Redraw()

    print("Boundary faces: {}".format(len(boundary_faces)))
    print("Inside faces: {}".format(len(inside_faces)))
    print("Output faces: {}".format(len(final_faces)))


if __name__ == "__main__":
    main()