use axiolid_contracts::{
Backend, BackendDescriptor, BackendId, Determinism, ExecutionTarget, GeomError, GeomResult,
};
use axiolid_core::{Frame3, Point3, Scalar, Vec3};
use axiolid_curve::{Curve3, SeamSide};
use axiolid_curve_evaluate_contract::{CurveEvaluator, CurveMeasure, DistanceConvention};
use crate::arc_length::{elevated_point, elevated_tangent};
use crate::banked::{banked_point, banked_section, banked_tangent};
use crate::frenet::{frenet_point, frenet_tangent};
use crate::polyline_length::polyline_parameter;
use crate::station::{station_on_seam3, station_section3_on, SectionFrame};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ReferenceCurveEvaluator {
up: Vec3,
}
impl ReferenceCurveEvaluator {
pub const ID: BackendId = BackendId::new("axiolid-evaluate");
#[must_use]
pub const fn new() -> Self {
Self { up: Vec3::Z }
}
#[must_use]
pub fn with_up(up: Vec3) -> Option<Self> {
let length = up.length();
if !length.is_finite() || length <= 0.0 {
return None;
}
Some(Self { up: up / length })
}
#[must_use]
pub const fn up(&self) -> Vec3 {
self.up
}
}
impl ReferenceCurveEvaluator {
fn banked_frame(&self, curve: &axiolid_curve::Banked3, at: CurveMeasure) -> GeomResult<Frame3> {
self.banked_up()?;
let distance = match at {
CurveMeasure::Distance(distance) | CurveMeasure::Parameter(distance) => distance,
_ => return Err(unsupported()),
};
if !distance.is_finite() {
return Err(invalid("curve measure must be finite"));
}
Ok(banked_section(curve, distance)?.frame())
}
fn banked_up(&self) -> GeomResult<()> {
if self.up != Vec3::Z {
return Err(invalid(
"a banked curve's cant is measured against +Z; this evaluator's reference up differs",
));
}
Ok(())
}
fn reference_frame(&self, origin: Point3, tangent: Vec3) -> GeomResult<Frame3> {
let right = tangent.cross(self.up);
let magnitude = right.length();
if !magnitude.is_finite() || magnitude <= 1e-12 {
return Err(invalid(
"tangent is parallel to the reference up direction, so roll is undefined",
));
}
let right = right / magnitude;
let up = right.cross(tangent);
Ok(Frame3 {
origin,
x: tangent,
y: up,
z: right,
})
}
fn seam_section(
&self,
curve: &Curve3,
at: CurveMeasure,
side: SeamSide,
) -> GeomResult<Option<SectionFrame>> {
if !matches!(side, SeamSide::Outgoing | SeamSide::Incoming) {
return Err(unsupported());
}
let distance = match at {
CurveMeasure::Distance(distance) if convention_for(curve).is_supported() => distance,
CurveMeasure::Parameter(parameter)
if matches!(curve, Curve3::Elevated(_) | Curve3::Banked(_)) =>
{
parameter
}
CurveMeasure::Parameter(_)
if side == SeamSide::Incoming
&& matches!(curve, Curve3::Polyline(_) | Curve3::BSpline(_)) =>
{
return Err(GeomError::UnsupportedInput {
backend: Self::ID,
operation: axiolid_contracts::Operation::CurveEvaluation,
input: PARAMETER_SIDE,
});
}
_ => return Ok(None),
};
if !distance.is_finite() || !station_on_seam3(curve, distance)? {
return Ok(None);
}
station_section3_on(curve, distance, side).map(Some)
}
}
const PARAMETER_SIDE: &str =
"the incoming side at a native parameter of a polyline or a B-spline: seams are located \
by station distance";
impl Default for ReferenceCurveEvaluator {
fn default() -> Self {
Self::new()
}
}
impl Backend for ReferenceCurveEvaluator {
fn descriptor(&self) -> BackendDescriptor {
BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
}
}
fn unsupported() -> GeomError {
GeomError::Unsupported {
backend: ReferenceCurveEvaluator::ID,
operation: axiolid_contracts::Operation::CurveEvaluation,
}
}
fn invalid(detail: &str) -> GeomError {
GeomError::InvalidInput(detail.into())
}
fn finite_value(at: CurveMeasure) -> GeomResult<Scalar> {
let value = at.value();
if !value.is_finite() {
return Err(invalid("curve measure must be finite"));
}
Ok(value)
}
fn convention_for(curve: &Curve3) -> DistanceConvention {
match curve {
Curve3::Intrinsic(_) | Curve3::Line(_) | Curve3::Circle(_) | Curve3::Polyline(_) => {
DistanceConvention::ArcLength3d
}
Curve3::Elevated(_) | Curve3::Banked(_) => DistanceConvention::PlanDistance,
_ => DistanceConvention::Unsupported,
}
}
fn parameter_for(curve: &Curve3, distance: Scalar) -> GeomResult<Scalar> {
if !distance.is_finite() {
return Err(invalid("distance along a curve must be finite"));
}
match curve {
Curve3::Intrinsic(_) | Curve3::Elevated(_) | Curve3::Banked(_) => Ok(distance),
Curve3::Line(l) => {
let speed = l.direction.length();
if !speed.is_finite() || speed <= 0.0 {
return Err(invalid("line has no direction, so no distance along it"));
}
Ok(distance / speed)
}
Curve3::Circle(c) => {
if !c.radius.is_finite() || c.radius <= 0.0 {
return Err(invalid("circle has no positive radius"));
}
Ok(distance / c.radius)
}
Curve3::Polyline(p) => polyline_parameter(p, distance),
_ => Err(unsupported()),
}
}
impl CurveEvaluator for ReferenceCurveEvaluator {
fn distance_convention(&self, curve: &Curve3) -> DistanceConvention {
convention_for(curve)
}
fn determinism(&self) -> Determinism {
Determinism::Bitwise
}
fn point_at(&self, curve: &Curve3, at: CurveMeasure) -> GeomResult<Point3> {
let CurveMeasure::Distance(distance) = at else {
return crate::curve::evaluate3(curve, finite_value(at)?);
};
match curve {
Curve3::Intrinsic(i) => frenet_point(i, distance),
Curve3::Elevated(e) => elevated_point(e, distance),
Curve3::Banked(b) => banked_point(b, distance),
Curve3::Line(_) | Curve3::Circle(_) | Curve3::Polyline(_) => {
crate::curve::evaluate3(curve, parameter_for(curve, distance)?)
}
_ => Err(unsupported()),
}
}
fn tangent_at(&self, curve: &Curve3, at: CurveMeasure) -> GeomResult<Vec3> {
let raw = match at {
CurveMeasure::Parameter(_) => crate::curve::derivative3(curve, finite_value(at)?)?,
CurveMeasure::Distance(distance) => match curve {
Curve3::Intrinsic(i) => frenet_tangent(i, distance)?,
Curve3::Elevated(e) => elevated_tangent(e, distance)?,
Curve3::Banked(b) => banked_tangent(b, distance)?,
Curve3::Line(_) | Curve3::Circle(_) | Curve3::Polyline(_) => {
crate::curve::derivative3(curve, parameter_for(curve, distance)?)?
}
_ => return Err(unsupported()),
},
_ => return Err(unsupported()),
};
let length = raw.length();
if !length.is_finite() || length <= 0.0 {
return Err(invalid("curve has no tangent direction there"));
}
Ok(raw / length)
}
fn frame_at(&self, curve: &Curve3, at: CurveMeasure) -> GeomResult<Frame3> {
if let Curve3::Banked(banked) = curve {
return self.banked_frame(banked, at);
}
let origin = self.point_at(curve, at)?;
let tangent = self.tangent_at(curve, at)?;
self.reference_frame(origin, tangent)
}
fn point_at_on(&self, curve: &Curve3, at: CurveMeasure, side: SeamSide) -> GeomResult<Point3> {
match self.seam_section(curve, at, side)? {
Some(section) => Ok(section.point),
None => self.point_at(curve, at),
}
}
fn tangent_at_on(&self, curve: &Curve3, at: CurveMeasure, side: SeamSide) -> GeomResult<Vec3> {
match self.seam_section(curve, at, side)? {
Some(section) => Ok(section.tangent),
None => self.tangent_at(curve, at),
}
}
fn frame_at_on(&self, curve: &Curve3, at: CurveMeasure, side: SeamSide) -> GeomResult<Frame3> {
let Some(section) = self.seam_section(curve, at, side)? else {
return self.frame_at(curve, at);
};
if let Curve3::Banked(_) = curve {
self.banked_up()?;
return Ok(section.frame());
}
if self.up == Vec3::Z {
return Ok(section.frame());
}
self.reference_frame(section.point, section.tangent)
}
}