Curve.OffsetTangentToSurface direction isn't predictable

Hello,

I’m tinkering with a script that’s meant to create a custom ribbon around a 3D edge, for which I’m extruding a curve using the new Curve method OffsetTangentToSurface(Surface surface, Double length). The issue is that, for some imported geometries that have been repaired and standardized explicitly, two continuous edges that share the same face are extruded in different directions.


When touching the Brep seen in the pictures in the code, I’ve ran a check through the trims and there’s A LOT of them that will return True at IsReversed(). Nonetheless, the function Reverse() returns False (fails at reversing them). Both at the Trim itself and at its associated Edge.

Is there a way to obtain the expected behaviour, running checks or directly processing the trims or the surfaces in a certain way? For reference, the way I generated the trims uses the following function:

def ExtrudeEdgesNormalToSurface(brepGeometry):
    nakedEdges = list()
    extrusions = list()
    workEdges = brepGeometry.Edges
    for edge in workEdges:
        if edge.Valence == Rhino.Geometry.EdgeAdjacency.Naked:
            nakedEdges.append(edge)
    for nakedEdge in nakedEdges:
        adjacentFace = nakedEdge.AdjacentFaces()[0]
        edgeCurve = nakedEdge.EdgeCurve
        extrudedCurve = edgeCurve.OffsetTangentToSurface(brepGeometry.Faces[adjacentFace], System.Double(30.0))
        extrudedSurface = Rhino.Geometry.Brep.CreateFromLoft([edgeCurve, extrudedCurve], Rhino.Geometry.Point3d.Unset, Rhino.Geometry.Point3d.Unset, Rhino.Geometry.LoftType.Straight, False)
        extrusions.append(extrudedSurface)
    return extrusions

The brep geometry that is used as an input is repaired, standardized, and compacted. Then, the resulting extrusions are added to the document objects table.

Are there any obvious solutions to this? Thanks in advance.

  • Jose.

Hi @Jose10,

Curve.OffsetTangentToSurface is what is used by Rhino’s Fin command.

You’ll need to post some geometry and a script that allows us to see what you see.

Also, you mention ribbon, which makes me think of Curve.RibbonOffset. Not sure this this is related or helpful.

– Dale

Thanks for the response Dale,

Here’s a test script I generated to demonstrate this:

#! python3
import time
import System
import System.Collections.Generic
import rhinoscriptsyntax as rs
import scriptcontext as sc
import Rhino
import math
import Rhino.Geometry as rg
import System.Drawing
import Rhino.DocObjects as OT

def SetupDocument(unitSystem):
    if rs.UnitSystem != unitSystem:
        rs.UnitSystem(unitSystem, True)
        print("Units converted to " + rs.UnitSystemName(False, False, True, True))
    objectsTable = sc.doc.Objects
    layerTable = sc.doc.Layers
    absoluteDimensionTolerance = rs.UnitAbsoluteTolerance()
    relativeDimensionTolerance = rs.UnitRelativeTolerance()
    #angleTolerance = rs.UnitAngleTolerance()
    angleTolerance = 0.05 #Tested best angle tolerance for most stuff
    return objectsTable, layerTable, absoluteDimensionTolerance, relativeDimensionTolerance, angleTolerance

def SelectSingleGeometry(prompt, geometryFilters, doPreselect, doSelect):
    selectedID = rs.GetObject(prompt, geometryFilters, doPreselect, doSelect)
    selectedObject = sc.doc.Objects.FindId(selectedID)
    selectedGeometry = selectedObject.Geometry
    return selectedID, selectedObject, selectedGeometry
	
def ProcessGeometry(brep, doRepair, doForceOutwards, doFindNull):
    ### REPAIR
    if doRepair:
        print ("Attempting to repair geometry...")
        brep.Repair(absoluteDimensionTolerance)
        print("Brep repaired")
        brep.Standardize()
        print("Brep standardized")
        brep.Compact()
        print("Brep compacted")
    ### FORCE OUTWARDS
    if doForceOutwards:
        if brepSelectedGeometry.IsSolid == True:
            print("Brep is solid")
            if brepSelectedGeometry.SolidOrientation == rg.BrepSolidOrientation.Inward:
                brepSelectedGeometry.Flip
                print("Polysurface flipped, now it is facing" + brepSelectedGeometry.SolidOrientation.ToString())
    ### FIND NULL
    if doFindNull:
        if brepSelectedGeometry.GetArea() == None:
            print("Null surfaces detected.")
            # Do something about them or not?
            # Find the face IDs, save them, and prompt the user later whether he wants to extract them or not
        else:
            print ("No null surfaces detected.")
    print("Repair finished!")
	
def CreateEdgeLoopsFromNakedEdges(allNakedEdges):
    groupContainer = list()
    workNakedEdges = allNakedEdges
    # As long as there are edges that haven't been tested:
    while workNakedEdges:
        # Initialize the chain
        chain = list()
        chain.append(workNakedEdges[0])
        chainStart = chain[0].StartVertex.VertexIndex
        chainEnd = chain[0].EndVertex.VertexIndex
        workNakedEdges.pop(0)
        number = len(allNakedEdges) * 4
        # As long as the end vertex isn't equal to the Start Vertex:
        while chainEnd != chainStart:
            if workNakedEdges:
                for edge in workNakedEdges:
                    if (chainEnd == edge.StartVertex.VertexIndex):
                        #print ("End to start " + str(chainEnd) + " -> " + str(edge.StartVertex.VertexIndex))
                        chain.append(edge)
                        chainEnd = edge.EndVertex.VertexIndex
                        number = len(allNakedEdges)*4
                        workNakedEdges.remove(edge)
                    elif (chainEnd == edge.EndVertex.VertexIndex):
                        #print ("End to end " + str(chainEnd) + " -> " + str(edge.EndVertex.VertexIndex))
                        chain.append(edge)
                        chainEnd = edge.StartVertex.VertexIndex
                        number = len(allNakedEdges)*4
                        workNakedEdges.remove(edge)
                    elif (chainStart == edge.StartVertex.VertexIndex):
                        #print ("Start to start " + str(chainStart) + " -> " + str(edge.StartVertex.VertexIndex))
                        chain.insert(0,edge)
                        chainStart = edge.EndVertex.VertexIndex
                        number = len(allNakedEdges)*4
                        workNakedEdges.remove(edge)
                    elif (chainStart == edge.EndVertex.VertexIndex):
                        #print ("Start to end " + str(chainStart) + " -> " + str(edge.EndVertex.VertexIndex))
                        chain.insert(0,edge)
                        chainStart = edge.StartVertex.VertexIndex
                        number = len(allNakedEdges)*4
                        workNakedEdges.remove(edge)
                    elif (chainStart == chainEnd):
                        #print ("Chain is closed!")
                        break
            else:
                if (chainStart == chainEnd):
                    #print ("Chain is closed!")
                    break
            # Crash prevention...
            number -= 1
            if number <= 0:
                #print ("Loop timeout! "+str(chainStart)+ " -> " + str(chainEnd) + " can't find match.")
                print (str(edge.StartVertex.VertexIndex) + " " + str(edge.EndVertex.VertexIndex))
                break
        '''
        chainedges = list()
        for edge in chain:
            chainedges.append(edge.EdgeIndex)
        print(chainedges)
        '''
        groupContainer.append(chain)
        #print ("Container made")
    #print (len(groupContainer))
    return groupContainer

def CreatePartEdges(brepGeometry):
    workEdges = brepGeometry.Edges
    edgeCurves = list()
    nakedEdges = list()
    for edge in workEdges:
        if edge.Valence == Rhino.Geometry.EdgeAdjacency.Naked:
            edgeCurves.append(edge.EdgeCurve)
            nakedEdges.append(edge)

    edgeLoops = CreateEdgeLoopsFromNakedEdges(nakedEdges)
    curvesV2 = list()
    for group in edgeLoops:
        curveLoop = list()
        for edge in group:
            curveLoop.append(edge.EdgeCurve)
        curvesV2.append(curveLoop)
    # Join edge curves in loops
    curvesV3 = list()
    planarEdgeCurves = list()
    nonPlanarEdgeCurves = list()
    circleEdgeCurves = list()
    for curveLoop in curvesV2:
        loopedEdges = Rhino.Geometry.Curve.JoinCurves(curveLoop)
        if loopedEdges:
            for loop in loopedEdges:
                curvesV3.append(loopedEdges[0])
                if loop.IsCircle(relativeDimensionTolerance):
                    circleEdgeCurves.append(loop)
                elif loop.IsEllipse(relativeDimensionTolerance):
                    circleEdgeCurves.append(loop)
                elif loop.IsPlanar(relativeDimensionTolerance):
                    planarEdgeCurves.append(loop)
                else:
                    nonPlanarEdgeCurves.append(loop)
    print (len(planarEdgeCurves), len(nonPlanarEdgeCurves), len(circleEdgeCurves))
    return planarEdgeCurves, nonPlanarEdgeCurves, circleEdgeCurves, curvesV3

def ExtrudeEdgesNormalToSurface(brepGeometry):
    nakedEdges = list()
    extrusions = list()
    workEdges = brepGeometry.Edges
    
    for edge in workEdges:
        if edge.Valence == Rhino.Geometry.EdgeAdjacency.Naked:
            
            nakedEdges.append(edge)
    for nakedEdge in nakedEdges:
        adjacentFace = nakedEdge.AdjacentFaces()[0]
        edgeCurve = nakedEdge.EdgeCurve
        extrudedCurve = edgeCurve.OffsetTangentToSurface(brepGeometry.Faces[adjacentFace], System.Double(3.0))
        extrudedSurface = Rhino.Geometry.Brep.CreateFromLoft([edgeCurve, extrudedCurve], Rhino.Geometry.Point3d.Unset, Rhino.Geometry.Point3d.Unset, Rhino.Geometry.LoftType.Straight, False)
        extrusions.append(extrudedSurface)
    return extrusions

# Setup
objectsTable, layerTable, absoluteDimensionTolerance, relativeDimensionTolerance, angleTolerance = SetupDocument(2)

# Select the brep to process
brepSelectedID, brepSelectedObject, brepSelectedGeometry = SelectSingleGeometry("Select a polysurface", rs.filter.polysurface, True, True)
allFaces = brepSelectedGeometry.Faces
allEdges = brepSelectedGeometry.Edges

# Process geometry
ProcessGeometry(brepSelectedGeometry, True, True, True)

planarEdgeCurves, nonPlanarEdgeCurves, circleEdgeCurves, allEdgeCurves = CreatePartEdges(brepSelectedGeometry)

extrusions = ''
for curveSet in allEdgeCurves:
    extrusions = ExtrudeEdgesNormalToSurface(brepSelectedGeometry)

for extrusion in extrusions:
        GUID = objectsTable.AddBrep(extrusion[0])

And the file I used for the testing. The result (computed in my machine) is in a different layer to the main surface. Used Rhino 8.4.24044.150001.

Reference3dm.3dm (1.4 MB)

The goal of the script is to generate a constant height surface that is tangent (G1) to the geometry, which then will be closed, sewn, and used as a base for more operations involving normal, tangent, and linear extrusions from the edges.

The difficulty I found is that, since I need to perform this edge by edge, some edges do not extrude in the same direction as the neighbour’s, within the same base surface. I’m guessing that it has to do with the orientation of the trim, but I would really appreciate your insight; and any suggestions on how to make this extrusion direction predictable.

Lastly, what I’m doing is similar to a Ribbon as in Curve.RibbonOffset, but it’s abstracted to 3 dimensions using custom logic, and has to discriminate between offsetting tangent or normal.

Thanks in advance,

  • Jose

Hi Jose - the edges have different directions - if you look at the trims, you can find out if the trim is reversed, then either reverse, or not, the curve from the 3d edge that is associated with the trim:

def ExtrudeEdgesNormalToSurface(brepGeometry):
    faces = brepGeometry.Faces
    trims = brepGeometry.Trims
    for trim in trims:

        e = trim.Edge
        if e.Valence== Rhino.Geometry.EdgeAdjacency.Naked:

            crv = e.EdgeCurve
            if trim.IsReversed():
                crv.Reverse()
         
            extrudedCurve = crv.OffsetTangentToSurface(e.AdjacentFaces()[0], System.Double(3.0))
            lofts = Rhino.Geometry.Brep.CreateFromLoft([crv, extrudedCurve], Rhino.Geometry.Point3d.Unset, Rhino.Geometry.Point3d.Unset, Rhino.Geometry.LoftType.Straight, False)
            
            for item in lofts:
                if item.IsValid:
                  #etc etc, for example
                 sc.doc.Objects.AddBrep(item)


    
    sc.doc.Views.Redraw()
        

Does that help at all?

-Pascal

1 Like

Here is another approach in similar spirit to the sample @pascal posted.

import Rhino
import scriptcontext as sc

def TestOffsetTangentToSurface():
    filter = Rhino.DocObjects.ObjectType.Surface
    rc, objref = Rhino.Input.RhinoGet.GetOneObject("Select surface", False, filter)
    if not objref or rc != Rhino.Commands.Result.Success:
        return
        
    brep = objref.Brep()
    if not brep:
        return
    
    for trim in brep.Trims:
        edge = trim.Edge
        if edge:
            dist = 3.0
            # Get orientation of trim with respect to it's corresponding edge
            if trim.IsReversed():
                dist = dist * -1
            curve = edge.OffsetTangentToSurface(bcopy.Faces[0], dist)
            if curve:
                sc.doc.Objects.AddCurve(curve)
                
    sc.doc.Views.Redraw()

if __name__ == "__main__":
    TestOffsetTangentToSurface()

– Dale

Thank you very much, @dale and @pascal. I have tried Pascal’s solution and it works just as expected.

I have one final question in this topic. Is it possible to process the brep beforehand, so that no trims are reversed? Or do I have to invert the curves/distances at the moment of creating the curve?

Thanks in advance.

-Jose

Hi @Jose10,

Sorry, no.

– Dale

Understood, thank you very much!

-Jose