1use crate::{Contour, Point};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct PathSample {
10 pub position: Point,
12 pub tangent: Point,
14}
15
16#[derive(Clone, Debug)]
20pub struct ContourMeasure {
21 points: Vec<Point>,
22 distances: Vec<f32>,
26 closed: bool,
27}
28
29impl ContourMeasure {
30 pub(crate) fn of(contour: &Contour) -> Option<Self> {
34 let mut points = Vec::with_capacity(contour.points.len());
35 let mut distances = Vec::with_capacity(contour.points.len());
36 let mut total = 0.0;
37 for &point in &contour.points {
38 match points.last() {
39 None => {
40 points.push(point);
41 distances.push(0.0);
42 }
43 Some(&previous) => {
44 let step = distance_between(previous, point);
45 if step > 0.0 {
46 total += step;
47 points.push(point);
48 distances.push(total);
49 }
50 }
51 }
52 }
53 (points.len() > 1).then_some(Self {
54 points,
55 distances,
56 closed: contour.closed,
57 })
58 }
59
60 pub fn length(&self) -> f32 {
62 *self
63 .distances
64 .last()
65 .expect("measured contours have length")
66 }
67
68 pub fn is_closed(&self) -> bool {
70 self.closed
71 }
72
73 pub fn sample(&self, distance: f32) -> PathSample {
77 let distance = if distance.is_nan() { 0.0 } else { distance };
81 let distance = distance.clamp(0.0, self.length());
82 let index = self.segment_containing(distance);
83 let (start, end) = (self.points[index], self.points[index + 1]);
84 let span = self.distances[index + 1] - self.distances[index];
85 let fraction = (distance - self.distances[index]) / span;
86 PathSample {
87 position: lerp(start, end, fraction),
88 tangent: unit_vector(start, end),
89 }
90 }
91
92 pub fn segment(&self, start: f32, end: f32) -> Option<Contour> {
97 if start.is_nan() || end.is_nan() {
98 return None;
99 }
100 let length = self.length();
101 let (start, end) = (start.clamp(0.0, length), end.clamp(0.0, length));
102 if end <= start {
103 return None;
104 }
105 let mut points = vec![self.sample(start).position];
106 let first = self.segment_containing(start);
107 let last = self.segment_containing(end);
108 for index in first + 1..=last {
109 points.push(self.points[index]);
110 }
111 points.push(self.sample(end).position);
112 Some(Contour {
113 points: dedup_adjacent(points),
114 closed: false,
115 has_segments: true,
118 })
119 }
120
121 fn segment_containing(&self, distance: f32) -> usize {
123 match self
124 .distances
125 .binary_search_by(|entry| entry.partial_cmp(&distance).expect("finite distances"))
126 {
127 Ok(index) => index.min(self.points.len() - 2),
128 Err(insert) => insert - 1,
129 }
130 }
131}
132
133fn distance_between(a: Point, b: Point) -> f32 {
134 ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
135}
136
137fn lerp(a: Point, b: Point, t: f32) -> Point {
138 Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
139}
140
141fn unit_vector(a: Point, b: Point) -> Point {
142 let length = distance_between(a, b);
143 Point::new((b.x - a.x) / length, (b.y - a.y) / length)
144}
145
146fn dedup_adjacent(points: Vec<Point>) -> Vec<Point> {
147 let mut out: Vec<Point> = Vec::with_capacity(points.len());
148 for point in points {
149 if out.last() != Some(&point) {
150 out.push(point);
151 }
152 }
153 out
154}
155
156#[cfg(test)]
157mod tests {
158 use crate::{Path, PathBuilder, Point};
159
160 fn measured(build: impl FnOnce(&mut PathBuilder)) -> Vec<crate::ContourMeasure> {
161 let mut builder = PathBuilder::new();
162 build(&mut builder);
163 let path: std::sync::Arc<Path> = builder.build();
164 path.measure(0.05)
165 }
166
167 #[test]
168 fn a_straight_line_measures_its_own_length() {
169 let measures = measured(|b| {
170 b.move_to((10.0, 10.0)).line_to((10.0, 60.0));
171 });
172 assert_eq!(measures.len(), 1);
173 assert!((measures[0].length() - 50.0).abs() < 1e-3);
174 assert!(!measures[0].is_closed());
175 }
176
177 #[test]
178 fn sampling_walks_the_line_and_clamps_past_its_ends() {
179 let measures = measured(|b| {
180 b.move_to((0.0, 0.0)).line_to((100.0, 0.0));
181 });
182 let middle = measures[0].sample(25.0);
183 assert!((middle.position.x - 25.0).abs() < 1e-3);
184 assert!((middle.tangent.x - 1.0).abs() < 1e-3);
185 assert!(middle.tangent.y.abs() < 1e-3);
186
187 assert!((measures[0].sample(-10.0).position.x - 0.0).abs() < 1e-3);
189 assert!((measures[0].sample(500.0).position.x - 100.0).abs() < 1e-3);
190 }
191
192 #[test]
193 fn a_closed_square_measures_its_whole_perimeter() {
194 let measures = measured(|b| {
195 b.rect(crate::Rect::new(0.0, 0.0, 30.0, 30.0));
196 });
197 assert_eq!(measures.len(), 1);
198 assert!(measures[0].is_closed());
199 assert!((measures[0].length() - 120.0).abs() < 1e-3);
201 }
202
203 #[test]
204 fn a_circle_measures_near_two_pi_r() {
205 let measures = measured(|b| {
206 b.circle((0.0, 0.0), 50.0);
207 });
208 let circumference = std::f32::consts::TAU * 50.0;
209 let error = (measures[0].length() - circumference).abs() / circumference;
211 assert!(error < 0.001, "circumference off by {error}");
212 }
213
214 #[test]
215 fn each_contour_is_measured_separately() {
216 let measures = measured(|b| {
217 b.move_to((0.0, 0.0)).line_to((10.0, 0.0));
218 b.move_to((0.0, 20.0)).line_to((0.0, 60.0));
219 });
220 assert_eq!(measures.len(), 2);
221 assert!((measures[0].length() - 10.0).abs() < 1e-3);
222 assert!((measures[1].length() - 40.0).abs() < 1e-3);
223 }
224
225 #[test]
226 fn a_segment_spans_exactly_the_requested_stretch() {
227 let measures = measured(|b| {
228 b.move_to((0.0, 0.0))
229 .line_to((100.0, 0.0))
230 .line_to((100.0, 100.0));
231 });
232 let segment = measures[0].segment(50.0, 150.0).expect("non-empty");
233 assert_eq!(segment.points.first(), Some(&Point::new(50.0, 0.0)));
234 assert_eq!(segment.points.last(), Some(&Point::new(100.0, 50.0)));
235 assert!(segment.points.contains(&Point::new(100.0, 0.0)));
237 assert!(!segment.closed);
238 }
239
240 #[test]
241 fn an_empty_or_reversed_range_measures_nothing() {
242 let measures = measured(|b| {
243 b.move_to((0.0, 0.0)).line_to((10.0, 0.0));
244 });
245 assert!(measures[0].segment(5.0, 5.0).is_none());
246 assert!(measures[0].segment(8.0, 2.0).is_none());
247 }
248
249 #[test]
250 fn non_finite_distances_answer_instead_of_panicking() {
251 let measures = measured(|b| {
252 b.move_to((0.0, 0.0)).line_to((10.0, 0.0));
253 });
254 assert_eq!(measures[0].sample(f32::NAN).position, Point::new(0.0, 0.0));
255 assert!(measures[0].segment(f32::NAN, 5.0).is_none());
256 assert!(measures[0].segment(0.0, f32::INFINITY).is_some());
257 }
258
259 #[test]
260 fn degenerate_contours_are_dropped_rather_than_measured() {
261 let measures = measured(|b| {
263 b.move_to((5.0, 5.0));
264 });
265 assert!(measures.is_empty());
266 }
267}