# Math & Core Primitives

The `oxide_engine::math` module is the foundation every other system depends on.
It builds on [`glam`](https://docs.rs/glam) for vectors, quaternions, and
matrices, and adds the engine's higher-level geometric and utility types.

This document is the usage reference for the module as delivered in **Stage 1**.
For the conventions these types follow (handedness, units, color space), see
[conventions.md](conventions.md).

## Importing

Everything is available through the module path or the prelude:

```rust
use oxide_engine::math::{Transform, Aabb, Ray, Plane, Frustum, Color, Rect, Range3};
// or, more commonly:
use oxide_engine::prelude::*;
```

The prelude also re-exports the `glam` types you need for everyday work — `Vec2`,
`Vec3`, `Vec4`, `Quat`, `Mat3`, `Mat4`, and `EulerRot` — so downstream crates
need no direct `glam` dependency.

## Contents

| Type | Role |
|------|------|
| [`Transform`](#transform) | Placement: translation + rotation + scale |
| [`Aabb`](#aabb) | Axis-aligned bounding box (geometry/bounds/culling) |
| [`Ray`](#ray) | Origin + normalized direction (picking, queries) |
| [`Plane`](#plane) | Infinite plane in Hessian normal form |
| [`Frustum`](#frustum) | Six-plane view volume for visibility culling |
| [`Color`](#color) | Linear RGBA color with sRGB conversion |
| [`Rect`](#rect) | 2D rectangle (UI, viewports, texture regions) |
| [`Range3`](#range3) | 3D value range (clamp, lerp, remap) |

All types are `Copy`, `PartialEq`, and `serde`-(de)serializable.

---

## Transform

A 3D affine transform stored in **decomposed** form — `translation` (`Vec3`),
`rotation` (`Quat`), and `scale` (`Vec3`) — so each channel stays editable
without matrix round-trips. The effective matrix is `T * R * S`.

### Construction

```rust
use oxide_engine::prelude::*;

let a = Transform::IDENTITY;
let b = Transform::from_translation(Vec3::new(0.0, 1.0, 0.0));
let c = Transform::from_rotation(Quat::from_rotation_y(90_f32.to_radians()));
let d = Transform::from_scale(Vec3::splat(2.0));
let e = Transform::from_trs(
    Vec3::new(1.0, 2.0, 3.0),
    Quat::from_euler(EulerRot::XYZ, 0.1, 0.2, 0.3),
    Vec3::splat(1.0),
);

// From / to a 4x4 matrix:
let m = e.to_matrix();          // glam::Mat4
let back = Transform::from_matrix(m);
let affine = e.to_affine();     // glam::Affine3A (cheaper to compose)
```

### Composition and hierarchy

`mul_transform` composes parent-first, so this is how you resolve a child into
its parent's space:

```rust
let parent = Transform::from_trs(
    Vec3::new(10.0, 0.0, 0.0),
    Quat::from_rotation_y(90_f32.to_radians()),
    Vec3::splat(2.0),
);
let child_local = Transform::from_translation(Vec3::new(0.0, 0.0, 1.0));
let child_world = parent.mul_transform(&child_local);
// child_world.translation == (12, 0, 0)
```

Composition is exact for uniform scale and a closest-fit approximation for
non-uniform scale combined with rotation (see
[conventions](conventions.md#transform-composition)).

### Applying a transform

```rust
let t = Transform::from_trs(
    Vec3::new(1.0, 0.0, 0.0),
    Quat::from_rotation_z(90_f32.to_radians()),
    Vec3::splat(2.0),
);

let p = t.transform_point(Vec3::new(1.0, 0.0, 0.0));   // affected by T, R, S
let v = t.transform_vector(Vec3::new(1.0, 0.0, 0.0));  // R and S only (no translation)
```

### Inverse

`inverse()` returns a transform that undoes this one. It is exact for uniform
scale; with any zero scale component the transform is not invertible and the
inverse scale will contain infinities (check with `is_finite()`).

```rust
let undo = t.inverse();
let identity = t.mul_transform(&undo);   // ≈ Transform::IDENTITY
```

### Direction and orientation helpers

```rust
let dir_forward = t.forward();   // local -Z
let dir_up      = t.up();        // local +Y
let dir_right   = t.right();     // local +X

// Build an orientation that looks from `eye` toward `target`:
let cam = Transform::looking_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y);
// cam.forward() points at the target. Degenerate (eye == target) → identity rotation.
```

### Edge cases handled

- **Zero scale** → non-invertible; the forward transform is still finite.
- **Gimbal-lock orientations** (e.g. ±90° pitch) round-trip through a matrix
  without losing orthonormality of the basis vectors.
- **Degenerate `looking_at`** (eye == target) returns identity rotation rather
  than producing NaNs.

---

## Aabb

An axis-aligned bounding box defined by `min` and `max` corners. Used for bounds,
broad-phase overlap, and frustum culling. A box is *empty* when any `min`
component exceeds its `max`; `Aabb::EMPTY` is the identity for `union`.

```rust
use oxide_engine::prelude::*;

let a = Aabb::new(Vec3::new(1.0, 5.0, -2.0), Vec3::new(-1.0, 0.0, 3.0)); // corners auto-sorted
let b = Aabb::from_center_half_extents(Vec3::ZERO, Vec3::splat(1.0));
let c = Aabb::from_points([Vec3::ZERO, Vec3::new(2.0, -1.0, 4.0), Vec3::new(-3.0, 5.0, 1.0)]);

// Queries:
let center = b.center();
let size   = b.size();
let inside = b.contains_point(Vec3::ZERO);
let near   = b.closest_point(Vec3::new(5.0, 0.0, 0.0));

// Set operations:
let u = a.union(&b);
let i = a.intersection(&b);       // Aabb::EMPTY if disjoint
let hit = a.intersects(&b);       // bool (touching counts)

// Acceleration-structure metrics:
let area = b.surface_area();      // for SAH
let vol  = b.volume();
let pts  = b.corners();           // [Vec3; 8]

// Ray test (slab method): returns entry distance t, or None.
let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::X);
if let Some(t) = b.ray_intersection(&ray) {
    let point = ray.at(t);
}
```

A ray whose origin is inside the box returns `Some(0.0)`.

---

## Ray

A half-line with an `origin` and a **normalized** `direction`. Because the
direction is unit-length, the parameter `t` in `at(t)` is a true distance.

```rust
use oxide_engine::prelude::*;

let r = Ray::new(Vec3::ZERO, Vec3::new(0.0, 5.0, 0.0)); // direction normalized to +Y
let r2 = Ray::from_to(Vec3::ZERO, Vec3::new(0.0, 0.0, 10.0));

let p = r.at(4.0);                       // origin + direction * 4
let valid = r.is_valid();                // false if direction was zero-length
let near = r.closest_point(target);      // clamped to t >= 0
let dist = r.distance_to_point(target);
```

If you pass a zero-length direction, the ray is left degenerate; check
`is_valid()` before relying on it.

---

## Plane

An infinite plane in Hessian normal form: a unit `normal` and a signed distance
`d`, such that every point on the plane satisfies `normal·p + d = 0`. The
positive half-space is the side the normal points toward.

```rust
use oxide_engine::prelude::*;

let p1 = Plane::from_point_normal(Vec3::new(0.0, 2.0, 0.0), Vec3::Y);
let p2 = Plane::from_points(Vec3::ZERO, Vec3::X, Vec3::Y); // normal via right-hand rule → +Z
let p3 = Plane::new(Vec3::new(0.0, 0.0, 4.0), 8.0);        // normalized on construction

let sd = p1.signed_distance(Vec3::new(0.0, 5.0, 0.0));     // +3.0 (in front)
let proj = p1.project_point(Vec3::new(3.0, 7.0, -2.0));    // orthogonal projection onto plane
let flipped = p1.flipped();                                // same plane, reversed normal

// Ray test: None if parallel or pointing away.
let ray = Ray::new(Vec3::new(0.0, 5.0, 0.0), Vec3::NEG_Y);
if let Some(t) = p1.ray_intersection(&ray) {
    let hit = ray.at(t);
}
```

---

## Frustum

A view volume represented by six planes (left, right, bottom, top, near, far),
each with its normal pointing **inward**. A point is inside when it lies in the
positive half-space of every plane. Built from a combined view-projection matrix
via the Gribb–Hartmann method; works for perspective and orthographic
projections alike.

```rust
use oxide_engine::prelude::*;

let proj = Mat4::perspective_rh(60_f32.to_radians(), 16.0 / 9.0, 0.1, 100.0);
let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y);
let frustum = Frustum::from_view_projection(proj * view);

let visible_point = frustum.contains_point(Vec3::ZERO);

let bb = Aabb::from_center_half_extents(Vec3::ZERO, Vec3::splat(1.0));
let visible_box = frustum.intersects_aabb(&bb);

let visible_sphere = frustum.intersects_sphere(Vec3::ZERO, 1.0);
```

`intersects_aabb` is **conservative**: it never culls a box that is actually
visible, though it may very rarely keep one that is just outside a corner. That
is the correct trade-off for rendering, where false positives cost a wasted draw
but false negatives cause visible pop-out.

---

## Color

Linear RGBA color with `f32` channels. The engine works in **linear** space;
conversions to/from 8-bit sRGB are explicit. Values may exceed `1.0` to represent
HDR/emissive intensity and are not clamped in storage.

```rust
use oxide_engine::prelude::*;

let white = Color::WHITE;
let custom = Color::rgba(0.2, 0.4, 0.6, 1.0);
let from_pdf = Color::from_srgb_u8(135, 206, 235);   // sRGB bytes → linear
let from_hex = Color::from_hex(0x87CEEB);            // #87CEEB → linear

let bytes = custom.to_srgb_u8();        // [u8; 4] sRGB, clamped to [0,1]
let v4 = custom.to_vec4();              // Vec4 [r,g,b,a]
let v3 = custom.to_vec3();              // Vec3 rgb
let faded = custom.with_alpha(0.5);
let mid = Color::BLACK.lerp(Color::WHITE, 0.5); // t clamped to [0,1]
```

Constants: `BLACK`, `WHITE`, `RED`, `GREEN`, `BLUE`, `TRANSPARENT`.

---

## Rect

A 2D axis-aligned rectangle for UI, viewports, and texture regions.

```rust
use oxide_engine::prelude::*;

let r1 = Rect::new(Vec2::new(4.0, 1.0), Vec2::new(0.0, 5.0)); // corners auto-sorted
let r2 = Rect::from_min_size(Vec2::ZERO, Vec2::new(3.0, 4.0));
let r3 = Rect::from_center_size(Vec2::ZERO, Vec2::splat(2.0));

let (w, h) = (r2.width(), r2.height());
let size = r2.size();
let area = r2.area();
let c = r2.center();
let empty = Rect::ZERO.is_empty();

let inside = r2.contains_point(Vec2::ONE);
let hit = r2.intersects(&r3);
let i = r2.intersection(&r3);   // Rect::ZERO if disjoint
let u = r2.union(&r3);
let near = r2.closest_point(Vec2::new(5.0, -1.0));
let grown = r2.expanded(1.0);   // negative shrinks
```

---

## Range3

A 3D **value** range — an inclusive `[min, max]` interval per axis. Unlike
`Aabb` (which models geometry), `Range3` models a *value range* for clamping,
interpolation, and remapping. It deliberately provides `lerp`/`inverse_lerp`/
`remap`, which an `Aabb` does not.

```rust
use oxide_engine::prelude::*;

let r = Range3::new(Vec3::ZERO, Vec3::splat(100.0));
let unit = Range3::UNIT;                 // [0, 1] per axis
let sym = Range3::symmetric(Vec3::splat(2.0)); // [-2, 2] per axis

let span = r.span();
let center = r.center();
let clamped = r.clamp(Vec3::new(-5.0, 50.0, 200.0)); // → (0, 50, 100)
let inside = r.contains(Vec3::splat(50.0));

let v = r.lerp(Vec3::splat(0.25));       // NOT clamped — extrapolates outside [0,1]
let t = r.inverse_lerp(Vec3::splat(25.0)); // → 0.25 per axis (0.0 on zero-span axes)
let remapped = r.remap(Vec3::splat(50.0), &unit); // 50 in [0,100] → 0.5 in [0,1]
```

`inverse_lerp` guards against division by zero: an axis with zero span yields
`0.0` rather than `NaN`/`inf`.

---

## Testing and performance

- **Unit tests** for every type live in that type's source file
  (`engine/src/math/*.rs`), covering core behavior and edge cases (zero scale,
  gimbal lock, degenerate rays/planes, empty boxes, zero-span ranges).
- **Integration / fuzz tests** live in the `oxide-tests` crate
  (`stage1::fuzz_transform_chains_stay_stable` builds long random transform
  chains and verifies stability and inverse round-trips).
- **Benchmark:** `cargo bench -p oxide-engine` runs the `transform` benchmark.
  The Stage 1 budget — 1,000,000 transform compositions in under 10 ms — is met
  with margin (~5 ms on a typical desktop).

## Runnable example

```sh
cargo run -p oxide-examples --bin math_demo
```

`examples/src/bin/math_demo.rs` exercises every type above and prints the
results — a good place to see the API in use end to end.
