use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Frame2, Point2, Point3, Scalar, Vec2, Vec3};
use axiolid_curve::{Curve2, Elevated3, Intrinsic2};
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_3,
0.222_381_034_453_374_5,
0.313_706_645_877_887_3,
0.362_683_783_378_361_9,
0.362_683_783_378_361_9,
0.313_706_645_877_887_3,
0.222_381_034_453_374_5,
0.101_228_536_290_376_3,
];
const PANELS_PER_RADIAN: Scalar = 4.0;
const MAX_PANELS: usize = 4096;
fn unsupported() -> GeomError {
GeomError::Unsupported {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
}
}
fn invalid(detail: &str) -> GeomError {
GeomError::InvalidInput(detail.to_owned())
}
fn panel_count(curve: &Intrinsic2, s: Scalar) -> GeomResult<usize> {
let variation = curve
.turning_variation_bound(s)
.ok_or_else(|| invalid("curvature law does not integrate over the requested span"))?;
let wanted = (variation * PANELS_PER_RADIAN).ceil().max(1.0);
if !wanted.is_finite() || wanted > MAX_PANELS as Scalar {
return Err(invalid("curvature law needs an unbounded number of panels"));
}
Ok(wanted as usize)
}
pub fn intrinsic_point(curve: &Intrinsic2, s: Scalar) -> GeomResult<Point2> {
if !s.is_finite() {
return Err(invalid("arc length must be finite"));
}
let mut bounds = vec![0.0];
bounds.extend(curve.curvature.seams_within(s));
bounds.push(s);
let mut x = 0.0;
let mut y = 0.0;
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 step = span / panels as Scalar;
for panel in 0..panels {
let a = lo + step * panel as Scalar;
let half = step / 2.0;
let mid = a + half;
for (node, weight) in GAUSS_NODES.iter().zip(GAUSS_WEIGHTS.iter()) {
let u = mid + half * node;
let heading = curve
.heading_at(u)
.ok_or_else(|| invalid("curvature law does not integrate to the sample"))?;
x += weight * half * heading.cos();
y += weight * half * heading.sin();
}
}
}
Ok(place2(&curve.start, Vec2::new(x, y)))
}
pub fn intrinsic_tangent(curve: &Intrinsic2, s: Scalar) -> GeomResult<Vec2> {
if !s.is_finite() {
return Err(invalid("arc length must be finite"));
}
let heading = curve
.heading_at(s)
.ok_or_else(|| invalid("curvature law does not integrate to the requested arc length"))?;
let local = Vec2::new(heading.cos(), heading.sin());
Ok(rotate2(&curve.start, local))
}
fn place2(frame: &Frame2, local: Vec2) -> Point2 {
Point2::new(
frame.origin.x + frame.x.x * local.x + frame.y.x * local.y,
frame.origin.y + frame.x.y * local.x + frame.y.y * local.y,
)
}
fn rotate2(frame: &Frame2, local: Vec2) -> Vec2 {
Vec2::new(
frame.x.x * local.x + frame.y.x * local.y,
frame.x.y * local.x + frame.y.y * local.y,
)
}
fn plan_point(plan: &Curve2, d: Scalar) -> GeomResult<Point2> {
match plan {
Curve2::Line(line) => {
let direction = unit2(line.direction)?;
Ok(Point2::new(
line.origin.x + direction.x * d,
line.origin.y + direction.y * d,
))
}
Curve2::Circle(circle) => {
if circle.radius <= 0.0 || !circle.radius.is_finite() {
return Err(invalid("circle radius must be positive and finite"));
}
let angle = d / circle.radius;
Ok(place2(
&circle.frame,
Vec2::new(circle.radius * angle.cos(), circle.radius * angle.sin()),
))
}
Curve2::Intrinsic(intrinsic) => intrinsic_point(intrinsic, d),
_ => Err(unsupported()),
}
}
fn plan_tangent(plan: &Curve2, d: Scalar) -> GeomResult<Vec2> {
match plan {
Curve2::Line(line) => unit2(line.direction),
Curve2::Circle(circle) => {
if circle.radius <= 0.0 || !circle.radius.is_finite() {
return Err(invalid("circle radius must be positive and finite"));
}
let angle = d / circle.radius;
Ok(rotate2(&circle.frame, Vec2::new(-angle.sin(), angle.cos())))
}
Curve2::Intrinsic(intrinsic) => intrinsic_tangent(intrinsic, d),
_ => Err(unsupported()),
}
}
fn unit2(v: Vec2) -> GeomResult<Vec2> {
let length = (v.x * v.x + v.y * v.y).sqrt();
if !length.is_finite() || length == 0.0 {
return Err(invalid("direction must be finite and non-zero"));
}
Ok(Vec2::new(v.x / length, v.y / length))
}
pub fn elevated_point(curve: &Elevated3, d: Scalar) -> GeomResult<Point3> {
if !d.is_finite() {
return Err(invalid("plan distance must be finite"));
}
let planar = plan_point(&curve.plan, d)?;
let height = curve
.elevation
.height_at(d)
.ok_or_else(|| invalid("elevation law has no height at that distance"))?;
Ok(Point3::new(planar.x, planar.y, height))
}
pub fn elevated_tangent(curve: &Elevated3, d: Scalar) -> GeomResult<Vec3> {
if !d.is_finite() {
return Err(invalid("plan distance must be finite"));
}
let planar = plan_tangent(&curve.plan, d)?;
let grade = curve
.elevation
.grade_at(d)
.ok_or_else(|| invalid("elevation law has no grade at that distance"))?;
let scale = (1.0 + grade * grade).sqrt();
if !scale.is_finite() || scale == 0.0 {
return Err(invalid("grade does not give a finite tangent"));
}
Ok(Vec3::new(planar.x / scale, planar.y / scale, grade / scale))
}