axiolid_evaluate/
arc_length.rs1use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
22use axiolid_core::{Frame2, Point2, Point3, Scalar, Vec2, Vec3};
23use axiolid_curve::{Curve2, Elevated3, Intrinsic2};
24
25const GAUSS_NODES: [Scalar; 8] = [
31 -0.960_289_856_497_536_2,
32 -0.796_666_477_413_626_7,
33 -0.525_532_409_916_328_9,
34 -0.183_434_642_495_649_8,
35 0.183_434_642_495_649_8,
36 0.525_532_409_916_328_9,
37 0.796_666_477_413_626_7,
38 0.960_289_856_497_536_2,
39];
40
41const GAUSS_WEIGHTS: [Scalar; 8] = [
43 0.101_228_536_290_376_3,
44 0.222_381_034_453_374_5,
45 0.313_706_645_877_887_3,
46 0.362_683_783_378_361_9,
47 0.362_683_783_378_361_9,
48 0.313_706_645_877_887_3,
49 0.222_381_034_453_374_5,
50 0.101_228_536_290_376_3,
51];
52
53const PANELS_PER_RADIAN: Scalar = 4.0;
59
60const MAX_PANELS: usize = 4096;
62
63fn unsupported() -> GeomError {
64 GeomError::Unsupported {
65 backend: BackendId::new("axiolid-evaluate"),
66 operation: Operation::CurveEvaluation,
67 }
68}
69
70fn invalid(detail: &str) -> GeomError {
71 GeomError::InvalidInput(detail.to_owned())
72}
73
74fn panel_count(curve: &Intrinsic2, s: Scalar) -> GeomResult<usize> {
84 let variation = curve
85 .turning_variation_bound(s)
86 .ok_or_else(|| invalid("curvature law does not integrate over the requested span"))?;
87 let wanted = (variation * PANELS_PER_RADIAN).ceil().max(1.0);
88 if !wanted.is_finite() || wanted > MAX_PANELS as Scalar {
89 return Err(invalid("curvature law needs an unbounded number of panels"));
90 }
91 Ok(wanted as usize)
92}
93
94pub fn intrinsic_point(curve: &Intrinsic2, s: Scalar) -> GeomResult<Point2> {
99 if !s.is_finite() {
100 return Err(invalid("arc length must be finite"));
101 }
102 let mut bounds = vec![0.0];
105 bounds.extend(curve.curvature.seams_within(s));
106 bounds.push(s);
107 let mut x = 0.0;
108 let mut y = 0.0;
109 for window in bounds.windows(2) {
110 let (lo, hi) = (window[0], window[1]);
111 if hi <= lo {
112 continue;
113 }
114 let span = hi - lo;
115 let panels = panel_count(curve, hi)?.max(1);
119 let step = span / panels as Scalar;
120 for panel in 0..panels {
121 let a = lo + step * panel as Scalar;
122 let half = step / 2.0;
123 let mid = a + half;
124 for (node, weight) in GAUSS_NODES.iter().zip(GAUSS_WEIGHTS.iter()) {
125 let u = mid + half * node;
126 let heading = curve
127 .heading_at(u)
128 .ok_or_else(|| invalid("curvature law does not integrate to the sample"))?;
129 x += weight * half * heading.cos();
130 y += weight * half * heading.sin();
131 }
132 }
133 }
134 Ok(place2(&curve.start, Vec2::new(x, y)))
137}
138
139pub fn intrinsic_tangent(curve: &Intrinsic2, s: Scalar) -> GeomResult<Vec2> {
143 if !s.is_finite() {
144 return Err(invalid("arc length must be finite"));
145 }
146 let heading = curve
147 .heading_at(s)
148 .ok_or_else(|| invalid("curvature law does not integrate to the requested arc length"))?;
149 let local = Vec2::new(heading.cos(), heading.sin());
150 Ok(rotate2(&curve.start, local))
151}
152
153fn place2(frame: &Frame2, local: Vec2) -> Point2 {
155 Point2::new(
156 frame.origin.x + frame.x.x * local.x + frame.y.x * local.y,
157 frame.origin.y + frame.x.y * local.x + frame.y.y * local.y,
158 )
159}
160
161fn rotate2(frame: &Frame2, local: Vec2) -> Vec2 {
163 Vec2::new(
164 frame.x.x * local.x + frame.y.x * local.y,
165 frame.x.y * local.x + frame.y.y * local.y,
166 )
167}
168
169fn plan_point(plan: &Curve2, d: Scalar) -> GeomResult<Point2> {
176 match plan {
177 Curve2::Line(line) => {
178 let direction = unit2(line.direction)?;
179 Ok(Point2::new(
180 line.origin.x + direction.x * d,
181 line.origin.y + direction.y * d,
182 ))
183 }
184 Curve2::Circle(circle) => {
185 if circle.radius <= 0.0 || !circle.radius.is_finite() {
187 return Err(invalid("circle radius must be positive and finite"));
188 }
189 let angle = d / circle.radius;
190 Ok(place2(
191 &circle.frame,
192 Vec2::new(circle.radius * angle.cos(), circle.radius * angle.sin()),
193 ))
194 }
195 Curve2::Intrinsic(intrinsic) => intrinsic_point(intrinsic, d),
196 _ => Err(unsupported()),
197 }
198}
199
200fn plan_tangent(plan: &Curve2, d: Scalar) -> GeomResult<Vec2> {
202 match plan {
203 Curve2::Line(line) => unit2(line.direction),
204 Curve2::Circle(circle) => {
205 if circle.radius <= 0.0 || !circle.radius.is_finite() {
206 return Err(invalid("circle radius must be positive and finite"));
207 }
208 let angle = d / circle.radius;
209 Ok(rotate2(&circle.frame, Vec2::new(-angle.sin(), angle.cos())))
210 }
211 Curve2::Intrinsic(intrinsic) => intrinsic_tangent(intrinsic, d),
212 _ => Err(unsupported()),
213 }
214}
215
216fn unit2(v: Vec2) -> GeomResult<Vec2> {
217 let length = (v.x * v.x + v.y * v.y).sqrt();
218 if !length.is_finite() || length == 0.0 {
219 return Err(invalid("direction must be finite and non-zero"));
220 }
221 Ok(Vec2::new(v.x / length, v.y / length))
222}
223
224pub fn elevated_point(curve: &Elevated3, d: Scalar) -> GeomResult<Point3> {
230 if !d.is_finite() {
231 return Err(invalid("plan distance must be finite"));
232 }
233 let planar = plan_point(&curve.plan, d)?;
234 let height = curve
235 .elevation
236 .height_at(d)
237 .ok_or_else(|| invalid("elevation law has no height at that distance"))?;
238 Ok(Point3::new(planar.x, planar.y, height))
239}
240
241pub fn elevated_tangent(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
247 if !d.is_finite() {
248 return Err(invalid("plan distance must be finite"));
249 }
250 let planar = plan_tangent(&curve.plan, d)?;
251 let grade = curve
252 .elevation
253 .grade_at(d)
254 .ok_or_else(|| invalid("elevation law has no grade at that distance"))?;
255 let scale = (1.0 + grade * grade).sqrt();
256 if !scale.is_finite() || scale == 0.0 {
257 return Err(invalid("grade does not give a finite tangent"));
258 }
259 Ok(Vec3::new(planar.x / scale, planar.y / scale, grade / scale))
260}