Python Rhino: Offset Multiple

November 28, 2011
by admin

    After hearing some desire for a script to offset multiple times I decided to go ahead and tackle this problem. Rhino, like many programs, has an offset command.  The offset command allows you to select a curve and offset by a specific amount.  There are quick ways of duplicating that offset but to my knowledge there is no internal way of auto creating the offsets (there is probably another script out there).  As an overall offset multiple tool I found it necessary to write a few functions to make this script useful. 
    1) The first option is to offset from the outside of a polygon to the inside with a stop from the centroid.  The user can select a distance from the center that they dont want to offset past.  Within this option, there are two sub-options.  The first allows the user to enter an offset spacing, this will offset at a specific spacing until it reaches the bound.  The second allows the user to enter a number of offsets, this will automatically create the offset distance for that number between the exterior and bounding radius.  This function will also work to offset outside instead of inside.

    def Offset_CntrDist():
    #Description: Offset between polygon and interior boundry
    #Parameters: None
    #Returns: None
    #Notes:
        off_num=rs.GetInteger("Enter number of offsets, 'Enter' for auto", 0)
        off_dist=rs.GetReal("Enter distance from center to stop offset", 1)
        curve_sel=rs.GetObject("Select Curve")
        #find centroid for drawing circle
        #crv_centroid=rs.CurveAreaCentroid(curve_sel)
        cntr_pnt=Offset_Direction(curve_sel)
        #create radius of circle based on distance from center to stop offset
        circ_radius = (.5*off_dist)
        #create circle to use as intersection curve, delete after
        cntr_circle=rs.AddCircle(cntr_pnt, circ_radius)
        dist=Get_Dist(cntr_circle, curve_sel)
        if off_num==0:
            off_space=rs.GetReal("Enter offset spacing",1)
        else:
            off_space=(dist/off_num)
        if rs.CurveArea(cntr_circle) < rs.CurveArea(curve_sel):
            off_direct = Offset_Direction(curve_sel)
            direct=0
        elif rs.CurveArea(cntr_circle) > rs.CurveArea(curve_sel):
            off_direct = Reverse_Direction(curve_sel)
            direct=1
        else:
            print"Direction Assignment Fail"
        old_curve=curve_sel
        total_off=0
        while total_off < dist:
            new_curve=rs.OffsetCurve(old_curve,off_direct,off_space)
            old_curve=new_curve
            #Direct, 0 inside, 1 outside
            if rs.CurveCurveIntersection(cntr_circle, new_curve) and direct==1:
                rs.DeleteObject(new_curve)
                break
            if rs.CurveArea(cntr_circle) > rs.CurveArea(new_curve) and direct==0:
                rs.DeleteObject(new_curve)
                break
            if rs.CurveArea(cntr_circle) < rs.CurveArea(new_curve) and direct==1:
                rs.DeleteObject(new_curve)
                break
            if len(old_curve)>1:
                rs.DeleteObjects(curve_sel)
                break
            total_off=total_off+off_space
    
        rs.DeleteObject(cntr_circle)
    

    In order to get the bounding distance, this function calls Get_Dist():

    def Get_Dist(cntr_circle, curve_sel):
    #Description: Finds the two closest points from radius of boundry and polygon 
    #Parameters: Takes in a Circle and Curve
    #Returns: Distance between closest points
    #Notes:
        curve_pt=rs.CurvePoints(cntr_circle)
        close_point=rs.CurveClosestObject(cntr_circle, curve_sel)
        #Distance between points
        x=close_point[1][0] - close_point[2][0]
        y=close_point[1][1] - close_point[2][1]
        dist=sqrt(pow(x,2)+pow(y,2))
    
        return dist
    

    2) The second option is a middle ground between the first and third.  The user specifies the offset distance and the number of offsets.  They can also select to offset inside or outside.

    #Offsets curve by distance and number of offsets
    def Offset_limit():
        #Get input 
        off_num=rs.GetInteger("Enter number of offsets", 1)
        off_dist=rs.GetReal("Enter distance between offsets", 1)
        off_direct_choice=rs.GetInteger("1 For inside, 2 for outside", 1)
        curve_sel=rs.GetObject("Select curve")
        if off_direct_choice==1:
            off_direct=Offset_Direction(curve_sel)
        elif off_direct_choice==2:
            off_direct=Reverse_Direction(curve_sel)
        else:
            print"Broke"
        
        for i in range (off_num):
            #rs.AddPoint(rs.CurveMidPoint(curve_sel))
            new_curve=rs.OffsetCurve(curve_sel, off_direct, off_dist)
            curve_sel=new_curve
            #If offset creates more than one curve, delete those curves and exit
            #Stops infinite Loops in the other versions of offset
            if len(curve_sel)>1:
                rs.DeleteObjects(curve_sel)
                break
            curve_sel_area=rs.CurveArea(curve_sel)
            #print(curve_sel_area)
            new_curve_area=rs.CurveArea(new_curve)
            #print(new_curve_area)
    
    

    3) The last option is closest to the normal offset function but continues until it is done. The user selects a curve and enters the distance of the offset.  The curve will offset until the offset creates a curve that does not match the original.

    #Offsets curve by distance until curve is too small
    def Offset_total():
        off_dist=rs.GetReal("Enter distance of offsets",1)
        off_dist=abs(off_dist)
        curve_sel=rs.GetObject("Select curve")
        off_direct=Offset_Direction(curve_sel)
        new_curve=rs.OffsetCurve(curve_sel, off_direct, off_dist)
        #get curve area  for safety break if offset goes outside (it would be infinite)
        if rs.IsCurveClosed(curve_sel):
            curve_sel_area=rs.CurveArea(curve_sel)
            print(curve_sel_area)
            new_curve_area=rs.CurveArea(new_curve)
            print(new_curve_area)   
        i=0
        while curve_sel_area > new_curve_area and new_curve !=None:
            curve_sel=new_curve
            new_curve=rs.OffsetCurve(curve_sel, off_direct, off_dist)
            if new_curve is None:
                break
            if len(new_curve)>1:
                rs.DeleteObjects(new_curve)
                break
            i=i+1   
            curve_sel_area=rs.CurveArea(curve_sel)
            new_curve_area=rs.CurveArea(new_curve)
            if curve_sel_area < new_curve_area:
                rs.DeleteObjects(new_curve)
    

    These three offset functions all utilize similar functions. The most obvious is the internal rhino offset, declared by rs.OffsetCurve(). The also call one or both of the functions Offset_Direction() and Reverse_Direction(). If you read the previous post on python centroids, it may be clear now why I went through the trouble of figuring out the centroid. The since rhino needs to know which direction to offset the curve, and normally a user clicks the side, I needed a way to specify where the offset should take place. Before I knew there was a centroid module I wrote my own that worked with this function, but for ease of other people using the script I use the one internal to rhino. Within the offset functions, the user specifies to offset internal or external. For an internal offset I call my Offset_Direction() function that returns the centroid as the point to offset towards:

    #Function to find direction of Offset, Works on normal of curve.  User must set normal for specified task
    def Offset_Direction(curve_sel):
        crv_centroid=rs.CurveAreaCentroid(curve_sel)
        off_direct=crv_centroid[0]
        return off_direct
    

    If the user selects to offset to the outside I call Reverse_Direction() which will take the centroid and multiply by -1. This creates a point outside of the centroid bounds, which is sufficient for rhino to offset outside:

    #can be used to change offset direction
    def Reverse_Direction(curve_sel):
        crv_centroid=rs.CurveAreaCentroid(curve_sel)
        off_direct=crv_centroid[0]
        off_direct=off_direct*(-1)
        return off_direct
    

    The main function is just an if else statement that selects which one to go into:

    #Main function for deciding which offset system to use 
    def Offset_main():
        tool_select=rs.GetInteger("1) Offset as many as possible (Closed Curve Only), 2) Offset by specified amount, 3) Ooffset between a distance", 3)
        if tool_select==1:
            Offset_total()
        elif tool_select==2:
            Offset_limit()
        else:
            Offset_CntrDist()
    Offset_main()
    
    

    The entire code is below. I havent tested alot of cases, there are some times when you can plug in a negative to get the opposite effect. If you figure out any problems, especially if you fix it please let me know!

    #Offset Multiple Module
    import rhinoscriptsyntax as rs
    import sys
    from math import *
    #import UserInput as ui
    #import Centroid as ctd
    
    def Offset_CntrDist():
    #Description: Offset between polygon and interior boundry
    #Parameters: None
    #Returns: None
    #Notes:
        off_num=rs.GetInteger("Enter number of offsets, 'Enter' for auto", 0)
        off_dist=rs.GetReal("Enter distance from center to stop offset", 1)
        curve_sel=rs.GetObject("Select Curve")
        #find centroid for drawing circle
        #crv_centroid=rs.CurveAreaCentroid(curve_sel)
        cntr_pnt=Offset_Direction(curve_sel)
        #create radius of circle based on distance from center to stop offset
        circ_radius = (.5*off_dist)
        #create circle to use as intersection curve, delete after
        cntr_circle=rs.AddCircle(cntr_pnt, circ_radius)
        dist=Get_Dist(cntr_circle, curve_sel)
        if off_num==0:
            off_space=rs.GetReal("Enter offset spacing",1)
        else:
            off_space=(dist/off_num)
        if rs.CurveArea(cntr_circle) < rs.CurveArea(curve_sel):
            off_direct = Offset_Direction(curve_sel)
            direct=0
        elif rs.CurveArea(cntr_circle) > rs.CurveArea(curve_sel):
            off_direct = Reverse_Direction(curve_sel)
            direct=1
        else:
            print"Direction Assignment Fail"
        old_curve=curve_sel
        total_off=0
        while total_off < dist:
            new_curve=rs.OffsetCurve(old_curve,off_direct,off_space)
            old_curve=new_curve
            #Direct, 0 inside, 1 outside
            if rs.CurveCurveIntersection(cntr_circle, new_curve) and direct==1:
                rs.DeleteObject(new_curve)
                break
            if rs.CurveArea(cntr_circle) > rs.CurveArea(new_curve) and direct==0:
                rs.DeleteObject(new_curve)
                break
            if rs.CurveArea(cntr_circle) < rs.CurveArea(new_curve) and direct==1:
                rs.DeleteObject(new_curve)
                break
            if len(old_curve)>1:
                rs.DeleteObjects(curve_sel)
                break
            total_off=total_off+off_space
    
        rs.DeleteObject(cntr_circle)
          
    def Get_Dist(cntr_circle, curve_sel):
    #Description: Finds the two closest points from radius of boundry and polygon 
    #Parameters: Takes in a Circle and Curve
    #Returns: Distance between closest points
    #Notes:
        curve_pt=rs.CurvePoints(cntr_circle)
        close_point=rs.CurveClosestObject(cntr_circle, curve_sel)
        #Distance between points
        x=close_point[1][0] - close_point[2][0]
        y=close_point[1][1] - close_point[2][1]
        dist=sqrt(pow(x,2)+pow(y,2))
    
        return dist
        
    #Offsets curve by distance and number of offsets
    def Offset_limit():
        #Get input 
        off_num=rs.GetInteger("Enter number of offsets", 1)
        off_dist=rs.GetReal("Enter distance between offsets", 1)
        off_direct_choice=rs.GetInteger("1 For inside, 2 for outside", 1)
        curve_sel=rs.GetObject("Select curve")
        if off_direct_choice==1:
            off_direct=Offset_Direction(curve_sel)
        elif off_direct_choice==2:
            off_direct=Reverse_Direction(curve_sel)
        else:
            print"Broke"
        
        for i in range (off_num):
            #rs.AddPoint(rs.CurveMidPoint(curve_sel))
            new_curve=rs.OffsetCurve(curve_sel, off_direct, off_dist)
            curve_sel=new_curve
            #If offset creates more than one curve, delete those curves and exit
            #Stops infinite Loops in the other versions of offset
            if len(curve_sel)>1:
                rs.DeleteObjects(curve_sel)
                break
            curve_sel_area=rs.CurveArea(curve_sel)
            #print(curve_sel_area)
            new_curve_area=rs.CurveArea(new_curve)
            #print(new_curve_area)
    
    
    #Offsets curve by distance until curve is too small
    def Offset_total():
        off_dist=rs.GetReal("Enter distance of offsets",1)
        off_dist=abs(off_dist)
        curve_sel=rs.GetObject("Select curve")
        off_direct=Offset_Direction(curve_sel)
        new_curve=rs.OffsetCurve(curve_sel, off_direct, off_dist)
        #get curve area  for safety break if offset goes outside (it would be infinite)
        if rs.IsCurveClosed(curve_sel):
            curve_sel_area=rs.CurveArea(curve_sel)
            print(curve_sel_area)
            new_curve_area=rs.CurveArea(new_curve)
            print(new_curve_area)   
        i=0
        while curve_sel_area > new_curve_area and new_curve !=None:
            curve_sel=new_curve
            new_curve=rs.OffsetCurve(curve_sel, off_direct, off_dist)
            if new_curve is None:
                break
            if len(new_curve)>1:
                rs.DeleteObjects(new_curve)
                break
            i=i+1   
            curve_sel_area=rs.CurveArea(curve_sel)
            new_curve_area=rs.CurveArea(new_curve)
            if curve_sel_area < new_curve_area:
                rs.DeleteObjects(new_curve)
    
    #Function to find direction of Offset, Works on normal of curve.  User must set normal for specified task
    def Offset_Direction(curve_sel):
        crv_centroid=rs.CurveAreaCentroid(curve_sel)
        off_direct=crv_centroid[0]
        #rs.AddPoint(off_direct)
        print(off_direct)
        return off_direct
    
    #can be used to change offset direction
    def Reverse_Direction(curve_sel):
        crv_centroid=rs.CurveAreaCentroid(curve_sel)
        off_direct=crv_centroid[0]
        off_direct=off_direct*(-1)
        print("Centroid",off_direct)
        return off_direct
        
    #Main function for deciding which offset system to use 
    def Offset_main():
        tool_select=rs.GetInteger("1) Offset as many as possible (Closed Curve Only), 2) Offset by specified amount, 3) Ooffset between a distance", 3)
        if tool_select==1:
            Offset_total()
        elif tool_select==2:
            Offset_limit()
        else:
            Offset_CntrDist()
    Offset_main()
    
    

    2 comments

    1. |

      Hi i searched so much for this command in rhino and the I found this page how can I use your codes in rhino 6?

      • admin
        |

        I haven’t tried it in awhile, did you get an error? You should be able to run ‘editpythonscript’ command in rhino and copy this code in.

    Leave a Comment