Skip to main content

pcloud_cat_viewer/
algebra.rs

1//! GPU-oriented linear algebra types (f32).
2//!
3//! [`Vec3f`], [`Vec4f`], and [`Mat4`] use named fields internally
4//! (no array indexing).  [`Mat4::as_array`] produces the column-major
5//! `[f32; 16]` needed at the WebGL boundary.
6
7// ── Vec3f ────────────────────────────────────────────────────────
8
9/// A 3-component f32 vector with named fields.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Vec3f {
12    vx: f32,
13    vy: f32,
14    vz: f32,
15}
16
17impl Vec3f {
18    /// Construct from three components.
19    #[must_use]
20    pub fn new(x: f32, y: f32, z: f32) -> Self {
21        Self { vx: x, vy: y, vz: z }
22    }
23
24    /// The zero vector.
25    #[must_use]
26    pub fn zero() -> Self {
27        Self { vx: 0.0, vy: 0.0, vz: 0.0 }
28    }
29
30    /// The positive-Y unit vector.
31    #[must_use]
32    pub fn unit_y() -> Self {
33        Self { vx: 0.0, vy: 1.0, vz: 0.0 }
34    }
35
36    /// X component.
37    #[must_use]
38    pub fn x(self) -> f32 {
39        self.vx
40    }
41
42    /// Y component.
43    #[must_use]
44    pub fn y(self) -> f32 {
45        self.vy
46    }
47
48    /// Z component.
49    #[must_use]
50    pub fn z(self) -> f32 {
51        self.vz
52    }
53
54    /// Dot product.
55    #[must_use]
56    pub fn dot(self, rhs: Self) -> f32 {
57        self.vx * rhs.vx + self.vy * rhs.vy + self.vz * rhs.vz
58    }
59
60    /// Cross product.
61    #[must_use]
62    pub fn cross(self, rhs: Self) -> Self {
63        Self {
64            vx: self.vy * rhs.vz - self.vz * rhs.vy,
65            vy: self.vz * rhs.vx - self.vx * rhs.vz,
66            vz: self.vx * rhs.vy - self.vy * rhs.vx,
67        }
68    }
69
70    /// Squared length.
71    #[must_use]
72    pub fn length_squared(self) -> f32 {
73        self.dot(self)
74    }
75
76    /// Euclidean length.
77    #[must_use]
78    pub fn length(self) -> f32 {
79        self.length_squared().sqrt()
80    }
81
82    /// Unit vector, falling back to +Z if near zero.
83    #[must_use]
84    pub fn normalized(self) -> Self {
85        let len = self.length();
86        if len < 1e-10 {
87            Self { vx: 0.0, vy: 0.0, vz: 1.0 }
88        } else {
89            self.scale(1.0 / len)
90        }
91    }
92
93    /// Scalar multiplication.
94    #[must_use]
95    pub fn scale(self, s: f32) -> Self {
96        Self {
97            vx: self.vx * s,
98            vy: self.vy * s,
99            vz: self.vz * s,
100        }
101    }
102}
103
104impl std::ops::Add for Vec3f {
105    type Output = Self;
106    fn add(self, rhs: Self) -> Self {
107        Self {
108            vx: self.vx + rhs.vx,
109            vy: self.vy + rhs.vy,
110            vz: self.vz + rhs.vz,
111        }
112    }
113}
114
115impl std::ops::Sub for Vec3f {
116    type Output = Self;
117    fn sub(self, rhs: Self) -> Self {
118        Self {
119            vx: self.vx - rhs.vx,
120            vy: self.vy - rhs.vy,
121            vz: self.vz - rhs.vz,
122        }
123    }
124}
125
126impl std::ops::Neg for Vec3f {
127    type Output = Self;
128    fn neg(self) -> Self {
129        Self {
130            vx: -self.vx,
131            vy: -self.vy,
132            vz: -self.vz,
133        }
134    }
135}
136
137// ── Vec4f ────────────────────────────────────────────────────────
138
139/// A 4-component f32 vector (one column of a [`Mat4`]).
140#[derive(Debug, Clone, Copy, PartialEq)]
141pub struct Vec4f {
142    vx: f32,
143    vy: f32,
144    vz: f32,
145    vw: f32,
146}
147
148impl Vec4f {
149    /// Construct from four components.
150    #[must_use]
151    pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
152        Self { vx: x, vy: y, vz: z, vw: w }
153    }
154
155    /// X component.
156    #[must_use]
157    pub fn x(self) -> f32 {
158        self.vx
159    }
160
161    /// Y component.
162    #[must_use]
163    pub fn y(self) -> f32 {
164        self.vy
165    }
166
167    /// Z component.
168    #[must_use]
169    pub fn z(self) -> f32 {
170        self.vz
171    }
172
173    /// W component.
174    #[must_use]
175    pub fn w(self) -> f32 {
176        self.vw
177    }
178
179    /// Dot product.
180    #[must_use]
181    pub fn dot(self, rhs: Self) -> f32 {
182        self.vx * rhs.vx + self.vy * rhs.vy + self.vz * rhs.vz + self.vw * rhs.vw
183    }
184}
185
186// ── Mat4 ─────────────────────────────────────────────────────────
187
188/// A 4x4 column-major matrix stored as four [`Vec4f`] columns.
189#[derive(Debug, Clone, Copy, PartialEq)]
190pub struct Mat4 {
191    c0: Vec4f,
192    c1: Vec4f,
193    c2: Vec4f,
194    c3: Vec4f,
195}
196
197impl Mat4 {
198    /// Construct from four column vectors.
199    #[must_use]
200    pub fn from_columns(c0: Vec4f, c1: Vec4f, c2: Vec4f, c3: Vec4f) -> Self {
201        Self { c0, c1, c2, c3 }
202    }
203
204    /// Column 0.
205    #[must_use]
206    pub fn col0(self) -> Vec4f {
207        self.c0
208    }
209
210    /// Column 1.
211    #[must_use]
212    pub fn col1(self) -> Vec4f {
213        self.c1
214    }
215
216    /// Column 2.
217    #[must_use]
218    pub fn col2(self) -> Vec4f {
219        self.c2
220    }
221
222    /// Column 3.
223    #[must_use]
224    pub fn col3(self) -> Vec4f {
225        self.c3
226    }
227
228    /// Row `r` as a [`Vec4f`] (extracts one component from each column).
229    #[must_use]
230    fn row(self, pick: fn(Vec4f) -> f32) -> Vec4f {
231        Vec4f::new(
232            pick(self.c0),
233            pick(self.c1),
234            pick(self.c2),
235            pick(self.c3),
236        )
237    }
238
239    /// Matrix-matrix product.
240    #[must_use]
241    pub fn mul_mat(self, rhs: Self) -> Self {
242        let row_x = self.row(Vec4f::x);
243        let row_y = self.row(Vec4f::y);
244        let row_z = self.row(Vec4f::z);
245        let row_w = self.row(Vec4f::w);
246
247        let mul_col = |col: Vec4f| -> Vec4f {
248            Vec4f::new(
249                row_x.dot(col),
250                row_y.dot(col),
251                row_z.dot(col),
252                row_w.dot(col),
253            )
254        };
255
256        Self {
257            c0: mul_col(rhs.c0),
258            c1: mul_col(rhs.c1),
259            c2: mul_col(rhs.c2),
260            c3: mul_col(rhs.c3),
261        }
262    }
263
264    /// Flatten to a column-major `[f32; 16]` for the WebGL boundary.
265    #[must_use]
266    pub fn as_array(self) -> [f32; 16] {
267        [
268            self.c0.vx, self.c0.vy, self.c0.vz, self.c0.vw,
269            self.c1.vx, self.c1.vy, self.c1.vz, self.c1.vw,
270            self.c2.vx, self.c2.vy, self.c2.vz, self.c2.vw,
271            self.c3.vx, self.c3.vy, self.c3.vz, self.c3.vw,
272        ]
273    }
274
275    /// Perspective projection matrix.
276    #[must_use]
277    pub fn perspective(fov_y: f32, aspect: f32, near: f32, far: f32) -> Self {
278        let f = 1.0 / (fov_y / 2.0).tan();
279        let nf = 1.0 / (near - far);
280        Self {
281            c0: Vec4f::new(f / aspect, 0.0, 0.0, 0.0),
282            c1: Vec4f::new(0.0, f, 0.0, 0.0),
283            c2: Vec4f::new(0.0, 0.0, (far + near) * nf, -1.0),
284            c3: Vec4f::new(0.0, 0.0, 2.0 * far * near * nf, 0.0),
285        }
286    }
287
288    /// Look-at view matrix.
289    #[must_use]
290    pub fn look_at(eye: Vec3f, target: Vec3f, up: Vec3f) -> Self {
291        let fwd = (target - eye).normalized();
292        let right = fwd.cross(up).normalized();
293        let cam_up = right.cross(fwd);
294
295        Self {
296            c0: Vec4f::new(right.x(), cam_up.x(), -fwd.x(), 0.0),
297            c1: Vec4f::new(right.y(), cam_up.y(), -fwd.y(), 0.0),
298            c2: Vec4f::new(right.z(), cam_up.z(), -fwd.z(), 0.0),
299            c3: Vec4f::new(
300                -right.dot(eye),
301                -cam_up.dot(eye),
302                fwd.dot(eye),
303                1.0,
304            ),
305        }
306    }
307}
308
309impl std::ops::Mul for Mat4 {
310    type Output = Self;
311    fn mul(self, rhs: Self) -> Self {
312        self.mul_mat(rhs)
313    }
314}