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 90o 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) # => 90
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.


Discover more from FreeCAD News

Subscribe to get the latest posts sent to your email.

Discover more from FreeCAD News

Subscribe now to keep reading and get access to the full archive.

Continue reading