Note
Go to the end to download the full example code.
Affine transformations¶
AffineTransform represents an affine map T(x) = A x + b
with factory methods for translation, scaling, rotation, mirroring, and shear.
Transforms compose with @ and apply to any geometry via geom.transform(T).
Because a transform acts on the control points – not on sampled points – the result
is another exact Bspline, not a discretization. This tutorial
builds a few transforms and applies them to a single base shape (the layout trick used
back in Constructive CAD modeling).
import numpy as np
from pantr import viz
from pantr.cad import create_cylinder
from pantr.transform import AffineTransform
base = create_cylinder(radius=0.4, height=1.0)
Individual transforms¶
Each transform produces a new, independent geometry; the original is untouched.
rotated = base.transform(AffineTransform.rotation_3d(np.pi / 4, axis=0)) # tilt about x
scaled = base.transform(AffineTransform.scaling([1.0, 1.0, 1.6])) # stretch in z
sheared = base.transform(AffineTransform.shear(dim=3, component=0, direction=2, factor=0.5))
scene = viz.Scene()
for i, geom in enumerate([base, rotated, scaled, sheared]):
placed = geom.transform(AffineTransform.translation([1.5 * i, 0.0, 0.0]))
scene.add(placed, color="lightsteelblue", show_knot_lines=True)
scene.show()

<pyvista.plotting.plotter.Plotter object at 0x71ece1819970>
Composing transforms¶
@ composes transforms right-to-left, exactly like matrix multiplication:
(T2 @ T1)(x) == T2(T1(x)). Here we rotate, then scale, then translate.
combined = (
AffineTransform.translation([0.0, 0.0, 0.5])
@ AffineTransform.scaling([1.3, 1.3, 1.3])
@ AffineTransform.rotation_3d(np.pi / 3, axis=1)
)
viz.plot(base.transform(combined), color="thistle", show_knot_lines=True)

<pyvista.plotting.plotter.Plotter object at 0x71ece2366300>
Composition equals sequential application¶
t1 = AffineTransform.rotation_3d(0.7, axis=2)
t2 = AffineTransform.translation([1.0, -0.5, 0.2])
once = base.transform(t2 @ t1)
twice = base.transform(t1).transform(t2)
err = float(np.max(np.abs(once.control_points - twice.control_points)))
print(f"max |(t2@t1) - t2∘t1| on control points = {err:.2e}")
max |(t2@t1) - t2∘t1| on control points = 0.00e+00
Total running time of the script: (0 minutes 0.482 seconds)