1use 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 Curve2::Chain(chain) => crate::chain::chain_point(chain, d),
205 _ => Err(unsupported()),
206 }
207}
208
209fn plan_tangent(plan: &Curve2, d: Scalar) -> GeomResult<Vec2> {
211 match plan {
212 Curve2::Line(line) => unit2(line.direction),
213 Curve2::Circle(circle) => {
214 if circle.radius <= 0.0 || !circle.radius.is_finite() {
215 return Err(invalid("circle radius must be positive and finite"));
216 }
217 let angle = d / circle.radius;
218 Ok(rotate2(&circle.frame, Vec2::new(-angle.sin(), angle.cos())))
219 }
220 Curve2::Intrinsic(intrinsic) => intrinsic_tangent(intrinsic, d),
221 Curve2::Chain(chain) => unit2(crate::chain::chain_tangent(chain, d)?),
222 _ => Err(unsupported()),
223 }
224}
225
226pub(crate) fn plan_derivative(plan: &Curve2, d: Scalar) -> GeomResult<Vec2> {
229 match plan {
230 Curve2::Chain(chain) => crate::chain::chain_tangent(chain, d),
231 other => plan_tangent(other, d),
232 }
233}
234
235pub(crate) fn plan_second_derivative(plan: &Curve2, d: Scalar) -> GeomResult<Vec2> {
238 if !d.is_finite() {
239 return Err(invalid("plan distance must be finite"));
240 }
241 match plan {
242 Curve2::Line(line) => {
243 unit2(line.direction)?;
244 Ok(Vec2::ZERO)
245 }
246 Curve2::Circle(circle) => {
247 if circle.radius <= 0.0 || !circle.radius.is_finite() {
248 return Err(invalid("circle radius must be positive and finite"));
249 }
250 let angle = d / circle.radius;
251 Ok(rotate2(
252 &circle.frame,
253 Vec2::new(-angle.cos(), -angle.sin()) / circle.radius,
254 ))
255 }
256 Curve2::Intrinsic(intrinsic) => {
257 let heading = intrinsic
258 .heading_at(d)
259 .ok_or_else(|| invalid("curvature law does not integrate to that distance"))?;
260 let kappa = crate::frenet::law_value(&intrinsic.curvature, d)
261 .ok_or_else(|| invalid("curvature law is not defined at that distance"))?;
262 Ok(rotate2(
263 &intrinsic.start,
264 Vec2::new(-heading.sin(), heading.cos()) * kappa,
265 ))
266 }
267 Curve2::Chain(chain) => crate::chain::chain_second_derivative(chain, d),
268 _ => Err(unsupported()),
269 }
270}
271
272fn unit2(v: Vec2) -> GeomResult<Vec2> {
273 let length = (v.x * v.x + v.y * v.y).sqrt();
274 if !length.is_finite() || length == 0.0 {
275 return Err(invalid("direction must be finite and non-zero"));
276 }
277 Ok(Vec2::new(v.x / length, v.y / length))
278}
279
280pub fn elevated_point(curve: &Elevated3, d: Scalar) -> GeomResult<Point3> {
286 if !d.is_finite() {
287 return Err(invalid("plan distance must be finite"));
288 }
289 let planar = plan_point(&curve.plan, d)?;
290 let height = crate::elevation::elevation_height(&curve.elevation, d)?;
292 Ok(Point3::new(planar.x, planar.y, height))
293}
294
295pub fn elevated_tangent(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
301 if !d.is_finite() {
302 return Err(invalid("plan distance must be finite"));
303 }
304 let planar = plan_tangent(&curve.plan, d)?;
305 let grade = crate::elevation::elevation_grade(&curve.elevation, d)?;
306 let scale = (1.0 + grade * grade).sqrt();
307 if !scale.is_finite() || scale == 0.0 {
308 return Err(invalid("grade does not give a finite tangent"));
309 }
310 Ok(Vec3::new(planar.x / scale, planar.y / scale, grade / scale))
311}