use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{Frame3, Point3, Scalar, Vec3};
use axiolid_curve::{CurvatureLaw, Intrinsic3};
use crate::frenet::{frenet_frame, frenet_point};
fn unsupported() -> GeomError {
GeomError::Unsupported {
backend: BackendId::new("axiolid-evaluate"),
operation: Operation::CurveEvaluation,
}
}
fn invalid(detail: &str) -> GeomError {
GeomError::InvalidInput(detail.to_owned())
}
pub fn trim_intrinsic3(curve: &Intrinsic3, start: Scalar, end: Scalar) -> GeomResult<Intrinsic3> {
if !start.is_finite() || !end.is_finite() {
return Err(invalid("trim bounds must be finite"));
}
if end <= start {
return Err(invalid("trim end must exceed trim start"));
}
if start < 0.0 || end > curve.length {
return Err(invalid("trim bounds must lie within the curve"));
}
let anchor = frenet_frame(curve, start)?;
let curvature = curve
.curvature
.shifted(start)
.ok_or_else(|| invalid("curvature law cannot be re-anchored at the trim start"))?;
let torsion = curve
.torsion
.shifted(start)
.ok_or_else(|| invalid("torsion law cannot be re-anchored at the trim start"))?;
Ok(Intrinsic3::new(anchor, curvature, torsion, end - start))
}
fn helix_parameters(curve: &Intrinsic3) -> Option<(Scalar, Scalar)> {
let k = curve.curvature.constant_value()?;
let tau = curve.torsion.constant_value()?;
let denominator = k * k + tau * tau;
if denominator <= 0.0 || !denominator.is_finite() {
return None;
}
Some((k / denominator, tau / denominator))
}
pub fn offset_intrinsic3(curve: &Intrinsic3, distance: Scalar) -> GeomResult<Intrinsic3> {
if !distance.is_finite() {
return Err(invalid("offset distance must be finite"));
}
if distance == 0.0 {
return Ok(curve.clone());
}
if !curve.is_helical() {
return Err(unsupported());
}
let (a, b) = helix_parameters(curve).ok_or_else(|| invalid("degenerate helix parameters"))?;
let radius = a - distance;
let c2 = radius.hypot(b);
if c2 <= 0.0 || !c2.is_finite() {
return Err(invalid("offset distance collapses the helix onto its axis"));
}
let base = a.hypot(b);
if base <= 0.0 || !base.is_finite() {
return Err(invalid("degenerate helix parameters"));
}
let curvature = CurvatureLaw::circular(radius / (c2 * c2));
let torsion = CurvatureLaw::circular(b / (c2 * c2));
let start = offset_start_frame(curve, distance, a, b)?;
Ok(Intrinsic3::new(
start,
curvature,
torsion,
curve.length * c2 / base,
))
}
fn offset_start_frame(
curve: &Intrinsic3,
distance: Scalar,
a: Scalar,
b: Scalar,
) -> GeomResult<Frame3> {
let base = a.hypot(b);
let tangent = curve.start.x;
let normal = curve.start.y;
let binormal = curve.start.z;
let axis = (tangent * b + binormal * a) / base;
let axial_component = tangent.dot(axis);
let perpendicular = tangent - axis * axial_component;
let perpendicular_length = perpendicular.length();
if perpendicular_length <= 0.0 || !perpendicular_length.is_finite() {
return Err(invalid("helix axis is parallel to its tangent"));
}
let tangential = perpendicular / perpendicular_length;
let radius = a - distance;
let velocity = tangential * radius + axis * b;
let speed = velocity.length();
if speed <= 0.0 || !speed.is_finite() {
return Err(invalid("offset has no well-defined tangent"));
}
let new_tangent = velocity / speed;
let new_normal = if radius >= 0.0 { normal } else { -normal };
let new_binormal = new_tangent.cross(new_normal);
let binormal_length = new_binormal.length();
if binormal_length <= 0.0 || !binormal_length.is_finite() {
return Err(invalid("offset frame is degenerate"));
}
Ok(Frame3 {
origin: curve.start.origin + normal * distance,
x: new_tangent,
y: new_normal,
z: new_binormal / binormal_length,
})
}
pub fn join_intrinsic3(
first: &Intrinsic3,
second: &Intrinsic3,
position_tolerance: Scalar,
direction_tolerance: Scalar,
) -> GeomResult<Intrinsic3> {
if !position_tolerance.is_finite() || position_tolerance < 0.0 {
return Err(invalid(
"position tolerance must be finite and non-negative",
));
}
if !direction_tolerance.is_finite() || direction_tolerance < 0.0 {
return Err(invalid(
"direction tolerance must be finite and non-negative",
));
}
let end_point = frenet_point(first, first.length)?;
let end_frame = frenet_frame(first, first.length)?;
if !meets(end_point, second.start.origin, position_tolerance) {
return Err(invalid(
"curves do not meet: the second does not start where the first ends",
));
}
if !aligned(end_frame.x, second.start.x, direction_tolerance) {
return Err(invalid(
"curves meet but their tangents disagree, so the join would kink",
));
}
let curvature = CurvatureLaw::piecewise(
vec![first.length],
vec![first.curvature.clone(), second.curvature.clone()],
);
let torsion = CurvatureLaw::piecewise(
vec![first.length],
vec![first.torsion.clone(), second.torsion.clone()],
);
Ok(Intrinsic3::new(
first.start,
curvature,
torsion,
first.length + second.length,
))
}
fn meets(a: Point3, b: Point3, tolerance: Scalar) -> bool {
(a - b).length() <= tolerance
}
fn aligned(a: Vec3, b: Vec3, tolerance: Scalar) -> bool {
(a - b).length() <= tolerance
}