Example 02: Frame Hierarchies and Dynamic Transformations

This example shows the power of hierarchical coordinate frames. We’ll create a laser mounted on a rotating disk - when the disk rotates, the laser automatically rotates with it. This demonstrates:

  1. Parent-child frame relationships: child frames inherit parent transformations

  2. Persistent primitives: Points/Vectors update automatically when their frame is modified

  3. Cache invalidation: transformations stay correct even when modifying frames after creating primitives

Setup

[1]:
from hazy import Frame, Point, Vector
from dataclasses import dataclass
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np

Build the frame hierarchy

We create three frames in a parent-child hierarchy:

  1. root: The world coordinate system (origin of everything)

  2. disk_frame: Child of root - will rotate around z-axis

  3. laser_frame: Child of disk_frame, offset by 1 unit in y direction

When disk_frame rotates, laser_frame automatically rotates with it because of the parent-child relationship.

[2]:
@dataclass
class Laser:
    origin: Point
    direction: Vector


root = Frame.make_root("root")
disk_frame = root.make_child("disk")
laser_frame = disk_frame.make_child("laser").translate(y=1)

laser = Laser(origin=laser_frame.point(0, 0, 0), direction=laser_frame.vector(1, 0, 0))

Key insight: The laser is defined once in its local frame as origin=(0,0,0) and direction=(1,0,0). When we later rotate the disk_frame, calling .to_global() on these primitives automatically returns the updated transformed coordinates.

Rotate the disk and visualize laser positions

We rotate the disk in small increments and draw the laser at each position. The laser primitives (origin and direction) were created once above - they don’t need to be recreated in each iteration.

How it works:

  • disk_frame.rotate_euler(z=step) rotates the disk by one step

  • Because laser_frame is a child of disk_frame, it rotates too

  • .to_global() always returns current transformed coordinates (cache is invalidated automatically)

  • Each laser beam has a different color to show the rotation sequence

  • Beam length increases with each step for better visualization (purely aesthetic)

[3]:
fig, ax = plt.subplots(figsize=(8, 8))

angles, step = np.linspace(0, 2 * np.pi, 15, retstep=True, endpoint=False)
colors = plt.cm.viridis(np.linspace(0, 1, len(angles)))

for i, angle in enumerate(angles):
    origin_global = laser.origin.to_global()
    end_point = laser.origin + laser.direction * (i + 1) / 5
    end_global = end_point.to_global()

    laser_line = np.vstack([np.array(origin_global), np.array(end_global)])

    ax.plot(*laser_line[:, :2].T, color=colors[i], linewidth=2, alpha=0.8)

    disk_frame.rotate_euler(z=step)

ax.add_artist(
    Circle((0, 0), 1.0, edgecolor="black", facecolor="lightgray", linewidth=2)
)
ax.set_aspect("equal")
ax.set_xlabel("x", fontsize=12)
ax.set_ylabel("y", fontsize=12)
ax.set_title("Laser on Rotating Disk", fontsize=14, fontweight="bold")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
../_images/examples_02-frame-hierarchies_7_0.png

The plot shows laser beams emanating from the disk edge (at radius 1) in different directions as the disk rotates around its center. Each beam is a snapshot at a different rotation angle.

Key Takeaways

Frame hierarchies: Parent-child relationships allow natural modeling of connected systems (robot arms, camera rigs, mechanical assemblies). When a parent frame transforms, all descendants transform automatically.

Persistent primitives: Create Points and Vectors once, store them in data structures (like the Laser dataclass), and transformations stay current when you modify frames later.

Automatic cache invalidation: When you modify a frame with .rotate_euler(), .translate(), or .scale(), the transformation cache is invalidated recursively through all child frames. No manual bookkeeping needed.

Without hazy: Track transformation matrices manually, remember multiplication order, update all dependent objects when parent transforms change.

With hazy: Define the hierarchy once, modify frames freely, call .to_global() to get current coordinates.