Hello dear readers,
I think I'd better wrap this up with my findings before I forget, I'm so busy that otherwise I'll forget that I left some strings untied.
Nesting of assemblies is extremely important for organizing work. I now interpret the 'loc' associated with Assembly to mean, w.r.t. the parent (or "containing") frame of reference, where is the assembly placed? Where is it located, what is its orientation? The matrix tells you that. This is good 3D computer graphics organizational practice.
As an example, this was one of my previous projects in OpenSCAD, where there were multiple frames of reference. You can think of each object being an assembly, in other words there are six rigid bodies rotating independently. The hierarchy looked like so:
You can see the nesting between frames of reference, but in each nesting there is essentially one rotation along one axis, except for the last piece, the green shaft. Each of the angles ('delta', 'beta', 'omega', 'alpha', etc) is defined as a function which takes the same two input parameters: white_theta and gamma. In the CadQuery world each of these six objects, white_shaft, orange_ring_gear, blue_ring, purple_bevel_gear, red_inner_cross, & green_shaft, would be an Assembly consisting of multiple pieces (e.g. screws, shims, etc, each having a different color). So it's totally obvious to me that rotations/translations on Assemblies are A MUST but are you convinced of that?

So my transformation code is working, and handles all object types: Workplane, Shape, & Assembly. While Workplane & Shape have the .translate() and .rotate() member functions, I can't hack the Assembly source code to do that, so I'm using a different approach, and "external tranformator" sort of coding pattern. But the important part is that my code works on all object types, I can transform more than once and the transform will be concatenated in the correct order, and the nesting between assemblies is also working w.r.t. transformations. The full listing of the code is here:
https://svn.code.sf.net/p/nl10/code/cq-code/common/transforms.py
I'm pasting an incomplete snippet of the code linked above to this correspondence, for reference. You can use that code, to implement the .rotate() and .translate() in Assembly class if you like. I'm not going to go through that effort right now. Back to work, I have so many important things to take care of. Have a nice day.
import math
from cadquery import Assembly
from cadquery import Location
from cadquery import Shape
from cadquery import Workplane as Workplane
from OCP.gp import gp_Trsf
# Returns an angle equivalent to the input angle but in the range [0,360).
# Negative and positive input values accepted. E.g., -21.5 -> 338.5.
def deg_canonical_360(deg_theta):
return deg_theta - math.floor(deg_theta / 360) * 360
# Convenience method for translating a Workplane, a Shape, or an Assembly, in
# CadQuery. The 'x', 'y', and 'z' all default to zero, such that only single
# values could be explicitly specified.
def translate(piece, x= 0, y= 0, z= 0):
return _transform_3D_general(piece, __translate(x, y, z))
def __translate(x, y, z): # Ooohhh, double underscore! These return xforms.
return (( 1, 0, 0, x ),
( 0, 1, 0, y ),
( 0, 0, 1, z ))
# Convenience method for mirroring a Workplane, a Shape, or an Assembly in
# CadQuery, along the z axis. More specifically, all z values of coordinates
# are negated. An even number of reflections, of any kind, brings the object
# to a state which can be reached via rigid body transform.
def mirrorZ(piece):
return _scale(piece, z= -1)
# Performs a uniform scale in 3D. The 'piece' can be a Workplane, a Shape, or
# an Assembly, in CadQuery.
def scale(piece, scale_f= 1):
return _scale(piece, x= scale_f, y= scale_f, z= scale_f)
# Internal function which potentially performs non-uniform scale operations in
# 3D.
def _scale(piece, x= 1, y= 1, z= 1):
return _transform_3D_general(piece, __scale(x, y, z))
def __scale(x, y, z): # Ooohhh, double underscore! These return xforms.
return (( x, 0, 0, 0 ),
( 0, y, 0, 0 ),
( 0, 0, z, 0))
# Convenience function for rotating a piece around the z axis, right hand rule.
# The 'piece' can be a Workplane, a Shape, or an Assembly, in CadQuery. 90°
# orthogonal rotations, and multiples thereof, are handled as special cases in
# order to provide mathematical exactness. The default value for 'degrees' is
# 90, such that a shorthand 'rotateZ(piece)' could be used to rotate an object
# by 90° along the z axis.
def rotateZ(piece, degrees= 90):
return _transform_3D_general(piece, __rotateZ(degrees))
def __rotateZ(degrees): # Ooohhh, double underscore! These return xforms.
canonical = deg_canonical_360(degrees)
xform = None
if canonical == 0: xform = (( 1, 0, 0, 0 ), ########################
( 0, 1, 0, 0 ),
( 0, 0, 1, 0 ))
if canonical == 90: xform = (( 0, -1, 0, 0 ), ########################
( 1, 0, 0, 0 ),
( 0, 0, 1, 0 ))
if canonical == 180: xform = ((-1, 0, 0, 0 ), ########################
( 0, -1, 0, 0 ),
( 0, 0, 1, 0 ))
if canonical == 270: xform = (( 0, 1, 0, 0 ), ########################
(-1, 0, 0, 0 ),
( 0, 0, 1, 0 ))
if xform is None:
angle = math.radians(degrees)
sa = math.sin(angle)
ca = math.cos(angle)
xform = (( ca, -sa, 0, 0 ),
( sa, ca, 0, 0 ),
( 0, 0, 1, 0 ))
return xform
# Performs a general rotation through an axis which begins at the origin, whose
# direction is specified by the 3-tuple 'axis' (not necessarily normalized).
# The 'piece' can be a Workplane, a Shape, or an Assembly, in CadQuery.
# Special handling of 90° rotations (or multiples thereof) along the x,y,z
# coordinate axes is not done by this function; please use the other rotation
# functions for that. To rotate about an axis that isn't passing through the
# origin, it is recommended to first translate the piece, then rotate using
# this function, then translate by the inverse translation, or to do it all in
# one go by multipliying the corresponding matrices in the correct order.
def rotate_3D_general(piece, axis, degrees):
return _transform_3D_general(piece, __rotate_3D_general(axis, degrees))
def __rotate_3D_general(axis, degrees): # Double underscore! Returns xforms.
veclen = math.sqrt(pow(axis[0], 2) + pow(axis[1], 2) + pow(axis[2], 2))
x = axis[0] / veclen
y = axis[1] / veclen
z = axis[2] / veclen
angle = math.radians(degrees)
sa = math.sin(angle)
omca = 1 - math.cos(angle)
xform = (( 1-omca*(z*z+y*y), -z*sa+x*y*omca, y*sa+z*x*omca, 0 ),
( z*sa+x*y*omca, 1-omca*(z*z+x*x), -x*sa+z*y*omca, 0 ),
( -y*sa+z*x*omca, x*sa+z*y*omca, 1-omca*(y*y+x*x), 0 ))
return xform
# This is mostly a demonstration of how to concatenate transforms within this
# API. Please note that 'axis_end' is literally the tip of the axis of
# rotation; it's NOT a direction vector for the axis of rotation. The
# direction would in fact be the difference vector, end minus start.
def rotate_3D_arbitrary_axis(piece, axis_start= (0, 0, 0),
axis_end= (0, 0, 1), degrees= 0):
return _transform_3D_general(piece,
__rotate_3D_arbitrary_axis(axis_start,
axis_end,
degrees))
def __rotate_3D_arbitrary_axis(axis_start, axis_end, degrees):
# Ooohhh, double underscore! These return xforms.
tr_toO = __translate(-axis_start[0], -axis_start[1], -axis_start[2])
tr_rot = __rotate_3D_general((axis_end[0] - axis_start[0],
axis_end[1] - axis_start[1],
axis_end[2] - axis_start[2]), degrees)
tr_frO = __translate(axis_start[0], axis_start[1], axis_start[2])
return __multiply_transforms_3D(tr_frO,
__multiply_transforms_3D(tr_rot, tr_toO))
# A Workplane, a Shape, or an Assembly [in CadQuery] can be subjected to an
# arbitrary transformation (usually, but not always, a rigid body motion such
# as rotation or translation). This exposes that ability. I cannot advertise
# which sorts of transforms are supported and which ones are not; this depends
# on what the underlying OpenCascade layer accepts, and what it does not. This
# transform is compounded onto any transforms that the object is already
# subjected to. This is meant as a low level implementation facility for
# higher-level, user-friendly APIs such as rotate() and translate().
def _transform_3D_general(piece, xform= ((1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0)) ):
rowX_4tuple = xform[0]
rowY_4tuple = xform[1]
rowZ_4tuple = xform[2]
compound_this_xform = gp_Trsf()
compound_this_xform.SetValues(
rowX_4tuple[0], rowX_4tuple[1], rowX_4tuple[2], rowX_4tuple[3],
rowY_4tuple[0], rowY_4tuple[1], rowY_4tuple[2], rowY_4tuple[3],
rowZ_4tuple[0], rowZ_4tuple[1], rowZ_4tuple[2], rowZ_4tuple[3])
if isinstance(piece, Assembly):
existing_xform = piece.loc.wrapped.Transformation()
piece.loc = Location(compound_this_xform.Multiplied(existing_xform))
return piece
if isinstance(piece, Shape):
return piece._apply_transform(compound_this_xform)
if isinstance(piece, Workplane):
return piece.newObject(
[
obj._apply_transform(compound_this_xform)
if isinstance(obj, Shape)
else obj
for obj in piece.objects
]
)
raise TypeError(
"_transform_3D_general(): expected Workplane, Shape, or Assembly")
# Pass specified point in 3D through specified transformation. This is here
# for reference just to increase understanding where there is ambiguity.
def _pass_point_3D_transform(xform= ((1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0)), pt_3tuple= (0, 0, 0) ):
rowX_4tuple = xform[0]
rowY_4tuple = xform[1]
rowZ_4tuple = xform[2]
return ( _m44rp(rowX_4tuple, pt_3tuple),
_m44rp(rowY_4tuple, pt_3tuple),
_m44rp(rowZ_4tuple, pt_3tuple) )
def _m44rp(row_4tuple, pt_3tuple):
return (row_4tuple[0] * pt_3tuple[0] +
row_4tuple[1] * pt_3tuple[1] +
row_4tuple[2] * pt_3tuple[2] +
row_4tuple[3])
def __multiply_transforms_3D(xform_apply_last= ((1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0)),
xform_apply_first= ((1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0)) ):
m0 = xform_apply_last[0]
m1 = xform_apply_last[1]
m2 = xform_apply_last[2]
n = ( xform_apply_first[0],
xform_apply_first[1],
xform_apply_first[2],
(0, 0, 0, 1) )
return (( _m44rc(m0,n,0), _m44rc(m0,n,1), _m44rc(m0,n,2), _m44rc(m0,n,3) ),
( _m44rc(m1,n,0), _m44rc(m1,n,1), _m44rc(m1,n,2), _m44rc(m1,n,3) ),
( _m44rc(m2,n,0), _m44rc(m2,n,1), _m44rc(m2,n,2), _m44rc(m2,n,3) ))
def _m44rc(row_4tuple, mat_4x4, col):
return (row_4tuple[0] * mat_4x4[0][col] +
row_4tuple[1] * mat_4x4[1][col] +
row_4tuple[2] * mat_4x4[2][col] +
row_4tuple[3] * mat_4x4[3][col])