use axiolid_core::{Frame2, Point2, Vec2};
use axiolid_curve::{BSplineCurve2, Curve2, KnotSpec};
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::HorizontalSegment;
pub(super) const CUBIC: &str = "CUBIC";
pub(super) fn cubic_curve(segment: &HorizontalSegment, frame: Frame2) -> AlignmentResult<Curve2> {
if let Some(refusal) = refuse(segment) {
return Err(refusal);
}
let length = segment.segment_length;
let rise = length * length / (6.0 * segment.end_radius);
let local = [
(0.0, 0.0),
(length / 3.0, 0.0),
(2.0 * length / 3.0, 0.0),
(length, rise),
];
let control_points: Vec<Point2> = local
.iter()
.map(|(x, y)| frame.origin + frame.x * *x + frame.y * *y)
.collect();
if !rise.is_finite()
|| control_points
.iter()
.any(|p| !(p.x.is_finite() && p.y.is_finite()))
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CUBIC control points must be finite",
});
}
Ok(Curve2::BSpline(BSplineCurve2 {
degree: 3,
control_points,
knots: vec![0.0, length],
multiplicities: vec![4, 4],
weights: None,
closed: false,
self_intersect: Some(false),
knot_spec: KnotSpec::PiecewiseBezier,
}))
}
pub(super) fn start_frame(segment: &HorizontalSegment) -> Frame2 {
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
Frame2 {
origin: segment.start_point,
x: direction,
y: Vec2::new(-direction.y, direction.x),
}
}
pub(super) fn local_frame() -> Frame2 {
Frame2 {
origin: Point2::new(0.0, 0.0),
x: Vec2::new(1.0, 0.0),
y: Vec2::new(0.0, 1.0),
}
}
pub(super) fn refuse(segment: &HorizontalSegment) -> Option<AlignmentError> {
let invalid = |detail| {
Some(AlignmentError::InvalidSegment {
entity: segment.entity,
detail,
})
};
if !(segment.segment_length.is_finite() && segment.segment_length > 0.0) {
return invalid("CUBIC requires a finite, positive segment length");
}
if !(segment.start_radius.is_finite() && segment.end_radius.is_finite()) {
return invalid("CUBIC endpoint radii must be finite");
}
if segment.start_radius == segment.end_radius {
return invalid(
"CUBIC is a transition and must change curvature: its start and end radii must differ",
);
}
if segment.start_radius != 0.0 {
return Some(AlignmentError::Unsupported {
entity: segment.entity,
type_name: CUBIC.to_owned(),
detail: "IFC4.3 defines CUBIC as y = x^3/(6RL), which leaves a straight; a CUBIC \
starting curved (StartRadiusOfCurvature != 0) would begin part-way along \
a cubic at a point the standard does not fix",
});
}
None
}