Skip to main content

geometry_algorithm/
chaikin_smoothing.rs

1//! Chaikin corner-cutting smoothing.
2//!
3//! Each iteration replaces an edge with points one quarter and three quarters
4//! along it. Open lines retain their endpoints; rings remain rings and preserve
5//! their declared closure and orientation.
6
7use alloc::vec::Vec;
8
9use geometry_cs::{CartesianFamily, CoordinateSystem};
10use geometry_model::{Linestring, Polygon, Ring};
11use geometry_tag::SameAs;
12use geometry_trait::{Point, PointMut};
13
14/// Apply `iterations` rounds of Chaikin corner cutting.
15#[inline]
16#[must_use]
17pub fn chaikin_smoothing<G>(geometry: &G, iterations: usize) -> G::Output
18where
19    G: ChaikinSmoothing,
20{
21    geometry.chaikin_smoothing(iterations)
22}
23
24/// Per-model Chaikin dispatch.
25#[doc(hidden)]
26pub trait ChaikinSmoothing {
27    /// Smoothed geometry type.
28    type Output;
29
30    /// Apply the requested number of iterations.
31    fn chaikin_smoothing(&self, iterations: usize) -> Self::Output;
32}
33
34impl<P> ChaikinSmoothing for Linestring<P>
35where
36    P: Point<Scalar = f64> + PointMut + Default + Copy,
37    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
38{
39    type Output = Linestring<P>;
40
41    fn chaikin_smoothing(&self, iterations: usize) -> Self::Output {
42        let mut points = self.0.clone();
43        for _ in 0..iterations {
44            points = smooth_open(&points);
45        }
46        Linestring::from_vec(points)
47    }
48}
49
50impl<P, const CW: bool, const CL: bool> ChaikinSmoothing for Ring<P, CW, CL>
51where
52    P: Point<Scalar = f64> + PointMut + Default + Copy,
53    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
54{
55    type Output = Ring<P, CW, CL>;
56
57    fn chaikin_smoothing(&self, iterations: usize) -> Self::Output {
58        let mut points = self.0.clone();
59        for _ in 0..iterations {
60            points = smooth_ring::<P, CL>(&points);
61        }
62        Ring::from_vec(points)
63    }
64}
65
66impl<P, const CW: bool, const CL: bool> ChaikinSmoothing for Polygon<P, CW, CL>
67where
68    P: Point<Scalar = f64> + PointMut + Default + Copy,
69    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
70{
71    type Output = Polygon<P, CW, CL>;
72
73    fn chaikin_smoothing(&self, iterations: usize) -> Self::Output {
74        Polygon::with_inners(
75            self.outer.chaikin_smoothing(iterations),
76            self.inners
77                .iter()
78                .map(|ring| ring.chaikin_smoothing(iterations))
79                .collect(),
80        )
81    }
82}
83
84fn smooth_open<P>(points: &[P]) -> Vec<P>
85where
86    P: Point<Scalar = f64> + PointMut + Default + Copy,
87{
88    if points.len() < 2 {
89        return points.to_vec();
90    }
91    let mut output = Vec::with_capacity(points.len() * 2);
92    output.push(points[0]);
93    for edge in points.windows(2) {
94        output.push(blend(&edge[0], &edge[1], 0.25));
95        output.push(blend(&edge[0], &edge[1], 0.75));
96    }
97    output.push(*points.last().expect("non-empty line checked above"));
98    output
99}
100
101fn smooth_ring<P, const CLOSED: bool>(points: &[P]) -> Vec<P>
102where
103    P: Point<Scalar = f64> + PointMut + Default + Copy,
104{
105    let unique_len = if points.len() > 1 && same_xy(&points[0], &points[points.len() - 1]) {
106        points.len() - 1
107    } else {
108        points.len()
109    };
110    if unique_len < 3 {
111        return points.to_vec();
112    }
113
114    let mut output = Vec::with_capacity(unique_len * 2 + usize::from(CLOSED));
115    for index in 0..unique_len {
116        let next = (index + 1) % unique_len;
117        output.push(blend(&points[index], &points[next], 0.25));
118        output.push(blend(&points[index], &points[next], 0.75));
119    }
120    if CLOSED {
121        output.push(output[0]);
122    }
123    output
124}
125
126fn blend<P>(first: &P, second: &P, fraction: f64) -> P
127where
128    P: Point<Scalar = f64> + PointMut + Default,
129{
130    let mut output = P::default();
131    geometry_trait::fold_dims((), first, |(), _, dimension| {
132        let first_value = get_dimension(first, dimension);
133        let second_value = get_dimension(second, dimension);
134        set_dimension(
135            &mut output,
136            dimension,
137            first_value + fraction * (second_value - first_value),
138        );
139    });
140    output
141}
142
143#[allow(
144    clippy::float_cmp,
145    reason = "ring closure is represented by exact endpoint identity, matching the model's closure convention"
146)]
147fn same_xy<P: Point<Scalar = f64>>(first: &P, second: &P) -> bool {
148    first.get::<0>() == second.get::<0>() && first.get::<1>() == second.get::<1>()
149}
150
151fn get_dimension<P: Point<Scalar = f64>>(point: &P, dimension: usize) -> f64 {
152    match dimension {
153        0 => point.get::<0>(),
154        1 => point.get::<1>(),
155        2 => point.get::<2>(),
156        3 => point.get::<3>(),
157        _ => unreachable!("point folds are limited to four dimensions"),
158    }
159}
160
161fn set_dimension<P: PointMut<Scalar = f64>>(point: &mut P, dimension: usize, value: f64) {
162    match dimension {
163        0 => point.set::<0>(value),
164        1 => point.set::<1>(value),
165        2 => point.set::<2>(value),
166        3 => point.set::<3>(value),
167        _ => unreachable!("point folds are limited to four dimensions"),
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::chaikin_smoothing;
174    use geometry_cs::Cartesian;
175    use geometry_model::{Linestring, Point2D, Ring};
176
177    type P = Point2D<f64, Cartesian>;
178
179    #[test]
180    fn open_line_keeps_endpoints_and_closed_ring_stays_closed() {
181        let line = Linestring::from_vec(vec![P::new(0.0, 0.0), P::new(2.0, 0.0)]);
182        let smoothed = chaikin_smoothing(&line, 1);
183        assert_eq!(smoothed.0.first(), Some(&P::new(0.0, 0.0)));
184        assert_eq!(smoothed.0.last(), Some(&P::new(2.0, 0.0)));
185
186        let ring: Ring<P> = Ring::from_vec(vec![
187            P::new(0.0, 0.0),
188            P::new(0.0, 2.0),
189            P::new(2.0, 0.0),
190            P::new(0.0, 0.0),
191        ]);
192        let smoothed = chaikin_smoothing(&ring, 1);
193        assert_eq!(smoothed.0.first(), smoothed.0.last());
194        assert_eq!(smoothed.0.len(), 7);
195    }
196}