// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The `index` component of a `Vec2`, counted from zero.
fn component2(v : Vec2, index : Int32) -> Float {
if index == 0 {
v.x
} else {
v.y
}
}
///|
/// The `index` component of a `Vec3`, counted from zero.
fn component3(v : Vec3, index : Int32) -> Float {
if index == 0 {
v.x
} else if index == 1 {
v.y
} else {
v.z
}
}
///|
/// A half-line in the plane: a start and a direction.
///
/// `origin + direction * t` for `t >= 0`. The direction is not normalized here —
/// a ray whose direction carries the speed of something travelling along it is
/// as useful as a unit one, and `t` then reads as time.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Ray2 {
/// Where the ray starts.
origin : Vec2
/// Which way it goes. The intersections report `t` in units of this vector, so a unit direction makes `t` a distance.
direction : Vec2
}
///|
pub extend Ray2 with Eq::{equal, not_equal}
///|
/// The ray starting at `origin` and heading along `direction`.
pub fn Ray2::new(origin : Vec2, direction : Vec2) -> Ray2 {
{ origin, direction, }
}
///|
/// The point `t` along the ray.
pub fn Ray2::at(self : Ray2, t : Float) -> Vec2 {
self.origin + self.direction.scale(t)
}
///|
/// Where the ray first meets the box, as a `t`, or `None` when it misses.
///
/// A ray that starts inside the box meets its surface on the way out, and that
/// crossing is the one reported — the nearest one at `t >= 0`, rather than "the
/// one in front of the origin in the direction of travel".
pub fn Ray2::intersect_aabb(self : Ray2, box : Aabb2) -> Float? {
match self.intersect_aabb_interval(box) {
None => None
Some((entry, exit)) =>
if entry >= 0.0 {
Some(entry)
} else if exit >= 0.0 {
Some(exit)
} else {
None
}
}
}
///|
/// The stretch of the ray that lies inside the box, as a pair of `t` values.
///
/// A ray that starts inside the box is already inside at `t = 0`, and the entry
/// is then negative — the point was inside before the ray started, and the
/// interval says from where. `None` when the ray misses the box.
pub fn Ray2::intersect_aabb_interval(
self : Ray2,
box : Aabb2,
) -> (Float, Float)? {
let mut entry : Float = -3.4028234663852886e38
let mut exit : Float = 3.4028234663852886e38
for axis in 0..<2 {
let d = component2(self.direction, axis)
let origin = component2(self.origin, axis)
let low = component2(box.min, axis)
let high = component2(box.max, axis)
if d == 0.0 {
if origin < low || origin > high {
return None
}
} else {
let inv : Float = 1.0 / d
let mut near = (low - origin) * inv
let mut far = (high - origin) * inv
if near > far {
let swap = near
near = far
far = swap
}
if near > entry {
entry = near
}
if far < exit {
exit = far
}
if entry > exit {
return None
}
}
}
Some((entry, exit))
}
///|
/// Where the ray first meets the rectangle, as a `t`, or `None` when it misses.
pub fn Ray2::intersect_rect(self : Ray2, rect : Rect) -> Float? {
self.intersect_aabb(rect.to_aabb())
}
///|
/// Where the ray first meets the circle, as a `t`, or `None` when it misses.
///
/// A ray that starts inside the circle meets it on the way out.
pub fn Ray2::intersect_circle(
self : Ray2,
center : Vec2,
radius : Float,
) -> Float? {
let offset = self.origin - center
let a = self.direction.length_squared()
if a == 0.0 {
return None
}
let b = offset.dot(self.direction)
let c = offset.length_squared() - radius * radius
let discriminant = b * b - a * c
if discriminant < 0.0 {
return None
}
let root = discriminant.sqrt()
let far = (-b + root) / a
let near = (-b - root) / a
if near >= 0.0 {
Some(near)
} else if far >= 0.0 {
Some(far)
} else {
None
}
}
///|
/// Whether the two rays start at the same point and head the same way.
pub fn Ray2::approx(self : Ray2, other : Ray2, eps? : Float) -> Bool {
self.origin.approx(other.origin, eps?) &&
self.direction.approx(other.direction, eps?)
}
///|
pub impl Show for Ray2 with fn output(self, logger) {
logger.write_string("Ray2(\{self.origin}, \{self.direction})")
}
///|
pub extend Ray2 with Show::{output, to_string}
///|
pub impl Debug for Ray2 with fn to_repr(self) {
@debug.Repr::ctor("Ray2", [
(Some("origin"), self.origin.to_repr()),
(Some("direction"), self.direction.to_repr()),
])
}
///|
pub extend Ray2 with Debug::{to_repr}
///|
/// A half-line in space: a start and a direction.
///
/// What a picking click, a mouse cursor in a 3D scene and a photon all are. The
/// tests here return the parameter `t` of the first surface crossing rather than
/// the point, because the caller usually needs the distance and the point is one
/// multiplication away.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Ray3 {
/// Where the ray starts.
origin : Vec3
/// Which way it goes. The intersections report `t` in units of this vector, so a unit direction makes `t` a distance.
direction : Vec3
}
///|
pub extend Ray3 with Eq::{equal, not_equal}
///|
/// The ray starting at `origin` and heading along `direction`.
pub fn Ray3::new(origin : Vec3, direction : Vec3) -> Ray3 {
{ origin, direction, }
}
///|
/// The point `t` along the ray.
pub fn Ray3::at(self : Ray3, t : Float) -> Vec3 {
self.origin + self.direction.scale(t)
}
///|
/// Where the ray first meets the box, as a `t`, or `None` when it misses.
///
/// A ray that starts inside the box reports its exit.
pub fn Ray3::intersect_aabb(self : Ray3, box : Aabb3) -> Float? {
match self.intersect_aabb_interval(box) {
None => None
Some((entry, exit)) =>
if entry >= 0.0 {
Some(entry)
} else if exit >= 0.0 {
Some(exit)
} else {
None
}
}
}
///|
/// The stretch of the ray that lies inside the box, as a pair of `t` values.
///
/// A ray that starts inside has an entry before its origin. This is the slab test: for each axis the ray is inside the box over an
/// interval, and the intersection of the three intervals is the stretch wanted.
/// A ray parallel to a face never leaves that axis' slab, or never enters it.
pub fn Ray3::intersect_aabb_interval(
self : Ray3,
box : Aabb3,
) -> (Float, Float)? {
let mut entry : Float = -3.4028234663852886e38
let mut exit : Float = 3.4028234663852886e38
for axis in 0..<3 {
let d = component3(self.direction, axis)
let origin = component3(self.origin, axis)
let low = component3(box.min, axis)
let high = component3(box.max, axis)
if d == 0.0 {
if origin < low || origin > high {
return None
}
} else {
let inv : Float = 1.0 / d
let mut near = (low - origin) * inv
let mut far = (high - origin) * inv
if near > far {
let swap = near
near = far
far = swap
}
if near > entry {
entry = near
}
if far < exit {
exit = far
}
if entry > exit {
return None
}
}
}
Some((entry, exit))
}
///|
/// Where the ray first meets the sphere, as a `t`, or `None` when it misses.
///
/// A ray that starts inside the sphere reports its exit.
pub fn Ray3::intersect_sphere(self : Ray3, sphere : Sphere) -> Float? {
let offset = self.origin - sphere.center
let a = self.direction.length_squared()
if a == 0.0 {
return None
}
let b = offset.dot(self.direction)
let c = offset.length_squared() - sphere.radius * sphere.radius
let discriminant = b * b - a * c
if discriminant < 0.0 {
return None
}
let root = discriminant.sqrt()
let far = (-b + root) / a
let near = (-b - root) / a
if near >= 0.0 {
Some(near)
} else if far >= 0.0 {
Some(far)
} else {
None
}
}
///|
/// Where the ray meets the plane, as a `t`, or `None` when it runs parallel.
///
/// A ray lying inside the plane meets it everywhere and nowhere in particular;
/// that comes back as `None` too.
pub fn Ray3::intersect_plane(self : Ray3, plane : Plane) -> Float? {
let denominator = plane.normal.dot(self.direction)
if denominator == 0.0 {
return None
}
let t = (plane.distance - plane.normal.dot(self.origin)) / denominator
if t >= 0.0 {
Some(t)
} else {
None
}
}
///|
/// Where the ray meets the triangle, as a `t`, or `None` when it misses.
///
/// Möller–Trumbore: the ray is solved against the triangle's barycentric
/// coordinates directly, without building the plane first, and a hit behind the
/// origin or outside the triangle gives `None`.
pub fn Ray3::intersect_triangle(self : Ray3, triangle : Triangle) -> Float? {
let edge1 = triangle.b - triangle.a
let edge2 = triangle.c - triangle.a
let h = self.direction.cross(edge2)
let determinant = edge1.dot(h)
if determinant.abs() < default_epsilon {
// The ray runs parallel to the triangle's plane.
return None
}
let inv_determinant : Float = 1.0 / determinant
let s = self.origin - triangle.a
let u = s.dot(h) * inv_determinant
let one : Float = 1.0
if u < 0.0 || u > one {
return None
}
let q = s.cross(edge1)
let v = self.direction.dot(q) * inv_determinant
if v < 0.0 || u + v > one {
return None
}
let t = edge2.dot(q) * inv_determinant
if t >= 0.0 {
Some(t)
} else {
None
}
}
///|
/// Whether the two rays start at the same point and head the same way.
pub fn Ray3::approx(self : Ray3, other : Ray3, eps? : Float) -> Bool {
self.origin.approx(other.origin, eps?) &&
self.direction.approx(other.direction, eps?)
}
///|
pub impl Show for Ray3 with fn output(self, logger) {
logger.write_string("Ray3(\{self.origin}, \{self.direction})")
}
///|
pub extend Ray3 with Show::{output, to_string}
///|
pub impl Debug for Ray3 with fn to_repr(self) {
@debug.Repr::ctor("Ray3", [
(Some("origin"), self.origin.to_repr()),
(Some("direction"), self.direction.to_repr()),
])
}
///|
pub extend Ray3 with Debug::{to_repr}