Skip to main content

axiolid_curve_evaluate_contract/
conformance.rs

1//! Conformance suite every curve-evaluation provider must pass.
2//!
3//! These are the properties a CALLER is entitled to assume. A provider
4//! that passes them can be swapped for another without a consumer
5//! noticing, which is the whole point of naming the capability.
6
7use 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/// One failed conformance expectation.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ConformanceFailure {
20    /// Which expectation failed.
21    pub check: &'static str,
22    /// What went wrong.
23    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/// Run every conformance check against `provider`.
34///
35/// Returns the failures; an empty vector means conformant.
36#[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
55/// A line with a NON-UNIT direction must still advance true distance.
56///
57/// This is the check that catches a provider handing back the native
58/// parameter as though it were a distance: with `|direction| = 2` the two
59/// differ by a factor of two, while a unit direction would hide the bug.
60fn 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
84/// A circle of radius r must reach angle d/r at distance d.
85fn 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    // A quarter of the circumference must land on the +y axis.
101    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        // An arc-length-native family. Included because a provider may route
134        // it past the distance-to-parameter conversion entirely, so its
135        // guards would otherwise go unexercised by this suite.
136        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
150/// The tangent must be unit length wherever it is defined.
151fn 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
171/// The frame must be right-handed orthonormal with `x` on the tangent.
172///
173/// A caller builds a rotation from this directly, so a frame that is
174/// merely close to orthonormal shears whatever it places.
175fn 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            // Right-handed: x cross y must be z, not -z.
202            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            // The frame must sit ON the curve.
210            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            // And its x axis must be the tangent.
220            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
234/// Bad input must be refused, not answered with a guess.
235///
236/// A NaN distance that returns a NaN point is the dangerous case: it
237/// propagates silently into a placement instead of failing at the call.
238fn 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    // A provider must not claim a convention it cannot honour: if it
275    // reports Unsupported it must actually refuse.
276    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
293/// A parameter and a distance must not be silently interchangeable.
294///
295/// On a circle of radius 4 the same number means two different places:
296/// `Parameter(1.5)` is 1.5 radians round, `Distance(1.5)` is 1.5 m along,
297/// i.e. 0.375 rad. A provider that ignored the method of measurement would
298/// return the same point for both -- wrong, finite, and plausible.
299///
300/// Also pins that the parameter route stays OPEN where the distance route
301/// is refused: an ellipse has no closed-form arc length, but its native
302/// parameter is perfectly meaningful, and a consumer holding an authored
303/// parameter must still be able to evaluate it.
304fn 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
330// --- seam sides (#286) --------------------------------------------------------
331
332/// Whether a sided query was refused because the provider does not read
333/// that side, as the contract's defaults refuse it: unsupported, typed.
334fn 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
352/// The three sided answers at `at`, or `None` when the provider refused
353/// `Incoming` as unsupported on all three (it does not read sides).
354type 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
385/// Off a seam both sides read the same piece: `Outgoing` must be the
386/// side-less answer, and `Incoming`, where a provider reads it, too. A
387/// provider that does not read `Incoming` must refuse it as UNSUPPORTED,
388/// never answer it with something else or refuse it as bad input. A
389/// sided query keeps the side-less refusals of a non-finite measure.
390fn 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
447/// A seam, its distance, the point there and the unit tangents of the
448/// pieces ending and starting there, from closed forms.
449struct 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        // A polyline's corner: 4 m along +x, then 3 m along +y.
462        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        // A grade break at plan distance 50 on a straight plan: +2% up to
478        // it, -1% after.
479        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
502/// On a seam each side reads its own piece -- for a provider that reads
503/// sides also a hair either side of the seam, within the ADR 0082
504/// tolerance `1e-12 * max(1, s)` -- and `Outgoing` is what the side-less
505/// query reads exactly on it. A provider that does not read `Incoming`
506/// must refuse it as unsupported rather than answer with the outgoing
507/// piece.
508fn 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        // The tolerance is part of the seam rule a provider reading sides
528        // implements; one that does not answers `Outgoing` side-lessly,
529        // which reads the piece the measure falls in.
530        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
579// --- curve paths (#290) -------------------------------------------------------
580
581/// A place along a path and what each side reads there, from closed forms.
582struct PathPlace {
583    distance: Scalar,
584    point: Point3,
585    incoming: Vec3,
586    outgoing: Vec3,
587    /// Whether each side's frame is read on or after the arc, so never
588    /// exact: `(incoming, outgoing)`.
589    past_arc: (bool, bool),
590}
591
592/// Every path answer at `at` from `side`, or `None` when the provider
593/// refused all of them as unsupported (it does not read paths).
594type 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
620/// The places [`check_paths`] reads its path at, forwards and reversed.
621fn 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        // Reversed: the arc from its end, then the line back to the origin.
654        [
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
680/// The failures of one path answer against its closed form.
681fn 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
715/// A path is read end to end as ADR 0082 reads a composite station basis:
716/// a line along `+x` over `[0, 10]` (a non-unit direction, so its measure
717/// is not its parameter), then a quarter of a circle of radius 5 about
718/// `(15, 0, 0)` turning left to `(15, 5, 0)` -- a right-angle corner at
719/// distance 10, incoming `+x`, outgoing `+y` -- read forwards and
720/// reversed, on the joint and within the seam tolerance either side of
721/// it. A provider that does not read paths must refuse every path query
722/// as unsupported and report no convention; one that reads them must read
723/// each side of the joint from its own piece, answer the plain queries as
724/// the outgoing side, refuse a distance off the path and a path whose
725/// pieces do not meet, measure it by arc length, and claim no frame on or
726/// after the arc exact.
727fn 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                    // The plain queries are the outgoing reading.
789                    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    // Pieces that do not meet: no distance runs across the gap.
842    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
858// --- joint distances (#294) ---------------------------------------------------
859
860/// The tolerance [`check_path_joints`] asks for.
861const JOINT_TOLERANCE: Scalar = 1e-6;
862
863/// Whether `interval` is a certificate for `truth` at `tolerance`: finite,
864/// ordered, no wider than asked, and containing it (up to the rounding of
865/// `truth` itself, a few units in its last place).
866fn 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
883/// Joint distances are certified (#294): a provider either refuses
884/// [`CurveEvaluator::path_joint_distances`] as unsupported, typed, or
885/// answers one interval per interior joint that contains the joint's true
886/// distance and is no wider than the tolerance. Two paths: the line and
887/// quarter arc of [`check_paths`] (every length stored, the joint at 10),
888/// and a numerical piece first -- an offset of a line whose lateral offset
889/// widens linearly from 0 to 1 over 10, a straight segment of true length
890/// `sqrt(101)` that a provider measures by quadrature -- then a line. A
891/// non-positive or non-finite tolerance and a path whose pieces do not
892/// meet must be refused.
893fn 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
985// --- joints near a window (#305) ---------------------------------------------
986
987/// A line from `from` along `direction` (unit), `length` long, as a piece.
988fn 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
999/// Whether `found`, the answer to a window `[around - window, around +
1000/// window]` over joints whose true distances are `truths`, is right: joint
1001/// indices in increasing order and in range, each interval a certificate
1002/// of its joint at `tolerance` that meets the window, and every joint whose
1003/// true distance lies in the window present. Fails with the check's name.
1004fn 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
1049/// Joints near a window (#305): a provider either refuses
1050/// [`CurveEvaluator::path_joint_distances_within`] as unsupported, typed,
1051/// or answers, for each window, the joints whose certified interval meets
1052/// it: in order, each with its index and a certificate of its true
1053/// distance no wider than the tolerance, and none whose true distance lies
1054/// in the window left out. Two paths: four lines turning at right angles
1055/// (every length stored, the joints at 10, 20 and 30), windows on a joint,
1056/// spanning two with their ends on them, between joints, past the end and
1057/// over the whole path; and the widening offset then line of
1058/// [`check_path_joints`] (the joint at `sqrt(101)`, a quadrature), a window
1059/// on its joint and one after it. Where the provider also certifies the
1060/// whole path, each joint it returns must agree with that answer (their
1061/// intervals meet). A window centre that is not finite, a window that is
1062/// negative or not finite, and a non-positive tolerance must be refused.
1063fn 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
1158// --- open ends (#298) ---------------------------------------------------------
1159
1160/// A piece may leave its end open (#298), and a provider that reads it
1161/// resolves the end with its own length of the curve. The path: the whole
1162/// of an offset whose lateral offset widens linearly from 0 to 1 beside
1163/// the whole of a line along `+x` of direction `(10, 0, 0)` (an unbounded
1164/// line's whole is its parameter domain, `[0, 10]`), a straight segment of
1165/// true length `sqrt(101)` no length of which is stated, then a line from
1166/// `(10, 1, 0)` along `+x` to its end at `(15, 1, 0)`. Read forwards and
1167/// reversed, its points must lie where the closed forms put them, a
1168/// distance past its end must be refused, and its joint, if certified,
1169/// must contain `sqrt(101)`. A provider may refuse an open end as
1170/// unsupported, typed, as it may refuse paths.
1171fn 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
1265// --- bounds at a parameter or a point (#304) ------------------------------------
1266
1267/// A piece may start or end at a native parameter or a point (#304), and a
1268/// provider that reads it resolves the distance itself. The path: a line
1269/// from the origin along `+x` of direction `(10, 0, 0)` from parameter
1270/// `0.2` to the point `(8, 0, 0)` (length 6), then a circle of radius 2
1271/// about `(8, 2, 0)` from the point `(8, 0, 0)` (angle `3 pi / 2`) to
1272/// parameter `0`, which follows it round the seam: a quarter arc of
1273/// length `pi` to `(10, 2, 0)`. No distance is stated. Read forwards and
1274/// reversed, its points must lie where the closed forms put them, and its
1275/// joint, if certified, at 6 (reversed, `pi`). A point off its curve, and
1276/// a closed polyline's seam (two parameters name it), must be refused,
1277/// never read. A provider may refuse bounds as unsupported, typed, as it
1278/// may refuse paths.
1279fn 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}