Extract mesh holes in Rhino3dm

Dear McNeel,

Is it possible to extract inner naked (hole) lines, of a mesh via Rhino3dmNET library?
I would like to extract the blue colored lines:

I tried converting the mesh to a brep (Brep.CreateFromMesh), but without success. The mesh is successfully created to a brep, but then Brep.DuplicateNakedEdgeCurves simply threats both outer and hole naked edges as the same:

Running Brep.MergeCoplanarFaces before Brep.DuplicateNakedEdgeCurves - would solve the issue, but sadly that method does not exist in Rhino3dmNET.

At the moment, I am finding the longest naked polyline - and stating that: this is the outer one.
And all others are inner naked (hole) polylines.
I am an interested if there is another way, to extract the mesh holes? Maybe with a “topological” approach, instead of upper: longest polyline approach?

Attached is the mesh
holes.gh (22.1 KB)

Right, Brep.MergeCoplanarFaces is part of the Rhino Core which is not available in rhino3dm.

I am curious as to how you would do this within Rhino if you had all of the SDK tools available?

1 Like

Something like this should work in rhino3dm (thanks to @DavidRutten for the tip). You will have to decide how to filter out the outer edge loop.

        // mesh = your mesh
        var edges = new List<Curve>();
        
        for(int i = 0; i < mesh.TopologyEdges.Count; i ++ )
        {
            var faceCnt = mesh.TopologyEdges.GetConnectedFaces(i).Length;
            if(faceCnt == 1)
                edges.Add(mesh.TopologyEdges.EdgeLine(i).ToNurbsCurve());
        }

        var result = Curve.JoinCurves(edges);
1 Like

Hi @fraguada ,
Thank you very much for the detailed reply and your help.

I thought that calling Brep.MergeCoplanarFaces before Brep.DuplicateNakedEdgeCurves, would enable me to differentiate between the outer naked, and inner hole lines.
But now I see that this does not work, even in Rhino, without using Rhino3dm.
It seems that RhinoCommon threats the inner hole lines: as only those, whose trim curves are not intersecting the BrepFace outer trim curves:

This code:

# python
tol = 0.01
success = brep.MergeCoplanarFaces(tol)

if success:
    # outer naked lines
    outer = True
    inner = False
    outerNaked_L = brep.DuplicateNakedEdgeCurves(outer, inner)
    
    # inner (hole) lines
    outer = False
    inner = True
    hole_L = brep.DuplicateNakedEdgeCurves(outer, inner)

results in:

Of course, I always assume that: the longest joined curve in outerNaked_L is the outer one I need. While the other one in the same list - is a hole curve.

Thank you for this solution Luis. Indeed clever solution.
I was still puzzled that neither Brep nor Mesh RhinoCommmon objects having a method which can differentiate between an outer naked edge. And an inner naked edge (hole).

1 Like

For breps, look at the trims not the edges.

— Dale

2 Likes

Hi @dale ,
Thank you very much for the suggestion.

If I understood you correctly, I have to check if trim loop, is outer/inner type?

If this is so, I still can’t extract the biggest hole curve:

The following code:

brepLoops = brep.Loops
brepEdges = brep.Edges


hole_L = []

for brepLoop in brepLoops:
    if (brepLoop.LoopType == rg.BrepLoopType.Inner):
        for brepTrim in brepLoop.Trims:
            
            BEI = brepTrim.Edge.ComponentIndex().Index
            BE = brepEdges[BEI]
            BE_crv = BE.DuplicateCurve()
            hole_L.append( BE_crv )

Results in:

Hi @dale ,
Any help how to extract the middle hole?
Is my approach with trims wrong?

A couple of weeks ago I started typing a short C# script using topology edges from meshes. Other things got in the way, but let me post what I had cooked up thus far:

// #! csharp
using System;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.DocObjects;
using Rhino.Input;
using Rhino.Input.Custom;

var rng = new System.Random();

Dictionary<Tuple<int, int>, List<int>> edge_to_face = new ();
Dictionary<int, HashSet<int>> vertex_face_count = new ();

void UpdateVertexFaceSet(int fidx, int a) {
    if(!vertex_face_count.ContainsKey(a)) {
        vertex_face_count[a] = new ();
    }
    vertex_face_count[a].Add(fidx);
}

void UpdateMaps(int fidx, int a, int b) {
    Tuple<int, int> edge = (a <= b) ? new Tuple<int, int>(a, b) : new Tuple<int, int>(b, a);
    if(!edge_to_face.ContainsKey(edge)) {
        edge_to_face[edge] = new ();
    }
    edge_to_face[edge].Add(fidx);

    UpdateVertexFaceSet(fidx, a);
    UpdateVertexFaceSet(fidx, b);
}

Rhino.DocObjects.ObjRef ob;
var res = Rhino.Input.RhinoGet.GetOneObject(
    "Select mesh",
    false,
    Rhino.DocObjects.ObjectType.Mesh,
    out ob);

List<int> edges_one_face = new ();
Dictionary<int, int> edge_face_map = new();
List<HashSet<int>> loops = new();

if(
    res == Rhino.Commands.Result.Success
    &&
    ob != null
) {
    Mesh m = ob.Mesh();
    m.TopologyVertices.SortEdges();
    for(int fidx = 0; fidx < m.Faces.Count; fidx++) {
        int[] conn_edges = m.TopologyEdges.GetEdgesForFace(fidx);
        foreach(int cei in conn_edges) {
            int[] conn_faces = m.TopologyEdges.GetConnectedFaces(cei);
            if(conn_faces.Count() == 1) {
                edge_face_map[cei] = conn_faces[0];
                StringBuilder sb = new();
                sb.Append($"TopologyEdge {cei}\n");
                edges_one_face.Add(cei);
                IndexPair ip = m.TopologyEdges.GetTopologyVertices(cei);
                sb.Append($"\ti: {ip.I}, j: {ip.J}\n");
                Line l = m.TopologyEdges.EdgeLine(cei);
                //ObjectAttributes oattr = new ();
                //Console.Write(sb.ToString());
                //oattr.UserDictionary["toe"] = sb.ToString();
                //__rhino_doc__.Objects.AddLine(l);
            }
        }
    }
    StringBuilder efm_sb = new();
    foreach(KeyValuePair<int, int> kvp in edge_face_map) {
        efm_sb.Append($"edge {kvp.Key}: face {kvp.Value}\n");
        IndexPair ip = m.TopologyEdges.GetTopologyVertices(kvp.Key);
        efm_sb.Append($"\t{ip.I}, {ip.J}\n");
    }
    //Console.WriteLine(efm_sb.ToString());
    //Console.WriteLine("-------------------\n\n");
    List<int> todo = new List<int>(edges_one_face);
    HashSet<int> done = new ();
    int current = todo.First();
    todo.Remove(current);
    int next = -1;
    done.Add(current);
    Console.Write($"{current} -> ");
    while(todo.Count > 0) {
    
        var nextlist = (from
            edge in todo
            where
                   edge != current
                && !done.Contains(edge)
                && (
                     m.TopologyEdges.GetTopologyVertices(current).J == m.TopologyEdges.GetTopologyVertices(edge).I
                  || m.TopologyEdges.GetTopologyVertices(current).J == m.TopologyEdges.GetTopologyVertices(edge).J
                  || m.TopologyEdges.GetTopologyVertices(current).I == m.TopologyEdges.GetTopologyVertices(edge).J 
                  || m.TopologyEdges.GetTopologyVertices(current).I == m.TopologyEdges.GetTopologyVertices(edge).I 
                )
               select edge).ToList();
        //Console.WriteLine($"edge {current} hits: {nextlist.Count()}");

        if(false && nextlist.Count > 0) {
            Console.Write("\t");
            foreach(int heid in nextlist) {
                Console.Write($"{heid} ");
            }
            Console.WriteLine("");
        }
        next = nextlist.FirstOrDefault(-1);
        
        if(next < 0) {
            Console.WriteLine($"Loop ready. Edge count {done.Count}");
            loops.Add(done);
            current = todo.First();
            todo.Remove(current);
            done = new ();
            done.Add(current);
            Console.Write($"{current} -> ");
        } else {
            Console.Write($"{next} -> ");
            todo.Remove(next);
            done.Add(next);
            current = next;
            if(todo.Count == 0 && loops.Count > 0) {
                Console.WriteLine($"Final edge handled, add one more found loop. Edge count {done.Count}");
                loops.Add(done);
            }
        }
    }
    Console.WriteLine("");
    Console.WriteLine($"List length: {loops.Count}, {todo.Count}");
    foreach(HashSet<int> loop in loops) {
        foreach(int dei in loop) {
            Line l = m.TopologyEdges.EdgeLine(dei);
            __rhino_doc__.Objects.AddLine(l);
        }
    }
}

This script adds lines where naked edges are found. Next up was going to be to determine inner vs outer edge loop, but that is where I had to stop for the moment.

Perhaps winding could be possible if you could find a plane to project edge loops and mesh face centers to. Then take a face adjacent to an edge and do a winding count. Essentially see if the face center is outside of the loop or inside of the loop. Inside of the loop would give you a full circle when summing angles of all edges, meaning it would be the outer loop. Otherwise it is an inner loop.

1 Like

Here is my contribution.

holes.gh (6.2 KB)

– Dale

1 Like

Thank you both @dale and @nathanletwory !

Dale, I can’t open the .gh file. It does not recognize the Rhino 8 C# component on my Rhino 6.
Can I ask for an additional favor, to attach the C# code as the .cs file? I apologize for additionally disturbing you.

Here you go.

TestDjordje.cs (3.5 KB)

– Dale

1 Like

Thank you for the the help, and your time!
All of you. @fraguada , @nathanletwory , @dale !

@djordje ok, so I have now finished and cleaned up the C# script somewhat.

I don’t know if exactly everything is in rhino3dm (any of Python, JavaScript or .NET), but I think it should give you some idea. Here a quick recording of it working:

And the code

// #! csharp

// Written by Nathan 'jesterKing' Letwory
// angle sum algorithm based on solution 4 of
// https://www.eecs.umich.edu/courses/eecs380/HANDOUTS/PROJ2/InsidePoly.html
using System;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.DocObjects;
using Rhino.Input;
using Rhino.Input.Custom;

bool addDebugGeometry = false;

var rng = new System.Random();

Dictionary<Tuple<int, int>, List<int>> edge_to_face = new ();
Dictionary<int, HashSet<int>> vertex_face_count = new ();

void UpdateVertexFaceSet(int fidx, int a) {
    if(!vertex_face_count.ContainsKey(a)) {
        vertex_face_count[a] = new ();
    }
    vertex_face_count[a].Add(fidx);
}

void UpdateMaps(int fidx, int a, int b) {
    Tuple<int, int> edge = (a <= b) ? new Tuple<int, int>(a, b) : new Tuple<int, int>(b, a);
    if(!edge_to_face.ContainsKey(edge)) {
        edge_to_face[edge] = new ();
    }
    edge_to_face[edge].Add(fidx);

    UpdateVertexFaceSet(fidx, a);
    UpdateVertexFaceSet(fidx, b);
}

Rhino.DocObjects.ObjRef ob;
var res = Rhino.Input.RhinoGet.GetOneObject(
    "Select mesh",
    false,
    Rhino.DocObjects.ObjectType.Mesh,
    out ob);

List<int> edges_one_face = new ();
Dictionary<int, int> edge_face_map = new();
List<HashSet<int>> loops = new();

if(
    res == Rhino.Commands.Result.Success
    &&
    ob != null
) {
    Mesh m = ob.Mesh();
    m.TopologyVertices.SortEdges();

    // build edge face map and list of edges that have only one face connected
    for(int fidx = 0; fidx < m.Faces.Count; fidx++) {
        int[] conn_edges = m.TopologyEdges.GetEdgesForFace(fidx);
        foreach(int cei in conn_edges) {
            int[] conn_faces = m.TopologyEdges.GetConnectedFaces(cei);
            if(conn_faces.Count() == 1) {
                edge_face_map[cei] = conn_faces[0];
                edges_one_face.Add(cei);
                Line l = m.TopologyEdges.EdgeLine(cei);
            }
        }
    }

    // create list of loops
    List<int> todo = new List<int>(edges_one_face);
    HashSet<int> done = new ();
    int current = todo.First();
    todo.Remove(current);
    int next = -1;
    done.Add(current);
    while(todo.Count > 0) {
    
        var nextlist = (from
            edge in todo
            where
                   edge != current
                && !done.Contains(edge)
                && (
                     m.TopologyEdges.GetTopologyVertices(current).J == m.TopologyEdges.GetTopologyVertices(edge).I
                  || m.TopologyEdges.GetTopologyVertices(current).J == m.TopologyEdges.GetTopologyVertices(edge).J
                  || m.TopologyEdges.GetTopologyVertices(current).I == m.TopologyEdges.GetTopologyVertices(edge).J 
                  || m.TopologyEdges.GetTopologyVertices(current).I == m.TopologyEdges.GetTopologyVertices(edge).I 
                )
               select edge).ToList();

        next = nextlist.FirstOrDefault(-1);
        
        if(next < 0) {
            loops.Add(done);
            current = todo.First();
            todo.Remove(current);
            done = new ();
            done.Add(current);
        } else {
            todo.Remove(next);
            done.Add(next);
            current = next;
            if(todo.Count == 0 && loops.Count > 0) {
                loops.Add(done);
            }
        }
    }

    // from the edge loops construct joined curves and determine
    // whether they represent an outer naked edge loop or an inner naked
    // edge loop.
    int loopidx = 0;
    foreach(HashSet<int> loop in loops) {
        List<Curve> crvs= new();
        MeshFace mf;
        Point3d pp = Point3d.Unset;
        foreach(int dei in loop) {
            Line l = m.TopologyEdges.EdgeLine(dei);
            int[] conn_faces = m.TopologyEdges.GetConnectedFaces(dei);
            mf = m.Faces[conn_faces[0]];
            if(pp == Point3d.Unset) {
                if(mf.IsQuad) {
                    pp = Point3d.Divide(
                        m.Vertices[mf.A] + m.Vertices[mf.B] + m.Vertices[mf.C] + m.Vertices[mf.D], 4);
                }
                else {
                    pp = Point3d.Divide(
                        m.Vertices[mf.A] + m.Vertices[mf.B] + m.Vertices[mf.C], 3);
                }
            }
            crvs.Add(l.ToNurbsCurve());
        }
        // there should be only one curve here, add checks if you think this is too dangerous
        Curve joinedCurve = Curve.JoinCurves(crvs, 0.0001, false)[0];

        // finally figure out if this is that outer or inner naked edge loop.
        bool isouter = IsOuterNakedEdgeloop(joinedCurve, pp);

        // add to the document.
        ObjectAttributes oa = new ();
        oa.SetUserString("outer naked edge", $"{isouter}");
        oa.ColorSource = ObjectColorSource.ColorFromObject;
        oa.ObjectColor = isouter ? 
                    System.Drawing.Color.Purple :
                    System.Drawing.Color.DarkGreen;
        __rhino_doc__.Objects.AddCurve(joinedCurve, oa);

        loopidx++;
    }
}

/// <summary>
/// Determine if given curve represents outer naked edge (true) or inner naked edge (false)
/// The point pp is the centroid of a mesh face.
/// The algorithm used here is sum of angles, which determines if pp is inside the curve (outer naked edge loop)
/// or outside the curve (inner naked edge loop)
/// </summary>
bool IsOuterNakedEdgeloop(Curve joinedCurve, Point3d pp) {
    // angle (sum) we calculate
    double angle = 0.0;

    // explode joined curve into its subcurves, then find
    // all start and end points. Points we use to fit a plane through.
    Curve[] crvs = joinedCurve.GetSubCurves();
    HashSet<Point3d> pts = new ();
    foreach(Curve c in crvs) {
        pts.Add(c.PointAtStart);
        pts.Add(c.PointAtEnd);
    }

    // Find a plane through our edge loop points. Use this to project the joined curve
    // and the initial mesh face point to.
    Plane plane;
    PlaneFitResult fitres = Plane.FitPlaneToPoints(pts, out plane);
    if(fitres == PlaneFitResult.Success) {
        Line extra = new Line(pp, crvs[0].PointAtStart);
        Curve projectedExtra = Curve.ProjectToPlane(extra.ToNurbsCurve(), plane);
        Curve projectedCurve = Curve.ProjectToPlane(joinedCurve, plane);

        if(addDebugGeometry) { __rhino_doc__.Objects.AddCurve(projectedCurve); }
        if(addDebugGeometry) { __rhino_doc__.Objects.AddCurve(projectedExtra); }

        Curve[] projectedSubCurves = projectedCurve.GetSubCurves();
        if(addDebugGeometry) {
            foreach(Curve pc in projectedSubCurves) {
                Line _l = new Line(projectedExtra.PointAtStart, pc.PointAtMid);
                __rhino_doc__.Objects.AddLine(_l);
            }
        }

        // the actual point we want to check - the projected start point which is the projected meshface mid point.
        Point3d q = projectedExtra.PointAtStart;

        int n = projectedSubCurves.Length;
        double m1 = 0.0;
        double m2 = 0.0;
        double costheta = 0.0;

        for(int i = 0; i < n; i++) {
            Vector3d v1 = projectedSubCurves[i].PointAtStart - q;
            // note, fetch next curve, or wrap around to first
            Vector3d v2 = projectedSubCurves[(i+1)%n].PointAtStart - q;

            // determine angle between the two vectors, 
            double _a = Vector3d.VectorAngle(v1, v2);
            angle += _a;
        }
    }
    // We have an outer naked edge if the angle sum equals 2*pi.
    return RhinoMath.EpsilonEquals(angle, RhinoMath.TwoPI, 0.000001);
}

extract_nakededgeloops_from_meshes.cs (7.4 KB)

3 Likes

Hi @nathanletwory ,
Brilliant work! Thank you for the help!

1 Like