1use axiolid_contracts::GeomError;
8use axiolid_core::{Frame3, Point2, Point3, Scalar, Vec2, Vec3};
9use axiolid_curve::{
10 Circle3, CurvatureLaw, Curve2, Curve3, CurvePath, Elevated3, ElevationLaw, Intrinsic3, Line2,
11 Line3, OffsetFrame, OffsetLaw, PathBound, PathCurve, PathOffset, PathOffsets, PathPiece,
12 Polyline3, SeamSide,
13};
14
15use crate::{CurveEvaluator, CurveMeasure, DistanceConvention, DistanceInterval};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ConformanceFailure {
20 pub check: &'static str,
22 pub detail: String,
24}
25
26fn fail(check: &'static str, detail: impl Into<String>) -> ConformanceFailure {
27 ConformanceFailure {
28 check,
29 detail: detail.into(),
30 }
31}
32
33#[must_use]
37pub fn check<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
38 let mut out = Vec::new();
39 out.extend(check_line(provider));
40 out.extend(check_circle(provider));
41 out.extend(check_tangent_is_unit(provider));
42 out.extend(check_frame_is_orthonormal(provider));
43 out.extend(check_refusals(provider));
44 out.extend(check_measure_routes_differ(provider));
45 out.extend(check_sides_agree_off_a_seam(provider));
46 out.extend(check_sides_at_seams(provider));
47 out.extend(check_paths(provider));
48 out.extend(check_path_joints(provider));
49 out.extend(check_path_joints_within(provider));
50 out.extend(check_open_paths(provider));
51 out.extend(check_bounded_paths(provider));
52 out
53}
54
55fn check_line<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
61 let mut out = Vec::new();
62 let line = Curve3::Line(Line3 {
63 origin: Point3::new(1.0, 2.0, 3.0),
64 direction: Vec3::new(2.0, 0.0, 0.0),
65 });
66 if !provider.distance_convention(&line).is_supported() {
67 return out;
68 }
69 match provider.point_at(&line, CurveMeasure::Distance(5.0)) {
70 Ok(p) => {
71 let moved = (p - Point3::new(1.0, 2.0, 3.0)).length();
72 if (moved - 5.0).abs() > 1e-9 {
73 out.push(fail(
74 "line advances true distance",
75 format!("distance 5 moved {moved}, not 5"),
76 ));
77 }
78 }
79 Err(e) => out.push(fail("line advances true distance", format!("{e:?}"))),
80 }
81 out
82}
83
84fn check_circle<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
86 let mut out = Vec::new();
87 let radius = 4.0;
88 let circle = Curve3::Circle(Circle3 {
89 frame: Frame3 {
90 origin: Point3::ZERO,
91 x: Vec3::X,
92 y: Vec3::Y,
93 z: Vec3::Z,
94 },
95 radius,
96 });
97 if !provider.distance_convention(&circle).is_supported() {
98 return out;
99 }
100 let quarter = core::f64::consts::FRAC_PI_2 * radius;
102 match provider.point_at(&circle, CurveMeasure::Distance(quarter)) {
103 Ok(p) => {
104 let want = Point3::new(0.0, radius, 0.0);
105 let error = (p - want).length();
106 if error > 1e-9 {
107 out.push(fail(
108 "circle reaches angle d/r",
109 format!("quarter circumference landed {error:e} away from the +y axis"),
110 ));
111 }
112 }
113 Err(e) => out.push(fail("circle reaches angle d/r", format!("{e:?}"))),
114 }
115 out
116}
117
118fn sample_curves() -> Vec<Curve3> {
119 vec![
120 Curve3::Line(Line3 {
121 origin: Point3::new(0.0, 0.0, 0.0),
122 direction: Vec3::new(3.0, 4.0, 0.0),
123 }),
124 Curve3::Circle(Circle3 {
125 frame: Frame3 {
126 origin: Point3::new(2.0, 0.0, 1.0),
127 x: Vec3::X,
128 y: Vec3::Y,
129 z: Vec3::Z,
130 },
131 radius: 3.0,
132 }),
133 Curve3::Intrinsic(Intrinsic3::new(
137 Frame3 {
138 origin: Point3::ZERO,
139 x: Vec3::X,
140 y: Vec3::Y,
141 z: Vec3::Z,
142 },
143 CurvatureLaw::circular(0.1),
144 CurvatureLaw::circular(0.04),
145 10.0,
146 )),
147 ]
148}
149
150fn check_tangent_is_unit<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
152 let mut out = Vec::new();
153 for curve in sample_curves() {
154 if !provider.distance_convention(&curve).is_supported() {
155 continue;
156 }
157 for distance in [0.0, 1.5, 4.0] {
158 if let Ok(t) = provider.tangent_at(&curve, CurveMeasure::Distance(distance)) {
159 if (t.length() - 1.0).abs() > 1e-9 {
160 out.push(fail(
161 "tangent is unit length",
162 format!("|t| = {} at distance {distance}", t.length()),
163 ));
164 }
165 }
166 }
167 }
168 out
169}
170
171fn check_frame_is_orthonormal<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
176 let mut out = Vec::new();
177 for curve in sample_curves() {
178 if !provider.distance_convention(&curve).is_supported() {
179 continue;
180 }
181 for distance in [0.0, 1.5, 4.0] {
182 let Ok(frame) = provider.frame_at(&curve, CurveMeasure::Distance(distance)) else {
183 continue;
184 };
185 let checks = [
186 ("x unit", frame.x.length() - 1.0),
187 ("y unit", frame.y.length() - 1.0),
188 ("z unit", frame.z.length() - 1.0),
189 ("x.y", frame.x.dot(frame.y)),
190 ("x.z", frame.x.dot(frame.z)),
191 ("y.z", frame.y.dot(frame.z)),
192 ];
193 for (name, value) in checks {
194 if value.abs() > 1e-9 {
195 out.push(fail(
196 "frame is orthonormal",
197 format!("{name} off by {value:e} at distance {distance}"),
198 ));
199 }
200 }
201 let handed = frame.x.cross(frame.y).dot(frame.z);
203 if (handed - 1.0).abs() > 1e-9 {
204 out.push(fail(
205 "frame is right-handed",
206 format!("x cross y . z = {handed} at distance {distance}"),
207 ));
208 }
209 if let Ok(point) = provider.point_at(&curve, CurveMeasure::Distance(distance)) {
211 let off = (frame.origin - point).length();
212 if off > 1e-9 {
213 out.push(fail(
214 "frame origin is the curve point",
215 format!("origin {off:e} away at distance {distance}"),
216 ));
217 }
218 }
219 if let Ok(tangent) = provider.tangent_at(&curve, CurveMeasure::Distance(distance)) {
221 let off = (frame.x - tangent).length();
222 if off > 1e-9 {
223 out.push(fail(
224 "frame x is the tangent",
225 format!("x differs from tangent by {off:e}"),
226 ));
227 }
228 }
229 }
230 }
231 out
232}
233
234fn check_refusals<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
239 let mut out = Vec::new();
240 for curve in sample_curves() {
241 if !provider.distance_convention(&curve).is_supported() {
242 continue;
243 }
244 for bad in [Scalar::NAN, Scalar::INFINITY, Scalar::NEG_INFINITY] {
245 if provider
246 .point_at(&curve, CurveMeasure::Distance(bad))
247 .is_ok()
248 {
249 out.push(fail(
250 "non-finite distance is refused",
251 format!("point_at accepted {bad}"),
252 ));
253 }
254 if provider
255 .tangent_at(&curve, CurveMeasure::Distance(bad))
256 .is_ok()
257 {
258 out.push(fail(
259 "non-finite distance is refused",
260 format!("tangent_at accepted {bad}"),
261 ));
262 }
263 if provider
264 .frame_at(&curve, CurveMeasure::Distance(bad))
265 .is_ok()
266 {
267 out.push(fail(
268 "non-finite distance is refused",
269 format!("frame_at accepted {bad}"),
270 ));
271 }
272 }
273 }
274 let vertical = Curve3::Line(Line3 {
277 origin: Point3::ZERO,
278 direction: Vec3::ZERO,
279 });
280 if provider.distance_convention(&vertical).is_supported()
281 && provider
282 .point_at(&vertical, CurveMeasure::Distance(1.0))
283 .is_ok()
284 {
285 out.push(fail(
286 "degenerate curve is refused",
287 "a zero-direction line was evaluated instead of refused",
288 ));
289 }
290 out
291}
292
293fn check_measure_routes_differ<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
305 let mut out = Vec::new();
306 let radius = 4.0;
307 let circle = Curve3::Circle(Circle3 {
308 frame: Frame3 {
309 origin: Point3::ZERO,
310 x: Vec3::X,
311 y: Vec3::Y,
312 z: Vec3::Z,
313 },
314 radius,
315 });
316 let value = 1.5;
317 let by_parameter = provider.point_at(&circle, CurveMeasure::Parameter(value));
318 let by_distance = provider.point_at(&circle, CurveMeasure::Distance(value));
319 if let (Ok(p), Ok(d)) = (by_parameter, by_distance) {
320 if (p - d).length() < 1e-9 {
321 out.push(fail(
322 "parameter and distance are distinct",
323 format!("{value} gave the same point as a parameter and as a distance"),
324 ));
325 }
326 }
327 out
328}
329
330fn side_refused(error: &GeomError) -> bool {
335 matches!(
336 error,
337 GeomError::Unsupported { .. } | GeomError::UnsupportedInput { .. }
338 )
339}
340
341fn frame_off(a: &Frame3, b: &Frame3) -> Scalar {
342 [
343 (a.origin - b.origin).length(),
344 (a.x - b.x).length(),
345 (a.y - b.y).length(),
346 (a.z - b.z).length(),
347 ]
348 .into_iter()
349 .fold(0.0, Scalar::max)
350}
351
352type Sided = Option<(Point3, Vec3, Frame3)>;
355
356fn sided<E: CurveEvaluator>(
357 provider: &E,
358 curve: &Curve3,
359 at: CurveMeasure,
360 side: SeamSide,
361) -> Result<Sided, String> {
362 match (
363 provider.point_at_on(curve, at, side),
364 provider.tangent_at_on(curve, at, side),
365 provider.frame_at_on(curve, at, side),
366 ) {
367 (Ok(p), Ok(t), Ok(f)) => Ok(Some((p, t, f))),
368 (Err(a), Err(b), Err(c))
369 if side == SeamSide::Incoming
370 && side_refused(&a)
371 && side_refused(&b)
372 && side_refused(&c) =>
373 {
374 Ok(None)
375 }
376 (p, t, f) => Err(format!(
377 "point {:?}, tangent {:?}, frame {:?}",
378 p.err(),
379 t.err(),
380 f.err()
381 )),
382 }
383}
384
385fn check_sides_agree_off_a_seam<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
391 let mut out = Vec::new();
392 for curve in sample_curves() {
393 if !provider.distance_convention(&curve).is_supported() {
394 continue;
395 }
396 for distance in [0.0, 1.5, 4.0] {
397 let at = CurveMeasure::Distance(distance);
398 let (Ok(point), Ok(tangent), Ok(frame)) = (
399 provider.point_at(&curve, at),
400 provider.tangent_at(&curve, at),
401 provider.frame_at(&curve, at),
402 ) else {
403 continue;
404 };
405 for side in [SeamSide::Outgoing, SeamSide::Incoming] {
406 match sided(provider, &curve, at, side) {
407 Ok(Some((p, t, f))) => {
408 let off = (p - point)
409 .length()
410 .max((t - tangent).length())
411 .max(frame_off(&f, &frame));
412 if off > 1e-9 {
413 out.push(fail(
414 "both sides agree off a seam",
415 format!(
416 "{side:?} is {off:e} off the side-less answer at {distance}"
417 ),
418 ));
419 }
420 }
421 Ok(None) => {}
422 Err(detail) => out.push(fail(
423 "a side is answered or refused as unsupported",
424 format!("{side:?} at {distance}: {detail}"),
425 )),
426 }
427 }
428 }
429 for bad in [Scalar::NAN, Scalar::INFINITY] {
430 let at = CurveMeasure::Distance(bad);
431 for side in [SeamSide::Outgoing, SeamSide::Incoming] {
432 if provider.point_at_on(&curve, at, side).is_ok()
433 || provider.tangent_at_on(&curve, at, side).is_ok()
434 || provider.frame_at_on(&curve, at, side).is_ok()
435 {
436 out.push(fail(
437 "non-finite distance is refused",
438 format!("a {side:?} query accepted {bad}"),
439 ));
440 }
441 }
442 }
443 }
444 out
445}
446
447struct Seam {
450 what: &'static str,
451 curve: Curve3,
452 distance: Scalar,
453 point: Point3,
454 incoming: Vec3,
455 outgoing: Vec3,
456}
457
458fn seams() -> Vec<Seam> {
459 let grade = |g: Scalar| Vec3::new(1.0, 0.0, g) / g.hypot(1.0);
460 vec![
461 Seam {
463 what: "polyline corner",
464 curve: Curve3::Polyline(Polyline3 {
465 points: vec![
466 Point3::new(0.0, 0.0, 0.0),
467 Point3::new(4.0, 0.0, 0.0),
468 Point3::new(4.0, 3.0, 0.0),
469 ],
470 closed: false,
471 }),
472 distance: 4.0,
473 point: Point3::new(4.0, 0.0, 0.0),
474 incoming: Vec3::X,
475 outgoing: Vec3::Y,
476 },
477 Seam {
480 what: "grade break",
481 curve: Curve3::Elevated(Elevated3::new(
482 Curve2::Line(Line2 {
483 origin: Point2::ZERO,
484 direction: Vec2::X,
485 }),
486 ElevationLaw::Piecewise {
487 breaks: vec![50.0],
488 laws: vec![
489 ElevationLaw::constant_grade(0.0, 0.02),
490 ElevationLaw::constant_grade(1.0, -0.01),
491 ],
492 },
493 )),
494 distance: 50.0,
495 point: Point3::new(50.0, 0.0, 1.0),
496 incoming: grade(0.02),
497 outgoing: grade(-0.01),
498 },
499 ]
500}
501
502fn check_sides_at_seams<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
509 let mut out = Vec::new();
510 for seam in seams() {
511 if !provider.distance_convention(&seam.curve).is_supported() {
512 continue;
513 }
514 let (what, curve) = (seam.what, &seam.curve);
515 let on = CurveMeasure::Distance(seam.distance);
516 if let (Ok(t), Ok(t_on)) = (
517 provider.tangent_at(curve, on),
518 provider.tangent_at_on(curve, on, SeamSide::Outgoing),
519 ) {
520 if (t - t_on).length() > 1e-9 {
521 out.push(fail(
522 "outgoing is the side-less reading on a seam",
523 format!("{what}: {t:?} side-less, {t_on:?} outgoing"),
524 ));
525 }
526 }
527 let reads_sides = matches!(sided(provider, curve, on, SeamSide::Incoming), Ok(Some(_)));
531 let hair = 0.25e-12 * seam.distance;
532 let distances = if reads_sides {
533 vec![seam.distance, seam.distance - hair, seam.distance + hair]
534 } else {
535 vec![seam.distance]
536 };
537 for distance in distances {
538 let at = CurveMeasure::Distance(distance);
539 for (side, want) in [
540 (SeamSide::Outgoing, seam.outgoing),
541 (SeamSide::Incoming, seam.incoming),
542 ] {
543 let (point, tangent, frame) = match sided(provider, curve, at, side) {
544 Ok(Some(answers)) => answers,
545 Ok(None) => continue,
546 Err(detail) => {
547 out.push(fail(
548 "a seam side reads its own piece",
549 format!("{what}, {side:?} at {distance}: {detail}"),
550 ));
551 continue;
552 }
553 };
554 let checks = [
555 ("point", (point - seam.point).length()),
556 ("tangent", (tangent - want).length()),
557 ("frame x", (frame.x - want).length()),
558 ("frame origin", (frame.origin - seam.point).length()),
559 ("frame x.y", frame.x.dot(frame.y)),
560 (
561 "frame handedness",
562 frame.x.cross(frame.y).dot(frame.z) - 1.0,
563 ),
564 ];
565 for (name, value) in checks {
566 if value.abs() > 1e-9 {
567 out.push(fail(
568 "a seam side reads its own piece",
569 format!("{what}, {side:?} at {distance}: {name} off by {value:e}"),
570 ));
571 }
572 }
573 }
574 }
575 }
576 out
577}
578
579struct PathPlace {
583 distance: Scalar,
584 point: Point3,
585 incoming: Vec3,
586 outgoing: Vec3,
587 past_arc: (bool, bool),
590}
591
592type PathAnswers = Option<(Point3, Vec3, Frame3)>;
595
596fn path_answers<E: CurveEvaluator>(
597 provider: &E,
598 path: &CurvePath,
599 at: CurveMeasure,
600 side: SeamSide,
601) -> Result<PathAnswers, String> {
602 match (
603 provider.path_point_at_on(path, at, side),
604 provider.path_tangent_at_on(path, at, side),
605 provider.path_frame_at_on(path, at, side),
606 ) {
607 (Ok(p), Ok(t), Ok(f)) => Ok(Some((p, t, f))),
608 (Err(a), Err(b), Err(c)) if side_refused(&a) && side_refused(&b) && side_refused(&c) => {
609 Ok(None)
610 }
611 (p, t, f) => Err(format!(
612 "point {:?}, tangent {:?}, frame {:?}",
613 p.err(),
614 t.err(),
615 f.err()
616 )),
617 }
618}
619
620fn path_places(radius: Scalar) -> [[PathPlace; 3]; 2] {
622 let quarter = core::f64::consts::FRAC_PI_2 * radius;
623 let length = 10.0 + quarter;
624 let (sin, cos) = core::f64::consts::FRAC_PI_4.sin_cos();
625 let mid = Point3::new(15.0 - radius * cos, radius * sin, 0.0);
626 let mid_tangent = Vec3::new(sin, cos, 0.0);
627 let joint = Point3::new(10.0, 0.0, 0.0);
628 let four = Point3::new(4.0, 0.0, 0.0);
629 [
630 [
631 PathPlace {
632 distance: 4.0,
633 point: four,
634 incoming: Vec3::X,
635 outgoing: Vec3::X,
636 past_arc: (false, false),
637 },
638 PathPlace {
639 distance: 10.0,
640 point: joint,
641 incoming: Vec3::X,
642 outgoing: Vec3::Y,
643 past_arc: (false, true),
644 },
645 PathPlace {
646 distance: 10.0 + quarter / 2.0,
647 point: mid,
648 incoming: mid_tangent,
649 outgoing: mid_tangent,
650 past_arc: (true, true),
651 },
652 ],
653 [
655 PathPlace {
656 distance: quarter / 2.0,
657 point: mid,
658 incoming: -mid_tangent,
659 outgoing: -mid_tangent,
660 past_arc: (true, true),
661 },
662 PathPlace {
663 distance: quarter,
664 point: joint,
665 incoming: -Vec3::Y,
666 outgoing: -Vec3::X,
667 past_arc: (true, true),
668 },
669 PathPlace {
670 distance: length - 4.0,
671 point: four,
672 incoming: -Vec3::X,
673 outgoing: -Vec3::X,
674 past_arc: (true, true),
675 },
676 ],
677 ]
678}
679
680fn path_answer_failures(
682 what: &str,
683 side: SeamSide,
684 distance: Scalar,
685 (point, tangent, frame): (Point3, Vec3, Frame3),
686 place: &PathPlace,
687 want: Vec3,
688) -> Vec<ConformanceFailure> {
689 let checks = [
690 ("point", (point - place.point).length()),
691 ("tangent", (tangent - want).length()),
692 ("frame x", (frame.x - want).length()),
693 ("frame origin", (frame.origin - place.point).length()),
694 ("frame x.y", frame.x.dot(frame.y)),
695 ("frame x.z", frame.x.dot(frame.z)),
696 ("frame y.z", frame.y.dot(frame.z)),
697 ("frame y unit", frame.y.length() - 1.0),
698 (
699 "frame handedness",
700 frame.x.cross(frame.y).dot(frame.z) - 1.0,
701 ),
702 ];
703 checks
704 .into_iter()
705 .filter(|(_, value)| value.abs() > 1e-9)
706 .map(|(name, value)| {
707 fail(
708 "a path reads each side of a joint from its own piece",
709 format!("{what} path, {side:?} at {distance}: {name} off by {value:e}"),
710 )
711 })
712 .collect()
713}
714
715fn check_paths<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
728 let mut out = Vec::new();
729 let line = Curve3::Line(Line3 {
730 origin: Point3::ZERO,
731 direction: Vec3::new(2.0, 0.0, 0.0),
732 });
733 let radius = 5.0;
734 let circle = Curve3::Circle(Circle3 {
735 frame: Frame3 {
736 origin: Point3::new(15.0, 0.0, 0.0),
737 x: -Vec3::X,
738 y: Vec3::Y,
739 z: -Vec3::Z,
740 },
741 radius,
742 });
743 let quarter = core::f64::consts::FRAC_PI_2 * radius;
744 let length = 10.0 + quarter;
745 let forwards = CurvePath::new(vec![
746 PathPiece::new(PathCurve::Three(line.clone()), 0.0, 10.0),
747 PathPiece::new(PathCurve::Three(circle.clone()), 0.0, quarter),
748 ]);
749 let [forwards_places, reversed_places] = path_places(radius);
750 let paths = [
751 ("forwards", forwards.clone(), forwards_places),
752 ("reversed", forwards.clone().reversed(), reversed_places),
753 ];
754 let mut reads_paths = false;
755 for (what, path, places) in &paths {
756 for place in places {
757 let hair = 0.25e-12 * place.distance;
758 for distance in [place.distance - hair, place.distance, place.distance + hair] {
759 let at = CurveMeasure::Distance(distance);
760 for (side, want, past_arc) in [
761 (SeamSide::Outgoing, place.outgoing, place.past_arc.1),
762 (SeamSide::Incoming, place.incoming, place.past_arc.0),
763 ] {
764 let answers = match path_answers(provider, path, at, side) {
765 Ok(Some(answers)) => answers,
766 Ok(None) => continue,
767 Err(detail) => {
768 out.push(fail(
769 "a path is answered or refused as unsupported",
770 format!("{what} path, {side:?} at {distance}: {detail}"),
771 ));
772 continue;
773 }
774 };
775 reads_paths = true;
776 out.extend(path_answer_failures(
777 what, side, distance, answers, place, want,
778 ));
779 if past_arc && provider.path_frame_is_exact_at(path, at, side) {
780 out.push(fail(
781 "a path frame is exact only on exactly placed lines",
782 format!("{what} path, {side:?} at {distance}: claimed exact"),
783 ));
784 }
785 if side != SeamSide::Outgoing {
786 continue;
787 }
788 let (point, tangent, frame) = answers;
790 let off = match (
791 provider.path_point_at(path, at),
792 provider.path_tangent_at(path, at),
793 provider.path_frame_at(path, at),
794 ) {
795 (Ok(p), Ok(t), Ok(f)) => (p - point)
796 .length()
797 .max((t - tangent).length())
798 .max(frame_off(&f, &frame)),
799 _ => Scalar::INFINITY,
800 };
801 if off > 1e-9 {
802 out.push(fail(
803 "a plain path query is the outgoing reading",
804 format!("{what} path at {distance}: {off:e} off"),
805 ));
806 }
807 }
808 }
809 }
810 }
811 let convention = provider.path_distance_convention(&forwards);
812 if !reads_paths {
813 if convention.is_supported() {
814 out.push(fail(
815 "a provider that refuses paths reports them unsupported",
816 format!("every path query was refused, but {convention:?} was reported"),
817 ));
818 }
819 return out;
820 }
821 if convention != DistanceConvention::ArcLength3d {
822 out.push(fail(
823 "a path of lines and arcs is measured by arc length",
824 format!("{convention:?}"),
825 ));
826 }
827 for bad in [Scalar::NAN, Scalar::INFINITY, -1.0, length + 1e-3] {
828 let at = CurveMeasure::Distance(bad);
829 for side in [SeamSide::Outgoing, SeamSide::Incoming] {
830 if provider.path_point_at_on(&forwards, at, side).is_ok()
831 || provider.path_tangent_at_on(&forwards, at, side).is_ok()
832 || provider.path_frame_at_on(&forwards, at, side).is_ok()
833 {
834 out.push(fail(
835 "a distance off the path is refused",
836 format!("a {side:?} path query accepted {bad}"),
837 ));
838 }
839 }
840 }
841 let gapped = CurvePath::new(vec![
843 PathPiece::new(PathCurve::Three(line.clone()), 0.0, 9.0),
844 PathPiece::new(PathCurve::Three(circle.clone()), 0.0, quarter),
845 ]);
846 if provider
847 .path_point_at(&gapped, CurveMeasure::Distance(1.0))
848 .is_ok()
849 {
850 out.push(fail(
851 "a path whose pieces do not meet is refused",
852 "a path with a 1 m gap was evaluated",
853 ));
854 }
855 out
856}
857
858const JOINT_TOLERANCE: Scalar = 1e-6;
862
863fn certifies(interval: DistanceInterval, truth: Scalar, tolerance: Scalar) -> Result<(), String> {
867 let rounding = 8.0 * Scalar::EPSILON * truth.abs().max(1.0);
868 if !(interval.lower.is_finite() && interval.upper.is_finite()) {
869 return Err(format!("{interval:?} is not finite"));
870 }
871 if interval.lower > interval.upper {
872 return Err(format!("{interval:?} is empty"));
873 }
874 if interval.width() > tolerance {
875 return Err(format!("{interval:?} is wider than {tolerance:e}"));
876 }
877 if interval.lower > truth + rounding || interval.upper < truth - rounding {
878 return Err(format!("{interval:?} does not contain {truth}"));
879 }
880 Ok(())
881}
882
883fn check_path_joints<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
894 let mut out = Vec::new();
895 let line = Curve3::Line(Line3 {
896 origin: Point3::ZERO,
897 direction: Vec3::new(2.0, 0.0, 0.0),
898 });
899 let radius = 5.0;
900 let circle = Curve3::Circle(Circle3 {
901 frame: Frame3 {
902 origin: Point3::new(15.0, 0.0, 0.0),
903 x: -Vec3::X,
904 y: Vec3::Y,
905 z: -Vec3::Z,
906 },
907 radius,
908 });
909 let quarter = core::f64::consts::FRAC_PI_2 * radius;
910 let exact = CurvePath::new(vec![
911 PathPiece::new(PathCurve::Three(line.clone()), 0.0, 10.0),
912 PathPiece::new(PathCurve::Three(circle.clone()), 0.0, quarter),
913 ]);
914 let widening = PathOffset::new(
915 PathPiece::new(PathCurve::Three(line.clone()), 0.0, 10.0),
916 OffsetLaw::Linear {
917 start: PathOffsets::new(0.0, 0.0, 0.0),
918 end: PathOffsets::new(1.0, 0.0, 0.0),
919 frame: OffsetFrame::Section,
920 },
921 );
922 let slant = 101.0_f64.sqrt();
923 let numeric = CurvePath::new(vec![
924 PathPiece::new(PathCurve::Offset(Box::new(widening)), 0.0, slant),
925 PathPiece::new(
926 PathCurve::Three(Curve3::Line(Line3 {
927 origin: Point3::new(10.0, 1.0, 0.0),
928 direction: Vec3::X,
929 })),
930 0.0,
931 5.0,
932 ),
933 ]);
934 let mut answered = false;
935 for (what, path, truth) in [("line and arc", &exact, 10.0), ("offset", &numeric, slant)] {
936 match provider.path_joint_distances(path, JOINT_TOLERANCE) {
937 Ok(intervals) => {
938 answered = true;
939 if intervals.len() != 1 {
940 out.push(fail(
941 "joint distances are certified",
942 format!("{what} path: {} intervals for one joint", intervals.len()),
943 ));
944 } else if let Err(detail) = certifies(intervals[0], truth, JOINT_TOLERANCE) {
945 out.push(fail(
946 "joint distances are certified",
947 format!("{what} path: {detail}"),
948 ));
949 }
950 }
951 Err(error) if side_refused(&error) => {}
952 Err(error) => out.push(fail(
953 "joint distances are certified or refused as unsupported",
954 format!("{what} path: {error}"),
955 )),
956 }
957 }
958 if !answered {
959 return out;
960 }
961 for bad in [0.0, -1.0, Scalar::NAN, Scalar::INFINITY] {
962 if provider.path_joint_distances(&exact, bad).is_ok() {
963 out.push(fail(
964 "a tolerance that is not finite and positive is refused",
965 format!("joint distances were answered at tolerance {bad}"),
966 ));
967 }
968 }
969 let gapped = CurvePath::new(vec![
970 PathPiece::new(PathCurve::Three(line), 0.0, 9.0),
971 PathPiece::new(PathCurve::Three(circle), 0.0, quarter),
972 ]);
973 if provider
974 .path_joint_distances(&gapped, JOINT_TOLERANCE)
975 .is_ok()
976 {
977 out.push(fail(
978 "a path whose pieces do not meet is refused",
979 "the joint distances of a path with a 1 m gap were answered",
980 ));
981 }
982 out
983}
984
985fn segment(from: Point3, direction: Vec3, length: Scalar) -> PathPiece {
989 PathPiece::new(
990 PathCurve::Three(Curve3::Line(Line3 {
991 origin: from,
992 direction,
993 })),
994 0.0,
995 length,
996 )
997}
998
999fn window_answer(
1005 found: &[(usize, DistanceInterval)],
1006 truths: &[Scalar],
1007 around: Scalar,
1008 window: Scalar,
1009 tolerance: Scalar,
1010) -> Result<(), (&'static str, String)> {
1011 let span = DistanceInterval::new(around - window, around + window);
1012 if found.windows(2).any(|pair| pair[1].0 <= pair[0].0) {
1013 return Err((
1014 "joints near a window are certified",
1015 format!("joints out of order: {found:?}"),
1016 ));
1017 }
1018 for (joint, interval) in found {
1019 let Some(truth) = truths.get(*joint) else {
1020 return Err((
1021 "joints near a window are certified",
1022 format!("joint {joint} of {}", truths.len()),
1023 ));
1024 };
1025 certifies(*interval, *truth, tolerance).map_err(|detail| {
1026 (
1027 "joints near a window are certified",
1028 format!("joint {joint}: {detail}"),
1029 )
1030 })?;
1031 if !interval.meets(&span) {
1032 return Err((
1033 "joints near a window meet it",
1034 format!("joint {joint} at {interval:?} is outside {span:?}"),
1035 ));
1036 }
1037 }
1038 for (joint, truth) in truths.iter().enumerate() {
1039 if span.contains(*truth) && !found.iter().any(|(j, _)| *j == joint) {
1040 return Err((
1041 "every joint within a window is returned",
1042 format!("joint {joint} at {truth} is missing from {span:?}"),
1043 ));
1044 }
1045 }
1046 Ok(())
1047}
1048
1049fn check_path_joints_within<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
1064 let mut out = Vec::new();
1065 let turns = CurvePath::new(vec![
1066 segment(Point3::ZERO, Vec3::X, 10.0),
1067 segment(Point3::new(10.0, 0.0, 0.0), Vec3::Y, 10.0),
1068 segment(Point3::new(10.0, 10.0, 0.0), Vec3::X, 10.0),
1069 segment(Point3::new(20.0, 10.0, 0.0), Vec3::Y, 10.0),
1070 ]);
1071 let widening = PathOffset::new(
1072 segment(Point3::ZERO, Vec3::X, 10.0),
1073 OffsetLaw::Linear {
1074 start: PathOffsets::new(0.0, 0.0, 0.0),
1075 end: PathOffsets::new(1.0, 0.0, 0.0),
1076 frame: OffsetFrame::Section,
1077 },
1078 );
1079 let slant = 101.0_f64.sqrt();
1080 let numeric = CurvePath::new(vec![
1081 PathPiece::new(PathCurve::Offset(Box::new(widening)), 0.0, slant),
1082 segment(Point3::new(10.0, 1.0, 0.0), Vec3::X, 5.0),
1083 ]);
1084 let turn_truths = [10.0, 20.0, 30.0];
1085 let cases: [(&str, &CurvePath, &[Scalar], Scalar, Scalar); 9] = [
1086 ("turns", &turns, &turn_truths, 20.0, 1.0),
1087 ("turns", &turns, &turn_truths, 25.0, 5.0),
1088 ("turns", &turns, &turn_truths, 5.0, 1.0),
1089 ("turns", &turns, &turn_truths, 15.0, 4.5),
1090 ("turns", &turns, &turn_truths, 35.0, 10.0),
1091 ("turns", &turns, &turn_truths, 20.0, 100.0),
1092 ("turns", &turns, &turn_truths, 30.0, 0.0),
1093 ("offset", &numeric, &[slant], slant, 1e-2),
1094 ("offset", &numeric, &[slant], slant + 2.0, 1.0),
1095 ];
1096 let mut answered = false;
1097 for (what, path, truths, around, window) in cases {
1098 match provider.path_joint_distances_within(path, around, window, JOINT_TOLERANCE) {
1099 Ok(found) => {
1100 answered = true;
1101 if let Err((check, detail)) =
1102 window_answer(&found, truths, around, window, JOINT_TOLERANCE)
1103 {
1104 out.push(fail(
1105 check,
1106 format!("{what} path, window {window} around {around}: {detail}"),
1107 ));
1108 }
1109 if let Ok(all) = provider.path_joint_distances(path, JOINT_TOLERANCE) {
1110 for (joint, interval) in &found {
1111 if all.get(*joint).is_some_and(|whole| !whole.meets(interval)) {
1112 out.push(fail(
1113 "joints near a window agree with every joint's",
1114 format!(
1115 "{what} path, joint {joint}: {interval:?} against {:?}",
1116 all[*joint]
1117 ),
1118 ));
1119 }
1120 }
1121 }
1122 }
1123 Err(error) if side_refused(&error) => {}
1124 Err(error) => out.push(fail(
1125 "joints near a window are certified or refused as unsupported",
1126 format!("{what} path, window {window} around {around}: {error}"),
1127 )),
1128 }
1129 }
1130 if !answered {
1131 return out;
1132 }
1133 let bad = [
1134 (Scalar::NAN, 1.0, JOINT_TOLERANCE),
1135 (Scalar::INFINITY, 1.0, JOINT_TOLERANCE),
1136 (20.0, -1.0, JOINT_TOLERANCE),
1137 (20.0, Scalar::NAN, JOINT_TOLERANCE),
1138 (20.0, Scalar::INFINITY, JOINT_TOLERANCE),
1139 (20.0, 1.0, 0.0),
1140 (20.0, 1.0, Scalar::NAN),
1141 ];
1142 for (around, window, tolerance) in bad {
1143 if provider
1144 .path_joint_distances_within(&turns, around, window, tolerance)
1145 .is_ok()
1146 {
1147 out.push(fail(
1148 "a window or tolerance that is not finite, or negative, is refused",
1149 format!(
1150 "joints within {window} of {around} were answered at tolerance {tolerance}"
1151 ),
1152 ));
1153 }
1154 }
1155 out
1156}
1157
1158fn check_open_paths<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
1172 let mut out = Vec::new();
1173 let base = PathPiece::whole(PathCurve::Three(Curve3::Line(Line3 {
1174 origin: Point3::ZERO,
1175 direction: Vec3::new(10.0, 0.0, 0.0),
1176 })));
1177 let widening = PathOffset::new(
1178 base,
1179 OffsetLaw::Linear {
1180 start: PathOffsets::new(0.0, 0.0, 0.0),
1181 end: PathOffsets::new(1.0, 0.0, 0.0),
1182 frame: OffsetFrame::Section,
1183 },
1184 );
1185 let tail = Curve3::Line(Line3 {
1186 origin: Point3::new(10.0, 1.0, 0.0),
1187 direction: Vec3::new(5.0, 0.0, 0.0),
1188 });
1189 let forwards = CurvePath::new(vec![
1190 PathPiece::whole(PathCurve::Offset(Box::new(widening))),
1191 PathPiece::to_end(PathCurve::Three(tail), 0.0),
1192 ]);
1193 let slant = 101.0_f64.sqrt();
1194 let length = slant + 5.0;
1195 let half = Point3::new(5.0, 0.5, 0.0);
1196 let reversed = forwards.clone().reversed();
1197 let places = [
1198 ("forwards", &forwards, slant / 2.0, half),
1199 (
1200 "forwards",
1201 &forwards,
1202 slant + 2.0,
1203 Point3::new(12.0, 1.0, 0.0),
1204 ),
1205 ("forwards", &forwards, length, Point3::new(15.0, 1.0, 0.0)),
1206 ("reversed", &reversed, 2.0, Point3::new(13.0, 1.0, 0.0)),
1207 ("reversed", &reversed, 5.0 + slant / 2.0, half),
1208 ("reversed", &reversed, length, Point3::ZERO),
1209 ];
1210 let mut reads = false;
1211 for (what, path, distance, want) in places {
1212 match provider.path_point_at(path, CurveMeasure::Distance(distance)) {
1213 Ok(point) => {
1214 reads = true;
1215 let off = (point - want).length();
1216 if off > 1e-9 {
1217 out.push(fail(
1218 "an open end resolves to the curve's end",
1219 format!("{what} open path at {distance}: {off:e} off"),
1220 ));
1221 }
1222 }
1223 Err(error) if side_refused(&error) => {}
1224 Err(error) => out.push(fail(
1225 "an open path is answered or refused as unsupported",
1226 format!("{what} open path at {distance}: {error}"),
1227 )),
1228 }
1229 }
1230 if reads {
1231 let past = CurveMeasure::Distance(length + 1e-3);
1232 if provider.path_point_at(&forwards, past).is_ok() {
1233 out.push(fail(
1234 "a distance past an open end is refused",
1235 format!(
1236 "an open path of length {length} was read at {}",
1237 length + 1e-3
1238 ),
1239 ));
1240 }
1241 }
1242 match provider.path_joint_distances(&forwards, JOINT_TOLERANCE) {
1243 Ok(intervals) => {
1244 if intervals.len() != 1 {
1245 out.push(fail(
1246 "joint distances of an open path are certified",
1247 format!("{} intervals for one joint", intervals.len()),
1248 ));
1249 } else if let Err(detail) = certifies(intervals[0], slant, JOINT_TOLERANCE) {
1250 out.push(fail(
1251 "joint distances of an open path are certified",
1252 detail,
1253 ));
1254 }
1255 }
1256 Err(error) if side_refused(&error) => {}
1257 Err(error) => out.push(fail(
1258 "joint distances of an open path are certified or refused as unsupported",
1259 error.to_string(),
1260 )),
1261 }
1262 out
1263}
1264
1265fn check_bounded_paths<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
1280 let mut out = Vec::new();
1281 let line = PathCurve::Three(Curve3::Line(Line3 {
1282 origin: Point3::ZERO,
1283 direction: Vec3::new(10.0, 0.0, 0.0),
1284 }));
1285 let circle = PathCurve::Three(Curve3::Circle(Circle3 {
1286 frame: Frame3 {
1287 origin: Point3::new(8.0, 2.0, 0.0),
1288 x: Vec3::X,
1289 y: Vec3::Y,
1290 z: Vec3::Z,
1291 },
1292 radius: 2.0,
1293 }));
1294 let corner = PathBound::Point(Point3::new(8.0, 0.0, 0.0));
1295 let forwards = CurvePath::new(vec![
1296 PathPiece::bounded(line.clone(), PathBound::Parameter(0.2), corner),
1297 PathPiece::bounded(circle, corner, PathBound::Parameter(0.0)),
1298 ]);
1299 let pi = core::f64::consts::PI;
1300 let length = 6.0 + pi;
1301 let eighth = core::f64::consts::SQRT_2;
1302 let mid_arc = Point3::new(8.0 + eighth, 2.0 - eighth, 0.0);
1303 let reversed = forwards.clone().reversed();
1304 let places = [
1305 ("forwards", &forwards, 0.0, Point3::new(2.0, 0.0, 0.0)),
1306 ("forwards", &forwards, 3.0, Point3::new(5.0, 0.0, 0.0)),
1307 ("forwards", &forwards, 6.0 + pi / 2.0, mid_arc),
1308 ("forwards", &forwards, length, Point3::new(10.0, 2.0, 0.0)),
1309 ("reversed", &reversed, 0.0, Point3::new(10.0, 2.0, 0.0)),
1310 ("reversed", &reversed, pi / 2.0, mid_arc),
1311 ("reversed", &reversed, pi + 1.0, Point3::new(7.0, 0.0, 0.0)),
1312 ("reversed", &reversed, length, Point3::new(2.0, 0.0, 0.0)),
1313 ];
1314 let mut reads = false;
1315 for (what, path, distance, want) in places {
1316 match provider.path_point_at(path, CurveMeasure::Distance(distance)) {
1317 Ok(point) => {
1318 reads = true;
1319 let off = (point - want).length();
1320 if off > 1e-9 {
1321 out.push(fail(
1322 "a bound resolves to the distance of its parameter or point",
1323 format!("{what} bounded path at {distance}: {off:e} off"),
1324 ));
1325 }
1326 }
1327 Err(error) if side_refused(&error) => {}
1328 Err(error) => out.push(fail(
1329 "a bounded path is answered or refused as unsupported",
1330 format!("{what} bounded path at {distance}: {error}"),
1331 )),
1332 }
1333 }
1334 for (what, path, joint) in [("forwards", &forwards, 6.0), ("reversed", &reversed, pi)] {
1335 match provider.path_joint_distances(path, JOINT_TOLERANCE) {
1336 Ok(intervals) => {
1337 if intervals.len() != 1 {
1338 out.push(fail(
1339 "joint distances of a bounded path are certified",
1340 format!("{what}: {} intervals for one joint", intervals.len()),
1341 ));
1342 } else if let Err(detail) = certifies(intervals[0], joint, JOINT_TOLERANCE) {
1343 out.push(fail(
1344 "joint distances of a bounded path are certified",
1345 format!("{what}: {detail}"),
1346 ));
1347 }
1348 }
1349 Err(error) if side_refused(&error) => {}
1350 Err(error) => out.push(fail(
1351 "joint distances of a bounded path are certified or refused as unsupported",
1352 format!("{what}: {error}"),
1353 )),
1354 }
1355 }
1356 if !reads {
1357 return out;
1358 }
1359 let off_curve = CurvePath::from(PathPiece::bounded(
1360 line,
1361 PathBound::Parameter(0.2),
1362 PathBound::Point(Point3::new(5.0, 1e-3, 0.0)),
1363 ));
1364 let square = Curve3::Polyline(Polyline3 {
1365 points: vec![
1366 Point3::ZERO,
1367 Point3::new(1.0, 0.0, 0.0),
1368 Point3::new(1.0, 1.0, 0.0),
1369 Point3::new(0.0, 1.0, 0.0),
1370 ],
1371 closed: true,
1372 });
1373 let seam = CurvePath::from(PathPiece::bounded(
1374 PathCurve::Three(square),
1375 PathBound::Point(Point3::ZERO),
1376 PathBound::Point(Point3::new(1.0, 1.0, 0.0)),
1377 ));
1378 for (what, path) in [
1379 ("a point off its curve", &off_curve),
1380 ("a closed polyline's seam", &seam),
1381 ] {
1382 if let Ok(point) = provider.path_point_at(path, CurveMeasure::Distance(0.5)) {
1383 out.push(fail(
1384 "a point bound off the curve or named by several parameters is refused",
1385 format!("{what} was read, at {point:?}"),
1386 ));
1387 }
1388 }
1389 out
1390}