1pub const MIN_KEPT_POINTS: usize = 2;
7
8const SAMPLES_PER_SEGMENT: usize = 32;
13
14const MIN_CHORD: f32 = 1e-4;
17
18pub fn fit_catmull_rom_indices(points: &[[f32; 3]], epsilon_mm: f32) -> Vec<usize> {
28 let n = points.len();
29 if n <= 2 || epsilon_mm <= 0.0 {
30 return (0..n).collect();
31 }
32 let eps2 = epsilon_mm * epsilon_mm;
33
34 let mut kept: Vec<usize> = Vec::with_capacity(16);
35 kept.push(0);
36 kept.push(n - 1);
37
38 let mut tess_buf: Vec<[f32; 3]> = Vec::with_capacity(SAMPLES_PER_SEGMENT + 1);
39 let mut additions: Vec<usize> = Vec::with_capacity(16);
40
41 loop {
42 additions.clear();
43
44 for seg in 0..kept.len() - 1 {
45 let start = kept[seg];
46 let end = kept[seg + 1];
47 if end <= start + 1 {
48 continue; }
50
51 tessellate_segment(points, &kept, seg, &mut tess_buf);
52
53 let mut worst_idx = start + 1;
54 let mut worst_d2 = 0.0_f32;
55 for (input_idx, &point) in points.iter().enumerate().take(end).skip(start + 1) {
56 let d2 = point_to_polyline_dist2(point, &tess_buf);
57 if d2 > worst_d2 {
58 worst_d2 = d2;
59 worst_idx = input_idx;
60 }
61 }
62
63 if worst_d2 > eps2 {
64 additions.push(worst_idx);
65 }
66 }
67
68 if additions.is_empty() {
69 break;
70 }
71
72 merge_sorted_into(&mut kept, &additions);
75 }
76
77 kept
78}
79
80pub fn simplify_streamline(points: &[[f32; 3]], epsilon_mm: f32) -> Vec<[f32; 3]> {
82 let indices = fit_catmull_rom_indices(points, epsilon_mm);
83 indices.iter().map(|&i| points[i]).collect()
84}
85
86pub fn sample_catmull_rom(cps: &[[f32; 3]], samples_per_segment: usize) -> Vec<[f32; 3]> {
91 let mut out = Vec::new();
92 sample_catmull_rom_into(cps, samples_per_segment, &mut out);
93 out
94}
95
96pub fn sample_catmull_rom_into(
100 cps: &[[f32; 3]],
101 samples_per_segment: usize,
102 out: &mut Vec<[f32; 3]>,
103) {
104 out.clear();
105 let n = cps.len();
106 if n == 0 {
107 return;
108 }
109 if n < 2 || samples_per_segment == 0 {
110 out.extend_from_slice(cps);
111 return;
112 }
113 out.reserve((n - 1) * samples_per_segment + 1);
114 out.push(cps[0]);
115 let inv = 1.0 / samples_per_segment as f32;
116 for seg in 0..n - 1 {
117 let cps4 = segment_control_points_from_dense(cps, seg);
118 let knots = centripetal_knots(cps4);
119 for k in 1..=samples_per_segment {
120 let u = k as f32 * inv;
121 out.push(catmull_rom_segment_with_knots(cps4, &knots, u));
122 }
123 }
124}
125
126fn tessellate_segment(points: &[[f32; 3]], kept: &[usize], seg: usize, out: &mut Vec<[f32; 3]>) {
129 out.clear();
130 let cps = segment_control_points(points, kept, seg);
131 let knots = centripetal_knots(cps);
132 let inv = 1.0 / SAMPLES_PER_SEGMENT as f32;
133 for k in 0..=SAMPLES_PER_SEGMENT {
134 let u = k as f32 * inv;
135 out.push(catmull_rom_segment_with_knots(cps, &knots, u));
136 }
137}
138
139fn segment_control_points(points: &[[f32; 3]], kept: &[usize], seg: usize) -> [[f32; 3]; 4] {
143 let n = kept.len();
144 let p1 = points[kept[seg]];
145 let p2 = points[kept[seg + 1]];
146 let p0 = if seg == 0 {
147 reflect(p1, p2)
148 } else {
149 points[kept[seg - 1]]
150 };
151 let p3 = if seg + 2 == n {
152 reflect(p2, p1)
153 } else {
154 points[kept[seg + 2]]
155 };
156 [p0, p1, p2, p3]
157}
158
159fn segment_control_points_from_dense(cps: &[[f32; 3]], seg: usize) -> [[f32; 3]; 4] {
162 let n = cps.len();
163 let p1 = cps[seg];
164 let p2 = cps[seg + 1];
165 let p0 = if seg == 0 {
166 reflect(p1, p2)
167 } else {
168 cps[seg - 1]
169 };
170 let p3 = if seg + 2 == n {
171 reflect(p2, p1)
172 } else {
173 cps[seg + 2]
174 };
175 [p0, p1, p2, p3]
176}
177
178#[inline]
180fn reflect(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
181 [2.0 * a[0] - b[0], 2.0 * a[1] - b[1], 2.0 * a[2] - b[2]]
182}
183
184#[derive(Clone, Copy)]
200struct CentripetalKnots {
201 t0: f32,
202 t1: f32,
203 t2: f32,
204 t3: f32,
205}
206
207#[inline]
208fn centripetal_knots(cps: [[f32; 3]; 4]) -> CentripetalKnots {
209 let [p0, p1, p2, p3] = cps;
210 let t0 = 0.0;
211 let t1 = t0 + chord_centripetal(p0, p1);
212 let t2 = t1 + chord_centripetal(p1, p2);
213 let t3 = t2 + chord_centripetal(p2, p3);
214 CentripetalKnots { t0, t1, t2, t3 }
215}
216
217#[inline]
218fn chord_centripetal(a: [f32; 3], b: [f32; 3]) -> f32 {
219 let dx = b[0] - a[0];
221 let dy = b[1] - a[1];
222 let dz = b[2] - a[2];
223 let d2 = dx * dx + dy * dy + dz * dz;
224 d2.sqrt().sqrt().max(MIN_CHORD)
225}
226
227#[inline]
228fn catmull_rom_segment_with_knots(
229 cps: [[f32; 3]; 4],
230 knots: &CentripetalKnots,
231 u: f32,
232) -> [f32; 3] {
233 let [p0, p1, p2, p3] = cps;
234 let CentripetalKnots { t0, t1, t2, t3 } = *knots;
235 let t = t1 + u * (t2 - t1);
238
239 let a1 = lerp3(p0, p1, (t - t0) / (t1 - t0));
241 let a2 = lerp3(p1, p2, (t - t1) / (t2 - t1));
242 let a3 = lerp3(p2, p3, (t - t2) / (t3 - t2));
243
244 let b1 = lerp3(a1, a2, (t - t0) / (t2 - t0));
246 let b2 = lerp3(a2, a3, (t - t1) / (t3 - t1));
247
248 lerp3(b1, b2, (t - t1) / (t2 - t1))
250}
251
252#[inline]
253fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
254 [
255 a[0] + t * (b[0] - a[0]),
256 a[1] + t * (b[1] - a[1]),
257 a[2] + t * (b[2] - a[2]),
258 ]
259}
260
261#[inline]
264fn point_to_polyline_dist2(p: [f32; 3], polyline: &[[f32; 3]]) -> f32 {
265 let mut min_d2 = f32::INFINITY;
266 for window in polyline.windows(2) {
267 let d2 = point_to_segment_dist2(p, window[0], window[1]);
268 if d2 < min_d2 {
269 min_d2 = d2;
270 }
271 }
272 min_d2
273}
274
275#[inline]
276fn point_to_segment_dist2(p: [f32; 3], a: [f32; 3], b: [f32; 3]) -> f32 {
277 let abx = b[0] - a[0];
278 let aby = b[1] - a[1];
279 let abz = b[2] - a[2];
280 let denom = abx * abx + aby * aby + abz * abz;
281 if denom < f32::EPSILON {
282 let dx = p[0] - a[0];
283 let dy = p[1] - a[1];
284 let dz = p[2] - a[2];
285 return dx * dx + dy * dy + dz * dz;
286 }
287 let apx = p[0] - a[0];
288 let apy = p[1] - a[1];
289 let apz = p[2] - a[2];
290 let t = ((apx * abx + apy * aby + apz * abz) / denom).clamp(0.0, 1.0);
291 let dx = apx - abx * t;
292 let dy = apy - aby * t;
293 let dz = apz - abz * t;
294 dx * dx + dy * dy + dz * dz
295}
296
297fn merge_sorted_into(kept: &mut Vec<usize>, additions: &[usize]) {
300 if additions.is_empty() {
301 return;
302 }
303 let mut merged = Vec::with_capacity(kept.len() + additions.len());
304 let (mut i, mut j) = (0, 0);
305 while i < kept.len() && j < additions.len() {
306 let a = kept[i];
307 let b = additions[j];
308 if a < b {
309 merged.push(a);
310 i += 1;
311 } else if a > b {
312 merged.push(b);
313 j += 1;
314 } else {
315 merged.push(a);
316 i += 1;
317 j += 1;
318 }
319 }
320 merged.extend_from_slice(&kept[i..]);
321 merged.extend_from_slice(&additions[j..]);
322 *kept = merged;
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 fn cubic_curve(steps: usize) -> Vec<[f32; 3]> {
330 (0..=steps)
331 .map(|i| {
332 let t = i as f32 / steps as f32;
333 let x = -25.0 + 50.0 * t;
334 let y = 10.0 * (t * std::f32::consts::PI).sin();
335 let z = 5.0 * t;
336 [x, y, z]
337 })
338 .collect()
339 }
340
341 #[test]
342 fn endpoints_always_retained() {
343 let pts = cubic_curve(50);
344 let kept = fit_catmull_rom_indices(&pts, 1.0);
345 assert_eq!(kept.first(), Some(&0));
346 assert_eq!(kept.last(), Some(&50));
347 }
348
349 #[test]
350 fn straight_line_collapses_to_endpoints() {
351 let pts: Vec<[f32; 3]> = (0..20).map(|i| [i as f32, 0.0, 0.0]).collect();
352 let kept = fit_catmull_rom_indices(&pts, 0.01);
353 assert_eq!(kept, vec![0, 19]);
354 }
355
356 #[test]
357 fn fewer_than_three_points_pass_through() {
358 let pts = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
359 assert_eq!(fit_catmull_rom_indices(&pts, 1.0), vec![0, 1]);
360 let pts = vec![[0.0, 0.0, 0.0]];
361 assert_eq!(fit_catmull_rom_indices(&pts, 1.0), vec![0]);
362 assert_eq!(fit_catmull_rom_indices(&[], 1.0), Vec::<usize>::new());
363 }
364
365 #[test]
366 fn fit_within_tolerance_for_smooth_curve() {
367 let pts = cubic_curve(200);
368 let epsilon = 0.5;
369 let kept = fit_catmull_rom_indices(&pts, epsilon);
370 let kept_pts: Vec<[f32; 3]> = kept.iter().map(|&i| pts[i]).collect();
373 let dense = sample_catmull_rom(&kept_pts, 32);
374 let max_d = pts
375 .iter()
376 .map(|&p| point_to_polyline_dist2(p, &dense).sqrt())
377 .fold(0.0_f32, f32::max);
378 assert!(
379 max_d <= epsilon * 1.05,
380 "max error {max_d} exceeded tolerance {epsilon}"
381 );
382 assert!(
384 kept.len() < pts.len() / 4,
385 "expected sparse output (got {} from {})",
386 kept.len(),
387 pts.len()
388 );
389 }
390
391 #[test]
392 fn smaller_epsilon_keeps_more_points() {
393 let pts = cubic_curve(200);
394 let coarse = fit_catmull_rom_indices(&pts, 2.0).len();
395 let fine = fit_catmull_rom_indices(&pts, 0.1).len();
396 assert!(fine > coarse, "fine {fine} should exceed coarse {coarse}");
397 }
398
399 #[test]
400 fn sampler_passes_through_control_points() {
401 let cps = vec![
402 [0.0, 0.0, 0.0],
403 [1.0, 2.0, 0.0],
404 [3.0, 1.0, 0.0],
405 [4.0, 3.0, 0.0],
406 ];
407 let dense = sample_catmull_rom(&cps, 10);
408 assert_eq!(dense.first(), Some(&cps[0]));
409 assert_eq!(dense.last(), Some(&cps[3]));
410 for (i, cp) in cps.iter().enumerate() {
411 let idx = i * 10;
412 let d2 = (dense[idx][0] - cp[0]).powi(2)
413 + (dense[idx][1] - cp[1]).powi(2)
414 + (dense[idx][2] - cp[2]).powi(2);
415 assert!(d2 < 1e-8, "CP {i} not hit at sample {idx}");
416 }
417 }
418
419 #[test]
425 fn no_overshoot_with_clustered_cps() {
426 let cps = vec![
429 [0.0, 0.0, 0.0],
430 [10.0, 0.0, 0.0],
431 [10.5, 0.0, 0.0],
432 [20.0, 0.0, 0.0],
433 ];
434 let dense = sample_catmull_rom(&cps, 64);
435 let max_y = dense.iter().map(|p| p[1].abs()).fold(0.0_f32, f32::max);
438 let max_z = dense.iter().map(|p| p[2].abs()).fold(0.0_f32, f32::max);
439 assert!(max_y < 1e-3, "centripetal CR overshot in y: {max_y}");
440 assert!(max_z < 1e-3, "centripetal CR overshot in z: {max_z}");
441 }
442
443 #[test]
444 fn merge_sorted_dedup_works() {
445 let mut kept = vec![0, 5, 10];
446 merge_sorted_into(&mut kept, &[3, 7]);
447 assert_eq!(kept, vec![0, 3, 5, 7, 10]);
448
449 let mut kept = vec![0, 10];
450 merge_sorted_into(&mut kept, &[]);
451 assert_eq!(kept, vec![0, 10]);
452
453 let mut kept = vec![0, 5];
454 merge_sorted_into(&mut kept, &[5, 7]); assert_eq!(kept, vec![0, 5, 7]);
456 }
457}