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}