Geometry.Curve.Offset returning only one curve when self-intersecting

Hi,

Reading the docs, the Curve.Offstet method should return an Array with more than one curve when the curve offset results in self intersections: " If the original curve had kinks or the offset curve had self intersections, you will get multiple segments in the output array.", but this is not happening. The output is just the overlaping portion of the resulting offset, I think its a bug:


If I try with Rhino offset command and “Trim = No” I get a correct offset, but there is not a “Trim” property in the RhinoCommon Method.

Python Code:

from scriptcontext import doc
import Rhino
import rhinoscriptsyntax as rs
from Rhino import Geometry as rg

plane = rg.Plane.WorldXY
tol = doc.ModelAbsoluteTolerance
off = x.Offset(plane, 6, tol, rg.CurveOffsetCornerStyle(2))

print off

a = off

I tried the 3 offset methods already (with vector and point), with the same result. How can I get the complete output array, or the offset without trim?

This is the curve:
intersection bug curve.3dm (25.5 KB)

Thanks!

1 Like

Hi,

This is a known limitation of the offset Curve command.

It should be solved in Rhino WIP.

1 Like

I made a (very messy) workaround code in python that works in R7, if anyone interested I can clean the code and pass it to a C# component. It works with nurbs but needs aditional code for almost-intersecting curves.

The steps:

  1. Detect if the curve self intersects, and then trim it
  2. Find if the resultant curves can be closed
  3. Obtain the segments of the open lines and offset it
  4. Shatter all curves with intersection
  5. Clean the curves based on distance
  6. Join everything

The code (I plan to clean it up when I have time):

from scriptcontext import doc
import Rhino
import rhinoscriptsyntax as rs
from Rhino import Geometry as rg
from ghpythonlib.parallel import run
plane = rg.Plane.WorldXY
tol = doc.ModelAbsoluteTolerance


c = []
d = []

def shatterAll(crvs):
    if len(crvs) == 1:
        return crvs
    else:
        params = []
        for i in range(len(crvs)):
            params.append([])
        for i in range(len(crvs)):
            for j in range(i+1, len(crvs)):
                try:
                    events = rg.Intersect.Intersection.CurveCurve(crvs[i], crvs[j], tol, tol)
                    for event in events:
                        params[i].append(event.ParameterA)
                        params[j].append(event.ParameterB)
                except:
                    pass
        sub_curves = []
        for i in range(len(crvs)):
            if params[i]:
                split = crvs[i].Split(params[i])
                for split in split:
                    sub_curves.append(split)
            else:
                sub_curves.append(crvs[i])
        return sub_curves

def offsetCrvs(x):
    #the nasty mess is needed?
    defaultOffset = x.Offset(plane, distance, tol, rg.CurveOffsetCornerStyle(2))
    if defaultOffset[0].IsClosed:
        b = []
        #verify if curve interscets itself:
        int = rg.Intersect.Intersection.CurveSelf(x, tol)
        intCount = int.Count
        if intCount > 0:
            params = []
            for event in int:
                params.append(event.ParameterA)
                params.append(event.ParameterB)
            preSplit = rg.Curve.Split(x, params)
            for i, crvSplit in enumerate(preSplit):
                if crvSplit.IsClosed:
                    if str(crvSplit.ClosedCurveOrientation()) == 'Clockwise':
                        preSplit[i].Reverse()
        else:
            preSplit = [x]
        a =  preSplit
        valid = []
        
        #TODO: verify if bug will occur
        
        
        for crvS in preSplit:
            crvS = crvS.ToNurbsCurve()
            #get curve segments
            if isinstance(crvS, rg.NurbsCurve):
                SpanCount = crvS.SpanCount
                spanParams = []
                for i in range(SpanCount):
                    spanParams.append(crvS.SpanDomain(i)[0])
                    spanParams.append(crvS.SpanDomain(i)[1])
                splitAtSpans = crvS.Split(spanParams)
                toSegments = splitAtSpans
            else:
                toSegments = crvS
            try:
                segments = toSegments.DuplicateSegments()
            except:
                segments = toSegments
            #print segments
            for crv in segments:
                #verify if is closed
                offset = crv.Offset(plane, distance, tol, rg.CurveOffsetCornerStyle(2))
                if offset != None:
                    b.append(offset[0])
            #join reusults
            joined = rg.Curve.JoinCurves(b)
            #verify self intersections
            for crv in joined:
                selfInt = rg.Intersect.Intersection.CurveSelf(crv, tol)
                intersectionCount = selfInt.Count
                #split the damn crv:
                if intersectionCount > 0:
                    params = []
                    for event in selfInt:
                        params.append(event.ParameterA)
                        params.append(event.ParameterB)
                    split = crv.Split(params)
                    #filter the thingy things
                    for crvs in split:
                        #verify the distance from the input crv
                        minDist = crvs.ClosestPoints(x)
                        minDist = minDist[1].DistanceTo(minDist[2])
                        if minDist >= (distance-tol):
                            valid.append(crvs)
                else:
                    valid.append(crv)
        
        #need to cleanup the mess:
        dist_ = distance
        shatter = shatterAll(valid)
        if distance < 0:
            dist_ = abs(dist_)
        for crvs in shatter:
            cps = x.ClosestPoints(crvs)
            dist = cps[1].DistanceTo(cps[2])
            if dist > (dist_-tol):
                if crvs.IsValid:
                    if crvs.GetLength() > tol:
                        d.append(crvs.ToNurbsCurve())
    
        join = rg.Curve.JoinCurves(d, dist_)
        for crv in join:
            c.append(crv.ToNurbsCurve())
        
    else:
        c.append(defaultOffset[0])
    return c


run(offsetCrvs, x, True)
if bothSides:
    distance = -distance
    run(offsetCrvs, x, True)```

inputs: x[curve], distance[float], bothSides[bool]
output: c
1 Like

This is a good first cut! I’m running into the same issue, but need it to work with polycurves as well. I was hoping to get to steal your workaround while we wait for Rhino 8, but it looks like I’ll have to do some additional work:

OffsetInward

There would be an easier work around if we could have a trim option in RhinoCommon just like we do in the rhino command, then we could choose which loops to keep and which to throw out. I’m playing with the Rhino 8 version now, and it’s better behaved, but I’m still not happy about it. If the input curve isn’t self intersecting we get better behavior, but it if is, there’s some awkward results when offsetting on both sides.

Offsetting in one direction yields:


And trimming looses the offset around the section that self intersected:

Offsetting the other direction yields:


And trimming it does nothing other that weirdly splitting the resulting curve at the seam of the closed polycurve:

So I guess I’d comeback your observation that there is not an option for controlling whether or not the offset is trimmed in the RhinoCommon API. @dale Is this something we could have?

Hey Cullen!

I made a new version of this workaround that is compatible with polycurves too, but I need to decouple from a much larger script (it gets the distance from user strings, and offset both sides right now).

I’ll find some time on the weekend and post it here.

1 Like

@doo.cabral
Can you share your script here?
Maybe i can develop it in C#

Get the gist of the approach (shown inwards but the same goes for the other way) and try to do it using Polylines (the easy part).

For PolyCrvs it’s a bit different mind. That said that’s not the easiest of things (but hope dies last).