Angle finder script (questions and stuff)

here’s my first attempt at writing a tool type of script:

what it’s supposed to do:
• let you select two lines (which don’t necessarily have to share an endpoint) and the angle is returned
• pick one point on a polyline and the angle nearest the click is returned.

import rhinoscriptsyntax as rs



def pLine(line1):
    
    polyPts = rs.CurveEditPoints(line1[0])
    ##################################################### Question 1
    polyPts= rs.coerce3dpointlist(polyPts)
    pcp= rs.PointArrayClosestPoint (polyPts, line1[3])
    
    if pcp == (len(polyPts)-1):
        polyPts.insert(0,0)
    
    vertex= polyPts[pcp]
    endpt1= polyPts[pcp-1]
    endpt2= polyPts[pcp+1]

    ##################################################### Question 2
    if pcp == 0:
        vertex= polyPts[1]
        endpt1= polyPts[0]
        endpt2= polyPts[2]

    vec1= rs.VectorCreate (vertex, endpt1)
    vec2= rs.VectorCreate (vertex, endpt2)

    angle= rs.VectorAngle(vec1, vec2)
    print 'Angle = '+str(angle)


def twoLines(line1,line2):

    match= rs.CurveDirectionsMatch(line1[0], line2[0])
    if match == False:
        rs.ReverseCurve(line1[0])

    line1= rs.CurvePoints(line1[0])
    line2= rs.CurvePoints(line2[0])

    pt1= rs.coerce3dpoint(line1[0])
    pt2= rs.coerce3dpoint(line1[1])
    pt3= rs.coerce3dpoint(line2[0])
    pt4= rs.coerce3dpoint(line2[1])

    angle= rs.Angle2 ([pt1,pt2], [pt3,pt4])
    print 'Angle = '+str(angle[0])


def input ():
    line1 = rs.GetObjectEx('Pick a line or a point on polyline')
    if not line1: return
    if not line1[2] == 1:
        print 'MUST SELECT WITH A MOUSE CLICK'
        return
    
############################################################## Question 3
    if rs.IsPolyline(line1[0]) or rs.CurveDegree(line1[0]) == 1 \
    and not rs.IsCurveLinear(line1[0]):
        flshPt= rs.AddPoint(line1[3])
        rs.FlashObject(flshPt)
        rs.DeleteObject(flshPt)
        pLine(line1)
    else:
        line2 = rs.GetObjectEx('Pick another line')
        rs.FlashObject(line2[0])
        if rs.IsCurveLinear(line1[0]) and rs.IsCurveLinear(line2[0]):
            twoLines(line1,line2)
        else:
            print 'REQUIRES 2 STRAIGHT LINES OR A SINGLE POLYLINE'
            return
input()

if you like the idea of the script enough to re-write it properly… do that… i’d probably learn most that way :wink:

otherwise, some questions/complications:


1) when i did rs.CurveEditPoints, it returns

<Rhino.Collections.Point3dList object at 0x000000000000007E [Rhino.Collections.Point3dList]>

i didn’t know how to access the Rhino.Collections.Point3dList or what it meant (something to do with import Rhino?)

but, if i did this,

 for i, items in enumerate(polyPts):
    print i, items

…i could see inside

ended up doing the coerce3dpointlist thing since it gave me a list in a way i’m sort of used to seeing.


2)

since clicking a point at an end of a polyline will say the next point (which is needed for one vector) is the first in the list, i inserted another item into the list if a polyline endpoint was detected so the entire list would shift and the vertex would correspond with the proper index number of PointArrayClosestPoint.

however, i couldn’t do that with the start point of a list because it was saying the next index number in the list was -1…
or, the vertex was 0 and the two endpoints were -1 and 1… and i needed 0,1,2… so i ended up manually assigning if the point was near the start of a polyline but it seems like this could be done once for all three cases (inner angle, start of polyline and the end)

my question though is this- can you shift a list so the last item becomes the first then everything else moves accordingly?

3) i had this set up a bit better at first until during testing, i realized if you draw two lines then join them, they aren’t considered a polyline ??? so i had to do some stuff with the curve degree to sort through and this if statement became too long… if_or_and… is there a better way to break that down into simpler lines? like- one line is the if, one is the or, and one is the and?

thanks for any insight/wisdom/etc!!


[edit] i suppose this thing might fail in some circumstances with the PointArrayClosestPoint?? depending on the polyline, there might be a wrong vertex chosen since it’s closest to the pick point… i havne’t tested for it though… i’ll try it tomorrow… but if you see any other things that might break it, let me know.

OK, lots of questions to reply to…

Q#1:
There are some quirks in Python rhinoscriptsyntax, and you stumbled on one of them. @stevebaer IMO, CurveEditPoints should return a simple list and not a point3dlist object. A point3dlist is certainly a valid structure in RhinoCommon, and you saw you can convert it into a normal list, but in the context of rhinoscriptsyntax, I think returning a simple list would be more consistent with all the other functions.

However you can avoid all this by using rs.PolylineVertices() instead - the return is a normal list of points.

Q#2:
So, if I understood correctly, you if the user clicks near the start or end, you want to get the next vertex in for the angle measurement. What I would do is simply detect if the point picked is the curve start or curve end and then just get the next/previous index… It’s pretty easy to do (example later). I wouldn’t mess around with adding anything to or modifying the point list in this case, imagine that in the future you might want to use that list for something else…

To answer your shift list question, yes, it’s possible to shift a list in python, a general tool can be had by importing collections.deque and using the rotate() method. A simple way to shift a list without that is to use ***list.pop()***. As its name indicates it “pops” the last item off the list and returns it. You can then add it back to the front using list.insert() at position 0.

i_list=[1,2,3,4,5,6]
i_list.insert(0,i_list.pop())
print i_list
>>>[6, 1, 2, 3, 4, 5]

or the other direction:

i_list=[1,2,3,4,5,6]
i_list.insert(len(i_list)-1,i_list.pop(0))
print i_list

Check the python help on lists for more info on pop(), insert(), etc.

Q#3:
Two joined lines become a “polycurve”. There are probably various ways to workaround this, but unless you really need them to stay polycurves and not polylines, I would just apply rs.SimplifyCrv() to the input objects if they are polycurves. As far as the long lines in the script, what you did is fine, I simply have a tendency to break it up into smaller cascading if’s if it gets to long - at the risk of adding more indent levels. More indenting means your lines get even shorter. Catch-22.

HTH, --Mitch

Maybe something like the following:

(edit - included special case for closed polylines…)

import rhinoscriptsyntax as rs

def pLine(pl):
    polyPts = rs.PolylineVertices(pl[0])
    cp_index= rs.PointArrayClosestPoint (polyPts, pl[3])
    
    #special case for closed polylines and pick point is start/end
    if rs.IsCurveClosed(pl[0]) and (cp_index == 0 or cp_index == len(polyPts)-1):
        vertex= polyPts[0]
        endpt1= polyPts[1]
        endpt2= polyPts[-2]
        
    #for open crvs: if pick is at start, add 1 to index, if at end, subtract 1
    else:
        if cp_index == 0:
            cp_index+=1
        elif cp_index ==len(polyPts)-1:
            cp_index-=1
        vertex= polyPts[cp_index]
        endpt1= polyPts[cp_index-1]
        endpt2= polyPts[cp_index+1]
        
    angle,reflex=rs.Angle2([vertex, endpt1],[vertex, endpt2])
    if angle>reflex: angle=reflex
    return angle

def twoLines(line1,line2):
    L1=rs.coerceline(line1)
    L2=rs.coerceline(line2)
    angle,reflex= rs.Angle2 (L1, L2)
    pass
    if angle>reflex: angle=reflex
    pass
    return angle

def GetIncludedAngle():
    line1 = rs.GetObjectEx('Pick a line or a point on polyline',4)
    if not line1: return
    if not line1[2] == 1:
        print 'MUST SELECT WITH A MOUSE CLICK'
        rs.UnselectObject(line1[0]) ; return
        
    #line case first
    rs.FlashObject(line1[0])
    rs.SimplifyCurve(line1[0])
    if rs.IsLine(line1[0]):
        line2 = rs.GetObjectEx('Pick another line',4)
        if not line2: return
        if not line2[2] == 1:
            print 'MUST SELECT WITH A MOUSE CLICK'
            rs.UnselectObject(line2[0]) ; return
        rs.SimplifyCurve(line2[0])
        if rs.IsLine(line2[0]):
            result=twoLines(line1[0],line2[0])
        else:
            print 'REQUIRES 2 STRAIGHT LINES OR A SINGLE POLYLINE'
            return
            
    #polyline case second
    else:
        flshPt= rs.AddPoint(line1[3])
        rs.FlashObject(flshPt)
        rs.DeleteObject(flshPt)
        result=pLine(line1)
        rs.UnselectObject(line1[0])
    #print results
    print "Angle is {}".format(result)

GetIncludedAngle()

If I was doing this myself, I might do it this way:

import rhinoscriptsyntax as rs

def deg_one_filt(rhino_object, geometry, component_index):
    return rs.CurveDegree(geometry)==1
    
def line_filt(rhino_object, geometry, component_index):
    return rs.IsLine(geometry)
    
def Angle2Lines(line1,line2):
    angle,reflex= rs.Angle2(rs.coerceline(line1), rs.coerceline(line2))
    if angle>reflex: angle=reflex
    return angle
    
def PLVertexAngle(pl,ppt):
    pt_list=rs.CurvePoints(pl)
    cp_index= rs.PointArrayClosestPoint (pt_list, ppt)
    
    #special case for closed polylines and pick point is start/end
    if rs.IsCurveClosed(pl) and (cp_index == 0 or cp_index == len(pt_list)-1):
        vertex= pt_list[0]
        endpt1= pt_list[1]
        endpt2= pt_list[-2]
    #for open crvs: if pick is at start, add 1 to index, if at end, subtract 1
    else:
        if cp_index == 0:
            cp_index+=1
        elif cp_index ==len(pt_list)-1:
            cp_index-=1
        vertex= pt_list[cp_index]
        endpt1= pt_list[cp_index-1]
        endpt2= pt_list[cp_index+1]
        
    angle,reflex=rs.Angle2([vertex, endpt1],[vertex, endpt2])
    if angle>reflex: angle=reflex
    return angle


def GetIncludedAngle2():
    msg="Select polyline or first line for angle"
    objID1=rs.GetObject(msg, 4, select=True, custom_filter=deg_one_filt)
    if not objID1: return
    if rs.IsLine(objID1):
        msg="Select second line for angle"
        objID2=rs.GetObject(msg,4,custom_filter=line_filt)
        if not objID2: return
        result=Angle2Lines(objID1,objID2)
    else:
        msg="Pick point on curve near desired vertex"
        pick_pt=rs.GetPointOnCurve(objID1,msg)
        if not pick_pt: return
        result=PLVertexAngle(objID1,pick_pt)
        
    #print results
    print "Angle is {}".format(result)
        
GetIncludedAngle2()

PS, I assume that this is mainly for the exercise, and that you know that to get the angle between two lines, you can use the macro ! _Angle _TwoObjects

–Mitch

i think you understood correctly… but i get what you’re saying-- no need to start messing with the ordering of the lists here…note taken.

i tried that just to see if it would import but it doesn’t… so i guess it’s not part of the standard package? (like math)

but a polycurve is also an arc joined to a straight line. i just assumed joining linear curves together would fit under the polyline umbrella… it’s not a big deal- just something i didn’t know before.

nice… a couple of neat things i picked up from your first version-- rs.coerceline and assigning more than one variable name at a time. (angle,reflex= rs.Angle2 (L1, L2))

your second one looks even better… i like the single purpose functions… i thought that might be considered bad form or something but seeing an example of it looks sweet to me… or, it looks like a way to prevent long winded and/or confusing lines.

yes and no… you can’t (in v5) sub object select a curve of a polycurve so when measuring angles of polylines, i’d always have to do the 4click version… so that’s what gave me the idea to try this as a script in the first place… but, at some point, yeah-- it’s meant as an exercise… it’s short enough to not get too confusing yet still requires proper flow and functions etc… as far as i can gather so far, having a good flow (or maybe even style) is possibly one of the more important parts of writing this stuff ?

i’ll probably mess around with this one some more then try another little script for getting lengths of individual segments of joined curves (which is also not currently possible in a real easy manner)

i haven’t had time to run your scripts and just read through them so far… i’ll have more time to analyze tomorrow.

@stevebaer Hmm, you are correct, it autocompletes in the list when you type import collections., but it doesn’t actually seem to be there.

Yes, this is the implied tuple unpacking I spoke of earlier. You want to be sure that the number of items you’re unpacking corresponds exactly with what is returned by the function. You also need to be careful as some functions that return multiple items will return just None (one item) if they fail, so if you try to unpack implicitly, you will get an error, because the numbers of items don’t correspond. So when in doubt, throw the return into one variable, check for validity first, then unpack it literally. In this case I was sure that rs.Angle() would not fail.

–Mitch