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