base_2 0.1.0

Exact fixed-point geometry. Float in, float out, zero drift inside.
Documentation
//! # `base_2` API
//!
//! One in, two out. Freeze floats, stop drift.
//!
//! ## Coordinate units
//!
//! Units are user-defined. The coordinate system uses 1 unit = 2^-32 µm internally,
//! but you may treat inputs as any consistent unit (mm, cm, game units, etc).
//! The only requirement is that you use the same unit everywhere.
//!
//! ## Quick Start
//!
//! ```rust
//! use base_2::api::Mesh;
//!
//! let verts: Vec<[f32; 3]> = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
//!
//! let mesh = Mesh::from_floats(&verts)
//!     .translate(10.0, 0.0, 0.0)
//!     .rotate_quat(0.0, 0.0, 0.707, 0.707);
//!
//! // compute once, use twice — no redundant work
//! let pts   = mesh.compute();
//! let gpu   = pts.to_f32();
//! let logic = pts.to_i64();
//! ```

use crate::coord::Coord64;
use crate::point::Point3;
use crate::transforms;

// ── RotationMatrix ────────────────────────────────────────────────────────────

/// A 3x3 rotation matrix stored in row-major order.
///
/// Built once from Euler angles or a quaternion — applied directly to points
/// with no intermediate conversion. Single trig step, no double conversion error.
#[derive(Debug, Clone, Copy)]
pub struct RotationMatrix(
    /// Row-major 3x3 matrix: `[row][col]`
    pub [[f64; 3]; 3]
);

impl RotationMatrix {
    /// The identity rotation — no rotation applied.
    #[must_use]
    pub const fn identity() -> Self {
        Self([
            [1.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [0.0, 0.0, 1.0],
        ])
    }

    /// Build a rotation matrix from Euler angles (radians), applied X → Y → Z.
    ///
    /// ⚠️ Euler angles are order-dependent and subject to gimbal lock.
    /// Prefer [`RotationMatrix::from_quat`] for robust 3D rotation.
    #[must_use]
    pub fn from_euler(rx: f64, ry: f64, rz: f64) -> Self {
        let (sx, cx) = rx.sin_cos();
        let (sy, cy) = ry.sin_cos();
        let (sz, cz) = rz.sin_cos();

        Self([
            [cy*cz, cz.mul_add(-sx, -(sy*cx*cz)), cx.mul_add(cz, -(sy*sx*cz))],
            [cy*sz, sx.mul_add(sy*sz, cx*cz),      cx.mul_add(sy*sz, -(sx*cz))],
            [-sy,   cy*sx,                          cy*cx                      ],
        ])
    }

    /// Build a rotation matrix from a unit quaternion (x, y, z, w).
    ///
    /// Converts directly to matrix — no intermediate Euler angles, no double trig,
    /// no gimbal lock. This is the preferred rotation method.
    #[must_use]
    pub fn from_quat(qx: f64, qy: f64, qz: f64, qw: f64) -> Self {
        // normalize in case the input isn't unit length
        let len = qw.mul_add(qw, qx.mul_add(qx, qy.mul_add(qy, qz * qz))).sqrt();
        let (qx, qy, qz, qw) = (qx/len, qy/len, qz/len, qw/len);

        let (x2, y2, z2) = (qx+qx, qy+qy, qz+qz);
        let (xx, yy, zz) = (qx*x2, qy*y2, qz*z2);
        let (xy, xz, yz) = (qx*y2, qx*z2, qy*z2);
        let (wx, wy, wz) = (qw*x2, qw*y2, qw*z2);

        Self([
            [1.0 - (yy + zz), xy - wz,         xz + wy         ],
            [xy + wz,         1.0 - (xx + zz),  yz - wx         ],
            [xz - wy,         yz + wx,           1.0 - (xx + yy)],
        ])
    }
}

// ── Transform ─────────────────────────────────────────────────────────────────

/// A complete transform: translation, scale, and rotation.
///
/// Stored as separate components and applied fresh from the original frozen
/// points every frame — error never accumulates across frames.
#[derive(Debug, Clone, Copy)]
pub struct Transform {
    /// Translation offset along X. Units are user-defined.
    pub translate_x: f64,
    /// Translation offset along Y. Units are user-defined.
    pub translate_y: f64,
    /// Translation offset along Z. Units are user-defined.
    pub translate_z: f64,
    /// Scale factor along X.
    pub scale_x: f64,
    /// Scale factor along Y.
    pub scale_y: f64,
    /// Scale factor along Z.
    pub scale_z: f64,
    /// Rotation as a 3x3 matrix. Build with [`RotationMatrix::from_quat`] or [`RotationMatrix::from_euler`].
    pub rotation: RotationMatrix,
}

impl Transform {
    /// The identity transform — no translation, no rotation, scale of 1.
    #[must_use]
    pub const fn identity() -> Self {
        Self {
            translate_x: 0.0, translate_y: 0.0, translate_z: 0.0,
            scale_x:     1.0, scale_y:     1.0, scale_z:     1.0,
            rotation:    RotationMatrix::identity(),
        }
    }
}

// ── Points ────────────────────────────────────────────────────────────────────

/// A computed set of transformed points.
///
/// Returned by [`Mesh::compute`]. Call once per frame, then use
/// [`Points::to_f32`] for GPU output and [`Points::to_i64`] for logic.
#[must_use]
pub struct Points(Vec<Point3<i64>>);

impl Points {
    /// Borrow the transformed points as a slice.
    #[inline]
    #[must_use]
    pub fn as_slice(&self) -> &[Point3<i64>] {
        &self.0
    }

    /// Consume and return the inner `Vec` for cases where ownership is needed.
    #[must_use]
    pub fn into_vec(self) -> Vec<Point3<i64>> {
        self.0
    }

    /// Convert to clean floats for GPU rendering.
    ///
    /// Freshly derived from exact `Coord64` source — never accumulates drift.
    /// Deterministic for identical inputs on the same platform.
    #[inline]
    #[must_use]
    pub fn to_f32(&self) -> Vec<[f32; 3]> {
        self.0.iter().map(|p| [
            p.x.to_f32(),
            p.y.to_f32(),
            p.z.to_f32(),
        ]).collect()
    }

    /// Convert to exact `i64` coordinates for game logic, physics, and collision.
    ///
    /// Raw `Coord64` values — 1 unit = 2^-32 µm.
    /// Deterministic for identical inputs on the same platform.
    #[inline]
    #[must_use]
    pub fn to_i64(&self) -> Vec<[i64; 3]> {
        self.0.iter().map(|p| [
            p.x.raw(),
            p.y.raw(),
            p.z.raw(),
        ]).collect()
    }

    /// Number of points.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.0.len()
    }

    /// Returns `true` if there are no points.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

// ── Mesh ──────────────────────────────────────────────────────────────────────

/// A mesh with exact integer coordinates.
///
/// Original frozen points are never modified — all transforms are applied
/// fresh from the original every time [`Mesh::compute`] is called.
/// Rotation error is bounded at ≤ 0.116pm and never accumulates.
#[must_use]
pub struct Mesh {
    original:  Vec<Point3<i64>>,
    transform: Transform,
}

impl Mesh {
    /// Load from any float vertex array.
    ///
    /// Works with STL, OBJ, GLTF, FBX — anything that produces `[[f32; 3]]`.
    /// Floats are frozen to exact `Coord64` in one step.
    /// Maximum freeze error: ≤ 0.116pm, introduced once, never again.
    pub fn from_floats(verts: &[[f32; 3]]) -> Self {
        Self {
            original: verts.iter().map(|v| Point3 {
                x: Coord64::from_f32(v[0]),
                y: Coord64::from_f32(v[1]),
                z: Coord64::from_f32(v[2]),
            }).collect(),
            transform: Transform::identity(),
        }
    }

    /// Set translation offset. Overwrites any previous translation.
    ///
    /// Units are user-defined — use whatever unit you froze your floats in.
    pub fn translate(mut self, x: f32, y: f32, z: f32) -> Self {
        self.transform.translate_x = f64::from(x);
        self.transform.translate_y = f64::from(y);
        self.transform.translate_z = f64::from(z);
        self
    }

    /// Set scale factors. Overwrites any previous scale.
    pub fn scale(mut self, x: f32, y: f32, z: f32) -> Self {
        self.transform.scale_x = f64::from(x);
        self.transform.scale_y = f64::from(y);
        self.transform.scale_z = f64::from(z);
        self
    }

    /// Set rotation from Euler angles in radians, applied X → Y → Z.
    /// Overwrites any previous rotation.
    ///
    /// ⚠️ Euler angles are order-dependent and subject to gimbal lock.
    /// Prefer [`Mesh::rotate_quat`] for robust 3D rotation.
    pub fn rotate(mut self, x: f32, y: f32, z: f32) -> Self {
        self.transform.rotation = RotationMatrix::from_euler(
            f64::from(x), f64::from(y), f64::from(z),
        );
        self
    }

    /// Set rotation from a unit quaternion (x, y, z, w).
    /// Overwrites any previous rotation.
    ///
    /// Converts directly to a rotation matrix — no intermediate Euler angles,
    /// no double trig, no gimbal lock. This is the preferred rotation method.
    pub fn rotate_quat(mut self, qx: f32, qy: f32, qz: f32, qw: f32) -> Self {
        self.transform.rotation = RotationMatrix::from_quat(
            f64::from(qx), f64::from(qy), f64::from(qz), f64::from(qw),
        );
        self
    }

    /// Apply the current transform to the original frozen points.
    ///
    /// Call once per frame — use the returned [`Points`] for both GPU and logic.
    /// Always computed fresh from the original — sin/cos error never accumulates.
    pub fn compute(&self) -> Points {
        Points(transforms::apply_transform(&self.original, &self.transform))
    }

    /// Direct access to the original frozen points, untransformed.
    #[must_use]
    pub fn original_points(&self) -> &[Point3<i64>] {
        &self.original
    }

    /// Number of vertices.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.original.len()
    }

    /// Returns `true` if the mesh has no vertices.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.original.is_empty()
    }
}

// ── Point3 conversions ────────────────────────────────────────────────────────

impl Point3<i64> {
    /// Create an exact point from f32 coordinates.
    #[must_use]
    pub fn from_f32(x: f32, y: f32, z: f32) -> Self {
        Self {
            x: Coord64::from_f32(x),
            y: Coord64::from_f32(y),
            z: Coord64::from_f32(z),
        }
    }

    /// Convert back to f32 for rendering.
    #[must_use]
    pub fn to_f32(&self) -> (f32, f32, f32) {
        (self.x.to_f32(), self.y.to_f32(), self.z.to_f32())
    }

    /// Raw exact coordinates for logic.
    #[must_use]
    pub const fn to_i64(&self) -> (i64, i64, i64) {
        (self.x.raw(), self.y.raw(), self.z.raw())
    }
}