The Rotation API in FreeCAD

This guest post is by Ed Williams (edwilliams16). Inter­est­ed in con­tribut­ing an arti­cle? Con­tact Chris Hennes at chennes@freecad.org with your idea!

Much of FreeCAD script­ing con­sists of cre­at­ing objects and then plac­ing them some­where in 3D. For instance, we can cre­ate a default cube with

doc = App.ActiveDocument
cube = doc.addObject("Part::Box","Box")
doc.recompute()

The result­ing cube has its ori­gin at (0, 0, 0) and is in its default ori­en­ta­tion. To locate it else­where, with­out oth­er mod­i­fi­ca­tion, we can change its Place­ment prop­er­ty. Place­ment has two sub-prop­er­ties: Placement.Base and Placement.Rotation. Placement.Base is the loca­tion of the cube’s ori­gin and Placement.Rotation rotates the cube (with its ori­gin fixed) into its ori­en­ta­tion. If we exam­ine the cube’s cur­rent Place­ment in the Data pan­el, we see 

indi­cat­ing an “iden­ti­ty” Place­ment. The Posi­tion (Base) is (0, 0, 0) and the Rota­tion is 0o around the z (0,0,1) axis. If now, for exam­ple, we change the cube’s Place­ment with 

cube.Placement = App.Placement(App.Vector (10, 0, 0), App.Rotation(App.Vector(0, 0, 1), 20))
App.ActiveDocument.recompute()

this rotates the cube 20 degrees about its (0,0,1) axis and then trans­lates it so its ori­gin lies at (10, 0, 0), which we see reflect­ed in the Data window.

Rotation(Axis, Angle)

Of the var­i­ous ways of con­struct­ing rota­tions, per­haps the sim­plest to under­stand is

rot = App.Rotation(Axis, Angle)

where Axis, the axis of rota­tion, is an App.Vector and Angle is the angle in Degrees. The (non-zero!) length of the Axis vec­tor has no effect on its direc­tion, so axes App.Vector(1, 1, 1) and App.Vector(2, 2, 2) rep­re­sent the same direc­tion. The axis argu­ment of rot is stored as the prop­er­ty rot.RawAxis The Raw Axis is nor­mal­ized to unit length by FreeCAD and is avail­able as the prop­er­ty rot.Axis.

We see that in order to spec­i­fy a rota­tion there are three degrees of free­dom — two for the nor­mal­ized direc­tion and one for the rota­tion angle. How­ev­er, the axis/angle rep­re­sen­ta­tion is not unique. App.Rotation(App.Vector(0, 0, 1), 30), App.Rotation(App.Vector(0, 0, ‑1), 330) and App.Rotation(App.Vector(0, 0, 1), ‑330) all rep­re­sent the same rota­tion. To test if two rota­tions are the same use rot.isSame(rot1, tol­er­ance).

App.Rotation(vec1, vec2)

This rep­re­sents a rota­tion that takes the direc­tion defined by vec1 and rotates it into the direc­tion vec2

The axis of this rota­tion is nor­mal to the plane defined by the two vec­tors and the angle is the angle between them. 

vec1 = App.Vector(0,0,1)
vec2 = App.Vector(1, 0, 1)
rot1 = App.Rotation(vec1, vec2)
rot2 = App.Rotation(vec1.cross(vec2), Radian = vec1.getAngle(vec2))
rot1.isSame(rot2, 1e-14) #=> True

The rot1 form is more con­ve­nient than the equiv­a­lent, first prin­ci­ples, rot2 cal­cu­la­tion, which assumes famil­iar­i­ty withe vec­tor cross-prod­uct oper­a­tion. The Radi­an key­word is used because the default angle argu­ment is Degree.

Below we show this rota­tion act­ing on an object from the front and top views.

Fig (1) The App.Rotation(vec1, vec2) rotation front view.
Fig (2) The App.Rotation(vec1, vec2) rotation top view.

When we spec­i­fy the final direc­tion vec2 into which vec1 is rotat­ed, we have only con­strained two degrees of free­dom, those of the direc­tion spec­i­fied by vec2, yet a gen­er­al rota­tion has three degrees of free­dom. What hap­pened to the third? Sup­pose after our App.Rotation(vec1, vec2) rota­tion, we then spun our object around the vec2 axis. The result­ing com­pos­ite rota­tion would still have rotat­ed vec1 into vec2, but the axes per­pen­dic­u­lar to vec1 will end up in a dif­fer­ent direc­tions. This par­tic­u­lar con­struc­tor there­fore can­not cre­ate the most gen­er­al pos­si­ble rotation. 

App.Rotation(vecx, vecy, vecz, string)

This is a more pow­er­ful con­struc­tor capa­ble of cre­at­ing arbi­trary rotations. 

A nat­ur­al way to define a rota­tion is by its effect on the three coor­di­nate direc­tions. That is, pro­vide as input argu­ments the three direc­tions of the rotat­ed x, y and z axes. How­ev­er, since each direc­tion vec­tor has two degrees of free­dom, this spec­i­fi­ca­tion would pro­vide six con­straints on the three degrees of free­dom a rota­tion pos­sess­es. First we note that only two direc­tions need be spec­i­fied because the third is derived from them by the right-hand rule. This still leaves one over-spec­i­fi­ca­tion, which is resolved in the fol­low­ing manner.

from FreeCAD import Vector as V
rot = App.Rotation(V(1,0,0), V(0,0,0), V(1,0,1) 'ZXY')

What does this rep­re­sent? string is the pri­or­i­ty order of the axes. 

  • Z is first: the Z‑axis is rotat­ed into the direc­tion vecz: V(1,0,1). (Two degrees of free­dom used.)
  • X is sec­ond: the rotat­ed X‑axis is placed in the plane defined by the vecx and vecz argu­ments, orthog­o­nal to vecz. That is, the com­po­nent of vecx orthog­o­nal to vecz defines the rotat­ed X direc­tion. (Uses one more degree of freedom.)
  • Y is third: the vecy argu­ment is com­plete­ly ignored. The new Y‑axis is cre­at­ed form­ing an orthog­o­nal tri­ad with the new Z and X axes, con­struct­ed from the above by the right hand rule (in the direc­tion Z x X, for those famil­iar with the vec­tor cross-product).

Our exam­ple gen­er­ates the same rota­tion illus­trat­ed in Figs. (1) and (2) above. Note how the (blue) z‑axis and (red) x‑axis in Fig. (1) change. The map­ping from (xvec, yvec, zvec) to nor­mal­ized direc­tion vec­tors, in the sec­ond list­ed step above, is illus­trat­ed in Fig (3), the movie below.

Fig. (3) zvec and xvec inputs create the zdir xdir rotated normalized directions.

This rota­tion method forms the basis of the O‑Z-X attach­ment mode for an object in FreeCAD. 

App.Rotation(yaw, pitch, roll)

This is described and illus­trat­ed in the wiki. Here a rota­tion is described as the com­pos­ite of three suc­ces­sive rota­tions. First a yaw about the z‑axis, then a pitch about the rotat­ed y‑axis, then a roll about the rotat­ed x‑axis. More explic­it­ly, here a rotat­ed axis means that the axis is not the ini­tial coor­di­nate axis, but the axis that results from the pri­or rota­tions act­ing on the coor­di­nate axis. Think­ing of rota­tions of a body, the suc­ces­sive rota­tion axes are fixed in the body, not in space.

Compounding rotations

The action of a Rota­tion (or a Place­ment) on a vec­tor is often called mul­ti­pli­ca­tion. We can write

rot*vector #or rot.multVec(vector)
placement*vector #or placement.multVec(vector)

to com­pute the vec­tor result­ing from the trans­for­ma­tion. For suc­ces­sive trans­for­ma­tions, the ‘*’ is much tidier.

rot2 * rot1 * vector  rot2.multVec(rot1.multVec(vector)) rot2.multiply(rot1).multVec(vector) 

all com­pute the effect of first rotat­ing vec­tor by rot1, then rotat­ing the result by rot2. The result of apply­ing two rota­tions is itself a rota­tion, like­wise that of apply­ing two place­ments is a place­ment. So the com­pound rota­tion or place­ment is just

rot_compound = rot2 * rot1

and

placement_compound = placement2 * placement1

There are three impor­tant things to note:

  • Both placements/rotations ref­er­ence the same coor­di­nate system.
  • Unlike when mul­ti­ply­ing num­bers, the order mat­ters! If you rotate an object around two dif­fer­ent axes, you’ll gen­er­al­ly get a dif­fer­ent result if you reverse the order of operations.
  • The rota­tion rot2*rot1 means first rot1, then rot2. Think of them as func­tion appli­ca­tions to under­stand the order.

To illus­trate the sec­ond point, let us con­sid­er two eas­i­ly visu­al­ized rota­tions R and S and apply them in oppo­site orders.

from FreeCAD import Vector as V
R = App.Rotation(V(1,0,0), 90)#90o around x
S = App.Rotation(V(0,0,1), 90)#90o around z

In the top row of the fig­ure, we per­form the rota­tion S*R . We rotate 90o around the coor­di­nate x‑axis, fol­lowed by 90o around the coor­di­nate z‑axis and show the effect on a axis triad. 

In the sec­ond row, we per­form the rota­tion R*S and note that the result dif­fers from that of S*R.

The third row illus­trates anoth­er impor­tant prop­er­ty of rota­tions. First we do the S rota­tion (90o around z), then we per­form R’. R’ is again defined as 90o around x — BUT around the (already rotat­ed) body x‑axis, not the coor­di­nate x‑axis. This is as in the yaw-pitch-roll sec­tion above.

We see the result of R’*S is the same as S*R. This is in fact a gen­er­al result that I will not attempt to prove here. 

To per­form a series of rota­tions in body coor­di­nates instead of fixed coor­di­nates, reverse the order of multiplications.

As an exam­ple, we can con­struct a rota­tion with a yaw-pitch-roll of 20, 30, 40 degrees, respec­tive­ly, by post-mul­ti­ply­ing the indi­vid­ual rotations.

from FreeCAD import Vector as V
yaw = App.Rotation(V(0,0,1), 20)
pitch = App.Rotation(V(0,1,0), 30)
roll = App.Rotation(V(1,0,0), 40)
ypr = yaw*pitch*roll
ypr_direct = App.Rotation(20,30,40)
ypr.isSame(ypr_direct, 1e-14)#=> True
Offset rotations

If we want to rotate an object about an axis that is off­set from the ori­gin, we use the three argu­ment form of Placement:

App.Placement(base, rotation, center)

This means

  • Rotate by rota­tion about an axis through cen­ter
  • Trans­late the result by base

As an exer­cise, how could we have done this from first principles? 

  • Trans­late by -cen­ter
  • Rotate by rota­tion
  • Trans­late back by cen­ter
  • Trans­late by base
#sample data
base = App.Vector(50,0,0)
axis = App.Vector(1, 1, 1)
angle = 45
center = App.Vector(0, 10, 0)
rotation = App.Rotation(axis, angle)
#compare
ZeroRot = App.Rotation()
ZeroVec = App.Vector()
tr1 = App.Placement(-center, ZeroRot)
rot = App.Placement(ZeroVec, rotation)
tr2 = App.Placement(center, ZeroRot)
tr3 = App.Placement(base, ZeroRot)
offsetrot = tr3*tr2*rot*tr1
offsetrot_direct = App.Placement(base, rotation, center)
offsetrot.isSame(offsetrot_direct) #=> True
Inverse

Both rota­tions (rot) and place­ments (pl) have invers­es, rot-1 and pl-1 which reverse their effect. These satisfy

where I is the iden­ti­ty rota­tion or place­ment, respectively.

Recall from the com­pound place­ment sec­tion, we found that R’ * S = S * R. We could use the alge­bra of rota­tions to com­pute R’ explic­it­ly. Mul­ti­ply both sides of the equa­tion on the right by S-1, obtain­ing:

R’ * S * S-1 = R’ = S * R * S-1 In code, this becomes:

from FreeCAD import Vector as V
R = App.Rotation(V(1,0,0), 90)#90o around x
S = App.Rotation(V(0,0,1), 90)#90o around z
Rprime = S * R * S.inverted()

We find that Rprime is a 180o rota­tion around the (coor­di­nate!) y‑axis, in accord with the com­pos­ite rota­tion fig­ure above. 

from math import degrees
degrees(Rprime.Angle) # => 180
Rprime.Axis # => App.Vector(0, 1, 0)

Being able to manip­u­late rota­tions (and place­ments) sym­bol­i­cal­ly in this man­ner is a pow­er­ful tool. For anoth­er illus­tra­tion of this, con­sid­er an assem­bly in which our object is con­tained in a sequence of nest­ed con­tain­er sub-assemblies. 

The glob­al place­ment of the Cube is the prod­uct of the place­ments that con­tain it. Refer­ring to the Place­ments of Part, Part001, Part002, Part003 and Cube as pl0, pl1, pl2, pl3 and pl4 respec­tive­ly, the glob­al place­ment of the Cube is pl0 * pl1 * pl2 * pl3 * pl4. Sup­pose we want to change the glob­al Place­ment of the cube to gpl by alter­ing the place­ment of the Part001 sub-assem­bly, say by chang­ing pl1 to pl1′. Then we know that

solv­ing for the unknown place­ment pl1 by pre- and post-mul­ti­ply­ing by the appro­pri­ate inverses.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: