Skip to main content

brep_cam/model/
geometry.rs

1//! Plain geometric value types shared by every CAM algorithm.
2//!
3//! Points and vectors serialize as `[x, y, z]` so toolpaths and meshes stay
4//! compact in documents and worker payloads. Nothing here depends on the BREP
5//! kernel or on a renderer math library.
6
7use serde::{Deserialize, Serialize};
8
9/// A point or vector in 3D. Serialized as a three-element array.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(from = "[f64; 3]", into = "[f64; 3]")]
12pub struct Point3 {
13    pub x: f64,
14    pub y: f64,
15    pub z: f64,
16}
17
18/// A direction or displacement; the same representation as [`Point3`].
19pub type Vector3 = Point3;
20
21impl From<[f64; 3]> for Point3 {
22    fn from(v: [f64; 3]) -> Self {
23        Self { x: v[0], y: v[1], z: v[2] }
24    }
25}
26
27impl From<Point3> for [f64; 3] {
28    fn from(p: Point3) -> Self {
29        [p.x, p.y, p.z]
30    }
31}
32
33impl Point3 {
34    pub const ZERO: Point3 = Point3 { x: 0.0, y: 0.0, z: 0.0 };
35    pub const X: Point3 = Point3 { x: 1.0, y: 0.0, z: 0.0 };
36    pub const Y: Point3 = Point3 { x: 0.0, y: 1.0, z: 0.0 };
37    pub const Z: Point3 = Point3 { x: 0.0, y: 0.0, z: 1.0 };
38
39    pub const fn new(x: f64, y: f64, z: f64) -> Self {
40        Self { x, y, z }
41    }
42
43    pub fn is_finite(&self) -> bool {
44        self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
45    }
46
47    pub fn add(self, o: Point3) -> Point3 {
48        Point3::new(self.x + o.x, self.y + o.y, self.z + o.z)
49    }
50
51    pub fn sub(self, o: Point3) -> Point3 {
52        Point3::new(self.x - o.x, self.y - o.y, self.z - o.z)
53    }
54
55    pub fn scale(self, s: f64) -> Point3 {
56        Point3::new(self.x * s, self.y * s, self.z * s)
57    }
58
59    pub fn dot(self, o: Point3) -> f64 {
60        self.x * o.x + self.y * o.y + self.z * o.z
61    }
62
63    pub fn cross(self, o: Point3) -> Point3 {
64        Point3::new(
65            self.y * o.z - self.z * o.y,
66            self.z * o.x - self.x * o.z,
67            self.x * o.y - self.y * o.x,
68        )
69    }
70
71    pub fn length_squared(self) -> f64 {
72        self.dot(self)
73    }
74
75    pub fn length(self) -> f64 {
76        self.length_squared().sqrt()
77    }
78
79    /// Unit vector in this direction; `None` for a zero or non-finite vector.
80    pub fn normalized(self) -> Option<Point3> {
81        let len = self.length();
82        if !(len > 0.0) || !len.is_finite() {
83            return None;
84        }
85        Some(self.scale(1.0 / len))
86    }
87
88    pub fn distance(self, o: Point3) -> f64 {
89        self.sub(o).length()
90    }
91
92    /// Distance in the XY plane only (the cutter-axis projection).
93    pub fn distance_xy(self, o: Point3) -> f64 {
94        let dx = self.x - o.x;
95        let dy = self.y - o.y;
96        (dx * dx + dy * dy).sqrt()
97    }
98
99    pub fn lerp(self, o: Point3, t: f64) -> Point3 {
100        self.add(o.sub(self).scale(t))
101    }
102
103    pub fn min(self, o: Point3) -> Point3 {
104        Point3::new(self.x.min(o.x), self.y.min(o.y), self.z.min(o.z))
105    }
106
107    pub fn max(self, o: Point3) -> Point3 {
108        Point3::new(self.x.max(o.x), self.y.max(o.y), self.z.max(o.z))
109    }
110
111    /// Component by axis index (0 = x, 1 = y, 2 = z).
112    pub fn axis(self, axis: usize) -> f64 {
113        match axis {
114            0 => self.x,
115            1 => self.y,
116            _ => self.z,
117        }
118    }
119}
120
121/// A point in 2D (sketch/region space). Serialized as `[x, y]`.
122#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
123#[serde(from = "[f64; 2]", into = "[f64; 2]")]
124pub struct Point2 {
125    pub x: f64,
126    pub y: f64,
127}
128
129impl From<[f64; 2]> for Point2 {
130    fn from(v: [f64; 2]) -> Self {
131        Self { x: v[0], y: v[1] }
132    }
133}
134
135impl From<Point2> for [f64; 2] {
136    fn from(p: Point2) -> Self {
137        [p.x, p.y]
138    }
139}
140
141impl Point2 {
142    pub const fn new(x: f64, y: f64) -> Self {
143        Self { x, y }
144    }
145
146    pub fn is_finite(&self) -> bool {
147        self.x.is_finite() && self.y.is_finite()
148    }
149}
150
151/// Axis-aligned bounding box. An empty box has `min > max` on every axis.
152#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct Bounds3 {
155    pub min: Point3,
156    pub max: Point3,
157}
158
159impl Default for Bounds3 {
160    fn default() -> Self {
161        Self::empty()
162    }
163}
164
165impl Bounds3 {
166    /// The empty box: `union` with any point yields that point.
167    pub fn empty() -> Self {
168        Self {
169            min: Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
170            max: Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
171        }
172    }
173
174    pub fn new(min: Point3, max: Point3) -> Self {
175        Self { min, max }
176    }
177
178    pub fn from_point(p: Point3) -> Self {
179        Self { min: p, max: p }
180    }
181
182    pub fn from_points<I: IntoIterator<Item = Point3>>(points: I) -> Self {
183        let mut b = Self::empty();
184        for p in points {
185            b.include(p);
186        }
187        b
188    }
189
190    pub fn is_empty(&self) -> bool {
191        self.min.x > self.max.x || self.min.y > self.max.y || self.min.z > self.max.z
192    }
193
194    pub fn is_finite(&self) -> bool {
195        self.min.is_finite() && self.max.is_finite()
196    }
197
198    pub fn include(&mut self, p: Point3) {
199        self.min = self.min.min(p);
200        self.max = self.max.max(p);
201    }
202
203    pub fn union(&self, o: &Bounds3) -> Bounds3 {
204        Bounds3 { min: self.min.min(o.min), max: self.max.max(o.max) }
205    }
206
207    /// Grow every side by `d` (shrink for negative `d`).
208    pub fn expanded(&self, d: f64) -> Bounds3 {
209        let e = Point3::new(d, d, d);
210        Bounds3 { min: self.min.sub(e), max: self.max.add(e) }
211    }
212
213    /// Closed-interval overlap on all three axes (touching counts).
214    pub fn intersects(&self, o: &Bounds3) -> bool {
215        self.min.x <= o.max.x
216            && o.min.x <= self.max.x
217            && self.min.y <= o.max.y
218            && o.min.y <= self.max.y
219            && self.min.z <= o.max.z
220            && o.min.z <= self.max.z
221    }
222
223    pub fn contains_point(&self, p: Point3) -> bool {
224        p.x >= self.min.x
225            && p.x <= self.max.x
226            && p.y >= self.min.y
227            && p.y <= self.max.y
228            && p.z >= self.min.z
229            && p.z <= self.max.z
230    }
231
232    pub fn center(&self) -> Point3 {
233        self.min.add(self.max).scale(0.5)
234    }
235
236    pub fn size(&self) -> Point3 {
237        self.max.sub(self.min)
238    }
239
240    /// Length of the box diagonal; `0.0` for an empty box.
241    pub fn diagonal(&self) -> f64 {
242        if self.is_empty() {
243            0.0
244        } else {
245            self.size().length()
246        }
247    }
248
249    /// Index of the axis with the largest extent (0, 1 or 2).
250    pub fn longest_axis(&self) -> usize {
251        let s = self.size();
252        if s.x >= s.y && s.x >= s.z {
253            0
254        } else if s.y >= s.z {
255            1
256        } else {
257            2
258        }
259    }
260}
261
262/// A proper rigid transform (rotation + translation, no scale, no mirror).
263///
264/// `rotation` is row-major: `apply_vector(v) = rotation · v`. Build one with
265/// [`RigidTransform::new`] (validated) or the named constructors. The setup's
266/// model-to-machine transform is the ONLY rigid transform CAM applies to
267/// geometry; display conversion back uses [`RigidTransform::inverse`].
268#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct RigidTransform {
271    pub rotation: [[f64; 3]; 3],
272    pub translation: Point3,
273}
274
275impl Default for RigidTransform {
276    fn default() -> Self {
277        Self::identity()
278    }
279}
280
281/// Why a matrix was refused as a rigid transform.
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub enum TransformError {
284    NonFinite,
285    NotOrthonormal,
286    Mirrored,
287}
288
289impl std::fmt::Display for TransformError {
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        match self {
292            TransformError::NonFinite => write!(f, "rigid transform has a non-finite element"),
293            TransformError::NotOrthonormal => {
294                write!(f, "rigid transform rotation is not orthonormal")
295            }
296            TransformError::Mirrored => write!(f, "rigid transform mirrors (determinant is -1)"),
297        }
298    }
299}
300
301impl std::error::Error for TransformError {}
302
303impl RigidTransform {
304    /// Tolerance used by [`RigidTransform::validate`] on the orthonormality residual.
305    pub const ORTHONORMAL_EPS: f64 = 1e-9;
306
307    pub fn identity() -> Self {
308        Self {
309            rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
310            translation: Point3::ZERO,
311        }
312    }
313
314    /// Validated constructor: refuses non-finite, non-orthonormal or mirroring matrices.
315    pub fn new(rotation: [[f64; 3]; 3], translation: Point3) -> Result<Self, TransformError> {
316        let t = Self { rotation, translation };
317        t.validate()?;
318        Ok(t)
319    }
320
321    pub fn translation(t: Point3) -> Self {
322        Self { rotation: Self::identity().rotation, translation: t }
323    }
324
325    /// Rotation by `angle_rad` about the unit `axis` (right-hand rule), then translation `t`.
326    pub fn axis_angle(axis: Point3, angle_rad: f64, t: Point3) -> Result<Self, TransformError> {
327        let a = axis.normalized().ok_or(TransformError::NonFinite)?;
328        let (s, c) = angle_rad.sin_cos();
329        let k = 1.0 - c;
330        let rotation = [
331            [c + a.x * a.x * k, a.x * a.y * k - a.z * s, a.x * a.z * k + a.y * s],
332            [a.y * a.x * k + a.z * s, c + a.y * a.y * k, a.y * a.z * k - a.x * s],
333            [a.z * a.x * k - a.y * s, a.z * a.y * k + a.x * s, c + a.z * a.z * k],
334        ];
335        Self::new(rotation, t)
336    }
337
338    /// The transform that maps a frame given in model coordinates (origin,
339    /// X axis, Z axis) onto the machine frame: the frame origin goes to the
340    /// machine origin, the frame's X to +X and its Z to +Z. `x_axis` is
341    /// re-orthogonalized against `z_axis`.
342    pub fn from_frame(origin: Point3, x_axis: Point3, z_axis: Point3) -> Result<Self, TransformError> {
343        let z = z_axis.normalized().ok_or(TransformError::NonFinite)?;
344        let x0 = x_axis.sub(z.scale(x_axis.dot(z)));
345        let x = x0.normalized().ok_or(TransformError::NotOrthonormal)?;
346        let y = z.cross(x);
347        // Rows of the rotation are the frame axes: rotation · (p - origin)
348        // yields the coordinates of p in the frame.
349        let rotation = [[x.x, x.y, x.z], [y.x, y.y, y.z], [z.x, z.y, z.z]];
350        let rotated_origin = Self { rotation, translation: Point3::ZERO }.apply_vector(origin);
351        Self::new(rotation, rotated_origin.scale(-1.0))
352    }
353
354    pub fn validate(&self) -> Result<(), TransformError> {
355        let r = &self.rotation;
356        if !self.translation.is_finite() || r.iter().flatten().any(|v| !v.is_finite()) {
357            return Err(TransformError::NonFinite);
358        }
359        let row = |i: usize| Point3::new(r[i][0], r[i][1], r[i][2]);
360        let (a, b, c) = (row(0), row(1), row(2));
361        let residual = (a.dot(a) - 1.0).abs().max((b.dot(b) - 1.0).abs()).max((c.dot(c) - 1.0).abs())
362            .max(a.dot(b).abs()).max(a.dot(c).abs()).max(b.dot(c).abs());
363        if residual > Self::ORTHONORMAL_EPS {
364            return Err(TransformError::NotOrthonormal);
365        }
366        if a.cross(b).dot(c) < 0.0 {
367            return Err(TransformError::Mirrored);
368        }
369        Ok(())
370    }
371
372    pub fn is_identity(&self) -> bool {
373        *self == Self::identity()
374    }
375
376    pub fn apply_vector(&self, v: Point3) -> Point3 {
377        let r = &self.rotation;
378        Point3::new(
379            r[0][0] * v.x + r[0][1] * v.y + r[0][2] * v.z,
380            r[1][0] * v.x + r[1][1] * v.y + r[1][2] * v.z,
381            r[2][0] * v.x + r[2][1] * v.y + r[2][2] * v.z,
382        )
383    }
384
385    pub fn apply_point(&self, p: Point3) -> Point3 {
386        self.apply_vector(p).add(self.translation)
387    }
388
389    /// Transform a box: the box of the eight transformed corners.
390    pub fn apply_bounds(&self, b: &Bounds3) -> Bounds3 {
391        if b.is_empty() {
392            return Bounds3::empty();
393        }
394        let mut out = Bounds3::empty();
395        for i in 0..8 {
396            let p = Point3::new(
397                if i & 1 == 0 { b.min.x } else { b.max.x },
398                if i & 2 == 0 { b.min.y } else { b.max.y },
399                if i & 4 == 0 { b.min.z } else { b.max.z },
400            );
401            out.include(self.apply_point(p));
402        }
403        out
404    }
405
406    /// The inverse: rotation transposed, translation rotated back.
407    pub fn inverse(&self) -> Self {
408        let r = &self.rotation;
409        let rt = [[r[0][0], r[1][0], r[2][0]], [r[0][1], r[1][1], r[2][1]], [r[0][2], r[1][2], r[2][2]]];
410        let back = Self { rotation: rt, translation: Point3::ZERO };
411        let t = back.apply_vector(self.translation).scale(-1.0);
412        Self { rotation: rt, translation: t }
413    }
414
415    /// `self` followed by `next`: `(next ∘ self)(p) = next(self(p))`.
416    pub fn then(&self, next: &RigidTransform) -> Self {
417        let mut rotation = [[0.0; 3]; 3];
418        for (i, row) in rotation.iter_mut().enumerate() {
419            for (j, cell) in row.iter_mut().enumerate() {
420                *cell = (0..3).map(|k| next.rotation[i][k] * self.rotation[k][j]).sum();
421            }
422        }
423        Self { rotation, translation: next.apply_point(self.translation) }
424    }
425}