Public SplitEdge method for Brep in RhinoCommon API

Hello Rhino developers,

I have encountered a limitation in the RhinoCommon API that prevents automation of certain modeling workflows. I would like to request an enhancement to the API.

Problem description:

In the Rhino UI, the _SplitEdge command allows users to split a Brep edge at specified points, resulting in new separate edges within the Brep topology.
However, in the public API (RhinoCommon, both C# and Python), there is currently no way to programmatically split a Brep edge so that new edges appear in the Brep.
The method BrepEdge.Split(t) only returns a curve, but does not alter the underlying Brep topology or create new edges on the Brep.

Why this matters:

  • It is not possible to create an automated plugin that splits Brep edges at intersection points with curves or surfaces in one step.
  • For complex models with many edges, manual splitting is extremely time-consuming and error-prone.
  • Duplicating edges as curves and splitting those curves does not solve the problem, since the result is just a set of auxiliary curves, not new Brep edges.

Here is a sample C# code illustrating an attempt to programmatically split Brep edges using RhinoCommon:

using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Input.Custom;
using Rhino.Geometry.Intersect;

public class SplitMultiEdgesCommand : Command
{
    public SplitMultiEdgesCommand()
    {
        Instance = this;
    }
    public static SplitMultiEdgesCommand Instance { get; private set; }
    public override string EnglishName => "SplitMultiEdges";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        // 1. Select edges (SubObject)
        var go = new GetObject();
        go.SetCommandPrompt("Select edges (sub-objects)");
        go.GeometryFilter = ObjectType.EdgeFilter;
        go.SubObjectSelect = true;
        go.EnablePreSelect(false, true);
        go.GetMultiple(1, 0);
        if (go.CommandResult() != Result.Success)
            return go.CommandResult();

        List<ObjRef> edge_refs = new List<ObjRef>();
        for (int i = 0; i < go.ObjectCount; i++)
            edge_refs.Add(go.Object(i));

        if (edge_refs.Count == 0)
            return Result.Cancel;

        // 2. Select splitting curves
        var go2 = new GetObject();
        go2.SetCommandPrompt("Select curves to split edges");
        go2.GeometryFilter = ObjectType.Curve;
        go2.DeselectAllBeforePostSelect = false;
        go2.GetMultiple(1, 0);
        if (go2.CommandResult() != Result.Success)
            return go2.CommandResult();

        List<Curve> knives = new List<Curve>();
        for (int i = 0; i < go2.ObjectCount; i++)
        {
            var crv = go2.Object(i).Curve();
            if (crv != null)
                knives.Add(crv.DuplicateCurve());
        }
        if (knives.Count == 0)
            return Result.Cancel;

        // 3. Map: for each brep -> list of edges for split and objectId
        var brepEdgeMap = new Dictionary<Brep, List<int>>();
        var brepGuidMap = new Dictionary<Brep, System.Guid>();
        foreach (var edgeRef in edge_refs)
        {
            var edge = edgeRef.Edge();
            var brep = edge.Brep;
            if (!brepEdgeMap.ContainsKey(brep))
            {
                brepEdgeMap[brep] = new List<int>();
                var brepObjId = edgeRef.ObjectId; // <--- IMPORTANT: take ObjectId from ObjRef!
                brepGuidMap[brep] = brepObjId;
            }
            brepEdgeMap[brep].Add(edge.EdgeIndex);
        }

        // 4. For each Brep work with the required edges
        foreach (var kv in brepEdgeMap)
        {
            var brep = kv.Key;
            var edgeIndices = kv.Value;
            var brepGuid = brepGuidMap[brep];

            // For each edge — find intersection points with splitting curves
            foreach (int ei in edgeIndices)
            {
                var edge = brep.Edges[ei];
                var edgeCurve = edge.DuplicateCurve();
                List<double> splitParams = new List<double>();
                foreach (var knife in knives)
                {
                    var x = Intersection.CurveCurve(edgeCurve, knife, doc.ModelAbsoluteTolerance, doc.ModelAbsoluteTolerance);
                    if (x != null)
                        foreach (var it in x)
                        {
                            double t;
                            if (edgeCurve.ClosestPoint(it.PointA, out t))
                                splitParams.Add(t);
                        }
                }
                if (splitParams.Count > 0)
                {
                    // Remove duplicates and sort
                    splitParams.Sort();
                    for (int i = splitParams.Count - 1; i > 0; i--)
                        if (System.Math.Abs(splitParams[i] - splitParams[i - 1]) < doc.ModelAbsoluteTolerance)
                            splitParams.RemoveAt(i);

                    // Split the edge
                    foreach (var t in splitParams)
                    {
                        edge.Split(t);
                    }
                }
            }
            // Update the object in the document
            doc.Objects.Replace(brepGuid, brep);
        }

        doc.Views.Redraw();
        RhinoApp.WriteLine("Done!");
        return Result.Success;
    }
}

Requested functionality:

  • Addition of a public method in the RhinoCommon API to programmatically split Brep edges, analogous to the _SplitEdge command in the Rhino UI.
  • The ability to specify an edge (or a set of edges) and a list of parameters/points at which to split, resulting in new edges in the Brep topology.
  • Support for automatic updating of document objects.

Example use case:

  1. The user selects multiple Brep edges and splitting curves/knives.
  2. A plugin finds intersection points and calls a SplitEdge function at those points.
  3. The document is updated with a new Brep containing the newly created edges.

Thank you for your attention to this issue!
Any comments or plans regarding extending the API in this direction would be greatly appreciated.

Best regards,
leex

You will want to use Brep.Edges.SplitEdgeAtParameters. Here a simple script that lets you select a brep edge. The script will split it at the middle point. The new brep is added to the document for further inspection.

// #! csharp
using System;
using System.Linq;
using System.Collections.Generic;
using Rhino;

Rhino.DocObjects.ObjRef ob;

Rhino.Commands.Result res = Rhino.Input.RhinoGet.GetOneObject(
    "select brep edge",
    false,
    Rhino.DocObjects.ObjectType.EdgeFilter,
    out ob
);

if(res==Rhino.Commands.Result.Success && ob != null) {
    Rhino.Geometry.Brep brep = ob.Brep();
    Rhino.Geometry.ComponentIndex ci = ob.GeometryComponentIndex;
    Console.WriteLine($"{ci.Index}");
   
    Rhino.Geometry.BrepEdge bredge = brep.Edges[ci.Index];
    List<double> paramlist = new List<double>(1);
    paramlist.Add(bredge.Domain.Mid);
    brep.Edges.SplitEdgeAtParameters(ci.Index, paramlist.ToList());
    brep.Compact();

    Rhino.Geometry.Brep nbrep = brep.DuplicateBrep();
    System.Guid nguid = __rhino_doc__.Objects.AddBrep(nbrep);
    Console.WriteLine($"Added object with GUID {nguid}");
}

__rhino_doc__.Views.Redraw();

Hi Nathan!
Thank you for your sample script and the explanation about using Brep.Edges.SplitEdgeAtParameters - it helped me understand the basics of splitting a single edge.

However, in my case I need to split multiple selected edges on the same Brep, possibly at different locations for each edge.
I tried adapting the script to loop through multiple edges and call SplitEdgeAtParameters for each one, but I noticed that only one edge gets split - the others remain unchanged.
This happens even if I sort the edge indices in reverse order or try to update the edge list after each split.

Do you have any advice on how to reliably split several selected edges on the same Brep in one operation?
Is there a way to avoid issues with edge indices changing after each split, or maybe another recommended approach?

Thanks again for your help!

// This script attempts to split multiple selected edges of a Brep at intersections with selected curves.
// For each Brep, it tries to split all selected edges, but only one edge actually gets split.

using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Input.Custom;
using Rhino.Geometry.Intersect;

public class MultiEdgeSplitTestCommand : Command
{
    public override string EnglishName => "MultiEdgeSplitTest";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        var go = new GetObject();
        go.SetCommandPrompt("Select Brep edges (sub-objects)");
        go.GeometryFilter = ObjectType.EdgeFilter;
        go.SubObjectSelect = true;
        go.EnablePreSelect(false, true);
        go.GetMultiple(1, 0);
        if (go.CommandResult() != Result.Success)
            return go.CommandResult();

        var go2 = new GetObject();
        go2.SetCommandPrompt("Select curves for splitting");
        go2.GeometryFilter = ObjectType.Curve;
        go2.DeselectAllBeforePostSelect = false;
        go2.GetMultiple(1, 0);
        if (go2.CommandResult() != Result.Success)
            return go2.CommandResult();

        List<Curve> knives = new List<Curve>();
        for (int i = 0; i < go2.ObjectCount; i++)
        {
            var crv = go2.Object(i).Curve();
            if (crv != null)
                knives.Add(crv.DuplicateCurve());
        }
        if (knives.Count == 0)
            return Result.Cancel;

        // Group selected edges by their Brep
        var brepEdgeMap = new Dictionary<Brep, List<int>>();
        var brepGuidMap = new Dictionary<Brep, System.Guid>();
        foreach (var edgeRef in go.Objects())
        {
            var edge = edgeRef.Edge();
            var brep = edge.Brep;
            if (!brepEdgeMap.ContainsKey(brep))
            {
                brepEdgeMap[brep] = new List<int>();
                brepGuidMap[brep] = edgeRef.ObjectId;
            }
            if (!brepEdgeMap[brep].Contains(edge.EdgeIndex))
                brepEdgeMap[brep].Add(edge.EdgeIndex);
        }

        double tol = doc.ModelAbsoluteTolerance;

        foreach (var kv in brepEdgeMap)
        {
            var brep = kv.Key;
            var edgeIndices = new List<int>(kv.Value);
            var brepGuid = brepGuidMap[brep];

            // Try to split each selected edge
            foreach (var ei in edgeIndices)
            {
                if (ei < 0 || ei >= brep.Edges.Count)
                    continue;

                var edgeCurve = brep.Edges[ei].DuplicateCurve();
                if (edgeCurve == null)
                    continue;

                List<double> splitParams = new List<double>();
                foreach (var knife in knives)
                {
                    var x = Intersection.CurveCurve(edgeCurve, knife, tol, tol);
                    if (x != null)
                        foreach (var it in x)
                        {
                            double t;
                            if (edgeCurve.ClosestPoint(it.PointA, out t))
                                splitParams.Add(t);
                        }
                }
                if (splitParams.Count > 0)
                {
                    splitParams.Sort();
                    // Remove duplicate parameters
                    for (int i = splitParams.Count - 1; i > 0; i--)
                        if (System.Math.Abs(splitParams[i] - splitParams[i - 1]) < tol)
                            splitParams.RemoveAt(i);

                    brep.Edges.SplitEdgeAtParameters(ei, splitParams);
                }
            }
            doc.Objects.Replace(brepGuid, brep);
        }

        doc.Views.Redraw();
        RhinoApp.WriteLine("Done!");
        return Result.Success;
    }
}

Just a quick update after more experiments on my end.
I’ve tried a bunch of different ways to get multiple edges split in one go-going through the edges forwards, backwards, even from the middle out. I also tried updating or refreshing the Brep between splits, but no matter what I do, only one edge actually gets split each time. The others just won’t budge.

From the documentation, it does look like SplitEdgeAtParameters is intended to work one edge at a time, but I wanted to check if there’s any recommended workflow for handling multiple edges on a single Brep within one script.

Is this just how SplitEdgeAtParameters is supposed to work, or am I missing some trick?
I’m wondering if there’s a way to “reset” or update the Brep’s topology after splitting, so I could keep going with the other edges in the same script. Or maybe there’s a better way to approach this that I haven’t thought of?

If you have any extra tips or ideas, I’d really appreciate it. Thanks again!

It is possible to handle multiple edges and doing multiple splits for an edge in one loop. I slightly adapted my example to split at three parameters along the edge, 0.25, 0.50 and 0.75. I select the edges of the original surface (left), split them and add the new brep (after compacting) to the document (right).

The slightly adapted script:

// #! csharp
using System;
using System.Linq;
using System.Collections.Generic;
using Rhino;

Rhino.DocObjects.ObjRef[] obs;

Rhino.Commands.Result res = Rhino.Input.RhinoGet.GetMultipleObjects(
    "select brep edges",
    false,
    Rhino.DocObjects.ObjectType.EdgeFilter,
    out obs
);

if(res==Rhino.Commands.Result.Success && obs != null) {
    Rhino.Geometry.Brep brep = null;
    foreach(var ob in obs) {
        Rhino.DocObjects.RhinoObject rhob = ob.Object();
        if(brep == null) {
            brep = ob.Brep();
        }
        Rhino.Geometry.ComponentIndex ci = ob.GeometryComponentIndex;
        Console.WriteLine($"{rhob.Id} {ci.Index}");
        Rhino.Geometry.BrepEdge bredge = brep.Edges[ci.Index];
        
        List<double> paramlist = new List<double>(1);
        paramlist.Add(bredge.Domain.ParameterAt(0.25));
        paramlist.Add(bredge.Domain.Mid);
        paramlist.Add(bredge.Domain.ParameterAt(0.75));
        brep.Edges.SplitEdgeAtParameters(ci.Index, paramlist.ToList());        
    }
    brep.Compact();
    Rhino.Geometry.Brep nbrep = brep.DuplicateBrep();
    System.Guid nguid = __rhino_doc__.Objects.AddBrep(nbrep);
    Console.WriteLine($"Added object with GUID {nguid}");
}

__rhino_doc__.Views.Redraw();

Note that the compacting should happen only after you’re done, otherwise the indices will be incorrect.

1 Like

Hello Nathan,

Thank you very much for your example and explanations, and for showing the correct way to use compact().
In my implementation, I used from your answer the logic for iterating over the selected edges, working with indices, calling SplitEdgeAtParameters for each edge, and applying compact() only after all split operations.

However, I ran into a different problem: when trying to implement edge splitting by points (Point = On), I can’t get the cursor to snap to multiple edges at once.
I tried constructions like:

foreach (var crv in edgeCurves)
    gp.Constrain(crv, false);

as well as

gp.Constrain(edgeCurves.ToArray(), false);

But the result is always the same: the cursor only snaps to the first edge in the list, and it’s impossible to pick a point on another edge. For polylines or SubD, this problem doesn’t exist - the cursor snaps properly to all segments.

It seems that the RhinoCommon API in Rhino 8 (64) is missing the necessary functionality for proper snapping between multiple BrepEdge objects when using functions like GetPoint.Constrain or similar.

I would greatly appreciate any advice or workarounds.


using System;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Geometry.Intersect;
using Rhino.Input;
using Rhino.Input.Custom;

public class SplitMultiEdgesCommand : Command
{
    public override string EnglishName => "SplitMultiEdges";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        // 1. Select edges
        ObjRef[] obs;
        var res = RhinoGet.GetMultipleObjects(
            "Select edges to split",
            false,
            ObjectType.EdgeFilter,
            out obs
        );
        if (res != Result.Success || obs == null)
            return res;

        // List of (Guid, ComponentIndex) pairs to highlight edges
        var highlightEdgeRefs = new List<(Guid, ComponentIndex)>();

        // Edges grouped by Brep object
        var objEdgeMap = new Dictionary<Guid, List<int>>();
        var objRefMap = new Dictionary<Guid, ObjRef>();
        foreach (var ob in obs)
        {
            var rhObj = ob.Object();
            if (rhObj == null) continue;
            var id = rhObj.Id;
            int edgeIdx = ob.GeometryComponentIndex.Index;
            if (edgeIdx < 0) continue;

            if (!objEdgeMap.ContainsKey(id))
            {
                objEdgeMap[id] = new List<int>();
                objRefMap[id] = ob;
            }
            if (!objEdgeMap[id].Contains(edgeIdx))
                objEdgeMap[id].Add(edgeIdx);

            // For subobject highlighting
            highlightEdgeRefs.Add((id, new ComponentIndex(ComponentIndexType.BrepEdge, edgeIdx)));
        }

        // Exclude the source Breps from being picked as cutting objects
        var excludeBrepIds = new HashSet<Guid>();
        foreach (var ob in obs)
        {
            var rhObj = ob.Object();
            if (rhObj != null)
                excludeBrepIds.Add(rhObj.Id);
        }

        // 2. Select cutting objects or Point option
        var getKnife = new GetObject();
        getKnife.SetCommandPrompt("Select cutting objects");
        getKnife.GeometryFilter = ObjectType.Curve | ObjectType.Surface | ObjectType.Brep;
        getKnife.SubObjectSelect = false;
        getKnife.GroupSelect = true;
        getKnife.SetCustomGeometryFilter((rhObj, geometry, ci) => !excludeBrepIds.Contains(rhObj.Id));

        // Add the Point option (split by points mode)
        var optPoint = new OptionToggle(false, "No", "Yes");
        getKnife.AddOptionToggle("Point", ref optPoint);

        bool usePoints = false;
        List<ObjRef> knifeRefs = new List<ObjRef>();

        while (true)
        {
            var resGet = getKnife.GetMultiple(1, 0);
            if (getKnife.CommandResult() != Result.Success)
                return getKnife.CommandResult();

            if (getKnife.OptionIndex() > 0)
            {
                if (optPoint.CurrentValue)
                {
                    usePoints = true;
                    break;
                }
                else
                {
                    continue;
                }
            }
            if (getKnife.ObjectCount > 0)
            {
                for (int i = 0; i < getKnife.ObjectCount; i++)
                    knifeRefs.Add(getKnife.Object(i));
                break;
            }
        }

        if (!usePoints && knifeRefs.Count == 0)
            return Result.Cancel;

        double tol = doc.ModelAbsoluteTolerance;

        // 3. Prepare knives or split points
        List<Curve> knives = new List<Curve>();
        List<Brep> brepKnives = new List<Brep>();
        List<Point3d> splitPoints = new List<Point3d>();

        // For selecting split edges by points
        var toSelect = new List<(Guid, int)>();

        if (usePoints)
        {
            // --- HIGHLIGHT selected edges ---
            foreach (var (objId, compIdx) in highlightEdgeRefs)
            {
                var rhObj = doc.Objects.FindId(objId);
                if (rhObj != null)
                    rhObj.HighlightSubObject(compIdx, true);
            }
            doc.Views.Redraw();

            // Allow picking points only on selected edges
            var edgeCurves = new List<Curve>();
            foreach (var ob in obs)
            {
                var brep = ob.Brep();
                int edgeIdx = ob.GeometryComponentIndex.Index;
                if (brep != null && edgeIdx >= 0 && edgeIdx < brep.Edges.Count)
                    edgeCurves.Add(brep.Edges[edgeIdx].DuplicateCurve());
            }

            RhinoApp.WriteLine("Pick split points on selected edges (multiple, Enter to finish)");
            while (true)
            {
                GetPoint gp = new GetPoint();
                gp.SetCommandPrompt("Pick split point (Enter to finish)");
                gp.AcceptNothing(true);

                // Here is the problem: there is not enough functionality to freely snap the cursor between multiple BrepEdge curves.
                // Methods like GetPoint.Constrain(crv, false) in a loop for each edge,
                // or using the GetPoint.Constrain(Curve[], false) overload, do not solve the issue.
                // The cursor always snaps only to the first edge in the list and cannot freely move between edges as it does for polylines or SubD.

                foreach (var crv in edgeCurves)
                    gp.Constrain(crv, false);
                // Alternatively (if available):
                // gp.Constrain(edgeCurves.ToArray(), false);

                var result = gp.Get();
                if (result == GetResult.Point)
                {
                    Point3d pt = gp.Point();
                    bool onEdge = false;
                    foreach (var crv in edgeCurves)
                    {
                        double t;
                        if (crv.ClosestPoint(pt, out t) && crv.PointAt(t).DistanceTo(pt) < tol * 10)
                        {
                            splitPoints.Add(crv.PointAt(t));
                            onEdge = true;
                            break;
                        }
                    }
                    if (!onEdge)
                    {
                        RhinoApp.WriteLine("Pick a point ON the selected edges!");
                        continue;
                    }
                }
                else
                {
                    break;
                }
            }

            // --- REMOVE HIGHLIGHT ---
            foreach (var (objId, compIdx) in highlightEdgeRefs)
            {
                var rhObj = doc.Objects.FindId(objId);
                if (rhObj != null)
                    rhObj.HighlightSubObject(compIdx, false);
            }
            doc.Views.Redraw();

            if (splitPoints.Count == 0)
            {
                RhinoApp.WriteLine("No split points selected.");
                return Result.Cancel;
            }
        }
        else
        {
            foreach (var kr in knifeRefs)
            {
                var crv = kr.Curve();
                if (crv != null)
                {
                    knives.Add(crv.DuplicateCurve());
                    continue;
                }
                var br = kr.Brep();
                if (br != null)
                {
                    brepKnives.Add(br);
                    continue;
                }
                var srf = kr.Surface();
                if (srf != null)
                {
                    brepKnives.Add(Brep.CreateFromSurface(srf));
                }
            }

            if (knives.Count == 0 && brepKnives.Count == 0)
            {
                RhinoApp.WriteLine("No cutting objects selected.");
                return Result.Cancel;
            }
        }

        // Only count actually split edges and their segments
        int totalSplitSegments = 0;
        int splitEdgeCount = 0;

        // 4. For each Brep, split all selected edges and replace the object
        foreach (var kv in objEdgeMap)
        {
            var objId = kv.Key;
            var edgeIndices = kv.Value;

            var ob = objRefMap[objId];
            var brep = ob.Brep();
            if (brep == null)
                continue;

            var edgeSplitParams = new Dictionary<int, List<double>>();
            foreach (int edgeIdx in edgeIndices)
            {
                if (edgeIdx < 0 || edgeIdx >= brep.Edges.Count)
                    continue;

                var bredge = brep.Edges[edgeIdx];
                var edgeCurve = bredge.DuplicateCurve();
                if (edgeCurve == null)
                    continue;

                List<double> paramlist = new List<double>();

                if (usePoints)
                {
                    foreach (var pt in splitPoints)
                    {
                        double t;
                        if (edgeCurve.ClosestPoint(pt, out t))
                        {
                            if (edgeCurve.PointAt(t).DistanceTo(pt) < tol * 10)
                                paramlist.Add(t);
                        }
                    }
                }
                else
                {
                    foreach (var knife in knives)
                    {
                        var x = Rhino.Geometry.Intersect.Intersection.CurveCurve(edgeCurve, knife, tol, tol);
                        if (x != null)
                        {
                            foreach (var it in x)
                            {
                                double t;
                                if (edgeCurve.ClosestPoint(it.PointA, out t))
                                    paramlist.Add(t);
                            }
                        }
                    }
                    foreach (var bknife in brepKnives)
                    {
                        Curve[] overlapCurves;
                        Point3d[] intersectionPoints;
                        bool found = Rhino.Geometry.Intersect.Intersection.CurveBrep(edgeCurve, bknife, tol, out overlapCurves, out intersectionPoints);
                        if (found && intersectionPoints != null && intersectionPoints.Length > 0)
                        {
                            foreach (var pt in intersectionPoints)
                            {
                                double t;
                                if (edgeCurve.ClosestPoint(pt, out t))
                                    paramlist.Add(t);
                            }
                        }
                    }
                }

                if (paramlist.Count > 0)
                {
                    paramlist.Sort();
                    for (int i = paramlist.Count - 1; i > 0; i--)
                        if (Math.Abs(paramlist[i] - paramlist[i - 1]) < tol)
                            paramlist.RemoveAt(i);
                    edgeSplitParams[edgeIdx] = paramlist;
                }
            }

            // Only count actually split edges (those with split parameters)
            foreach (int edgeIdx in edgeIndices)
            {
                if (edgeSplitParams.ContainsKey(edgeIdx) && edgeSplitParams[edgeIdx].Count > 0)
                {
                    splitEdgeCount += 1;
                    totalSplitSegments += edgeSplitParams[edgeIdx].Count + 1;
                    if (usePoints)
                        toSelect.Add((objId, edgeIdx));
                }
            }

            // Split edges in descending index order
            var sortedEdgeIndices = new List<int>(edgeSplitParams.Keys);
            sortedEdgeIndices.Sort();
            sortedEdgeIndices.Reverse();

            foreach (int edgeIdx in sortedEdgeIndices)
            {
                brep.Edges.SplitEdgeAtParameters(edgeIdx, edgeSplitParams[edgeIdx]);
            }

            brep.Compact();

            doc.Objects.Replace(objId, brep);
        }

        doc.Views.Redraw();

        // Select actually split edges (SubObject) if working by points
        if (usePoints && toSelect.Count > 0)
        {
            foreach (var (objId, edgeIdx) in toSelect)
            {
                var rhObj = doc.Objects.FindId(objId);
                if (rhObj != null)
                {
                    var ci = new ComponentIndex(ComponentIndexType.BrepEdge, edgeIdx);
                    rhObj.SelectSubObject(ci, true, true, true);
                }
            }
            doc.Views.Redraw();
        }

        // Final message (numbers only)
        RhinoApp.WriteLine($"{splitEdgeCount} edges split into {totalSplitSegments} segments.");

        return Result.Success;
    }
}

GetPoint currently has no capability to constrain to multiple curves. I’ve logged a request at RH-87693 Allow multiple curves to GetPoint.Constrain()

1 Like

Thank you, Nathan.

Additionally, I would like to ask: if possible, could you consider the idea of expanding the _SplitEdge functionality so that it would be possible to split an edge not only at manually selected points on a curve, but also using other objects—such as curves, surfaces, points, and so on? This would be very helpful for modeling automation and would significantly speed up working with complex models. It would greatly improve the flexibility and functionality if such an option became available, especially for regular users who may not have an advanced plugin installed by default.

Developer responded on the YouTrack issue: you should be able to use a polycurve. It does not have to be continuous: collect your curves, put them in a polycurve, use that. @leex, can you please try that and report back?

Hello Nathan,

Thank you very much for the feedback.
This information was really helpful.
I’ve made some progress – I managed to specify the cut location for each curve within a single loop.
But it’s not quite perfect yet.
I need some time to figure things out and refine the code.

I’ll definitely let you know once I’m ready.

1 Like

@leex were you able to add multiple edges as part of a poly curve to get this done?

1 Like

Hi Nathan,

Yes, exactly — combining multiple edges into a single PolyCurve using AppendSegment (not Append) was the key. That way, even non-contiguous edges work properly with GetPoint constraints, which didn’t work before.

After switching to AppendSegment, the edge-splitting command started working reliably not only on simple surfaces, but also on complex ones created with Loft, Patch, or Sweep2Rails — even when the selected edges weren’t connected end-to-end.

I also added a message to the command line showing how many edges were actually split and into how many segments.

Thanks again for the advice! :handshake:

1 Like

Here is the finalized code that allows you to split multiple edges on surfaces, either by using selected cutting objects or by manually picking split points.

// Author: leex
// Date: 2025-06-14

/// <summary>
/// Command for splitting multiple edges on Brep surfaces. Edges can be split using selected knife objects 
/// (curves, surfaces, or breps) or by picking split points manually on the edges.
/// </summary>

using System;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;

public class SplitMultiEdgesCommand : Command
{
    public override string EnglishName => "SplitMultiEdges";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        // 1. Select edges
        ObjRef[] obs;
        var res = RhinoGet.GetMultipleObjects(
            "Select edges to split",
            false,
            ObjectType.EdgeFilter,
            out obs
        );
        if (res != Result.Success || obs == null)
            return res;

        // Save the list of selected edges as ObjRef and their indices
        var selectedEdges = new List<(ObjRef ob, int edgeIdx)>();
        foreach (var ob in obs)
        {
            var rhObj = ob.Object();
            if (rhObj == null) continue;
            int edgeIdx = ob.GeometryComponentIndex.Index;
            if (edgeIdx < 0) continue;
            selectedEdges.Add((ob, edgeIdx));
        }

        // Prepare highlight information for the selected edges
        var highlightEdgeRefs = new List<(Guid, ComponentIndex)>();
        foreach (var (ob, edgeIdx) in selectedEdges)
        {
            var rhObj = ob.Object();
            if (rhObj == null) continue;
            highlightEdgeRefs.Add((rhObj.Id, new ComponentIndex(ComponentIndexType.BrepEdge, edgeIdx)));
        }

        // Exclude selected objects from being used as cutting objects
        var excludeBrepIds = new HashSet<Guid>();
        foreach (var (ob, _) in selectedEdges)
        {
            var rhObj = ob.Object();
            if (rhObj != null)
                excludeBrepIds.Add(rhObj.Id);
        }

        // 2. Select cutting objects
        var getKnife = new GetObject();
        getKnife.SetCommandPrompt("Select cutting objects");
        getKnife.GeometryFilter = ObjectType.Curve | ObjectType.Surface | ObjectType.Brep;
        getKnife.SubObjectSelect = false;
        getKnife.GroupSelect = true;
        getKnife.SetCustomGeometryFilter((rhObj, geometry, ci) => !excludeBrepIds.Contains(rhObj.Id));

        var optPoint = new OptionToggle(false, "No", "Yes");
        getKnife.AddOptionToggle("Point", ref optPoint);

        bool usePoints = false;
        List<ObjRef> knifeRefs = new List<ObjRef>();

        while (true)
        {
            var resGet = getKnife.GetMultiple(1, 0);
            if (getKnife.CommandResult() != Result.Success)
                return getKnife.CommandResult();

            if (getKnife.OptionIndex() > 0)
            {
                if (optPoint.CurrentValue)
                {
                    usePoints = true;
                    break;
                }
                else
                    continue;
            }

            if (getKnife.ObjectCount > 0)
            {
                for (int i = 0; i < getKnife.ObjectCount; i++)
                    knifeRefs.Add(getKnife.Object(i));
                break;
            }
        }

        if (!usePoints && knifeRefs.Count == 0)
            return Result.Cancel;

        double tol = doc.ModelAbsoluteTolerance;
        List<Curve> knives = new List<Curve>();
        List<Brep> brepKnives = new List<Brep>();
        List<(ObjRef ob, int edgeIdx, double t)> splitParams = new List<(ObjRef, int, double)>();
        var toSelect = new List<(Guid, int)>();

        if (usePoints)
        {
            // Highlight the selected edges before picking points
            foreach (var (objId, compIdx) in highlightEdgeRefs)
            {
                var rhObj = doc.Objects.FindId(objId);
                if (rhObj != null)
                    rhObj.HighlightSubObject(compIdx, true);
            }
            doc.Views.Redraw();

            // Gather edge curves for closest point search
            var edgeData = new List<(ObjRef ob, int edgeIdx, Curve curve)>();
            foreach (var (ob, edgeIdx) in selectedEdges)
            {
                var brep = ob.Brep();
                if (brep != null && edgeIdx >= 0 && edgeIdx < brep.Edges.Count)
                    edgeData.Add((ob, edgeIdx, brep.Edges[edgeIdx].DuplicateCurve()));
            }

            // Build a PolyCurve from all edge curves (for point constraint)
            PolyCurve poly = new PolyCurve();
            foreach (var d in edgeData)
            {
                poly.AppendSegment(d.curve);
            }

            while (true)
            {
                GetPoint gp = new GetPoint();
                gp.SetCommandPrompt("Pick split point (Enter to finish)");
                gp.AcceptNothing(true);
                gp.Constrain(poly, false);

                var result = gp.Get();
                if (result == GetResult.Point)
                {
                    Point3d pt = gp.Point();
                    double minDist = double.MaxValue;
                    int chosenEdgeIdx = -1;
                    double chosenT = 0;
                    ObjRef chosenOb = null;
                    foreach (var d in edgeData)
                    {
                        double t;
                        if (d.curve.ClosestPoint(pt, out t))
                        {
                            double dist = d.curve.PointAt(t).DistanceTo(pt);
                            if (dist < minDist && dist < tol * 10)
                            {
                                minDist = dist;
                                chosenEdgeIdx = d.edgeIdx;
                                chosenT = t;
                                chosenOb = d.ob;
                            }
                        }
                    }
                    if (chosenEdgeIdx != -1 && chosenOb != null)
                    {
                        splitParams.Add((chosenOb, chosenEdgeIdx, chosenT));
                    }
                    else
                    {
                        continue;
                    }
                }
                else break;
            }

            // Remove highlighting after picking points
            foreach (var (objId, compIdx) in highlightEdgeRefs)
            {
                var rhObj = doc.Objects.FindId(objId);
                if (rhObj != null)
                    rhObj.HighlightSubObject(compIdx, false);
            }
            doc.Views.Redraw();

            if (splitParams.Count == 0)
            {
                return Result.Cancel;
            }
        }
        else
        {
            foreach (var kr in knifeRefs)
            {
                var crv = kr.Curve();
                if (crv != null)
                {
                    knives.Add(crv.DuplicateCurve());
                    continue;
                }
                var br = kr.Brep();
                if (br != null)
                {
                    brepKnives.Add(br);
                    continue;
                }
                var srf = kr.Surface();
                if (srf != null)
                    brepKnives.Add(Brep.CreateFromSurface(srf));
            }

            if (knives.Count == 0 && brepKnives.Count == 0)
            {
                return Result.Cancel;
            }
        }

        // Group edges by Brep object for later update
        var edgesByObj = new Dictionary<Guid, List<(ObjRef ob, int edgeIdx)>>();
        foreach (var (ob, edgeIdx) in selectedEdges)
        {
            var id = ob.Object().Id;
            if (!edgesByObj.ContainsKey(id))
                edgesByObj[id] = new List<(ObjRef, int)>();
            edgesByObj[id].Add((ob, edgeIdx));
        }

        int splitEdgeCount = 0;
        int totalSplitSegments = 0;

        foreach (var kv in edgesByObj)
        {
            var objId = kv.Key;
            var edgeTuples = kv.Value;
            var ob = edgeTuples[0].ob;
            var brep = ob.Brep();
            if (brep == null)
                continue;

            var edgeSplitParams = new Dictionary<int, List<double>>();
            foreach (var (_, edgeIdx) in edgeTuples)
            {
                if (edgeIdx < 0 || edgeIdx >= brep.Edges.Count)
                    continue;

                var bredge = brep.Edges[edgeIdx];
                var edgeCurve = bredge.DuplicateCurve();
                if (edgeCurve == null)
                    continue;

                List<double> paramlist = new List<double>();

                if (usePoints)
                {
                    foreach (var s in splitParams)
                    {
                        if (s.ob.Object().Id == objId && s.edgeIdx == edgeIdx)
                        {
                            Interval dom = edgeCurve.Domain;
                            double t = s.t;
                            if (t > dom.T0 + tol && t < dom.T1 - tol)
                                paramlist.Add(t);
                        }
                    }
                }
                else
                {
                    foreach (var knife in knives)
                    {
                        var x = Rhino.Geometry.Intersect.Intersection.CurveCurve(edgeCurve, knife, tol, tol);
                        if (x != null)
                        {
                            foreach (var it in x)
                            {
                                double t;
                                if (edgeCurve.ClosestPoint(it.PointA, out t))
                                    if (t > edgeCurve.Domain.T0 + tol && t < edgeCurve.Domain.T1 - tol)
                                        paramlist.Add(t);
                            }
                        }
                    }
                    foreach (var bknife in brepKnives)
                    {
                        Curve[] overlapCurves;
                        Point3d[] intersectionPoints;
                        bool found = Rhino.Geometry.Intersect.Intersection.CurveBrep(edgeCurve, bknife, tol, out overlapCurves, out intersectionPoints);
                        if (found && intersectionPoints != null && intersectionPoints.Length > 0)
                        {
                            foreach (var pt in intersectionPoints)
                            {
                                double t;
                                if (edgeCurve.ClosestPoint(pt, out t))
                                    if (t > edgeCurve.Domain.T0 + tol && t < edgeCurve.Domain.T1 - tol)
                                        paramlist.Add(t);
                            }
                        }
                    }
                }

                if (paramlist.Count > 0)
                {
                    paramlist.Sort();
                    for (int i = paramlist.Count - 1; i > 0; i--)
                        if (Math.Abs(paramlist[i] - paramlist[i - 1]) < tol)
                            paramlist.RemoveAt(i);
                    edgeSplitParams[edgeIdx] = paramlist;
                }
            }

            // Count how many edges and segments were split
            foreach (var pair in edgeSplitParams)
            {
                if (pair.Value.Count > 0)
                {
                    splitEdgeCount += 1;
                    totalSplitSegments += pair.Value.Count + 1;
                }
            }

            foreach (int edgeIdx in edgeSplitParams.Keys)
            {
                brep.Edges.SplitEdgeAtParameters(edgeIdx, edgeSplitParams[edgeIdx]);
            }

            brep.Compact();
            doc.Objects.Replace(objId, brep);
        }

        doc.Views.Redraw();

        // Proper singular/plural output and handle zero/no split cases
        if (splitEdgeCount == 0)
        {
            RhinoApp.WriteLine("No edges were split.");
        }
        else if (totalSplitSegments == splitEdgeCount)
        {
            RhinoApp.WriteLine($"{splitEdgeCount} edge{(splitEdgeCount == 1 ? "" : "s")} selected, but no splits were made.");
        }
        else
        {
            string edgeWord = splitEdgeCount == 1 ? "edge" : "edges";
            string segmentWord = totalSplitSegments == 1 ? "segment" : "segments";
            RhinoApp.WriteLine($"{splitEdgeCount} {edgeWord} split into {totalSplitSegments} {segmentWord}.");
        }

        return Result.Success;
    }
}

1 Like

SplitMultiEdges.rhp (13 KB)

Command:
_SplitMultiEdges