Skip to main content

brepkit_geometry/sampling/
uniform.rs

1//! Uniform parameter-space curve sampling.
2
3use brepkit_math::traits::ParametricCurve;
4use brepkit_math::vec::Point3;
5
6/// Sample `n` evenly-spaced points in parameter space over `[t_start, t_end]`.
7///
8/// - `n == 0` returns an empty `Vec`.
9/// - `n == 1` returns a single point at `t_start`.
10/// - `n >= 2` returns points including both endpoints.
11#[must_use]
12pub fn sample_uniform<C: ParametricCurve>(
13    curve: &C,
14    t_start: f64,
15    t_end: f64,
16    n: usize,
17) -> Vec<Point3> {
18    sample_uniform_with_params(curve, t_start, t_end, n)
19        .into_iter()
20        .map(|(_, p)| p)
21        .collect()
22}
23
24/// Sample `n` evenly-spaced `(t, Point3)` pairs over `[t_start, t_end]`.
25///
26/// - `n == 0` returns an empty `Vec`.
27/// - `n == 1` returns `vec![(t_start, curve(t_start))]`.
28/// - `n >= 2` returns pairs including both endpoints.
29#[must_use]
30pub fn sample_uniform_with_params<C: ParametricCurve>(
31    curve: &C,
32    t_start: f64,
33    t_end: f64,
34    n: usize,
35) -> Vec<(f64, Point3)> {
36    if n == 0 {
37        return Vec::new();
38    }
39    if n == 1 {
40        return vec![(t_start, curve.evaluate(t_start))];
41    }
42    let step = (t_end - t_start) / (n - 1) as f64;
43    (0..n)
44        .map(|i| {
45            let t = if i == n - 1 {
46                t_end // avoid floating-point overshoot on last point
47            } else {
48                t_start + i as f64 * step
49            };
50            (t, curve.evaluate(t))
51        })
52        .collect()
53}
54
55#[cfg(test)]
56mod tests {
57    #![allow(clippy::unwrap_used, clippy::expect_used)]
58
59    use super::*;
60    use brepkit_math::curves::Circle3D;
61    use brepkit_math::vec::{Point3, Vec3};
62    use std::f64::consts::TAU;
63
64    fn unit_circle() -> Circle3D {
65        Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap()
66    }
67
68    #[test]
69    fn zero_samples_returns_empty() {
70        let c = unit_circle();
71        let pts = sample_uniform(&c, 0.0, TAU, 0);
72        assert!(pts.is_empty());
73    }
74
75    #[test]
76    fn one_sample_returns_start() {
77        let c = unit_circle();
78        let pts = sample_uniform(&c, 0.0, TAU, 1);
79        assert_eq!(pts.len(), 1);
80        // t=0 must lie on the unit circle (radius == 1).
81        let r =
82            (pts[0].x() * pts[0].x() + pts[0].y() * pts[0].y() + pts[0].z() * pts[0].z()).sqrt();
83        assert!((r - 1.0).abs() < 1e-12, "point not on unit circle: r={r}");
84    }
85
86    #[test]
87    fn four_samples_on_unit_circle() {
88        let c = unit_circle();
89        let pairs = sample_uniform_with_params(&c, 0.0, TAU, 4);
90        assert_eq!(pairs.len(), 4);
91
92        // All points should lie on the unit circle.
93        for (_, p) in &pairs {
94            let r = (p.x() * p.x() + p.y() * p.y() + p.z() * p.z()).sqrt();
95            assert!((r - 1.0).abs() < 1e-12, "point not on unit circle: r={r}");
96        }
97
98        // First and last parameter values must be endpoints.
99        assert!((pairs[0].0 - 0.0).abs() < 1e-12);
100        assert!((pairs[3].0 - TAU).abs() < 1e-12);
101
102        // First point (t=0) and last point (t=TAU) must coincide (full circle).
103        let p0 = pairs[0].1;
104        let p3 = pairs[3].1;
105        let dist =
106            ((p0.x() - p3.x()).powi(2) + (p0.y() - p3.y()).powi(2) + (p0.z() - p3.z()).powi(2))
107                .sqrt();
108        assert!(
109            dist < 1e-12,
110            "endpoints should coincide on full circle: dist={dist}"
111        );
112    }
113
114    #[test]
115    fn params_cover_full_range() {
116        let c = unit_circle();
117        let pairs = sample_uniform_with_params(&c, 0.0, TAU, 5);
118        assert_eq!(pairs.len(), 5);
119        assert!((pairs[0].0 - 0.0).abs() < 1e-12);
120        assert!((pairs[4].0 - TAU).abs() < 1e-12);
121    }
122}