Keep in mind that this problem has many solutions. A transformation matrix has no notion of rotation or direction. It depends on the choice of axes and the order in which rotations are applied (except if you have a simple case or constraints, like a given axes or a single angle).
The most common expectation is to extract angles in the order of Euler angles and in the direction of the smaller angle (< 180°).
This is a problem that many SketchUp Ruby developers have already worked with, myself included (see here). It’s great to have a shared library.
I’m sure this later one is much improved, but FYI, it’s almost ten years since I did this code below, as part of a bigger example over on SketchUcation.com…
I added modules etc to tidy it up here…
You pass a transformation tr with 0.1.2 or x,y,z axes…
The # sections in the rot’ methods are older alternatives that will sometimes not give the same results…
Usage example: rotx = TIG::Transformation.rotX(transformation)
module TIG
module Transformation
def self.euler_angle(tr, xyz=[])
m = tr.xaxis.to_a + tr.yaxis.to_a + tr.zaxis.to_a
if m[6] != 1 && m[6] != -1
ry = -Math.asin(m[6])
rx = Math.atan2(m[7]/Math.cos(ry), m[8]/Math.cos(ry))
rz = Math.atan2(m[3]/Math.cos(ry), m[0]/Math.cos(ry))
else
rz = 0
phi = Math.atan2(m[1], m[2])
if m[6] == -1
ry = Math::PI/2
rx = rz + phi
else
ry = -Math::PI/2
rx = -rz + phi
end
end
return -rx if xyz==0
return -ry if xyz==1
return -rz if xyz==2
return [-rx,-ry,-rz] if xyz==[]
end
###
def self.rotX(tr)
#(Math.atan2(self.to_a[9],self.to_a[10]))
#Math.acos(self.to_a[5])
self.euler_angle(tr, 0)
end
def self.rotY(tr)
#(Math.arcsin(self.to_a[8]))
#Math.acos(self.to_a[0])
self.euler_angle(tr, 1)
end
def self.rotZ(tr)
#(-Math.atan2(self.to_a[4],self.to_a[0]))
#Math.asin(self.to_a[4])
self.euler_angle(tr, 2)
end
def self.rotXYZ(tr)
self.euler_angle(tr)
end
end
end
Thanks for sharing, it’s certainly helpful and inspiring to compare with other implementations.
On a side-note, I personally favor having no more than a single possible return type (type safety) instead of a control parameter (control coupling). That makes code easier to maintain and refactor. The full vector is computed anyways and its components can be accessed with .x accessor methods.
I think this was one of the implementations I looked at when making my own. Aerilius also have an implementation is closer related to. They are both based on the same paper (that I sadly forgot to add as a comment).
Perhaps a tiny detail but I’d prefer having the method return angles in radians, as that is what is used in other geometric functions, both in the API and Ruby core. Converting to degrees can be done in the display layer using Sketchup.format_angle, which uses the system decimal separator and model angle precision.