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