brepkit_geometry/sampling/
uniform.rs1use brepkit_math::traits::ParametricCurve;
4use brepkit_math::vec::Point3;
5
6#[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#[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 } 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 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 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 assert!((pairs[0].0 - 0.0).abs() < 1e-12);
100 assert!((pairs[3].0 - TAU).abs() < 1e-12);
101
102 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}