1use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
48use axiolid_core::{Frame3, Point3, Scalar, Vec3};
49use axiolid_curve::Intrinsic3;
50
51const C1: Scalar = 0.211_324_865_405_187_1; const C2: Scalar = 0.788_675_134_594_812_9; const MAGNUS_COMMUTATOR: Scalar = 0.144_337_567_297_406_4;
58
59const PANELS_PER_RADIAN: Scalar = 4.0;
61
62const MAX_PANELS: usize = 4096;
64
65const GAUSS_NODES: [Scalar; 8] = [
68 -0.960_289_856_497_536_2,
69 -0.796_666_477_413_626_7,
70 -0.525_532_409_916_328_9,
71 -0.183_434_642_495_649_8,
72 0.183_434_642_495_649_8,
73 0.525_532_409_916_328_9,
74 0.796_666_477_413_626_7,
75 0.960_289_856_497_536_2,
76];
77
78const GAUSS_WEIGHTS: [Scalar; 8] = [
80 0.101_228_536_290_376_26,
81 0.222_381_034_453_374_5,
82 0.313_706_645_877_887_3,
83 0.362_683_783_378_362,
84 0.362_683_783_378_362,
85 0.313_706_645_877_887_3,
86 0.222_381_034_453_374_5,
87 0.101_228_536_290_376_26,
88];
89
90fn invalid(detail: &str) -> GeomError {
91 GeomError::InvalidInput(detail.to_owned())
92}
93
94fn unsupported() -> GeomError {
95 GeomError::Unsupported {
96 backend: BackendId::new("axiolid-evaluate"),
97 operation: Operation::CurveEvaluation,
98 }
99}
100
101#[derive(Debug, Clone, Copy)]
103struct Rotation {
104 tangent: Vec3,
105 normal: Vec3,
106 binormal: Vec3,
107}
108
109impl Rotation {
110 fn identity_of(frame: &Frame3) -> Self {
111 Self {
112 tangent: frame.x,
113 normal: frame.y,
114 binormal: frame.z,
115 }
116 }
117
118 fn compose(self, other: Self) -> Self {
120 Self {
121 tangent: self.apply(other.tangent),
122 normal: self.apply(other.normal),
123 binormal: self.apply(other.binormal),
124 }
125 }
126
127 fn apply(self, v: Vec3) -> Vec3 {
129 self.tangent * v.x + self.normal * v.y + self.binormal * v.z
130 }
131}
132
133fn exp_skew(w: Vec3) -> Rotation {
138 let theta_sq = w.x * w.x + w.y * w.y + w.z * w.z;
139 let theta = theta_sq.sqrt();
140 let (sin_over, one_minus_cos_over) = if theta < 1e-8 {
143 (1.0 - theta_sq / 6.0, 0.5 - theta_sq / 24.0)
144 } else {
145 (theta.sin() / theta, (1.0 - theta.cos()) / theta_sq)
146 };
147 Rotation {
149 tangent: Vec3::new(
150 1.0 + one_minus_cos_over * (-w.y * w.y - w.z * w.z),
151 sin_over * w.z + one_minus_cos_over * (w.x * w.y),
152 -sin_over * w.y + one_minus_cos_over * (w.x * w.z),
153 ),
154 normal: Vec3::new(
155 -sin_over * w.z + one_minus_cos_over * (w.x * w.y),
156 1.0 + one_minus_cos_over * (-w.x * w.x - w.z * w.z),
157 sin_over * w.x + one_minus_cos_over * (w.y * w.z),
158 ),
159 binormal: Vec3::new(
160 sin_over * w.y + one_minus_cos_over * (w.x * w.z),
161 -sin_over * w.x + one_minus_cos_over * (w.y * w.z),
162 1.0 + one_minus_cos_over * (-w.x * w.x - w.y * w.y),
163 ),
164 }
165}
166
167fn omega_at(curve: &Intrinsic3, s: Scalar) -> GeomResult<Vec3> {
172 let k = law_at(&curve.curvature, s)?;
173 let tau = law_at(&curve.torsion, s)?;
174 Ok(Vec3::new(tau, 0.0, k))
175}
176
177fn law_at(law: &axiolid_curve::CurvatureLaw, s: Scalar) -> GeomResult<Scalar> {
179 value_of(law, s).ok_or_else(|| invalid("law is not defined at that arc length"))
180}
181
182fn value_of(law: &axiolid_curve::CurvatureLaw, s: Scalar) -> Option<Scalar> {
184 use axiolid_curve::CurvatureLaw as L;
185 match law {
186 L::Constant { curvature } => Some(*curvature),
187 L::Polynomial { coefficients } => Some(horner(coefficients, s)),
188 L::Sinusoid {
189 mean,
190 amplitude,
191 angular_frequency,
192 phase,
193 } => Some(mean + amplitude * (angular_frequency * s + phase).sin()),
194 L::Composite {
195 polynomial,
196 harmonics,
197 } => Some(
198 horner(polynomial, s)
199 + harmonics
200 .iter()
201 .map(|h| h.amplitude * (h.angular_frequency * s + h.phase).sin())
202 .sum::<Scalar>(),
203 ),
204 L::Piecewise { breaks, laws } => {
205 if !law.is_well_formed() {
206 return None;
207 }
208 let mut start = 0.0;
211 for (index, piece) in laws.iter().enumerate() {
212 let end = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
213 if s <= end || index + 1 == laws.len() {
214 return value_of(piece, s - start);
215 }
216 start = end;
217 }
218 None
219 }
220 _ => None,
221 }
222}
223
224fn horner(coefficients: &[Scalar], s: Scalar) -> Scalar {
225 coefficients.iter().rev().fold(0.0, |acc, c| acc * s + c)
226}
227
228fn magnus_generator(curve: &Intrinsic3, s0: Scalar, h: Scalar) -> GeomResult<Vec3> {
233 let a = omega_at(curve, s0 + C1 * h)?;
234 let b = omega_at(curve, s0 + C2 * h)?;
235 let cross = Vec3::new(
237 b.y * a.z - b.z * a.y,
238 b.z * a.x - b.x * a.z,
239 b.x * a.y - b.y * a.x,
240 );
241 Ok((a + b) * (h / 2.0) - cross * (MAGNUS_COMMUTATOR * h * h))
242}
243
244fn panel_count(curve: &Intrinsic3, s: Scalar) -> GeomResult<usize> {
249 let planar = axiolid_curve::Intrinsic2::new(
250 axiolid_core::Frame2 {
251 origin: axiolid_core::Point2::new(0.0, 0.0),
252 x: axiolid_core::Vec2::X,
253 y: axiolid_core::Vec2::Y,
254 },
255 curve.curvature.clone(),
256 s,
257 );
258 let twist = axiolid_curve::Intrinsic2::new(
259 axiolid_core::Frame2 {
260 origin: axiolid_core::Point2::new(0.0, 0.0),
261 x: axiolid_core::Vec2::X,
262 y: axiolid_core::Vec2::Y,
263 },
264 curve.torsion.clone(),
265 s,
266 );
267 let bend = planar
268 .turning_variation_bound(s)
269 .ok_or_else(|| invalid("curvature law does not integrate over the requested span"))?;
270 let twist = twist
271 .turning_variation_bound(s)
272 .ok_or_else(|| invalid("torsion law does not integrate over the requested span"))?;
273 let wanted = ((bend + twist) * PANELS_PER_RADIAN).ceil().max(1.0);
274 if !wanted.is_finite() || wanted > MAX_PANELS as Scalar {
275 return Err(invalid(
276 "natural equations need an unbounded number of panels",
277 ));
278 }
279 Ok(wanted as usize)
280}
281
282pub fn frenet_frame(curve: &Intrinsic3, s: Scalar) -> GeomResult<Frame3> {
287 let (rotation, position) = integrate(curve, s)?;
288 Ok(Frame3 {
289 origin: position,
290 x: rotation.tangent,
291 y: rotation.normal,
292 z: rotation.binormal,
293 })
294}
295
296pub fn frenet_point(curve: &Intrinsic3, s: Scalar) -> GeomResult<Point3> {
298 Ok(integrate(curve, s)?.1)
299}
300
301pub fn frenet_tangent(curve: &Intrinsic3, s: Scalar) -> GeomResult<Vec3> {
303 Ok(integrate(curve, s)?.0.tangent)
304}
305
306fn integrate(curve: &Intrinsic3, s: Scalar) -> GeomResult<(Rotation, Point3)> {
308 if !s.is_finite() {
309 return Err(invalid("arc length must be finite"));
310 }
311 if !curve.length.is_finite() || curve.length <= 0.0 {
312 return Err(invalid("curve length must be positive and finite"));
313 }
314 if s < 0.0 || s > curve.length {
315 return Err(invalid("arc length lies outside the curve"));
316 }
317 if !is_orthonormal(&curve.start) {
318 return Err(unsupported());
319 }
320
321 let mut bounds = vec![0.0];
325 bounds.extend(curve.curvature.seams_within(s));
326 bounds.extend(curve.torsion.seams_within(s));
327 bounds.push(s);
328 bounds.sort_by(Scalar::total_cmp);
329 bounds.dedup();
330 let mut rotation = Rotation::identity_of(&curve.start);
331 let mut position = curve.start.origin;
332
333 for window in bounds.windows(2) {
334 let (lo, hi) = (window[0], window[1]);
335 if hi <= lo {
336 continue;
337 }
338 let span = hi - lo;
339 let panels = panel_count(curve, hi)?.max(1);
343 let h = span / panels as Scalar;
344 for panel in 0..panels {
345 let s0 = lo + panel as Scalar * h;
346 let mut tangent_integral = Vec3::ZERO;
348 for (node, weight) in GAUSS_NODES.iter().zip(GAUSS_WEIGHTS.iter()) {
349 let u = 0.5 * h * (node + 1.0);
350 let sub = magnus_generator(curve, s0, u)?;
353 tangent_integral += exp_skew(sub).tangent * *weight;
354 }
355 position += rotation.apply(tangent_integral * (0.5 * h));
356 rotation = rotation.compose(exp_skew(magnus_generator(curve, s0, h)?));
357 }
358 }
359
360 if !position.is_finite() {
361 return Err(invalid("natural equations did not give a finite point"));
362 }
363 Ok((rotation, position))
364}
365
366fn is_orthonormal(frame: &Frame3) -> bool {
373 const TOL: Scalar = 1e-9;
374 let unit = |v: Vec3| (v.length() - 1.0).abs() < TOL;
375 let perp = |a: Vec3, b: Vec3| a.dot(b).abs() < TOL;
376 unit(frame.x)
377 && unit(frame.y)
378 && unit(frame.z)
379 && perp(frame.x, frame.y)
380 && perp(frame.y, frame.z)
381 && perp(frame.z, frame.x)
382 && frame.x.cross(frame.y).dot(frame.z) > 0.0
383}