@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)