use axiolid_contracts::GeomResult;
use axiolid_core::{Point3, Scalar, Vec3};
use axiolid_curve::{OffsetLaw, SeamSide};
use super::beside::{BaseTable, Beside};
use super::panels::{Measured, PANEL_SHARE};
use super::{Native, PlanModel, POINT_ROUNDING};
use crate::station::composite::{StationCurve, StationPiece};
use crate::station::offset::{OffsetTable, StationOffset};
use crate::station::{reference_up_frame, SectionFrame, KEEPS_UP_TOLERANCE};
const AXIS_ROUNDING: Scalar = 1e-14;
enum Source<'c> {
Curve(Box<Native<'c>>),
Offset(Box<(StationOffset<'c>, Beside<'c>)>),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ReadLaw {
pub(crate) point: [Scalar; 2],
pub(crate) tangent: [Scalar; 2],
pub(crate) middle: Vec3,
pub(crate) planar: bool,
pub(crate) measure: Scalar,
pub(crate) point_error: Scalar,
pub(crate) axis_error: Scalar,
}
pub(crate) struct PieceReader<'c> {
source: Source<'c>,
table: BaseTable,
increasing: bool,
planar: bool,
corners: Vec<Scalar>,
}
fn framed(point: Point3, tangent: Vec3) -> GeomResult<SectionFrame> {
if tangent.z == 0.0 {
return Ok(SectionFrame {
point,
tangent,
lateral: Vec3::new(-tangent.y, tangent.x, 0.0),
up: Vec3::Z,
});
}
reference_up_frame(point, tangent)
}
impl<'c> PieceReader<'c> {
pub(crate) fn new(
piece: &StationPiece<'c>,
table: Option<&OffsetTable>,
tolerance: Scalar,
) -> GeomResult<Option<Self>> {
let target = PANEL_SHARE * tolerance;
match piece.curve {
StationCurve::Offset(offset) => {
if piece.placement.is_some() || !matches!(PlanModel::of(&offset), Ok(None)) {
return Ok(None);
}
let Some(table) = table else {
return Ok(None);
};
let a = offset.base_measure(piece.start, table)?;
let b = offset.base_measure(piece.end, table)?;
let Some(beside) = Beside::new(&offset, a.min(b), a.max(b), tolerance)? else {
return Ok(None);
};
let (t0, t1) = (beside.t0, beside.t1);
let lengths = beside
.measured(|curve| BaseTable::build(curve, t0, t1, &beside.breaks, target))?;
let base = offset.base();
let level = match offset.law() {
OffsetLaw::Planar { .. } => true,
OffsetLaw::Linear { start, end, .. } => start.vertical == end.vertical,
_ => false,
};
Ok(Some(Self {
planar: matches!(base.curve, StationCurve::Two(_)) && level,
increasing: base.reversed == piece.reversed,
source: Source::Offset(Box::new((offset, beside))),
table: lengths,
corners: Vec::new(),
}))
}
curve if curve.measure_is_exact() => Ok(None),
curve => {
if let Some(rigid) = piece.placement {
if (rigid.transform_vector3(Vec3::Z) - Vec3::Z).length() > KEEPS_UP_TOLERANCE {
return Ok(None);
}
}
let native = Native::of(curve, piece.start, piece.end)?;
let (ta, tb) = (native.parameter(piece.start)?, native.parameter(piece.end)?);
let (t0, t1) = (ta.min(tb), ta.max(tb));
let corners = native.breaks(1);
let mut breaks = native.breaks(2);
breaks.extend(&corners);
let lengths =
native.measured(|curve| BaseTable::build(curve, t0, t1, &breaks, target))?;
Ok(Some(Self {
planar: matches!(curve, StationCurve::Two(_)),
increasing: !piece.reversed,
source: Source::Curve(Box::new(native)),
table: lengths,
corners,
}))
}
}
}
fn measured<R>(&self, run: impl FnOnce(&Measured<'_>) -> GeomResult<R>) -> GeomResult<R> {
match &self.source {
Source::Curve(native) => native.measured(run),
Source::Offset(offset) => offset.1.measured(run),
}
}
fn travel(&self) -> Scalar {
match &self.source {
Source::Curve(_) => 1.0,
Source::Offset(offset) => offset.1.travel(),
}
}
fn parameter(&self, m: Scalar, table: Option<&OffsetTable>) -> GeomResult<Option<Scalar>> {
match &self.source {
Source::Curve(native) => native.parameter(m).map(Some),
Source::Offset(boxed) => {
let (offset, beside) = boxed.as_ref();
let Some(table) = table else {
return Ok(None);
};
let v = offset.base_measure(m, table)?;
let base = offset.base();
let mb = if base.reversed {
base.end - v
} else {
base.start + v
};
beside.native.parameter(mb).map(Some)
}
}
}
pub(crate) fn read(
&self,
piece: &StationPiece<'c>,
from: Scalar,
to: Scalar,
table: Option<&OffsetTable>,
) -> GeomResult<Option<ReadLaw>> {
let travel = self.travel();
let mut reads = Vec::with_capacity(2);
for (u, side) in [(from, SeamSide::Outgoing), (to, SeamSide::Incoming)] {
let (m, frame) = piece.raw_section(u, side, table)?;
let Some(t) = self.parameter(m, table)? else {
return Ok(None);
};
reads.push((u, frame, t));
}
let (ta, tb) = (reads[0].2, reads[1].2);
self.measured(|curve| {
let mut out = ReadLaw {
point: [0.0; 2],
tangent: [0.0; 2],
middle: Vec3::ZERO,
planar: self.planar,
measure: 0.0,
point_error: 0.0,
axis_error: 0.0,
};
for (u, read, t) in &reads {
let Some((measure, point_error, axis_error)) = self.compare(curve, read, *u, *t)?
else {
return Ok(None);
};
out.measure = out.measure.max(measure);
out.point_error = out.point_error.max(point_error);
out.axis_error = out.axis_error.max(axis_error);
}
let (lo, hi) = (ta.min(tb), ta.max(tb));
let (second, third) = (curve.bounds)(lo, hi)?;
let Some(third) = third.filter(|m| m.is_finite()) else {
return Ok(None);
};
let (velocity, slip) = (curve.velocity)(0.5 * (lo + hi))?;
let sigma = velocity.length() - slip - 0.5 * (hi - lo) * second;
if !(sigma.is_finite() && sigma > 0.0) {
return Ok(None);
}
let x = second / sigma;
let unit_second = third / sigma + 3.0 * x * x;
let k1 = second / (sigma * sigma);
let k2 = unit_second / (sigma * sigma) + x * x / (sigma * sigma);
out.point = [1.0, k1];
out.tangent = [k1, k2];
out.middle = (travel * velocity) / velocity.length();
let finite = [k1, k2, out.measure, out.point_error, out.axis_error]
.iter()
.all(|v| v.is_finite());
Ok(finite.then_some(out))
})
}
fn compare(
&self,
curve: &Measured<'_>,
frame: &SectionFrame,
u: Scalar,
t: Scalar,
) -> GeomResult<Option<(Scalar, Scalar, Scalar)>> {
let (point, error) = (curve.point)(t)?;
let (velocity, slip) = (curve.velocity)(t)?;
let speed = velocity.length();
if !(speed.is_finite() && speed > 2.0 * slip) {
return Ok(None);
}
let tangent = (self.travel() * velocity) / speed;
let exact = framed(point, tangent)?;
let scale = curve.scale.max(point.abs().max_element()).max(1.0);
let point_error = (frame.point - point).length() + error + POINT_ROUNDING * scale;
let corner =
self.corners.contains(&t) && (frame.point - point).length() <= POINT_ROUNDING * scale;
let axis_error = if corner {
AXIS_ROUNDING
} else {
let r_t = 2.0 * slip / speed + AXIS_ROUNDING;
let (r_l, r_u) = if self.planar {
(r_t, 0.0)
} else {
let flat = tangent.x.hypot(tangent.y) - r_t;
if flat.is_nan() || flat <= 0.0 {
return Ok(None);
}
let r_l = 2.0 * r_t / flat;
(r_l, r_l + r_t)
};
((frame.tangent - exact.tangent).length() + r_t)
.max((frame.lateral - exact.lateral).length() + r_l)
.max((frame.up - exact.up).length() + r_u)
+ AXIS_ROUNDING
};
let (lo, hi) = self.table.at(curve, t)?;
let (into_lo, into_hi) = if self.increasing {
(lo, hi)
} else {
let (whole_lo, whole_hi) = self.table.total();
(whole_lo - hi, whole_hi - lo)
};
let measure = (u - into_lo).max(into_hi - u).max(0.0);
Ok(Some((measure, point_error, axis_error)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use axiolid_core::{Frame2, Point2, Vec2};
use axiolid_curve::{Curve2, Ellipse2};
#[test]
fn a_read_off_the_exact_frame_is_charged_its_distance() {
let curve = Curve2::Ellipse(Ellipse2 {
frame: Frame2 {
origin: Point2::new(30.0, 0.0),
x: Vec2::X,
y: Vec2::Y,
},
semi_axis_x: 10.0,
semi_axis_y: 6.0,
});
let piece = StationPiece::between(StationCurve::Two(&curve), 0.0, 20.0).unwrap();
let reader = PieceReader::new(&piece, None, 1e-7).unwrap().unwrap();
let u = 7.0;
let (m, frame) = piece.raw_section(u, SeamSide::Outgoing, None).unwrap();
let t = reader.parameter(m, None).unwrap().unwrap();
let off = 1e-6;
let charge = |frame: SectionFrame| {
reader
.measured(|curve| reader.compare(curve, &frame, u, t))
.unwrap()
.unwrap()
};
let (_, point, axis) = charge(frame);
assert!(point <= 1e-12 && axis <= 1e-8, "{point} {axis}");
let moved = SectionFrame {
point: frame.point + off * Vec3::Z,
..frame
};
assert!(charge(moved).1 >= off);
let turned = |axis: fn(&mut SectionFrame) -> &mut Vec3| {
let mut out = frame;
*axis(&mut out) += off * Vec3::Z;
charge(out).2
};
assert!(turned(|f| &mut f.tangent) >= off);
assert!(turned(|f| &mut f.lateral) >= off);
assert!(turned(|f| &mut f.up) >= off);
}
}