Do you know ways of extracting aligned edges loops from mesh?


mesh sample.stl (167.9 KB)
In a mesh like this ^ we can see some parts being some generic triangulation, but some others being from obvious solid boolean operation from a revolved shape.

Edges there have a more coherent structure, if we “pair up” edges around every vector we might able to retrieve “U” and “V” isocurves/polylines of the original revolved shape.

Do you know any tool or logic to go in this direction?

(A first step would be to quadrangulate the mesh, probably… but maybe not, I might want to avoid the risk of deleting useful edges…)

Edit: I need to extract every edge-chain loops.

Hi @maje90,

I’m mobile and can’t check right now but does selchain work on mesh edges?

There’s also selU which selects in the U direction but I think that’s points only if I remember right.

2 Likes

Hi Michael.

I didn’t know selchain command, pretty useful.
(I usually go CTRL+SHIFT + double click, but selchain is more powerful indeed)

Thanks!

But I made a mistake, I should have said that this is for a coded context, no manual input.
I need all and every loops.
Best would be to have this inside c# , rhinocommon… but if it’s a plugin it’s ok…

Hi Riccardo,

i’ve tried something which might be a first step for further refinement:

The script loops over topology edges and finds the 2 connected triangles. It then finds the point on each triangle which is not on the edge. This is the origin to measure the angle of each triangle which should be close to 90 degree. If both triangle corner angles are within tolerance (i used 1.55 degree) it compares the difference between both face normals (0.02 degree) and selects the faces if it stays below normal tolerance.

In a second step you could extract the faces, filter mesh parts eg. if they have only 2 faces. Then from the rest duplicate the borders and split the polylines at the sharp angles which are all close to 90 degree…

MeshRectDetect.py (2.9 KB)

it might not be super fast on larger meshes. What is it btw. ?
_
c.

2 Likes

wow, thank you clement!

I’ll study this! Nice!

Yes, that chunk is about 1/100 of the total… but it’s no problem, I’ll try to convert your script into c# and multithread it there.

I’d rather not say in specific, but as said, the task is to re-retrieve the plane/axis of each revolved detail in a mesh…


Thank you again!
Have a nice weekend!

You could unweld the mesh and fit circles through the vertices of joined segments.

It would be easier if the inner upper edge of the conical hole wasn’t filleted.

unweld_mesh_edge_02_c.gh (29.9 KB)

2 Likes

That’s interesting… but I need “all” the edge-loops, with the unweld I can reach only some edges, not all.

Anyway thanks, that’s another idea to put in the mixer.

1 Like

Hi @maje90,

Here’s a shot at trying to extract and visualize edge chains based on an angle value and a minimum edge chain length.

I get “decent” results but it could be improved of course. Open to ideas… this is a bit out of my “wheel house” as I don’t deal with mesh topology often.

This does not currently handle overlapping segments FYI, nor does it clean or join them

Angle: 3
Min Length: 10

Angle: 5
Min Length: 10

Angle: 3
Min Length: 2

Code:

#! python 2
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext
import System
import collections
from math import degrees

# Tolerance for considering edges to be in the same chain
angle_tol = 3.0  # degrees
min_chain_length = 3  # Minimum number of edges in a chain to be considered

def GetEdgeVector(mesh, edge_index):
    """Get normalized vector for an edge"""
    vert_indices = mesh.TopologyEdges.GetTopologyVertices(edge_index)
    start = mesh.TopologyVertices.Item[vert_indices[0]]
    end = mesh.TopologyVertices.Item[vert_indices[1]]
    edge_vector = Rhino.Geometry.Vector3d(end - start)
    edge_vector.Unitize()
    return edge_vector, start, end

def AngleBetweenEdges(vector1, vector2):
    """Compute smallest angle in degrees between two vectors"""
    rad_angle = Rhino.Geometry.Vector3d.VectorAngle(vector1, vector2)
    angle = degrees(rad_angle)
    # Take the smaller angle (considering that vectors could point in opposite directions)
    if angle > 90:
        angle = 180 - angle
    return angle

def FindConnectedEdges(mesh, edge_index):
    """Find all edges connected to the endpoints of the given edge"""
    edge_vert_indices = mesh.TopologyEdges.GetTopologyVertices(edge_index)
    connected_edges = set()
    
    # For each vertex of the edge
    for vert_index in edge_vert_indices:
        # Get all edges connected to this vertex
        edges = mesh.TopologyVertices.ConnectedEdges(vert_index)
        for e in edges:
            if e != edge_index:  # Don't include the original edge
                connected_edges.add(e)
    
    return connected_edges

def FindNextEdgeInChain(mesh, current_edge, current_vector, visited_edges):
    """Find the next edge in the chain that has the smallest angle difference"""
    connected_edges = FindConnectedEdges(mesh, current_edge)
    best_edge = None
    best_angle = float('inf')
    
    for edge in connected_edges:
        if edge in visited_edges:
            continue
            
        edge_vector, _, _ = GetEdgeVector(mesh, edge)
        
        # Try both directions of the vector
        angle1 = AngleBetweenEdges(current_vector, edge_vector)
        angle2 = AngleBetweenEdges(current_vector, -edge_vector)
        angle = min(angle1, angle2)
        
        if angle < angle_tol and angle < best_angle:
            best_edge = edge
            best_angle = angle
    
    return best_edge

def FindEdgeChains(mesh, min_length=2):
    """Find all chains of connected edges within the angle tolerance"""
    chains = []
    visited_edges = set()
    
    # Try starting from each edge
    for start_edge in range(mesh.TopologyEdges.Count):
        if start_edge in visited_edges:
            continue
            
        # Start a new chain
        current_chain = [start_edge]
        visited_edges.add(start_edge)
        
        # Get the edge vector
        current_vector, _, _ = GetEdgeVector(mesh, start_edge)
        
        # Find the next edge in the chain
        next_edge = FindNextEdgeInChain(mesh, start_edge, current_vector, visited_edges)
        
        # Continue adding edges to the chain
        while next_edge is not None:
            current_chain.append(next_edge)
            visited_edges.add(next_edge)
            
            current_vector, _, _ = GetEdgeVector(mesh, next_edge)
            next_edge = FindNextEdgeInChain(mesh, next_edge, current_vector, visited_edges)
        
        # Only consider chains with at least the minimum length
        if len(current_chain) >= min_length:
            chains.append(current_chain)
    
    return chains

def FindEdgeLoops(mesh, chains):
    """Try to connect chains to form loops"""
    loops = []
    remaining_chains = chains[:]
    
    while remaining_chains:
        current_loop = remaining_chains.pop(0)
        
        # Try to extend the current loop with other chains
        changed = True
        while changed:
            changed = False
            for i, chain in enumerate(remaining_chains):
                # Check if the first edge of the chain connects to the last edge of the loop
                if chain[0] in FindConnectedEdges(mesh, current_loop[-1]):
                    current_loop.extend(chain)
                    del remaining_chains[i]
                    changed = True
                    break
                    
                # Check if the last edge of the chain connects to the first edge of the loop
                elif chain[-1] in FindConnectedEdges(mesh, current_loop[0]):
                    current_loop = chain + current_loop
                    del remaining_chains[i]
                    changed = True
                    break
        
        # Check if the loop is closed (first and last edges are connected)
        if current_loop[0] in FindConnectedEdges(mesh, current_loop[-1]):
            loops.append(current_loop)
        else:
            # It's an open chain
            loops.append(current_loop)
    
    return loops

def VisualizeEdgeChains(mesh, chains):
    """Visualize each edge chain with a different color"""
    colors = [
        System.Drawing.Color.Red,
        System.Drawing.Color.Blue,
        System.Drawing.Color.Green,
        System.Drawing.Color.Yellow,
        System.Drawing.Color.Magenta,
        System.Drawing.Color.Cyan,
        System.Drawing.Color.Orange,
        System.Drawing.Color.Purple,
        System.Drawing.Color.Brown,
        System.Drawing.Color.Pink
    ]
    
    for i, chain in enumerate(chains):
        color = colors[i % len(colors)]
        
        for edge in chain:
            # Get edge geometry
            line = mesh.TopologyEdges.EdgeLine(edge)
            
            # Add the line to the document with the chain's color
            attr = Rhino.DocObjects.ObjectAttributes()
            attr.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject
            attr.ObjectColor = color
            
            scriptcontext.doc.Objects.AddLine(line, attr)
    
    scriptcontext.doc.Views.Redraw()

def GetMeshEdgeChains():
    mesh_id = rs.GetObject("Mesh", 32, True, False)
    if mesh_id is None:
        return
        if not rs.IsObject(mesh_id): return
    
    mesh_obj = rs.coercerhinoobject(mesh_id)
    mesh = mesh_obj.Geometry
    
    if mesh.Faces.QuadCount != 0: 
        print "Triangulate mesh first"
        return
    
    # Let the user input the minimum chain length
    min_length = rs.GetInteger("Minimum number of edges in a chain", min_chain_length, 2, 100)
    if min_length is None: min_length = min_chain_length  # Use default if canceled
    
    # Let the user input the angle tolerance
    user_angle_tol = rs.GetReal("Angle tolerance in degrees", angle_tol, 0.1, 45.0)
    if user_angle_tol is not None: 
        global angle_tol
        angle_tol = user_angle_tol
    
    # Find all edge chains
    chains = FindEdgeChains(mesh, min_length)
    
    # Try to form loops
    loops = FindEdgeLoops(mesh, chains)
    
    print "Found {} edge chains with at least {} edges (angle tolerance: {:.1f} degrees)".format(
        len(chains), min_length, angle_tol)
    
    # Visualize the chains
    VisualizeEdgeChains(mesh, chains)
    
    # Optionally, also visualize the loops with different colors
    print "Found {} edge loops".format(len(loops))
    if loops:
        # Only visualize loops if they have at least the minimum length
        filtered_loops = [loop for loop in loops if len(loop) >= min_length]
        print "Found {} edge loops with at least {} edges".format(len(filtered_loops), min_length)
        if filtered_loops:
            VisualizeEdgeChains(mesh, filtered_loops)
    
GetMeshEdgeChains()
2 Likes