axiolid_evaluate/
arc_length.rs1use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
30use axiolid_core::{Frame2, Point2, Point3, Scalar, Vec2, Vec3};
31use axiolid_curve::{Curve2, Elevated3, Intrinsic2};
32
33const GAUSS_NODES: [Scalar; 8] = [
39 -0.960_289_856_497_536_2,
40 -0.796_666_477_413_626_7,
41 -0.525_532_409_916_328_9,
42 -0.183_434_642_495_649_8,
43 0.183_434_642_495_649_8,
44 0.525_532_409_916_328_9,
45 0.796_666_477_413_626_7,
46 0.960_289_856_497_536_2,
47];
48
49const GAUSS_WEIGHTS: [Scalar; 8] = [
51 0.101_228_536_290_376_3,
52 0.222_381_034_453_374_5,
53 0.313_706_645_877_887_3,
54 0.362_683_783_378_361_9,
55 0.362_683_783_378_361_9,
56 0.313_706_645_877_887_3,
57 0.222_381_034_453_374_5,
58 0.101_228_536_290_376_3,
59];
60
61const PANELS_PER_RADIAN: Scalar = 4.0;
67
68const MAX_PANELS: usize = 4096;
70
71fn unsupported() -> GeomError {
72 GeomError::Unsupported {
73 backend: BackendId::new("axiolid-evaluate"),
74 operation: Operation::CurveEvaluation,
75 }
76}
77
78fn invalid(detail: &str) -> GeomError {
79 GeomError::InvalidInput(detail.to_owned())
80}
81
82fn panel_count(curve: &Intrinsic2, s: Scalar) -> GeomResult<usize> {
92 let variation = curve
93 .turning_variation_bound(s)
94 .ok_or_else(|| invalid("curvature law does not integrate over the requested span"))?;
95 let wanted = (variation * PANELS_PER_RADIAN).ceil().max(1.0);
96 if !wanted.is_finite() || wanted > MAX_PANELS as Scalar {
97 return Err(invalid("curvature law needs an unbounded number of panels"));
98 }
99 Ok(wanted as usize)
100}
101
102pub fn intrinsic_point(curve: &Intrinsic2, s: Scalar) -> GeomResult<Point2> {
107 if !s.is_finite() {
108 return Err(invalid("arc length must be finite"));
109 }
110 let mut bounds = vec![0.0];
113 bounds.extend(curve.curvature.seams_within(s));
114 bounds.push(s);
115 let mut x = 0.0;
116 let mut y = 0.0;
117 for window in bounds.windows(2) {
118 let (lo, hi) = (window[0], window[1]);
119 if hi <= lo {
120 continue;
121 }
122 let span = hi - lo;
123 let panels = panel_count(curve, hi)?.max(1);
127 let step = span / panels as Scalar;
128 for panel in 0..panels {
129 let a = lo + step * panel as Scalar;
130 let half = step / 2.0;
131 let mid = a + half;
132 for (node, weight) in GAUSS_NODES.iter().zip(GAUSS_WEIGHTS.iter()) {
133 let u = mid + half * node;
134 let heading = curve
135 .heading_at(u)
136 .ok_or_else(|| invalid("curvature law does not integrate to the sample"))?;
137 x += weight * half * heading.cos();
138 y += weight * half * heading.sin();
139 }
140 }
141 }
142 Ok(place2(&curve.start, Vec2::new(x, y)))
145}
146
147pub fn intrinsic_tangent(curve: &Intrinsic2, s: Scalar) -> GeomResult<Vec2> {
151 if !s.is_finite() {
152 return Err(invalid("arc length must be finite"));
153 }
154 let heading = curve
155 .heading_at(s)
156 .ok_or_else(|| invalid("curvature law does not integrate to the requested arc length"))?;
157 let local = Vec2::new(heading.cos(), heading.sin());
158 Ok(rotate2(&curve.start, local))
159}
160
161fn place2(frame: &Frame2, local: Vec2) -> Point2 {
163 Point2::new(
164 frame.origin.x + frame.x.x * local.x + frame.y.x * local.y,
165 frame.origin.y + frame.x.y * local.x + frame.y.y * local.y,
166 )
167}
168
169fn rotate2(frame: &Frame2, local: Vec2) -> Vec2 {
171 Vec2::new(
172 frame.x.x * local.x + frame.y.x * local.y,
173 frame.x.y * local.x + frame.y.y * local.y,
174 )
175}
176
177fn plan_point(plan: &Curve2, d: Scalar) -> GeomResult<Point2> {
184 match plan {
185 Curve2::Line(line) => {
186 let direction = unit2(line.direction)?;
187 Ok(Point2::new(
188 line.origin.x + direction.x * d,
189 line.origin.y + direction.y * d,
190 ))
191 }
192 Curve2::Circle(circle) => {
193 if circle.radius <= 0.0 || !circle.radius.is_finite() {
195 return Err(invalid("circle radius must be positive and finite"));
196 }
197 let angle = d / circle.radius;
198 Ok(place2(
199 &circle.frame,
200 Vec2::new(circle.radius * angle.cos(), circle.radius * angle.sin()),
201 ))
202 }
203 Curve2::Intrinsic(intrinsic) => intrinsic_point(intrinsic, d),
204 _ => Err(unsupported()),
205 }
206}
207
208fn plan_tangent(plan: &Curve2, d: Scalar) -> GeomResult<Vec2> {
210 match plan {
211 Curve2::Line(line) => unit2(line.direction),
212 Curve2::Circle(circle) => {
213 if circle.radius <= 0.0 || !circle.radius.is_finite() {
214 return Err(invalid("circle radius must be positive and finite"));
215 }
216 let angle = d / circle.radius;
217 Ok(rotate2(&circle.frame, Vec2::new(-angle.sin(), angle.cos())))
218 }
219 Curve2::Intrinsic(intrinsic) => intrinsic_tangent(intrinsic, d),
220 _ => Err(unsupported()),
221 }
222}
223
224fn unit2(v: Vec2) -> GeomResult<Vec2> {
225 let length = (v.x * v.x + v.y * v.y).sqrt();
226 if !length.is_finite() || length == 0.0 {
227 return Err(invalid("direction must be finite and non-zero"));
228 }
229 Ok(Vec2::new(v.x / length, v.y / length))
230}
231
232pub fn elevated_point(curve: &Elevated3, d: Scalar) -> GeomResult<Point3> {
238 if !d.is_finite() {
239 return Err(invalid("plan distance must be finite"));
240 }
241 let planar = plan_point(&curve.plan, d)?;
242 let height = curve
243 .elevation
244 .height_at(d)
245 .ok_or_else(|| invalid("elevation law has no height at that distance"))?;
246 Ok(Point3::new(planar.x, planar.y, height))
247}
248
249pub fn elevated_tangent(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
255 if !d.is_finite() {
256 return Err(invalid("plan distance must be finite"));
257 }
258 let planar = plan_tangent(&curve.plan, d)?;
259 let grade = curve
260 .elevation
261 .grade_at(d)
262 .ok_or_else(|| invalid("elevation law has no grade at that distance"))?;
263 let scale = (1.0 + grade * grade).sqrt();
264 if !scale.is_finite() || scale == 0.0 {
265 return Err(invalid("grade does not give a finite tangent"));
266 }
267 Ok(Vec3::new(planar.x / scale, planar.y / scale, grade / scale))
268}