Skip to main content

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)]
41#[cfg_attr(
42    feature = "serde",
43    derive(serde::Serialize, serde::Deserialize),
44    serde(deny_unknown_fields)
45)]
46pub struct Frame2 {
47    /// Local origin.
48    pub origin: Point2,
49    /// Local x axis.
50    pub x: Vec2,
51    /// Local y axis.
52    pub y: Vec2,
53}
54
55/// Right-handed 3D local frame. Dirty imported frames remain representable.
56#[derive(Debug, Clone, Copy, PartialEq)]
57#[cfg_attr(
58    feature = "serde",
59    derive(serde::Serialize, serde::Deserialize),
60    serde(deny_unknown_fields)
61)]
62pub struct Frame3 {
63    /// Local origin.
64    pub origin: Point3,
65    /// Local x axis.
66    pub x: Vec3,
67    /// Local y axis.
68    pub y: Vec3,
69    /// Local z axis.
70    pub z: Vec3,
71}
72
73/// A finite parameter interval. The endpoint order carries orientation.
74#[derive(Debug, Clone, Copy, PartialEq)]
75#[cfg_attr(
76    feature = "serde",
77    derive(serde::Serialize, serde::Deserialize),
78    serde(deny_unknown_fields)
79)]
80pub struct Interval {
81    /// Start parameter.
82    pub start: Scalar,
83    /// End parameter.
84    pub end: Scalar,
85}
86
87impl Interval {
88    /// Unit parameter interval.
89    pub const UNIT: Self = Self {
90        start: 0.0,
91        end: 1.0,
92    };
93
94    /// Construct an oriented interval without sorting its endpoints.
95    pub const fn new(start: Scalar, end: Scalar) -> Self {
96        Self { start, end }
97    }
98
99    /// Absolute parameter span.
100    pub fn length(self) -> Scalar {
101        (self.end - self.start).abs()
102    }
103}
104
105/// A plane represented by an origin and unit-normal candidate.
106///
107/// Adapters may construct dirty input. Algorithms validate normalization using
108/// the operation's tolerance instead of hiding a global epsilon here.
109#[derive(Debug, Clone, Copy, PartialEq)]
110#[cfg_attr(
111    feature = "serde",
112    derive(serde::Serialize, serde::Deserialize),
113    serde(deny_unknown_fields)
114)]
115pub struct Plane3 {
116    /// Point on the plane.
117    pub origin: Point3,
118    /// Expected outward normal.
119    pub normal: Vec3,
120}
121
122/// A parametric three-dimensional ray.
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub struct Ray3 {
125    /// Ray start.
126    pub origin: Point3,
127    /// Ray direction. It need not be normalized at the storage boundary.
128    pub direction: Vec3,
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    /// The reason `Mat4` stopped being an alias for [`Transform3`].
136    ///
137    /// A perspective projection makes `w` vary per point, so recovering a
138    /// cartesian coordinate requires dividing by it. An affine transform
139    /// cannot express that at all, which is why the old alias could not close
140    /// this gap no matter how it was called.
141    #[test]
142    fn projective_matrix_divides_by_w_and_affine_cannot() {
143        // Looking down -Z with a 90 degree vertical field of view, so a point
144        // at depth d has its height scaled by 1/d.
145        let projection = Mat4::perspective_rh(std::f64::consts::FRAC_PI_2, 1.0, 1.0, 100.0);
146
147        let near = projection.project_point3(Point3::new(0.0, 1.0, -2.0));
148        let far = projection.project_point3(Point3::new(0.0, 1.0, -4.0));
149
150        // Same world height, twice the depth: the projected height halves.
151        // That ratio is the homogeneous divide doing its job.
152        assert!(
153            (near.y / far.y - 2.0).abs() < 1e-12,
154            "near {near:?} far {far:?}"
155        );
156
157        // The fourth row is what carries it. An affine transform's implicit
158        // bottom row is [0, 0, 0, 1], so w is constant and no such ratio can
159        // arise: identical input keeps its height at both depths.
160        let affine = Transform3::IDENTITY;
161        let near_affine = affine.transform_point3(Point3::new(0.0, 1.0, -2.0));
162        let far_affine = affine.transform_point3(Point3::new(0.0, 1.0, -4.0));
163        assert_eq!(near_affine.y, far_affine.y);
164    }
165
166    /// `Mat4` and `Transform3` are now genuinely different types.
167    ///
168    /// Asserted so a future "simplification" back to an alias fails here
169    /// rather than silently removing projection support again.
170    #[test]
171    fn a_projective_matrix_round_trips_through_its_affine_subset() {
172        let affine = Transform3::from_translation(Vec3::new(1.0, 2.0, 3.0));
173        let promoted = Mat4::from(affine);
174        let point = Point3::new(0.5, -0.5, 2.0);
175        // Promoting an affine transform must not change what it does.
176        assert_eq!(
177            promoted.project_point3(point),
178            affine.transform_point3(point)
179        );
180    }
181}