use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, EntityScanner, IfcType};
use nalgebra::{Point3, Vector3};
use std::sync::OnceLock;
use crate::{Error, Result};
macro_rules! ifc_type_fn {
($name:ident, $literal:expr) => {
fn $name() -> IfcType {
static T: OnceLock<IfcType> = OnceLock::new();
*T.get_or_init(|| IfcType::from_str($literal))
}
};
}
ifc_type_fn!(t_alignment_curve, "IFCALIGNMENTCURVE");
ifc_type_fn!(t_alignment_2d_horizontal, "IFCALIGNMENT2DHORIZONTAL");
ifc_type_fn!(t_alignment_2d_horizontal_segment, "IFCALIGNMENT2DHORIZONTALSEGMENT");
ifc_type_fn!(t_alignment_2d_vertical, "IFCALIGNMENT2DVERTICAL");
ifc_type_fn!(t_line_segment_2d, "IFCLINESEGMENT2D");
ifc_type_fn!(t_circular_arc_segment_2d, "IFCCIRCULARARCSEGMENT2D");
ifc_type_fn!(t_transition_curve_segment_2d, "IFCTRANSITIONCURVESEGMENT2D");
ifc_type_fn!(t_ver_seg_line, "IFCALIGNMENT2DVERSEGLINE");
ifc_type_fn!(t_ver_seg_parabolic, "IFCALIGNMENT2DVERSEGPARABOLICARC");
ifc_type_fn!(t_ver_seg_circular, "IFCALIGNMENT2DVERSEGCIRCULARARC");
ifc_type_fn!(t_alignment_2d_cant, "IFCALIGNMENT2DCANT");
ifc_type_fn!(t_cant_seg_const, "IFCALIGNMENT2DCANTSEGLINE");
ifc_type_fn!(t_cant_seg_transition, "IFCALIGNMENT2DCANTSEGTRANSITION");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TransitionKind {
Clothoid,
Bloss,
Cosine,
Sine,
CubicParabola,
BiquadraticParabola,
}
impl TransitionKind {
fn from_enum(name: &str) -> Self {
match name {
"CLOTHOIDCURVE" => Self::Clothoid,
"BLOSSCURVE" => Self::Bloss,
"COSINECURVE" => Self::Cosine,
"SINECURVE" => Self::Sine,
"CUBICPARABOLA" => Self::CubicParabola,
"BIQUADRATICPARABOLA" => Self::BiquadraticParabola,
_ => Self::Clothoid,
}
}
fn heading_integral(self, u: f64) -> f64 {
let u = u.clamp(0.0, 1.0);
match self {
Self::Clothoid | Self::CubicParabola => 0.5 * u * u,
Self::Bloss | Self::BiquadraticParabola => {
u * u * u - 0.5 * u * u * u * u
}
Self::Cosine => {
0.5 * u - (std::f64::consts::PI * u).sin() / (2.0 * std::f64::consts::PI)
}
Self::Sine => {
let two_pi = 2.0 * std::f64::consts::PI;
0.5 * u * u + ((two_pi * u).cos() - 1.0) / (4.0 * std::f64::consts::PI.powi(2))
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CantSegSpec {
pub start: f64,
pub length: f64,
pub roll_start: f64,
pub roll_end: f64,
}
#[derive(Debug, Clone, Copy)]
enum CantSeg {
Const { start: f64, length: f64, roll: f64 },
Linear {
start: f64,
length: f64,
roll_start: f64,
roll_end: f64,
},
}
impl CantSeg {
fn from_spec(spec: CantSegSpec) -> Self {
if (spec.roll_end - spec.roll_start).abs() < 1e-12 {
CantSeg::Const {
start: spec.start,
length: spec.length,
roll: spec.roll_start,
}
} else {
CantSeg::Linear {
start: spec.start,
length: spec.length,
roll_start: spec.roll_start,
roll_end: spec.roll_end,
}
}
}
}
pub fn parse_cant(entity: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Vec<CantSegSpec>> {
if entity.ifc_type != t_alignment_2d_cant() {
return Err(Error::geometry(format!(
"#{} is not IfcAlignment2DCant",
entity.id,
)));
}
let rail_head_distance = entity.get_float(0).unwrap_or(1.435); let segs_attr = entity
.get(1)
.ok_or_else(|| Error::geometry("IfcAlignment2DCant missing Segments".to_string()))?;
let seg_refs = segs_attr
.as_list()
.ok_or_else(|| Error::geometry("Cant Segments must be a list".to_string()))?;
let mut specs = Vec::with_capacity(seg_refs.len());
for r in seg_refs {
let sid = r.as_entity_ref().ok_or_else(|| {
Error::geometry("Cant segment ref is not an entity reference".to_string())
})?;
let seg = decoder.decode_by_id(sid)?;
let start = seg.get_float(3).ok_or_else(|| {
Error::geometry(format!("CantSegment #{} missing StartDistAlong", sid))
})?;
let length = seg.get_float(4).ok_or_else(|| {
Error::geometry(format!("CantSegment #{} missing HorizontalLength", sid))
})?;
let start_left = seg.get_float(5).unwrap_or(0.0);
let start_right = seg.get_float(6).unwrap_or(0.0);
let roll_start = ((start_right - start_left) / rail_head_distance.max(1e-9)).atan();
let is_transition = seg.ifc_type == t_cant_seg_transition();
let is_const = seg.ifc_type == t_cant_seg_const();
let (roll_end_left, roll_end_right) = if is_transition {
(
seg.get_float(7).unwrap_or(start_left),
seg.get_float(8).unwrap_or(start_right),
)
} else if is_const {
(start_left, start_right)
} else {
(start_left, start_right)
};
let roll_end = ((roll_end_right - roll_end_left) / rail_head_distance.max(1e-9)).atan();
specs.push(CantSegSpec {
start,
length,
roll_start,
roll_end,
});
}
Ok(specs)
}
fn read_bool(attr: Option<&AttributeValue>) -> bool {
attr.and_then(|v| v.as_enum()).map(|s| s == "T").unwrap_or(false)
}
#[derive(Debug, Clone, Copy)]
enum HSeg {
Line {
sx: f64,
sy: f64,
heading: f64,
length: f64,
cum_start: f64,
},
Arc {
sx: f64,
sy: f64,
heading: f64,
radius: f64,
length: f64,
ccw: bool,
cum_start: f64,
},
Transition {
sx: f64,
sy: f64,
heading: f64,
length: f64,
start_curv: f64,
end_curv: f64,
kind: TransitionKind,
cum_start: f64,
},
}
#[derive(Debug, Clone, Copy)]
enum VSeg {
Line {
start: f64,
length: f64,
h0: f64,
g0: f64,
},
Parabolic {
start: f64,
length: f64,
h0: f64,
g0: f64,
parabola_constant: f64,
is_convex: bool,
},
CircularArc {
start: f64,
length: f64,
h0: f64,
g0: f64,
radius: f64,
is_convex: bool,
},
}
#[derive(Debug, Clone, Copy)]
pub struct AlignmentFrame {
pub origin: Point3<f64>,
pub right: Vector3<f64>,
pub up: Vector3<f64>,
pub tangent: Vector3<f64>,
}
pub struct AlignmentCurve {
horizontal: Vec<HSeg>,
vertical: Vec<VSeg>,
cant: Vec<CantSeg>,
}
impl AlignmentCurve {
pub fn parse(directrix: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Option<Self>> {
if directrix.ifc_type == IfcType::IfcPolyline {
return Self::from_polyline(directrix, decoder).map(Some);
}
if directrix.ifc_type != t_alignment_curve() {
return Ok(None);
}
let angle_scale = decoder.plane_angle_to_radians();
let h_id = directrix.get_ref(0).ok_or_else(|| {
Error::geometry("IfcAlignmentCurve missing Horizontal".to_string())
})?;
let horizontal = parse_horizontal(h_id, decoder, angle_scale)?;
let vertical = match directrix.get(1) {
Some(v) if !v.is_null() => match v.as_entity_ref() {
Some(v_id) => parse_vertical(v_id, decoder)?,
None => Vec::new(),
},
_ => Vec::new(),
};
let cant = Vec::new();
Ok(Some(Self {
horizontal,
vertical,
cant,
}))
}
pub fn with_cant_segments(mut self, segments: Vec<CantSegSpec>) -> Self {
self.cant = segments.into_iter().map(CantSeg::from_spec).collect();
self
}
pub fn auto_attach_cant(self, content: &str, decoder: &mut EntityDecoder) -> Result<Self> {
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, _, _)) = scanner.next_entity() {
if type_name == "IFCALIGNMENT2DCANT" {
let entity = decoder.decode_by_id(id)?;
let specs = parse_cant(&entity, decoder)?;
return Ok(self.with_cant_segments(specs));
}
}
Ok(self)
}
pub fn horizontal_length(&self) -> f64 {
self.horizontal
.last()
.map(|s| h_cum_start(s) + h_length(s))
.unwrap_or(0.0)
}
fn from_polyline(curve: &DecodedEntity, decoder: &mut EntityDecoder) -> Result<Self> {
let points_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
let point_refs = points_attr
.as_list()
.ok_or_else(|| Error::geometry("IfcPolyline Points is not a list".to_string()))?;
if point_refs.len() < 2 {
return Err(Error::geometry(
"IfcPolyline directrix needs ≥ 2 points".to_string(),
));
}
let mut pts: Vec<(f64, f64, f64)> = Vec::with_capacity(point_refs.len());
for r in point_refs {
let pid = r
.as_entity_ref()
.ok_or_else(|| Error::geometry("Polyline point is not an entity ref".to_string()))?;
let p = decoder.decode_by_id(pid)?;
let coords = p
.get_list(0)
.ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
pts.push((x, y, z));
}
let mut horizontal: Vec<HSeg> = Vec::with_capacity(pts.len() - 1);
let mut vertical: Vec<VSeg> = Vec::with_capacity(pts.len() - 1);
let mut cum_xy = 0.0;
for w in pts.windows(2) {
let (x0, y0, z0) = w[0];
let (x1, y1, z1) = w[1];
let dx = x1 - x0;
let dy = y1 - y0;
let dz = z1 - z0;
let len_xy = (dx * dx + dy * dy).sqrt();
if len_xy < 1e-12 {
continue;
}
let heading = dy.atan2(dx);
horizontal.push(HSeg::Line {
sx: x0,
sy: y0,
heading,
length: len_xy,
cum_start: cum_xy,
});
let gradient = dz / len_xy;
vertical.push(VSeg::Line {
start: cum_xy,
length: len_xy,
h0: z0,
g0: gradient,
});
cum_xy += len_xy;
}
if horizontal.is_empty() {
return Err(Error::geometry(
"IfcPolyline directrix degenerated to zero horizontal length".to_string(),
));
}
Ok(Self {
horizontal,
vertical,
cant: Vec::new(),
})
}
pub fn evaluate(&self, station: f64) -> AlignmentFrame {
let (x, y, heading) = self.evaluate_horizontal(station);
let z = self.evaluate_vertical(station);
let slope = self.evaluate_vertical_slope(station);
let cos_h = heading.cos();
let sin_h = heading.sin();
let right = Vector3::new(sin_h, -cos_h, 0.0);
let up = Vector3::new(0.0, 0.0, 1.0);
let inv_norm = (1.0 + slope * slope).sqrt();
let tangent = Vector3::new(cos_h / inv_norm, sin_h / inv_norm, slope / inv_norm);
AlignmentFrame {
origin: Point3::new(x, y, z),
right,
up,
tangent,
}
}
pub fn cant_angle(&self, station: f64) -> f64 {
for seg in &self.cant {
match seg {
CantSeg::Const {
start,
length,
roll,
} => {
if station >= *start - 1e-9 && station <= start + length + 1e-9 {
return *roll;
}
}
CantSeg::Linear {
start,
length,
roll_start,
roll_end,
} => {
if station >= *start - 1e-9 && station <= start + length + 1e-9 {
let t = ((station - start) / length.max(1e-12)).clamp(0.0, 1.0);
return roll_start * (1.0 - t) + roll_end * t;
}
}
}
}
0.0
}
fn evaluate_vertical_slope(&self, station: f64) -> f64 {
if self.vertical.is_empty() {
return 0.0;
}
for seg in &self.vertical {
let start = v_start(seg);
let length = v_length(seg);
if station <= start + length + 1e-9 {
let local = (station - start).max(0.0).min(length);
return v_eval(seg, local).1;
}
}
let last = self.vertical.last().unwrap();
v_eval(last, v_length(last)).1
}
fn evaluate_horizontal(&self, station: f64) -> (f64, f64, f64) {
if self.horizontal.is_empty() {
return (0.0, 0.0, 0.0);
}
for seg in &self.horizontal {
let len = h_length(seg);
let cum = h_cum_start(seg);
if station <= cum + len + 1e-9 {
let local = (station - cum).max(0.0).min(len);
return h_eval(seg, local);
}
}
let last = self.horizontal.last().unwrap();
let len = h_length(last);
let (x, y, h) = h_eval(last, len);
let extra = station - (h_cum_start(last) + len);
(x + extra * h.cos(), y + extra * h.sin(), h)
}
fn evaluate_vertical(&self, station: f64) -> f64 {
if self.vertical.is_empty() {
return 0.0;
}
for seg in &self.vertical {
let start = v_start(seg);
let length = v_length(seg);
if station <= start + length + 1e-9 {
let local = (station - start).max(0.0).min(length);
return v_eval_height(seg, local);
}
}
let last = self.vertical.last().unwrap();
let length = v_length(last);
let (z_end, slope) = v_eval(last, length);
let extra = station - (v_start(last) + length);
z_end + slope * extra
}
}
fn parse_horizontal(
h_id: u32,
decoder: &mut EntityDecoder,
angle_scale: f64,
) -> Result<Vec<HSeg>> {
let h_entity = decoder.decode_by_id(h_id)?;
if h_entity.ifc_type != t_alignment_2d_horizontal() {
return Err(Error::geometry(format!(
"AlignmentCurve.Horizontal #{} is not IfcAlignment2DHorizontal",
h_id,
)));
}
let _start_dist_along = h_entity.get_float(0).unwrap_or(0.0);
let segs_attr = h_entity
.get(1)
.ok_or_else(|| Error::geometry("IfcAlignment2DHorizontal missing Segments".to_string()))?;
let seg_refs = segs_attr
.as_list()
.ok_or_else(|| Error::geometry("Horizontal Segments must be a list".to_string()))?;
let mut segments = Vec::with_capacity(seg_refs.len());
let mut cumulative = 0.0;
for seg_ref in seg_refs {
let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
Error::geometry("Horizontal segment ref is not an entity reference".to_string())
})?;
let seg = decoder.decode_by_id(seg_id)?;
if seg.ifc_type != t_alignment_2d_horizontal_segment() {
return Err(Error::geometry(format!(
"#{} is not IfcAlignment2DHorizontalSegment",
seg_id,
)));
}
let curve_id = seg.get_ref(3).ok_or_else(|| {
Error::geometry(format!(
"IfcAlignment2DHorizontalSegment #{} missing CurveGeometry",
seg_id,
))
})?;
let curve = decoder.decode_by_id(curve_id)?;
let sp_id = curve.get_ref(0).ok_or_else(|| {
Error::geometry(format!("CurveSegment #{} missing StartPoint", curve_id))
})?;
let sp = decoder.decode_by_id(sp_id)?;
let coords = sp
.get_list(0)
.ok_or_else(|| Error::geometry("StartPoint missing Coordinates".to_string()))?;
let sx = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let sy = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let heading_raw = curve.get_float(1).ok_or_else(|| {
Error::geometry(format!(
"CurveSegment #{} missing StartDirection",
curve_id,
))
})?;
let heading = heading_raw * angle_scale;
let length = curve.get_float(2).ok_or_else(|| {
Error::geometry(format!("CurveSegment #{} missing SegmentLength", curve_id))
})?;
let hseg = if curve.ifc_type == t_line_segment_2d() {
HSeg::Line {
sx,
sy,
heading,
length,
cum_start: cumulative,
}
} else if curve.ifc_type == t_circular_arc_segment_2d() {
let radius = curve.get_float(3).ok_or_else(|| {
Error::geometry(format!("CircularArcSegment2D #{} missing Radius", curve_id))
})?;
if radius < 1e-12 {
return Err(Error::geometry(format!(
"CircularArcSegment2D #{} has non-positive radius {}",
curve_id, radius,
)));
}
let ccw = read_bool(curve.get(4));
HSeg::Arc {
sx,
sy,
heading,
radius,
length,
ccw,
cum_start: cumulative,
}
} else if curve.ifc_type == t_transition_curve_segment_2d() {
let start_radius = curve.get_float(3);
let end_radius = curve.get_float(4);
let start_ccw = read_bool(curve.get(5));
let end_ccw = read_bool(curve.get(6));
let start_curv = match start_radius {
Some(r) if r.abs() > 1e-12 => (if start_ccw { 1.0 } else { -1.0 }) / r,
_ => 0.0,
};
let end_curv = match end_radius {
Some(r) if r.abs() > 1e-12 => (if end_ccw { 1.0 } else { -1.0 }) / r,
_ => 0.0,
};
let kind = curve
.get(7)
.and_then(|v| v.as_enum())
.map(TransitionKind::from_enum)
.unwrap_or(TransitionKind::Clothoid);
HSeg::Transition {
sx,
sy,
heading,
length,
start_curv,
end_curv,
kind,
cum_start: cumulative,
}
} else {
return Err(Error::geometry(format!(
"Unsupported horizontal curve geometry at #{}: {}",
curve_id, curve.ifc_type,
)));
};
cumulative += length;
segments.push(hseg);
}
Ok(segments)
}
fn parse_vertical(v_id: u32, decoder: &mut EntityDecoder) -> Result<Vec<VSeg>> {
let v_entity = decoder.decode_by_id(v_id)?;
if v_entity.ifc_type != t_alignment_2d_vertical() {
return Err(Error::geometry(format!(
"AlignmentCurve.Vertical #{} is not IfcAlignment2DVertical",
v_id,
)));
}
let segs_attr = v_entity
.get(0)
.ok_or_else(|| Error::geometry("IfcAlignment2DVertical missing Segments".to_string()))?;
let seg_refs = segs_attr
.as_list()
.ok_or_else(|| Error::geometry("Vertical Segments must be a list".to_string()))?;
let mut segments = Vec::with_capacity(seg_refs.len());
for seg_ref in seg_refs {
let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
Error::geometry("Vertical segment ref is not an entity reference".to_string())
})?;
let seg = decoder.decode_by_id(seg_id)?;
let start = seg.get_float(3).ok_or_else(|| {
Error::geometry(format!(
"VerticalSegment #{} missing StartDistAlong",
seg_id,
))
})?;
let length = seg.get_float(4).ok_or_else(|| {
Error::geometry(format!(
"VerticalSegment #{} missing HorizontalLength",
seg_id,
))
})?;
let h0 = seg
.get_float(5)
.ok_or_else(|| Error::geometry(format!("VerticalSegment #{} missing StartHeight", seg_id)))?;
let g0 = seg.get_float(6).ok_or_else(|| {
Error::geometry(format!(
"VerticalSegment #{} missing StartGradient",
seg_id,
))
})?;
let vseg = if seg.ifc_type == t_ver_seg_line() {
VSeg::Line {
start,
length,
h0,
g0,
}
} else if seg.ifc_type == t_ver_seg_parabolic() {
let parabola_constant = seg.get_float(7).ok_or_else(|| {
Error::geometry(format!(
"ParabolicVerSeg #{} missing ParabolaConstant",
seg_id,
))
})?;
let is_convex = read_bool(seg.get(8));
VSeg::Parabolic {
start,
length,
h0,
g0,
parabola_constant,
is_convex,
}
} else if seg.ifc_type == t_ver_seg_circular() {
let radius = seg.get_float(7).ok_or_else(|| {
Error::geometry(format!("CircularVerSeg #{} missing Radius", seg_id))
})?;
let is_convex = read_bool(seg.get(8));
VSeg::CircularArc {
start,
length,
h0,
g0,
radius,
is_convex,
}
} else {
VSeg::Line {
start,
length,
h0,
g0,
}
};
segments.push(vseg);
}
Ok(segments)
}
fn h_cum_start(seg: &HSeg) -> f64 {
match seg {
HSeg::Line { cum_start, .. }
| HSeg::Arc { cum_start, .. }
| HSeg::Transition { cum_start, .. } => *cum_start,
}
}
fn h_length(seg: &HSeg) -> f64 {
match seg {
HSeg::Line { length, .. }
| HSeg::Arc { length, .. }
| HSeg::Transition { length, .. } => *length,
}
}
fn h_eval(seg: &HSeg, s: f64) -> (f64, f64, f64) {
match seg {
HSeg::Line {
sx,
sy,
heading,
..
} => (sx + s * heading.cos(), sy + s * heading.sin(), *heading),
HSeg::Arc {
sx,
sy,
heading,
radius,
ccw,
..
} => {
let sign = if *ccw { 1.0 } else { -1.0 };
let theta = s / radius;
let new_heading = heading + sign * theta;
let (nx, ny) = if *ccw {
(-heading.sin(), heading.cos())
} else {
(heading.sin(), -heading.cos())
};
let cx = sx + radius * nx;
let cy = sy + radius * ny;
let start_angle = (-ny).atan2(-nx);
let new_angle = start_angle + sign * theta;
(
cx + radius * new_angle.cos(),
cy + radius * new_angle.sin(),
new_heading,
)
}
HSeg::Transition {
sx,
sy,
heading,
length,
start_curv,
end_curv,
kind,
..
} => {
let n = ((s.abs() * 0.5).ceil() as usize).max(16).min(4096);
let ds = s / n as f64;
let mut x = *sx;
let mut y = *sy;
let mut prev_cos = heading.cos();
let mut prev_sin = heading.sin();
for i in 1..=n {
let u = i as f64 * ds;
let h = *heading
+ start_curv * u
+ (end_curv - start_curv) * length * kind.heading_integral(u / length);
let cs = h.cos();
let sn = h.sin();
x += 0.5 * ds * (prev_cos + cs);
y += 0.5 * ds * (prev_sin + sn);
prev_cos = cs;
prev_sin = sn;
}
let final_h = *heading
+ start_curv * s
+ (end_curv - start_curv) * length * kind.heading_integral(s / length);
(x, y, final_h)
}
}
}
fn v_start(seg: &VSeg) -> f64 {
match seg {
VSeg::Line { start, .. }
| VSeg::Parabolic { start, .. }
| VSeg::CircularArc { start, .. } => *start,
}
}
fn v_length(seg: &VSeg) -> f64 {
match seg {
VSeg::Line { length, .. }
| VSeg::Parabolic { length, .. }
| VSeg::CircularArc { length, .. } => *length,
}
}
fn v_eval(seg: &VSeg, s: f64) -> (f64, f64) {
match seg {
VSeg::Line { h0, g0, .. } => (h0 + g0 * s, *g0),
VSeg::Parabolic {
h0,
g0,
parabola_constant,
is_convex,
..
} => {
let sign = if *is_convex { -1.0 } else { 1.0 };
let k = parabola_constant.abs().max(1e-12);
let z = h0 + g0 * s + sign * (s * s) / (2.0 * k);
let slope = g0 + sign * s / k;
(z, slope)
}
VSeg::CircularArc {
h0,
g0,
radius,
is_convex,
..
} => {
let sign = if *is_convex { -1.0 } else { 1.0 };
let r = radius.abs().max(1e-12);
let z = h0 + g0 * s + sign * (s * s) / (2.0 * r);
let slope = g0 + sign * s / r;
(z, slope)
}
}
}
fn v_eval_height(seg: &VSeg, s: f64) -> f64 {
v_eval(seg, s).0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_segment_evaluation() {
let seg = HSeg::Line {
sx: 0.0,
sy: 0.0,
heading: 0.0,
length: 10.0,
cum_start: 0.0,
};
let (x, y, h) = h_eval(&seg, 10.0);
assert!((x - 10.0).abs() < 1e-9);
assert!(y.abs() < 1e-9);
assert!(h.abs() < 1e-9);
}
#[test]
fn fixture_828_arc_endpoint() {
let seg = HSeg::Arc {
sx: 0.0,
sy: 0.0,
heading: 13.35833333_f64.to_radians(),
radius: 9279.0,
length: 2965.68,
ccw: false,
cum_start: 0.0,
};
let (x, y, _) = h_eval(&seg, 2965.68);
assert!((x - 2945.13).abs() < 5.0, "x = {} expected ~2945.13", x);
assert!((y - 216.39).abs() < 5.0, "y = {} expected ~216.39", y);
}
#[test]
fn cant_rotates_frame_axes() {
let curve = AlignmentCurve {
horizontal: vec![HSeg::Line {
sx: 0.0,
sy: 0.0,
heading: 0.0,
length: 100.0,
cum_start: 0.0,
}],
vertical: vec![],
cant: vec![],
};
let frame_no_cant = curve.evaluate(50.0);
assert!((frame_no_cant.right.x).abs() < 1e-9);
assert!((frame_no_cant.right.y + 1.0).abs() < 1e-9);
assert!((frame_no_cant.up.z - 1.0).abs() < 1e-9);
let with_cant = curve.with_cant_segments(vec![CantSegSpec {
start: 0.0,
length: 100.0,
roll_start: std::f64::consts::FRAC_PI_2,
roll_end: std::f64::consts::FRAC_PI_2,
}]);
let angle = with_cant.cant_angle(50.0);
assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
assert!(with_cant.cant_angle(150.0).abs() < 1e-9);
}
#[test]
fn polyline_directrix_evaluates_piecewise() {
let curve = AlignmentCurve {
horizontal: vec![
HSeg::Line {
sx: 0.0,
sy: 0.0,
heading: 0.0,
length: 10.0,
cum_start: 0.0,
},
HSeg::Line {
sx: 10.0,
sy: 0.0,
heading: std::f64::consts::FRAC_PI_2,
length: 10.0,
cum_start: 10.0,
},
],
vertical: vec![
VSeg::Line {
start: 0.0,
length: 10.0,
h0: 0.0,
g0: 0.1,
},
VSeg::Line {
start: 10.0,
length: 10.0,
h0: 1.0,
g0: 0.1,
},
],
cant: vec![],
};
let f1 = curve.evaluate(5.0);
assert!((f1.origin.x - 5.0).abs() < 1e-9);
assert!((f1.origin.y).abs() < 1e-9);
assert!((f1.origin.z - 0.5).abs() < 1e-9);
let f2 = curve.evaluate(15.0);
assert!((f2.origin.x - 10.0).abs() < 1e-9);
assert!((f2.origin.y - 5.0).abs() < 1e-9);
assert!((f2.origin.z - 1.5).abs() < 1e-9);
}
#[test]
fn transition_kind_heading_integral_normalised() {
for kind in [
TransitionKind::Clothoid,
TransitionKind::Bloss,
TransitionKind::Cosine,
TransitionKind::Sine,
TransitionKind::CubicParabola,
TransitionKind::BiquadraticParabola,
] {
assert!(kind.heading_integral(0.0).abs() < 1e-12, "{:?}", kind);
let mid = kind.heading_integral(0.5);
assert!(mid > 0.0 && mid < 0.5, "{:?} mid={}", kind, mid);
let end = kind.heading_integral(1.0);
assert!(end > 0.0 && end < 1.0, "{:?} end={}", kind, end);
}
}
#[test]
fn parabolic_vertical_segment() {
let seg = VSeg::Parabolic {
start: 3600.0,
length: 3685.68,
h0: 399.0,
g0: 0.0579,
parabola_constant: 36000.0,
is_convex: false,
};
let (z, slope) = v_eval(&seg, 1680.0);
assert!((z - 535.472).abs() < 0.01, "z = {}", z);
assert!((slope - 0.1046).abs() < 1e-3, "slope = {}", slope);
}
}