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

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?

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

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

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

@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 , 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

@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.

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()