1#![cfg_attr(not(feature = "std"), no_std)]
42#![deny(missing_docs)]
43
44mod differential;
45mod error;
46mod path;
47mod point;
48mod project;
49mod sampler;
50mod scalar;
51mod segment;
52mod transform;
53
54pub use {
55 differential::*, error::*, path::*, point::*, project::*, sampler::*, scalar::*, segment::*,
56 transform::*,
57};
58
59#[cfg(all(test, feature = "std"))]
60mod tests {
61 use super::*;
62
63 #[derive(Debug, Clone, Copy, PartialEq)]
65 struct Vec2(f64, f64);
66
67 impl core::ops::Add for Vec2 {
68 type Output = Self;
69 fn add(self, rhs: Self) -> Self {
70 Vec2(self.0 + rhs.0, self.1 + rhs.1)
71 }
72 }
73
74 impl core::ops::Sub for Vec2 {
75 type Output = Self;
76 fn sub(self, rhs: Self) -> Self {
77 Vec2(self.0 - rhs.0, self.1 - rhs.1)
78 }
79 }
80
81 impl core::ops::Mul<f64> for Vec2 {
82 type Output = Self;
83 fn mul(self, rhs: f64) -> Self {
84 Vec2(self.0 * rhs, self.1 * rhs)
85 }
86 }
87
88 impl Vector for Vec2 {
89 type Scalar = f64;
90 fn zero() -> Self {
91 Vec2(0.0, 0.0)
92 }
93 fn dot(self, rhs: Self) -> Self::Scalar {
94 self.0 * rhs.0 + self.1 * rhs.1
95 }
96 fn norm(self) -> Self::Scalar {
97 (self.0 * self.0 + self.1 * self.1).sqrt()
98 }
99 }
100
101 #[derive(Debug, Clone, Copy, PartialEq)]
103 struct Pt2(f64, f64);
104
105 impl Point for Pt2 {
106 type Scalar = f64;
107 type Vector = Vec2;
108 fn displacement(self, other: Self) -> Self::Vector {
109 Vec2(other.0 - self.0, other.1 - self.1)
110 }
111 fn translate(self, v: Self::Vector) -> Self {
112 Pt2(self.0 + v.0, self.1 + v.1)
113 }
114 }
115
116 #[derive(Debug, Clone)]
118 struct LineSegment2 {
119 a: Pt2,
121 b: Pt2,
123 len: f64,
125 }
126
127 impl LineSegment2 {
128 fn new(a: Pt2, b: Pt2) -> Self {
130 let len = a.distance(b);
131 Self { a, b, len }
132 }
133 }
134
135 impl Path for LineSegment2 {
136 type Scalar = f64;
137 type Point = Pt2;
138 type Error = PathError<f64>;
139
140 fn length(&self) -> Self::Scalar {
141 self.len
142 }
143
144 fn sample_at(&self, s: Self::Scalar) -> Result<Self::Point, Self::Error> {
145 if s < 0.0 || s > self.len {
146 return Err(PathError::out_of_domain(s, self.domain()));
147 }
148 if self.len == 0.0 {
149 return Ok(self.a);
150 }
151 let t = s / self.len;
152 Ok(Pt2(
153 self.a.0 + t * (self.b.0 - self.a.0),
154 self.a.1 + t * (self.b.1 - self.a.1),
155 ))
156 }
157 }
158
159 impl PathSegment for LineSegment2 {}
160
161 impl ParametricPath for LineSegment2 {
162 fn sample_t(&self, t: Self::Scalar) -> Result<Self::Point, Self::Error> {
163 if !(0.0..=1.0).contains(&t) {
164 return Err(PathError::out_of_domain(t, 0.0..=1.0));
165 }
166 Ok(Pt2(
167 self.a.0 + t * (self.b.0 - self.a.0),
168 self.a.1 + t * (self.b.1 - self.a.1),
169 ))
170 }
171 }
172
173 impl Tangent for LineSegment2 {
174 fn tangent_at(
175 &self,
176 s: Self::Scalar,
177 ) -> Result<<Self::Point as Point>::Vector, Self::Error> {
178 if s < 0.0 || s > self.len {
179 return Err(PathError::out_of_domain(s, self.domain()));
180 }
181 if self.len == 0.0 {
182 return Err(PathError::degenerate("zero-length segment"));
183 }
184 let dir = self.a.displacement(self.b);
185 Ok(dir * (1.0 / self.len))
186 }
187 }
188
189 impl Heading for LineSegment2 {
190 fn heading_at(&self, s: Self::Scalar) -> Result<Self::Scalar, Self::Error> {
191 if s < 0.0 || s > self.len {
192 return Err(PathError::out_of_domain(s, self.domain()));
193 }
194 if self.len == 0.0 {
195 return Err(PathError::degenerate("zero-length segment"));
196 }
197 let dir = self.a.displacement(self.b);
198 Ok(dir.1.atan2(dir.0))
199 }
200 }
201
202 impl Curved for LineSegment2 {
203 type Curvature = f64;
204 fn curvature_at(&self, s: Self::Scalar) -> Result<Self::Curvature, Self::Error> {
205 if s < 0.0 || s > self.len {
206 return Err(PathError::out_of_domain(s, self.domain()));
207 }
208 Ok(0.0)
209 }
210 }
211
212 #[derive(Debug, Clone)]
214 struct Polyline2 {
215 #[allow(dead_code)]
216 points: Vec<Pt2>,
218 segments: Vec<LineSegment2>,
220 seg_lengths: Vec<f64>,
222 total_len: f64,
224 }
225
226 impl Polyline2 {
227 fn new(points: Vec<Pt2>) -> Self {
229 assert!(points.len() >= 2);
230 let segments: Vec<_> = points
231 .windows(2)
232 .map(|w| LineSegment2::new(w[0], w[1]))
233 .collect();
234 let seg_lengths: Vec<_> = segments.iter().map(|s| s.len).collect();
235 let total_len = seg_lengths.iter().sum();
236 Self {
237 points,
238 segments,
239 seg_lengths,
240 total_len,
241 }
242 }
243 }
244
245 impl Path for Polyline2 {
246 type Scalar = f64;
247 type Point = Pt2;
248 type Error = PathError<f64>;
249
250 fn length(&self) -> Self::Scalar {
251 self.total_len
252 }
253
254 fn sample_at(&self, s: Self::Scalar) -> Result<Self::Point, Self::Error> {
255 if s < 0.0 || s > self.total_len {
256 return Err(PathError::out_of_domain(s, self.domain()));
257 }
258 let (idx, local) = self.locate(s)?;
259 self.segments[idx].sample_at(local)
260 }
261 }
262
263 impl SegmentedPath for Polyline2 {
264 type Segment = LineSegment2;
265
266 fn segment_count(&self) -> usize {
267 self.segments.len()
268 }
269
270 fn segments(&self) -> impl Iterator<Item = &Self::Segment> + '_ {
271 self.segments.iter()
272 }
273
274 fn locate(&self, s: Self::Scalar) -> Result<(usize, Self::Scalar), Self::Error> {
275 if s < 0.0 || s > self.total_len {
276 return Err(PathError::out_of_domain(s, self.domain()));
277 }
278 let mut remaining = s;
279 for (i, &seg_len) in self.seg_lengths.iter().enumerate() {
280 if remaining <= seg_len {
281 return Ok((i, remaining));
282 }
283 remaining -= seg_len;
284 }
285 Ok((
286 self.segments.len() - 1,
287 self.seg_lengths.last().copied().unwrap_or(0.0),
288 ))
289 }
290 }
291
292 impl Project for LineSegment2 {
293 fn project(&self, p: Self::Point) -> Result<Self::Scalar, Self::Error> {
294 if self.len == 0.0 {
295 return Ok(0.0);
296 }
297 let dir = self.a.displacement(self.b);
298 let to_p = self.a.displacement(p);
299 let t = (to_p.dot(dir)) / (dir.dot(dir));
300 let t_clamped = t.clamp(0.0, 1.0);
301 Ok(t_clamped * self.len)
302 }
303 }
304
305 #[test]
306 fn line_segment_length() {
307 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 4.0));
308 assert!((seg.length() - 5.0).abs() < 1e-10);
309 }
310
311 #[test]
312 fn line_segment_start_end() {
313 let seg = LineSegment2::new(Pt2(1.0, 2.0), Pt2(4.0, 6.0));
314 let start = seg.start().unwrap();
315 let end = seg.end().unwrap();
316 assert_eq!(start, Pt2(1.0, 2.0));
317 assert_eq!(end, Pt2(4.0, 6.0));
318 }
319
320 #[test]
321 fn line_segment_sample_mid() {
322 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(2.0, 0.0));
323 let mid = seg.sample_at(1.0).unwrap();
324 assert!((mid.0 - 1.0).abs() < 1e-10);
325 assert!((mid.1 - 0.0).abs() < 1e-10);
326 }
327
328 #[test]
329 fn line_segment_out_of_domain() {
330 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(1.0, 0.0));
331 let err = seg.sample_at(-0.1).unwrap_err();
332 assert!(
333 matches!(err, PathError::OutOfDomain { param, domain } if (param - -0.1).abs() < 1e-10 && *domain.start() == 0.0 && (*domain.end() - 1.0).abs() < 1e-10)
334 );
335 let err = seg.sample_at(1.1).unwrap_err();
336 assert!(
337 matches!(err, PathError::OutOfDomain { param, domain } if (param - 1.1).abs() < 1e-10 && *domain.start() == 0.0 && (*domain.end() - 1.0).abs() < 1e-10)
338 );
339 }
340
341 #[test]
342 fn polyline_segment_count() {
343 let pts = vec![Pt2(0.0, 0.0), Pt2(1.0, 0.0), Pt2(1.0, 1.0)];
344 let poly = Polyline2::new(pts);
345 assert_eq!(poly.segment_count(), 2);
346 assert_eq!(poly.segments().count(), 2);
347 }
348
349 #[test]
350 fn polyline_total_length() {
351 let pts = vec![Pt2(0.0, 0.0), Pt2(3.0, 0.0), Pt2(3.0, 4.0)];
352 let poly = Polyline2::new(pts);
353 assert!((poly.length() - 7.0).abs() < 1e-10);
354 }
355
356 #[test]
357 fn polyline_locate_roundtrip() {
358 let pts = vec![Pt2(0.0, 0.0), Pt2(2.0, 0.0), Pt2(2.0, 3.0)];
359 let poly = Polyline2::new(pts);
360 let (idx, local) = poly.locate(1.0).unwrap();
361 assert_eq!(idx, 0);
362 assert!((local - 1.0).abs() < 1e-10);
363 let (idx, local) = poly.locate(3.0).unwrap();
364 assert_eq!(idx, 1);
365 assert!((local - 1.0).abs() < 1e-10);
366 }
367
368 #[test]
369 fn line_segment_tangent() {
370 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 4.0));
371 let t = seg.tangent_at(2.5).unwrap();
372 let expected = Vec2(0.6, 0.8);
373 assert!((t.0 - expected.0).abs() < 1e-10);
374 assert!((t.1 - expected.1).abs() < 1e-10);
375 }
376
377 #[test]
378 fn line_segment_heading() {
379 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(1.0, 1.0));
380 let h = seg.heading_at(0.5).unwrap();
381 assert!((h - std::f64::consts::FRAC_PI_4).abs() < 1e-10);
382 }
383
384 #[test]
385 fn line_segment_curvature_zero() {
386 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(5.0, 0.0));
387 assert_eq!(seg.curvature_at(2.5).unwrap(), 0.0);
388 }
389
390 #[test]
391 fn equidistant_samples() {
392 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(4.0, 0.0));
393 let samples: Vec<_> = equidistant(&seg, 1.0).collect();
394 assert_eq!(samples.len(), 5);
395 for r in &samples {
396 assert!(r.is_ok());
397 }
398 }
399
400 #[test]
401 fn n_samples_count() {
402 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(10.0, 0.0));
403 let samples: Vec<_> = n_samples(&seg, 3).collect();
404 assert_eq!(samples.len(), 3);
405 }
406
407 #[test]
408 fn uniform_t_samples() {
409 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(10.0, 0.0));
410 let samples: Vec<_> = uniform_t(&seg, 5).collect();
411 assert_eq!(samples.len(), 5);
412 }
413
414 #[test]
415 fn reverse_double_equals_original() {
416 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(5.0, 0.0));
417 let rev = seg.clone().reverse();
418 let rev2 = rev.reverse();
419 for s in [0.0, 1.25, 2.5, 3.75, 5.0] {
420 let orig = seg.sample_at(s).unwrap();
421 let double_rev = rev2.sample_at(s).unwrap();
422 assert!((orig.0 - double_rev.0).abs() < 1e-10);
423 assert!((orig.1 - double_rev.1).abs() < 1e-10);
424 }
425 }
426
427 #[test]
428 fn concat_total_length() {
429 let a = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 0.0));
430 let b = LineSegment2::new(Pt2(3.0, 0.0), Pt2(3.0, 4.0));
431 let c = a.concat(b);
432 assert!((c.length() - 7.0).abs() < 1e-10);
433 }
434
435 #[test]
436 fn domain_matches_length() {
437 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 4.0));
438 let domain = seg.domain();
439 assert!(*domain.start() == 0.0);
440 assert!((*domain.end() - 5.0).abs() < 1e-10);
441 }
442
443 #[test]
444 fn project_onto_segment() {
445 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(4.0, 0.0));
446 let s = seg.project(Pt2(2.0, 3.0)).unwrap();
447 assert!((s - 2.0).abs() < 1e-10);
448 }
449
450 #[test]
451 fn project_clamped_to_end() {
452 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 0.0));
453 let s = seg.project(Pt2(5.0, 1.0)).unwrap();
454 assert!((s - 3.0).abs() < 1e-10);
455 }
456
457 #[test]
458 fn project_clamped_to_start() {
459 let seg = LineSegment2::new(Pt2(2.0, 0.0), Pt2(5.0, 0.0));
460 let s = seg.project(Pt2(0.0, 1.0)).unwrap();
461 assert!(s.abs() < 1e-10);
462 }
463
464 #[test]
465 fn closest_point_on_segment() {
466 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(4.0, 0.0));
467 let cp = seg.closest_point(Pt2(1.0, 5.0)).unwrap();
468 assert!((cp.0 - 1.0).abs() < 1e-10);
469 assert!((cp.1 - 0.0).abs() < 1e-10);
470 }
471
472 #[test]
473 fn concat_sample_across_boundary() {
474 let a = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 0.0));
475 let b = LineSegment2::new(Pt2(3.0, 0.0), Pt2(3.0, 4.0));
476 let c = a.concat(b);
477 let junction = c.sample_at(3.0).unwrap();
478 assert!((junction.0 - 3.0).abs() < 1e-10);
479 assert!((junction.1 - 0.0).abs() < 1e-10);
480 let into_b = c.sample_at(5.0).unwrap();
481 assert!((into_b.0 - 3.0).abs() < 1e-10);
482 assert!((into_b.1 - 2.0).abs() < 1e-10);
483 }
484
485 #[test]
486 fn reverse_tangent_negated() {
487 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(3.0, 4.0));
488 let rev = seg.clone().reverse();
489 let orig_t = seg.tangent_at(2.0).unwrap();
490 let rev_t = rev.tangent_at(seg.length() - 2.0).unwrap();
491 assert!((rev_t.0 - (-orig_t.0)).abs() < 1e-10);
492 assert!((rev_t.1 - (-orig_t.1)).abs() < 1e-10);
493 }
494
495 #[test]
496 fn reverse_curvature_negated() {
497 let seg = LineSegment2::new(Pt2(0.0, 0.0), Pt2(5.0, 0.0));
498 let rev = seg.clone().reverse();
499 let orig_k = seg.curvature_at(2.0).unwrap();
500 let rev_k = rev.curvature_at(3.0).unwrap();
501 assert!((rev_k - (-orig_k)).abs() < 1e-10);
502 }
503
504 #[test]
505 fn reverse_start_is_original_end() {
506 let seg = LineSegment2::new(Pt2(1.0, 2.0), Pt2(4.0, 6.0));
507 let rev = seg.clone().reverse();
508 let rev_start = rev.start().unwrap();
509 let orig_end = seg.end().unwrap();
510 assert_eq!(rev_start, orig_end);
511 }
512}