axiolid_curve_evaluate_contract/
conformance.rs1use axiolid_core::{Frame3, Point3, Scalar, Vec3};
8use axiolid_curve::{Circle3, CurvatureLaw, Curve3, Intrinsic3, Line3};
9
10use crate::{CurveEvaluator, CurveMeasure};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ConformanceFailure {
15 pub check: &'static str,
17 pub detail: String,
19}
20
21fn fail(check: &'static str, detail: impl Into<String>) -> ConformanceFailure {
22 ConformanceFailure {
23 check,
24 detail: detail.into(),
25 }
26}
27
28#[must_use]
32pub fn check<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
33 let mut out = Vec::new();
34 out.extend(check_line(provider));
35 out.extend(check_circle(provider));
36 out.extend(check_tangent_is_unit(provider));
37 out.extend(check_frame_is_orthonormal(provider));
38 out.extend(check_refusals(provider));
39 out.extend(check_measure_routes_differ(provider));
40 out
41}
42
43fn check_line<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
49 let mut out = Vec::new();
50 let line = Curve3::Line(Line3 {
51 origin: Point3::new(1.0, 2.0, 3.0),
52 direction: Vec3::new(2.0, 0.0, 0.0),
53 });
54 if !provider.distance_convention(&line).is_supported() {
55 return out;
56 }
57 match provider.point_at(&line, CurveMeasure::Distance(5.0)) {
58 Ok(p) => {
59 let moved = (p - Point3::new(1.0, 2.0, 3.0)).length();
60 if (moved - 5.0).abs() > 1e-9 {
61 out.push(fail(
62 "line advances true distance",
63 format!("distance 5 moved {moved}, not 5"),
64 ));
65 }
66 }
67 Err(e) => out.push(fail("line advances true distance", format!("{e:?}"))),
68 }
69 out
70}
71
72fn check_circle<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
74 let mut out = Vec::new();
75 let radius = 4.0;
76 let circle = Curve3::Circle(Circle3 {
77 frame: Frame3 {
78 origin: Point3::ZERO,
79 x: Vec3::X,
80 y: Vec3::Y,
81 z: Vec3::Z,
82 },
83 radius,
84 });
85 if !provider.distance_convention(&circle).is_supported() {
86 return out;
87 }
88 let quarter = core::f64::consts::FRAC_PI_2 * radius;
90 match provider.point_at(&circle, CurveMeasure::Distance(quarter)) {
91 Ok(p) => {
92 let want = Point3::new(0.0, radius, 0.0);
93 let error = (p - want).length();
94 if error > 1e-9 {
95 out.push(fail(
96 "circle reaches angle d/r",
97 format!("quarter circumference landed {error:e} away from the +y axis"),
98 ));
99 }
100 }
101 Err(e) => out.push(fail("circle reaches angle d/r", format!("{e:?}"))),
102 }
103 out
104}
105
106fn sample_curves() -> Vec<Curve3> {
107 vec![
108 Curve3::Line(Line3 {
109 origin: Point3::new(0.0, 0.0, 0.0),
110 direction: Vec3::new(3.0, 4.0, 0.0),
111 }),
112 Curve3::Circle(Circle3 {
113 frame: Frame3 {
114 origin: Point3::new(2.0, 0.0, 1.0),
115 x: Vec3::X,
116 y: Vec3::Y,
117 z: Vec3::Z,
118 },
119 radius: 3.0,
120 }),
121 Curve3::Intrinsic(Intrinsic3::new(
125 Frame3 {
126 origin: Point3::ZERO,
127 x: Vec3::X,
128 y: Vec3::Y,
129 z: Vec3::Z,
130 },
131 CurvatureLaw::circular(0.1),
132 CurvatureLaw::circular(0.04),
133 10.0,
134 )),
135 ]
136}
137
138fn check_tangent_is_unit<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
140 let mut out = Vec::new();
141 for curve in sample_curves() {
142 if !provider.distance_convention(&curve).is_supported() {
143 continue;
144 }
145 for distance in [0.0, 1.5, 4.0] {
146 if let Ok(t) = provider.tangent_at(&curve, CurveMeasure::Distance(distance)) {
147 if (t.length() - 1.0).abs() > 1e-9 {
148 out.push(fail(
149 "tangent is unit length",
150 format!("|t| = {} at distance {distance}", t.length()),
151 ));
152 }
153 }
154 }
155 }
156 out
157}
158
159fn check_frame_is_orthonormal<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
164 let mut out = Vec::new();
165 for curve in sample_curves() {
166 if !provider.distance_convention(&curve).is_supported() {
167 continue;
168 }
169 for distance in [0.0, 1.5, 4.0] {
170 let Ok(frame) = provider.frame_at(&curve, CurveMeasure::Distance(distance)) else {
171 continue;
172 };
173 let checks = [
174 ("x unit", frame.x.length() - 1.0),
175 ("y unit", frame.y.length() - 1.0),
176 ("z unit", frame.z.length() - 1.0),
177 ("x.y", frame.x.dot(frame.y)),
178 ("x.z", frame.x.dot(frame.z)),
179 ("y.z", frame.y.dot(frame.z)),
180 ];
181 for (name, value) in checks {
182 if value.abs() > 1e-9 {
183 out.push(fail(
184 "frame is orthonormal",
185 format!("{name} off by {value:e} at distance {distance}"),
186 ));
187 }
188 }
189 let handed = frame.x.cross(frame.y).dot(frame.z);
191 if (handed - 1.0).abs() > 1e-9 {
192 out.push(fail(
193 "frame is right-handed",
194 format!("x cross y . z = {handed} at distance {distance}"),
195 ));
196 }
197 if let Ok(point) = provider.point_at(&curve, CurveMeasure::Distance(distance)) {
199 let off = (frame.origin - point).length();
200 if off > 1e-9 {
201 out.push(fail(
202 "frame origin is the curve point",
203 format!("origin {off:e} away at distance {distance}"),
204 ));
205 }
206 }
207 if let Ok(tangent) = provider.tangent_at(&curve, CurveMeasure::Distance(distance)) {
209 let off = (frame.x - tangent).length();
210 if off > 1e-9 {
211 out.push(fail(
212 "frame x is the tangent",
213 format!("x differs from tangent by {off:e}"),
214 ));
215 }
216 }
217 }
218 }
219 out
220}
221
222fn check_refusals<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
227 let mut out = Vec::new();
228 for curve in sample_curves() {
229 if !provider.distance_convention(&curve).is_supported() {
230 continue;
231 }
232 for bad in [Scalar::NAN, Scalar::INFINITY, Scalar::NEG_INFINITY] {
233 if provider
234 .point_at(&curve, CurveMeasure::Distance(bad))
235 .is_ok()
236 {
237 out.push(fail(
238 "non-finite distance is refused",
239 format!("point_at accepted {bad}"),
240 ));
241 }
242 if provider
243 .tangent_at(&curve, CurveMeasure::Distance(bad))
244 .is_ok()
245 {
246 out.push(fail(
247 "non-finite distance is refused",
248 format!("tangent_at accepted {bad}"),
249 ));
250 }
251 if provider
252 .frame_at(&curve, CurveMeasure::Distance(bad))
253 .is_ok()
254 {
255 out.push(fail(
256 "non-finite distance is refused",
257 format!("frame_at accepted {bad}"),
258 ));
259 }
260 }
261 }
262 let vertical = Curve3::Line(Line3 {
265 origin: Point3::ZERO,
266 direction: Vec3::ZERO,
267 });
268 if provider.distance_convention(&vertical).is_supported()
269 && provider
270 .point_at(&vertical, CurveMeasure::Distance(1.0))
271 .is_ok()
272 {
273 out.push(fail(
274 "degenerate curve is refused",
275 "a zero-direction line was evaluated instead of refused",
276 ));
277 }
278 out
279}
280
281fn check_measure_routes_differ<E: CurveEvaluator>(provider: &E) -> Vec<ConformanceFailure> {
293 let mut out = Vec::new();
294 let radius = 4.0;
295 let circle = Curve3::Circle(Circle3 {
296 frame: Frame3 {
297 origin: Point3::ZERO,
298 x: Vec3::X,
299 y: Vec3::Y,
300 z: Vec3::Z,
301 },
302 radius,
303 });
304 let value = 1.5;
305 let by_parameter = provider.point_at(&circle, CurveMeasure::Parameter(value));
306 let by_distance = provider.point_at(&circle, CurveMeasure::Distance(value));
307 if let (Ok(p), Ok(d)) = (by_parameter, by_distance) {
308 if (p - d).length() < 1e-9 {
309 out.push(fail(
310 "parameter and distance are distinct",
311 format!("{value} gave the same point as a parameter and as a distance"),
312 ));
313 }
314 }
315 out
316}