axiolid_core/
primitives.rs

1//! Coordinate, direction, transform, and analytic support types.
2
3use crate::Scalar;
4
5/// Double-precision two-dimensional vector.
6pub type Vec2 = glam::DVec2;
7/// Double-precision three-dimensional vector.
8pub type Vec3 = glam::DVec3;
9/// A semantic alias used when a value is a two-dimensional position.
10pub type Point2 = Vec2;
11/// A semantic alias used when a value is a three-dimensional position.
12pub type Point3 = Vec3;
13/// Double-precision 3x3 matrix.
14pub type Mat3 = glam::DMat3;
15/// Double-precision 2D affine transform.
16pub type Transform2 = glam::DAffine2;
17/// Double-precision affine transform.
18///
19/// Affine transforms cannot represent perspective: the implicit bottom row is
20/// always `[0, 0, 0, 1]`. Use [`Mat4`] when a projection is needed.
21pub type Transform3 = glam::DAffine3;
22/// Double-precision general 4x4 matrix, including projective transforms.
23///
24/// Distinct from [`Transform3`], which is affine and therefore cannot express
25/// perspective. This type carries a real fourth row, so it can represent a
26/// projection whose `w` varies per point -- which is exactly what makes a
27/// homogeneous divide meaningful.
28///
29/// Prefer [`Transform3`] for the ordinary placement, scaling, and rotation of
30/// geometry: it is smaller, composes faster, and its inverse is always
31/// well-defined. Reach for `Mat4` only when the transform genuinely projects.
32///
33/// Note that the previous release aliased `Mat4` to [`Transform3`], so it was
34/// affine despite the name. Code that wants the old meaning should name
35/// [`Transform3`] explicitly; the mismatch in API surface makes that a compile
36/// error rather than a silent change in behaviour.
37pub type Mat4 = glam::DMat4;
38
39/// Right-handed 2D local frame. Algorithms validate orthonormality explicitly.
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct Frame2 {
42    /// Local origin.
43    pub origin: Point2,
44    /// Local x axis.
45    pub x: Vec2,
46    /// Local y axis.
47    pub y: Vec2,
48}
49
50/// Right-handed 3D local frame. Dirty imported frames remain representable.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct Frame3 {
53    /// Local origin.
54    pub origin: Point3,
55    /// Local x axis.
56    pub x: Vec3,
57    /// Local y axis.
58    pub y: Vec3,
59    /// Local z axis.
60    pub z: Vec3,
61}
62
63/// A finite parameter interval. The endpoint order carries orientation.
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct Interval {
66    /// Start parameter.
67    pub start: Scalar,
68    /// End parameter.
69    pub end: Scalar,
70}
71
72impl Interval {
73    /// Unit parameter interval.
74    pub const UNIT: Self = Self {
75        start: 0.0,
76        end: 1.0,
77    };
78
79    /// Construct an oriented interval without sorting its endpoints.
80    pub const fn new(start: Scalar, end: Scalar) -> Self {
81        Self { start, end }
82    }
83
84    /// Absolute parameter span.
85    pub fn length(self) -> Scalar {
86        (self.end - self.start).abs()
87    }
88}
89
90/// A plane represented by an origin and unit-normal candidate.
91///
92/// Adapters may construct dirty input. Algorithms validate normalization using
93/// the operation's tolerance instead of hiding a global epsilon here.
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct Plane3 {
96    /// Point on the plane.
97    pub origin: Point3,
98    /// Expected outward normal.
99    pub normal: Vec3,
100}
101
102/// A parametric three-dimensional ray.
103#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct Ray3 {
105    /// Ray start.
106    pub origin: Point3,
107    /// Ray direction. It need not be normalized at the storage boundary.
108    pub direction: Vec3,
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    /// The reason `Mat4` stopped being an alias for [`Transform3`].
116    ///
117    /// A perspective projection makes `w` vary per point, so recovering a
118    /// cartesian coordinate requires dividing by it. An affine transform
119    /// cannot express that at all, which is why the old alias could not close
120    /// this gap no matter how it was called.
121    #[test]
122    fn projective_matrix_divides_by_w_and_affine_cannot() {
123        // Looking down -Z with a 90 degree vertical field of view, so a point
124        // at depth d has its height scaled by 1/d.
125        let projection = Mat4::perspective_rh(std::f64::consts::FRAC_PI_2, 1.0, 1.0, 100.0);
126
127        let near = projection.project_point3(Point3::new(0.0, 1.0, -2.0));
128        let far = projection.project_point3(Point3::new(0.0, 1.0, -4.0));
129
130        // Same world height, twice the depth: the projected height halves.
131        // That ratio is the homogeneous divide doing its job.
132        assert!(
133            (near.y / far.y - 2.0).abs() < 1e-12,
134            "near {near:?} far {far:?}"
135        );
136
137        // The fourth row is what carries it. An affine transform's implicit
138        // bottom row is [0, 0, 0, 1], so w is constant and no such ratio can
139        // arise: identical input keeps its height at both depths.
140        let affine = Transform3::IDENTITY;
141        let near_affine = affine.transform_point3(Point3::new(0.0, 1.0, -2.0));
142        let far_affine = affine.transform_point3(Point3::new(0.0, 1.0, -4.0));
143        assert_eq!(near_affine.y, far_affine.y);
144    }
145
146    /// `Mat4` and `Transform3` are now genuinely different types.
147    ///
148    /// Asserted so a future "simplification" back to an alias fails here
149    /// rather than silently removing projection support again.
150    #[test]
151    fn a_projective_matrix_round_trips_through_its_affine_subset() {
152        let affine = Transform3::from_translation(Vec3::new(1.0, 2.0, 3.0));
153        let promoted = Mat4::from(affine);
154        let point = Point3::new(0.5, -0.5, 2.0);
155        // Promoting an affine transform must not change what it does.
156        assert_eq!(
157            promoted.project_point3(point),
158            affine.transform_point3(point)
159        );
160    }
161}