How to determine the 'direction' of an angle between two vectors?

I was working on a script to orient spotlights to a certain focus point. This requires the fixture to be rotated in 2 planes. The rotation angle I calculate by creating vectors between the light fixture and the focus point. See attached rhino file and python script.
Right now it works, but only trough iteration in ugly while loops. I want to know if there is a way I can determine the direction I need to rotate in.
This script is a continuation of a version posted here

spotlight_focus.3dm (496.8 KB)
spotFocusAdvanced-v03.py (3.8 KB)

edit: removed the while loops, now using singed angles to calculate the right rotations:
spotFocusAdvanced-v04.py (4.1 KB)

1 Like

Hey Gijs,

Just a thought but as spotlights seem to be round and therefore do not care about any possible rotation in the plane perpendicular to their light vectors - can you not create two planes from normal using the ‘from’ and ‘to’ vectors and then do a transformation plane-to-plane?

https://developer.rhino3d.com/api/RhinoScriptSyntax/#collapse-PlaneFromNormal
https://developer.rhino3d.com/api/RhinoScriptSyntax/#collapse-XformRotation1

Hi Guys,

Is atan2 the function you are looking for to get the signed angle ?

I haven’t looked at your files in detail so apologies if I’ve misunderstood…

-Graham

@Helvetosaur in the script I am orienting a light fixture that can have any shape. Imagine this light fixture that you want to focus on a point:


This means the light fixture needs to be rotated in two axes, which is what I am doing in the script. But the point is that the calculated angle between 2 vectors can be positive or negative and not necessarily correspond to the direction it needs to be rotated in.

@Graham, I’ll look into it, thanks. Edit: I actually have no idea how I could use atan2 to solve this

Had the wrong link in my other post…

thanks,

I tried to implement this in the second while loop since this one uses a single angle from 2 vectors. But it doesn’t seem to work. Maybe you can have a look if I’m doing something wrong:

i=0
while True:    
    #calculate horizontal rotation
    #get the plane of the spot_h_circle and calculate from there
    for item in items:
        if rs.ObjectName(item) == "spot_h_circle":
            h_circle = rs.coercecurve(item)
            rc, h_circle = h_circle.TryGetCircle()
        if rs.ObjectName(item) == "spot_target":
            target = rs.coerce3dpoint(item)
        if rs.ObjectName(item) == "spot_source":
            source = rs.coerce3dpoint(item)    
    plane = h_circle.Plane
    h_cen = h_circle.Center
    
    h_vec = rs.VectorCreate(pt_focus, h_cen)
    f_vec=rs.VectorCreate(target, source)
    
    angle = rs.VectorAngle(h_vec, f_vec)
    
    a=h_vec
    b=f_vec
    if(a.X*b.Y - a.Y*b.X < 0):
        angle=-angle
    print angle
    if abs(angle)<0.1 or i>20:
        print "went through second while loop %d times" %i
        break
    
    for item in items:
        if rs.ObjectName(item):
            if rs.ObjectName(item)[:6]!="spot_v" and rs.ObjectName(item)!="slider":
                rs.RotateObject(item, pivot, angle, plane.ZAxis)
    i+=1

Try this solution:

import Rhino
import scriptcontext as sc
from math import atan2
from Rhino.Geometry import Plane, Point3d, Transform, Vector3d

def signedVectorAngle(v1, v2):
    """
    Calculates the signed vector angle between two
    vectors v1 and v2.
    """
    
    # unitize both input vectors
    v1.Unitize()
    v2.Unitize()
    
    # create reference plane
    vXAxis = Vector3d.CrossProduct(v1, v2)
    plane = Plane(Point3d.Origin, v1, v2)
    
    # change base of vectors to reference plane
    xChangeBase = Transform.ChangeBasis(Plane.WorldXY, plane)
    v1.Transform(xChangeBase)
    v2.Transform(xChangeBase)
    
    # signed angle calculation, see:
    # https://stackoverflow.com/a/33920320
    return Rhino.RhinoMath.ToDegrees(atan2(Vector3d.CrossProduct(v1, v2) * plane.ZAxis, v1 * v2))
        
def calculateSignedAngleInteractive():
    
    result, v1 = Rhino.Input.RhinoGet.GetLine()
    if result != Rhino.Commands.Result.Success:
        return result
        
    result, v2 = Rhino.Input.RhinoGet.GetLine()
    if result != Rhino.Commands.Result.Success:
        return result

    angle = signedVectorAngle(v1.Direction, v2.Direction)
    
    print angle
    
if __name__ == "__main__":
    calculateSignedAngleInteractive()

keep in mind that for signed angle calculation, the order of the vectors matters.
e.g. two angles can be of angle 45° or - 45° depending on which one you pick first

3 Likes

@lando.schumpich thanks for the example code. I am sure I am still missing something, because when I use your signedVectorAngle definition, and pass my two vectors, it returns a different value than rs.VectorAngle. In fact it either returns 180 or 0, which leads me to think calculations are done in the wrong plane. But I don’t understand what your code is doing well enough to correct it for my situation. In my case the 2 vectors are in a plane that has its Z-axis always perpendicular to world Z-axis. Hope this gives you a clue.

An angle of 180° would mean the vectors are anti-parallel and for 0° they would have the same direction. I imagine the Plane constructor doesn’t like both cases too much. I’m on vacation over the weekend so I can’t test any improvements but for now maybe test for (anti)parallel before calculating the angle using https://developer.rhino3d.com/api/RhinoCommon/html/M_Rhino_Geometry_Vector3d_IsParallelTo.htm

@lando.schumpich :thinking: , that doesn’t make sense to me because the vectors I’m testing are neither parallel or anti-parallel. What I do not understand in your code is why you need to remap the vectors first?

Actually the remapping was from the first iteration if the code that operated on the .X and .y properties of the vectors directly. This is probably not needed anymore you can try removing the remap and see what happens :slight_smile:

thanks @lando.schumpich that was it. It now seems to work flawlessly :slight_smile:
I’ve uploaded a corrected version in the first post

@lando.schumpich, I don’t know what I am missing, but…
When I try to implement this script and the lines are NOT in WorldXY, then I always get 0 as a result.
When I go through the code, everything makes sense, though.

Hi @tobias.stoltmann,

looking at the code now, it is flawed. This updated version returns the correct angles, but to get a meaningful sign on the angle you would need a way to define a reference vector.

import Rhino
import scriptcontext as sc
from math import atan2
from Rhino.Geometry import Plane, Point3d, Transform, Vector3d

def signedVectorAngle(v1, v2):
    """
    Calculates the signed vector angle between two
    vectors v1 and v2.
    """
    
    # unitize both input vectors
    v1.Unitize()
    v2.Unitize()
    
    # create reference plane
    vZAxis = Vector3d.CrossProduct(v1, v2)
    plane = Plane(Point3d.Origin, vZAxis)
        
    # signed angle calculation, see:
    # https://stackoverflow.com/a/33920320
    return Rhino.RhinoMath.ToDegrees(atan2(Vector3d.CrossProduct(v1, v2) * plane.ZAxis, v1 * v2))
        
def calculateSignedAngleInteractive():
    
    result, v1 = Rhino.Input.RhinoGet.GetLine()
    if result != Rhino.Commands.Result.Success:
        return result
        
    result, v2 = Rhino.Input.RhinoGet.GetLine()
    if result != Rhino.Commands.Result.Success:
        return result

    angle = signedVectorAngle(v1.Direction, v2.Direction)
    
    print angle
    
if __name__ == "__main__":
    calculateSignedAngleInteractive()

You might also want to consider implementing one of the Vector3d.VectorAngle method overloads: