{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Example 01: Ray-Plane Intersection\n", "\n", "Computing where a ray intersects with a plane in 3D space - a common problem in graphics, CAD, and robotics. This example shows how `hazy` handles coordinate transformations when geometric objects are defined in different frames." ] }, { "cell_type": "markdown", "id": "1", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "code", "execution_count": null, "id": "2", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "from hazy import Frame, Point, Vector" ] }, { "cell_type": "markdown", "id": "3", "metadata": {}, "source": [ "## Create a root frame\n", "\n", "Every hierarchy starts with a root frame, the \"world\" coordinate system." ] }, { "cell_type": "code", "execution_count": null, "id": "4", "metadata": {}, "outputs": [], "source": [ "root = Frame.make_root(\"root\")" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "## Define a ray\n", "\n", "A ray has an origin (Point) and a direction (Vector). We create it in its own coordinate frame pointing along the x-axis." ] }, { "cell_type": "code", "execution_count": null, "id": "6", "metadata": {}, "outputs": [], "source": [ "@dataclass\n", "class Ray:\n", " origin: Point\n", " direction: Vector\n", "\n", "\n", "ray_frame = root.make_child(\"ray\")\n", "\n", "simple_ray = Ray(\n", " origin=ray_frame.point(0.0, 0.0, 0.0),\n", " direction=ray_frame.vector(1.0, 0.0, 0.0).normalize(),\n", ")" ] }, { "cell_type": "markdown", "id": "7", "metadata": {}, "source": [ "**Preferred API**: Use `frame.point(x, y, z)` and `frame.vector(x, y, z)` to create primitives. This is more concise than `Point(x, y, z, frame=frame)` and makes the frame relationship explicit." ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "## Define a plane\n", "\n", "A plane is defined by a point on the surface and a normal vector (perpendicular to the surface)." ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": {}, "outputs": [], "source": [ "@dataclass\n", "class Plane:\n", " point: Point\n", " normal: Vector\n", "\n", " def intersect(self, ray: Ray) -> Point | None:\n", " \"\"\"Find where ray intersects this plane.\n", "\n", " Returns None if ray is parallel to plane.\n", " \"\"\"\n", " local_ray = Ray(\n", " origin=ray.origin.to_frame(self.point.frame),\n", " direction=ray.direction.to_frame(self.point.frame),\n", " )\n", "\n", " denom = local_ray.direction.dot(self.normal)\n", "\n", " if abs(denom) < 1e-8:\n", " return None\n", "\n", " distance = (self.point - local_ray.origin).dot(self.normal) / denom\n", "\n", " return local_ray.origin + local_ray.direction * distance\n", "\n", "\n", "plane_frame = root.make_child(\"plane\")\n", "\n", "plane = Plane(point=plane_frame.point(0, 0, 0), normal=plane_frame.vector(-1, 0, 0))" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "The intersection algorithm:\n", "\n", "1. Transform ray to the plane's coordinate system using `.to_frame()`\n", "2. Check if ray is parallel to plane (`direction · normal ≈ 0`)\n", "3. Calculate distance along ray: `distance = (plane_point - ray_origin) · normal / (ray_direction · normal)`\n", "4. Return intersection: `origin + direction * distance`\n", "\n", "**Key insight**: By transforming to the plane's local frame first, we always use the same simple formula - even though the plane and ray might be positioned and oriented arbitrarily in world space. The plane is always centered at `(0,0,0)` in its own frame." ] }, { "cell_type": "markdown", "id": "11", "metadata": {}, "source": [ "## Position the plane\n", "\n", "Move the plane 5 units along the x-axis." ] }, { "cell_type": "code", "execution_count": null, "id": "12", "metadata": {}, "outputs": [], "source": [ "plane_frame.translate(x=5)" ] }, { "cell_type": "markdown", "id": "13", "metadata": {}, "source": [ "Ray starts at `(0, 0, 0)` pointing along `(1, 0, 0)`. Plane is at `x=5` with normal pointing toward negative x. Expected intersection: `(5, 0, 0)`." ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "## Compute intersection" ] }, { "cell_type": "code", "execution_count": null, "id": "15", "metadata": {}, "outputs": [], "source": [ "intersection = plane.intersect(simple_ray)\n", "\n", "if intersection:\n", " intersection_global = intersection.to_global()\n", " print(f\"Intersection point: {intersection_global}\")\n", " print(\"Expected (root): Point at x=5.0, y=0.0, z=0.0\")\n", "else:\n", " print(\"No intersection found.\")" ] }, { "cell_type": "markdown", "id": "16", "metadata": {}, "source": [ "## Verify the result" ] }, { "cell_type": "code", "execution_count": null, "id": "17", "metadata": {}, "outputs": [], "source": [ "distance_traveled = (intersection_global - simple_ray.origin.to_global()).magnitude\n", "print(f\"Distance ray traveled: {distance_traveled:.2f} units\")\n", "\n", "plane_center_global = plane.point.to_global()\n", "print(f\"Plane center position: {plane_center_global:.2f}\")\n", "\n", "is_on_plane = np.allclose(\n", " (intersection_global - plane_center_global).to_frame(plane_frame).dot(plane.normal),\n", " 0.0,\n", " atol=1e-10,\n", ")\n", "print(f\"Intersection point lies on plane: {is_on_plane}\")" ] }, { "cell_type": "markdown", "id": "18", "metadata": {}, "source": [ "## Key Takeaways\n", "\n", "**Type safety**: Points and Vectors are distinct types with mathematically correct operations:\n", "- `point - point = vector` (displacement)\n", "- `point + vector = point` (translation)\n", "- `vector.dot(vector) = scalar`\n", "- `point * scalar` → TypeError (geometrically undefined)\n", "\n", "**Coordinate frames**: Objects defined in separate frames are transformed automatically with `.to_frame()` and `.to_global()`.\n", "\n", "**Preferred API**: Use `frame.point(x, y, z)` and `frame.vector(x, y, z)` - more concise and explicit than `Point(..., frame=frame)`.\n", "\n", "**Easy opt-out**: Use `np.array(point)` to get raw coordinates `[x, y, z]` when you need direct coordinate manipulation." ] }, { "cell_type": "markdown", "id": "19", "metadata": {}, "source": [ "## Rotate and translate the ray frame\n", "\n", "Now let's make it more interesting: position the ray frame at `(-2, 0, 1)` with a 10° rotation around y. This shows the real power of `hazy` - working with objects in different coordinate systems." ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [], "source": [ "complex_ray_frame = (\n", " root.make_child(\"complex_ray\").translate(x=-2, z=1).rotate_euler(y=10, degrees=True)\n", ")\n", "\n", "complex_ray = Ray(\n", " origin=complex_ray_frame.point(0, 0, 0),\n", " direction=complex_ray_frame.vector(1, 0, 0).normalize(),\n", ")\n", "\n", "complex_intersection = plane.intersect(complex_ray)\n", "\n", "if complex_intersection:\n", " print(f\"Ray frame origin (global): {complex_ray.origin.to_global():.2f}\")\n", " print(f\"Ray direction (global): {complex_ray.direction.to_global():.2f}\")\n", " print(f\"Intersection point: {complex_intersection.to_global():.2f}\")\n", " print(\n", " f\"\\nDistance traveled: {(complex_intersection.to_global() - complex_ray.origin.to_global()).magnitude:.2f} units\"\n", " )\n", "else:\n", " print(\"No intersection found\")" ] }, { "cell_type": "markdown", "id": "21", "metadata": {}, "source": [ "Notice: we still define the ray as `origin=(0,0,0)` and `direction=(1,0,0)` in its local frame. The transformations are handled automatically when we call `.to_global()` or `.to_frame()`." ] }, { "cell_type": "markdown", "id": "22", "metadata": {}, "source": [ "## Visualize multiple rays\n", "\n", "Let's compare several rays from different positions and orientations:" ] }, { "cell_type": "code", "execution_count": null, "id": "23", "metadata": {}, "outputs": [], "source": [ "translated_frame = root.make_child(\"ray_translated\").translate(x=-2, z=1)\n", "rotated_frame = root.make_child(\"ray_rotated\").rotate_euler(y=10, degrees=True)\n", "\n", "rays_to_test = [\n", " (\"Simple (origin)\", simple_ray),\n", " (\n", " \"Translated\",\n", " Ray(\n", " origin=translated_frame.point(0, 0, 0),\n", " direction=translated_frame.vector(1, 0, 0).normalize(),\n", " ),\n", " ),\n", " (\n", " \"Rotated 10°\",\n", " Ray(\n", " origin=rotated_frame.point(0, 0, 0),\n", " direction=rotated_frame.vector(1, 0, 0).normalize(),\n", " ),\n", " ),\n", " (\"Translated + Rotated\", complex_ray),\n", "]\n", "\n", "fig = plt.figure(figsize=(12, 6))\n", "ax1 = fig.add_subplot(121)\n", "ax2 = fig.add_subplot(122, projection=\"3d\")\n", "\n", "colors = plt.cm.tab10(np.arange(len(rays_to_test)))\n", "\n", "for i, (label, ray) in enumerate(rays_to_test):\n", " intersection = plane.intersect(ray)\n", " if intersection:\n", " origin_global = ray.origin.to_global()\n", " intersection_global = intersection.to_global()\n", "\n", " lines = np.vstack([origin_global, intersection_global])\n", "\n", " ax1.plot(\n", " *lines[:, [0, 2]].T,\n", " \"--\",\n", " color=colors[i],\n", " alpha=0.7,\n", " linewidth=2,\n", " )\n", " ax1.scatter(\n", " *intersection_global[[0, 2]],\n", " c=[colors[i]],\n", " s=50,\n", " label=label,\n", " zorder=5,\n", " edgecolors=\"black\",\n", " linewidth=1.5,\n", " )\n", " ax1.scatter(\n", " *origin_global[[0, 2]],\n", " c=[colors[i]],\n", " s=50,\n", " marker=\"o\",\n", " zorder=4,\n", " edgecolors=\"black\",\n", " linewidth=1.5,\n", " )\n", "\n", " ax2.plot(\n", " *lines.T,\n", " \"--\",\n", " color=colors[i],\n", " alpha=0.7,\n", " linewidth=2,\n", " )\n", " ax2.scatter(\n", " *origin_global,\n", " c=[colors[i]],\n", " s=100,\n", " marker=\"o\",\n", " edgecolors=\"black\",\n", " linewidth=1.5,\n", " )\n", " ax2.scatter(\n", " *intersection_global,\n", " c=[colors[i]],\n", " s=200,\n", " edgecolors=\"black\",\n", " linewidth=1.5,\n", " )\n", "\n", "ax1.axvline(x=5, color=\"green\", linestyle=\"-\", linewidth=3, alpha=0.7, label=\"Plane\")\n", "ax1.set_xlabel(\"x\", fontsize=12)\n", "ax1.set_ylabel(\"z\", fontsize=12)\n", "ax1.set_title(\"Side view (x-z plane)\", fontsize=14, fontweight=\"bold\")\n", "ax1.grid(True, alpha=0.3)\n", "ax1.legend(fontsize=10, loc=3, frameon=False)\n", "ax1.set_aspect(\"equal\")\n", "\n", "yy, zz = np.meshgrid(np.linspace(-2, 3, 10), np.linspace(-1, 2, 10))\n", "xx = np.ones_like(yy) * 5\n", "ax2.plot_surface(xx, yy, zz, alpha=0.3, color=\"green\")\n", "ax2.set_xlabel(\"x\", fontsize=10)\n", "ax2.set_ylabel(\"y\", fontsize=10)\n", "ax2.set_zlabel(\"z\", fontsize=10)\n", "ax2.set_title(\"3D view\", fontsize=14, fontweight=\"bold\")\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\nIntersection summary:\")\n", "for label, ray in rays_to_test:\n", " intersection = plane.intersect(ray)\n", " if intersection:\n", " origin = ray.origin.to_global()\n", " inter = intersection.to_global()\n", " dist = (inter - origin).magnitude\n", " print(\n", " f\"{label:20s}: origin={origin:+.2f}, intersection={inter:+.2f}, distance={dist:+.2f}\"\n", " )" ] }, { "cell_type": "markdown", "id": "24", "metadata": {}, "source": [ "**The power of local coordinates:**\n", "\n", "Each ray is defined as `origin=(0,0,0)` and `direction=(1,0,0)` in its own local frame. Each plane is centered at `(0,0,0)` in its frame. The intersection algorithm always works in surface local coordinates - transformations happen automatically.\n", "\n", "Without `hazy`: manually compute transformation matrices, track multiplication order, transform each point explicitly. With 10 rays and 5 planes in different frames, this becomes error-prone fast.\n", "\n", "With `hazy`: define objects in natural local coordinates, let the library handle transformations." ] } ], "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 }