use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Frame3, Point3, Scalar, Vec3};
use axiolid_curve::Intrinsic3;
const 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;
const PANELS_PER_RADIAN: Scalar = 4.0;
const MAX_PANELS: usize = 4096;
const GAUSS_NODES: [Scalar; 8] = [
-0.960_289_856_497_536_2,
-0.796_666_477_413_626_7,
-0.525_532_409_916_328_9,
-0.183_434_642_495_649_8,
0.183_434_642_495_649_8,
0.525_532_409_916_328_9,
0.796_666_477_413_626_7,
0.960_289_856_497_536_2,
];
const GAUSS_WEIGHTS: [Scalar; 8] = [
0.101_228_536_290_376_26,
0.222_381_034_453_374_5,
0.313_706_645_877_887_3,
0.362_683_783_378_362,
0.362_683_783_378_362,
0.313_706_645_877_887_3,
0.222_381_034_453_374_5,
0.101_228_536_290_376_26,
];
fn invalid(detail: &str) -> GeomError {
GeomError::InvalidInput(detail.to_owned())
}
fn unsupported() -> GeomError {
GeomError::Unsupported {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
}
}
#[derive(Debug, Clone, Copy)]
struct Rotation {
tangent: Vec3,
normal: Vec3,
binormal: Vec3,
}
impl Rotation {
fn identity_of(frame: &Frame3) -> Self {
Self {
tangent: frame.x,
normal: frame.y,
binormal: frame.z,
}
}
fn compose(self, other: Self) -> Self {
Self {
tangent: self.apply(other.tangent),
normal: self.apply(other.normal),
binormal: self.apply(other.binormal),
}
}
fn apply(self, v: Vec3) -> Vec3 {
self.tangent * v.x + self.normal * v.y + self.binormal * v.z
}
}
fn exp_skew(w: Vec3) -> Rotation {
let theta_sq = w.x * w.x + w.y * w.y + w.z * w.z;
let theta = theta_sq.sqrt();
let (sin_over, one_minus_cos_over) = if theta < 1e-8 {
(1.0 - theta_sq / 6.0, 0.5 - theta_sq / 24.0)
} else {
(theta.sin() / theta, (1.0 - theta.cos()) / theta_sq)
};
Rotation {
tangent: Vec3::new(
1.0 + one_minus_cos_over * (-w.y * w.y - w.z * w.z),
sin_over * w.z + one_minus_cos_over * (w.x * w.y),
-sin_over * w.y + one_minus_cos_over * (w.x * w.z),
),
normal: Vec3::new(
-sin_over * w.z + one_minus_cos_over * (w.x * w.y),
1.0 + one_minus_cos_over * (-w.x * w.x - w.z * w.z),
sin_over * w.x + one_minus_cos_over * (w.y * w.z),
),
binormal: Vec3::new(
sin_over * w.y + one_minus_cos_over * (w.x * w.z),
-sin_over * w.x + one_minus_cos_over * (w.y * w.z),
1.0 + one_minus_cos_over * (-w.x * w.x - w.y * w.y),
),
}
}
fn omega_at(curve: &Intrinsic3, s: Scalar) -> GeomResult<Vec3> {
let k = law_at(&curve.curvature, s)?;
let tau = law_at(&curve.torsion, s)?;
Ok(Vec3::new(tau, 0.0, k))
}
fn law_at(law: &axiolid_curve::CurvatureLaw, s: Scalar) -> GeomResult<Scalar> {
value_of(law, s).ok_or_else(|| invalid("law is not defined at that arc length"))
}
fn value_of(law: &axiolid_curve::CurvatureLaw, s: Scalar) -> Option<Scalar> {
use axiolid_curve::CurvatureLaw as L;
match law {
L::Constant { curvature } => Some(*curvature),
L::Polynomial { coefficients } => Some(horner(coefficients, s)),
L::Sinusoid {
mean,
amplitude,
angular_frequency,
phase,
} => Some(mean + amplitude * (angular_frequency * s + phase).sin()),
L::Composite {
polynomial,
harmonics,
} => Some(
horner(polynomial, s)
+ harmonics
.iter()
.map(|h| h.amplitude * (h.angular_frequency * s + h.phase).sin())
.sum::<Scalar>(),
),
L::Piecewise { breaks, laws } => {
if !law.is_well_formed() {
return None;
}
let mut start = 0.0;
for (index, piece) in laws.iter().enumerate() {
let end = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
if s <= end || index + 1 == laws.len() {
return value_of(piece, s - start);
}
start = end;
}
None
}
_ => None,
}
}
fn horner(coefficients: &[Scalar], s: Scalar) -> Scalar {
coefficients.iter().rev().fold(0.0, |acc, c| acc * s + c)
}
fn magnus_generator(curve: &Intrinsic3, s0: Scalar, h: Scalar) -> GeomResult<Vec3> {
let a = omega_at(curve, s0 + C1 * h)?;
let b = omega_at(curve, s0 + C2 * h)?;
let cross = Vec3::new(
b.y * a.z - b.z * a.y,
b.z * a.x - b.x * a.z,
b.x * a.y - b.y * a.x,
);
Ok((a + b) * (h / 2.0) - cross * (MAGNUS_COMMUTATOR * h * h))
}
fn panel_count(curve: &Intrinsic3, s: Scalar) -> GeomResult<usize> {
let planar = axiolid_curve::Intrinsic2::new(
axiolid_core::Frame2 {
origin: axiolid_core::Point2::new(0.0, 0.0),
x: axiolid_core::Vec2::X,
y: axiolid_core::Vec2::Y,
},
curve.curvature.clone(),
s,
);
let twist = axiolid_curve::Intrinsic2::new(
axiolid_core::Frame2 {
origin: axiolid_core::Point2::new(0.0, 0.0),
x: axiolid_core::Vec2::X,
y: axiolid_core::Vec2::Y,
},
curve.torsion.clone(),
s,
);
let bend = planar
.turning_variation_bound(s)
.ok_or_else(|| invalid("curvature law does not integrate over the requested span"))?;
let twist = twist
.turning_variation_bound(s)
.ok_or_else(|| invalid("torsion law does not integrate over the requested span"))?;
let wanted = ((bend + twist) * PANELS_PER_RADIAN).ceil().max(1.0);
if !wanted.is_finite() || wanted > MAX_PANELS as Scalar {
return Err(invalid(
"natural equations need an unbounded number of panels",
));
}
Ok(wanted as usize)
}
pub fn frenet_frame(curve: &Intrinsic3, s: Scalar) -> GeomResult<Frame3> {
let (rotation, position) = integrate(curve, s)?;
Ok(Frame3 {
origin: position,
x: rotation.tangent,
y: rotation.normal,
z: rotation.binormal,
})
}
pub fn frenet_point(curve: &Intrinsic3, s: Scalar) -> GeomResult<Point3> {
Ok(integrate(curve, s)?.1)
}
pub fn frenet_tangent(curve: &Intrinsic3, s: Scalar) -> GeomResult<Vec3> {
Ok(integrate(curve, s)?.0.tangent)
}
fn integrate(curve: &Intrinsic3, s: Scalar) -> GeomResult<(Rotation, Point3)> {
if !s.is_finite() {
return Err(invalid("arc length must be finite"));
}
if !curve.length.is_finite() || curve.length <= 0.0 {
return Err(invalid("curve length must be positive and finite"));
}
if s < 0.0 || s > curve.length {
return Err(invalid("arc length lies outside the curve"));
}
if !is_orthonormal(&curve.start) {
return Err(unsupported());
}
let mut bounds = vec![0.0];
bounds.extend(curve.curvature.seams_within(s));
bounds.extend(curve.torsion.seams_within(s));
bounds.push(s);
bounds.sort_by(Scalar::total_cmp);
bounds.dedup();
let mut rotation = Rotation::identity_of(&curve.start);
let mut position = curve.start.origin;
for window in bounds.windows(2) {
let (lo, hi) = (window[0], window[1]);
if hi <= lo {
continue;
}
let span = hi - lo;
let panels = panel_count(curve, hi)?.max(1);
let h = span / panels as Scalar;
for panel in 0..panels {
let s0 = lo + panel as Scalar * h;
let mut tangent_integral = Vec3::ZERO;
for (node, weight) in GAUSS_NODES.iter().zip(GAUSS_WEIGHTS.iter()) {
let u = 0.5 * h * (node + 1.0);
let sub = magnus_generator(curve, s0, u)?;
tangent_integral += exp_skew(sub).tangent * *weight;
}
position += rotation.apply(tangent_integral * (0.5 * h));
rotation = rotation.compose(exp_skew(magnus_generator(curve, s0, h)?));
}
}
if !position.is_finite() {
return Err(invalid("natural equations did not give a finite point"));
}
Ok((rotation, position))
}
fn is_orthonormal(frame: &Frame3) -> bool {
const TOL: Scalar = 1e-9;
let unit = |v: Vec3| (v.length() - 1.0).abs() < TOL;
let perp = |a: Vec3, b: Vec3| a.dot(b).abs() < TOL;
unit(frame.x)
&& unit(frame.y)
&& unit(frame.z)
&& perp(frame.x, frame.y)
&& perp(frame.y, frame.z)
&& perp(frame.z, frame.x)
&& frame.x.cross(frame.y).dot(frame.z) > 0.0
}