1use 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#[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#[doc(hidden)]
26pub trait ChaikinSmoothing {
27 type Output;
29
30 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}