{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Example 02: Frame Hierarchies and Dynamic Transformations\n", "\n", "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:\n", "\n", "1. **Parent-child frame relationships**: child frames inherit parent transformations\n", "2. **Persistent primitives**: Points/Vectors update automatically when their frame is modified\n", "3. **Cache invalidation**: transformations stay correct even when modifying frames after creating primitives" ] }, { "cell_type": "markdown", "id": "1", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "code", "execution_count": null, "id": "2", "metadata": {}, "outputs": [], "source": [ "from hazy import Frame, Point, Vector\n", "from dataclasses import dataclass\n", "import matplotlib.pyplot as plt\n", "from matplotlib.patches import Circle\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "3", "metadata": {}, "source": [ "## Build the frame hierarchy\n", "\n", "We create three frames in a parent-child hierarchy:\n", "\n", "1. **root**: The world coordinate system (origin of everything)\n", "2. **disk_frame**: Child of root - will rotate around z-axis\n", "3. **laser_frame**: Child of disk_frame, offset by 1 unit in y direction\n", "\n", "When disk_frame rotates, laser_frame automatically rotates with it because of the parent-child relationship." ] }, { "cell_type": "code", "execution_count": null, "id": "4", "metadata": {}, "outputs": [], "source": [ "@dataclass\n", "class Laser:\n", " origin: Point\n", " direction: Vector\n", "\n", "\n", "root = Frame.make_root(\"root\")\n", "disk_frame = root.make_child(\"disk\")\n", "laser_frame = disk_frame.make_child(\"laser\").translate(y=1)\n", "\n", "laser = Laser(origin=laser_frame.point(0, 0, 0), direction=laser_frame.vector(1, 0, 0))" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "**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." ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "## Rotate the disk and visualize laser positions\n", "\n", "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.\n", "\n", "**How it works:**\n", "- `disk_frame.rotate_euler(z=step)` rotates the disk by one step\n", "- Because laser_frame is a child of disk_frame, it rotates too\n", "- `.to_global()` always returns current transformed coordinates (cache is invalidated automatically)\n", "- Each laser beam has a different color to show the rotation sequence\n", "- Beam length increases with each step for better visualization (purely aesthetic)" ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(8, 8))\n", "\n", "angles, step = np.linspace(0, 2 * np.pi, 15, retstep=True, endpoint=False)\n", "colors = plt.cm.viridis(np.linspace(0, 1, len(angles)))\n", "\n", "for i, angle in enumerate(angles):\n", " origin_global = laser.origin.to_global()\n", " end_point = laser.origin + laser.direction * (i + 1) / 5\n", " end_global = end_point.to_global()\n", "\n", " laser_line = np.vstack([np.array(origin_global), np.array(end_global)])\n", "\n", " ax.plot(*laser_line[:, :2].T, color=colors[i], linewidth=2, alpha=0.8)\n", "\n", " disk_frame.rotate_euler(z=step)\n", "\n", "ax.add_artist(\n", " Circle((0, 0), 1.0, edgecolor=\"black\", facecolor=\"lightgray\", linewidth=2)\n", ")\n", "ax.set_aspect(\"equal\")\n", "ax.set_xlabel(\"x\", fontsize=12)\n", "ax.set_ylabel(\"y\", fontsize=12)\n", "ax.set_title(\"Laser on Rotating Disk\", fontsize=14, fontweight=\"bold\")\n", "ax.grid(True, alpha=0.3)\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "id": "9", "metadata": {}, "source": [ "## Key Takeaways\n", "\n", "**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.\n", "\n", "**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.\n", "\n", "**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.\n", "\n", "**Without hazy**: Track transformation matrices manually, remember multiplication order, update all dependent objects when parent transforms change.\n", "\n", "**With hazy**: Define the hierarchy once, modify frames freely, call `.to_global()` to get current coordinates." ] } ], "metadata": { "kernelspec": { "display_name": "hazy-frames", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" } }, "nbformat": 4, "nbformat_minor": 5 }