use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Frame3, Point3, Scalar, Transform3, Vec3};
use axiolid_curve::{Curve2, Curve3, SeamSide};
pub use axiolid_curve_evaluate_contract::DistanceConvention;
pub mod certify;
pub mod composite;
pub mod frame_bound;
pub mod offset;
pub mod path_bound;
pub mod seam;
pub mod window;
pub use certify::{DistanceInterval, MAX_CERTIFIED_PANELS, POINT_ROUNDING, UNCERTIFIED_LENGTH};
pub use composite::{CompositeBasis, StationCurve, StationPiece, JOINT_TOLERANCE};
pub use frame_bound::{piece_frame_bounds, FrameLawBounds, FrameReading};
pub use offset::{offset_pieces, StationOffset, CUSP_TOLERANCE, OFFSET_TOLERANCE};
pub use path_bound::{
point_bound_tolerance, PATH_POINT_AMBIGUOUS, PATH_POINT_OFF_CURVE, POINT_BOUND_UNSUPPORTED,
};
pub use seam::{
exact_station_seams2, exact_station_seams3, station_seams2, station_seams3, Mitre, StationSeam,
MITRE_TOLERANCE, SEAM_TANGENT_TOLERANCE,
};
pub use window::{
snapped_distance2, snapped_distance3, SEAM_WINDOW_AMBIGUOUS, SEAM_WINDOW_STRADDLED,
WINDOW_CERTIFICATION,
};
use crate::arc_parameter::{
arc_length2, arc_length3, parameter_at_arc_length2, parameter_at_arc_length3,
ARC_LENGTH_TOLERANCE,
};
use crate::curve::{derivative2, derivative3, domain2, domain3, evaluate2, evaluate3};
pub const ORIENTATION_TOLERANCE: Scalar = 1e-9;
pub const KEEPS_UP_TOLERANCE: Scalar = 1e-12;
fn invalid(detail: String) -> GeomError {
GeomError::InvalidInput(detail)
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SectionFrame {
pub point: Point3,
pub tangent: Vec3,
pub lateral: Vec3,
pub up: Vec3,
}
impl SectionFrame {
#[must_use]
pub fn place(&self, lateral: Scalar, vertical: Scalar, longitudinal: Scalar) -> Point3 {
self.point + lateral * self.lateral + vertical * self.up + longitudinal * self.tangent
}
pub fn plan(&self) -> GeomResult<Self> {
let horizontal = Vec3::new(self.tangent.x, self.tangent.y, 0.0);
let length = horizontal.length();
if !length.is_finite() || length <= 1e-12 {
return Err(invalid(
"station: the tangent is vertical, so it has no plan direction".into(),
));
}
let tangent = horizontal / length;
Ok(Self {
point: self.point,
tangent,
lateral: Vec3::new(-tangent.y, tangent.x, 0.0),
up: Vec3::Z,
})
}
pub fn oriented(&self, axis: Option<Vec3>, ref_direction: Option<Vec3>) -> GeomResult<Self> {
let unit = |vector: Vec3, what: &str| {
let length = vector.length();
if vector.is_finite() && length.is_finite() && length > 1e-12 {
Ok(vector / length)
} else {
Err(invalid(format!(
"station: the orientation's {what} {vector:?} is zero or not finite"
)))
}
};
let axis = unit(axis.unwrap_or(Vec3::Z), "axis")?;
let reference = unit(ref_direction.unwrap_or(Vec3::X), "reference direction")?;
if axis.cross(reference).length() <= ORIENTATION_TOLERANCE {
return Err(invalid(format!(
"station: the orientation's axis {axis:?} and reference direction \
{reference:?} are parallel"
)));
}
let x = (reference - reference.dot(axis) * axis).normalize();
let y = axis.cross(x);
let world = |v: Vec3| v.x * self.tangent + v.y * self.lateral + v.z * self.up;
Ok(Self {
point: self.point,
tangent: world(x),
lateral: world(y),
up: world(axis),
})
}
#[must_use]
pub fn placement(&self) -> Transform3 {
Transform3::from_cols(self.tangent, self.lateral, self.up, self.point)
}
#[must_use]
pub fn moved(&self, rigid: Transform3) -> Self {
Self {
point: rigid.transform_point3(self.point),
tangent: rigid.transform_vector3(self.tangent),
lateral: rigid.transform_vector3(self.lateral),
up: rigid.transform_vector3(self.up),
}
}
pub fn carried(&self, rigid: Transform3, measure: DistanceConvention) -> GeomResult<Self> {
if (rigid.transform_vector3(Vec3::Z) - Vec3::Z).length() <= KEEPS_UP_TOLERANCE {
return Ok(self.moved(rigid));
}
if measure == DistanceConvention::PlanDistance {
return Err(GeomError::UnsupportedInput {
backend: axiolid_contracts::BackendId::new("axiolid-evaluate"),
operation: axiolid_contracts::Operation::CurveEvaluation,
input: PLAN_MEASURED_TILTED,
});
}
reference_up_frame(
rigid.transform_point3(self.point),
rigid.transform_vector3(self.tangent),
)
}
#[must_use]
pub fn frame(&self) -> Frame3 {
Frame3 {
origin: self.point,
x: self.tangent,
y: self.up,
z: -self.lateral,
}
}
}
const PLAN_MEASURED_TILTED: &str =
"a station along an elevated or banked curve placed at a station whose frame tilts +Z: its \
plan distance and its section frame carried by the placement are not the placed curve's own \
(#264)";
fn planar_frame(
point: axiolid_core::Point2,
tangent: axiolid_core::Vec2,
) -> GeomResult<SectionFrame> {
let length = tangent.length();
if !length.is_finite() || length <= 0.0 {
return Err(invalid(
"station: the curve has no tangent direction there".into(),
));
}
let t = tangent / length;
Ok(SectionFrame {
point: Point3::new(point.x, point.y, 0.0),
tangent: Vec3::new(t.x, t.y, 0.0),
lateral: Vec3::new(-t.y, t.x, 0.0),
up: Vec3::Z,
})
}
fn reference_up_frame(point: Point3, derivative: Vec3) -> GeomResult<SectionFrame> {
let length = derivative.length();
if !length.is_finite() || length <= 0.0 {
return Err(invalid(
"station: the curve has no tangent direction there".into(),
));
}
let tangent = derivative / length;
let right = tangent.cross(Vec3::Z);
let magnitude = right.length();
if !magnitude.is_finite() || magnitude <= 1e-12 {
return Err(invalid(
"station: the tangent is vertical, so the section has no lateral axis".into(),
));
}
let right = right / magnitude;
Ok(SectionFrame {
point,
tangent,
lateral: -right,
up: right.cross(tangent),
})
}
fn banked_frame(curve: &axiolid_curve::Banked3, d: Scalar) -> GeomResult<SectionFrame> {
let section = crate::banked::banked_section(curve, d)?;
Ok(SectionFrame {
point: section.point,
tangent: section.tangent,
lateral: section.lateral,
up: section.up,
})
}
fn admitted(distance: Scalar, length: Option<Scalar>) -> GeomResult<Scalar> {
if !distance.is_finite() {
return Err(invalid(format!(
"station: distance {distance} along the curve is not finite"
)));
}
if distance < 0.0 {
return Err(invalid(format!(
"station: distance {distance} is before the curve's start"
)));
}
match length {
None => Ok(distance),
Some(length) => {
let slack = ARC_LENGTH_TOLERANCE * length.abs().max(1.0);
if distance > length + slack {
Err(invalid(format!(
"station: distance {distance} is beyond the curve's length {length}"
)))
} else {
Ok(distance.min(length))
}
}
}
}
fn unmeasured(dimension: &str) -> GeomError {
invalid(format!(
"station: this {dimension} curve family has no distance a station can be measured by"
))
}
fn start2(curve: &Curve2) -> Scalar {
match curve {
Curve2::Polyline(_) | Curve2::BSpline(_) => {
let domain = domain2(curve);
domain.start.min(domain.end)
}
_ => 0.0,
}
}
fn start3(curve: &Curve3) -> Scalar {
match curve {
Curve3::Polyline(_) | Curve3::BSpline(_) => {
let domain = domain3(curve);
domain.start.min(domain.end)
}
_ => 0.0,
}
}
pub fn station_length2(curve: &Curve2) -> GeomResult<Option<Scalar>> {
match curve {
Curve2::Line(_) => Ok(None),
Curve2::Circle(_) | Curve2::Ellipse(_) => {
arc_length2(curve, 0.0, core::f64::consts::TAU).map(Some)
}
Curve2::Polyline(_) | Curve2::BSpline(_) => {
let domain = domain2(curve);
let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
if hi <= lo {
return Err(invalid("station: the curve has an empty domain".into()));
}
arc_length2(curve, lo, hi).map(Some)
}
Curve2::Intrinsic(intrinsic) => {
if intrinsic.length.is_finite() && intrinsic.length > 0.0 {
Ok(Some(intrinsic.length))
} else {
Err(invalid(
"station: the intrinsic curve has no positive finite length".into(),
))
}
}
Curve2::Chain(chain) => chain
.length()
.map(Some)
.ok_or_else(|| invalid("station: the chain has no valid length".into())),
_ => Err(unmeasured("2D")),
}
}
pub fn station_length3(curve: &Curve3) -> GeomResult<Option<Scalar>> {
match curve {
Curve3::Line(_) => Ok(None),
Curve3::Circle(_) | Curve3::Ellipse(_) => {
arc_length3(curve, 0.0, core::f64::consts::TAU).map(Some)
}
Curve3::Polyline(_) | Curve3::BSpline(_) => {
let domain = domain3(curve);
let (lo, hi) = (domain.start.min(domain.end), domain.start.max(domain.end));
if hi <= lo {
return Err(invalid("station: the curve has an empty domain".into()));
}
arc_length3(curve, lo, hi).map(Some)
}
Curve3::Intrinsic(intrinsic) => {
if intrinsic.length.is_finite() && intrinsic.length > 0.0 {
Ok(Some(intrinsic.length))
} else {
Err(invalid(
"station: the intrinsic curve has no positive finite length".into(),
))
}
}
Curve3::Elevated(elevated) => station_length2(&elevated.plan),
Curve3::Banked(banked) => {
let domain = domain3(curve);
if domain.end > domain.start {
Ok(Some(domain.end))
} else {
Err(invalid(format!(
"station: the banked curve's cant law has no valid span ({})",
banked.span()
)))
}
}
_ => Err(unmeasured("3D")),
}
}
#[must_use]
pub fn station_frame_is_exact2(curve: &Curve2) -> bool {
matches!(curve, Curve2::Line(_))
}
#[must_use]
pub fn station_frame_is_exact3(curve: &Curve3) -> bool {
matches!(curve, Curve3::Line(_))
}
pub fn station_section2(curve: &Curve2, distance: Scalar) -> GeomResult<SectionFrame> {
station_section2_on(curve, distance, SeamSide::Outgoing)
}
pub fn station_section2_on(
curve: &Curve2,
distance: Scalar,
side: SeamSide,
) -> GeomResult<SectionFrame> {
let distance = admitted(distance, station_length2(curve)?)?;
if let Some(section) = seam::seam_section2(curve, distance, side)? {
return Ok(section);
}
let t = parameter_at_arc_length2(curve, start2(curve), distance)?;
planar_frame(evaluate2(curve, t)?, derivative2(curve, t)?)
}
pub(crate) fn station_on_seam3(curve: &Curve3, distance: Scalar) -> GeomResult<bool> {
if !seam::sided3(curve) {
return Ok(false);
}
let Ok(distance) = admitted(distance, station_length3(curve)?) else {
return Ok(false);
};
Ok(seam::seam_at3(curve, distance)?.is_some())
}
pub fn station_section3(curve: &Curve3, distance: Scalar) -> GeomResult<SectionFrame> {
station_section3_on(curve, distance, SeamSide::Outgoing)
}
pub fn station_section3_on(
curve: &Curve3,
distance: Scalar,
side: SeamSide,
) -> GeomResult<SectionFrame> {
let distance = admitted(distance, station_length3(curve)?)?;
if let Some(section) = seam::seam_section3(curve, distance, side)? {
return Ok(section);
}
match curve {
Curve3::Banked(banked) => banked_frame(banked, distance),
Curve3::Elevated(elevated) => reference_up_frame(
crate::arc_length::elevated_point(elevated, distance)?,
crate::arc_length::elevated_tangent(elevated, distance)?,
),
_ => {
let t = parameter_at_arc_length3(curve, start3(curve), distance)?;
reference_up_frame(evaluate3(curve, t)?, derivative3(curve, t)?)
}
}
}