use axiolid_core::{Frame2, Vec2};
use axiolid_curve::{Chain2, ChainPiece2, CurvatureLaw, Curve2, Intrinsic2};
use ifc_model::{EntityId, Model};
use super::cubic::{cubic_curve, local_frame, CUBIC};
use super::seam::{check_direction, check_position, HorizontalSeam};
use super::spiral::{curvature_of, is_exactly_lowerable, refuse_unlowerable, transition_curvature};
use super::terminal::split_closing;
use crate::cant::CantLayout;
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::{
read_horizontal_segment, AlignmentUnits, HorizontalSegment, HorizontalSegmentType,
};
use crate::view::AlignmentView;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct HorizontalPlan {
pub curve: Curve2,
pub sources: Vec<EntityId>,
pub seams: Vec<HorizontalSeam>,
}
pub fn lower_horizontal_plan(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
cant: Option<&CantLayout>,
) -> AlignmentResult<HorizontalPlan> {
let view = AlignmentView::for_model(model)?;
let horizontal = model
.get(entity)
.ok_or(AlignmentError::MissingEntity { entity })?;
if !view
.schema
.is_a(&horizontal.type_name, "IfcAlignmentHorizontal")
{
return Err(AlignmentError::WrongType {
entity,
expected: "IfcAlignmentHorizontal",
actual: horizontal.type_name.to_string(),
});
}
let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
let segments = ids
.iter()
.map(|id| read_horizontal_segment(model, *id, units))
.collect::<AlignmentResult<Vec<_>>>()?;
let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
let Some(first) = body.first() else {
return Err(AlignmentError::SemanticViolation {
entity: Some(entity),
rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
});
};
let mut pieces: Vec<(f64, Piece)> = Vec::with_capacity(segments.len());
let mut seams = Vec::with_capacity(segments.len().saturating_sub(1));
let mut station = 0.0_f64;
let mut previous: Option<(&HorizontalSegment, Piece)> = None;
for segment in body {
let piece = segment_piece(segment, cant, station)?;
if let Some((before, before_piece)) = &previous {
seams.push(check_seam(before, before_piece, segment, station)?);
}
let end = station + segment.segment_length;
if end > station {
pieces.push((station, piece.clone()));
}
station = end;
previous = Some((segment, piece));
}
if let (Some(closing), Some((before, before_piece))) = (closing, &previous) {
seams.push(check_seam(before, before_piece, closing, station)?);
}
let direction = Vec2::new(first.start_direction.cos(), first.start_direction.sin());
let start = Frame2 {
origin: first.start_point,
x: direction,
y: Vec2::new(-direction.y, direction.x),
};
let malformed = || AlignmentError::Graph {
detail: "the horizontal layout did not assemble into a well-formed plan curve".to_owned(),
};
let curve = if pieces
.iter()
.all(|(_, piece)| matches!(piece, Piece::Law(_)))
{
let mut breaks = Vec::with_capacity(pieces.len());
let mut laws = Vec::with_capacity(pieces.len());
for (at, piece) in pieces {
if let Piece::Law(law) = piece {
if !laws.is_empty() {
breaks.push(at);
}
laws.push(law);
}
}
let curvature = if laws.len() == 1 {
laws.remove(0)
} else {
CurvatureLaw::piecewise(breaks, laws)
};
let curve = Intrinsic2::new(start, curvature, station);
if !curve.curvature.is_well_formed()
|| curve.total_turning().is_none_or(|turn| !turn.is_finite())
{
return Err(malformed());
}
Curve2::Intrinsic(curve)
} else {
let ends: Vec<f64> = pieces
.iter()
.skip(1)
.map(|(at, _)| *at)
.chain([station])
.collect();
let chain = Chain2::new(
start,
pieces
.into_iter()
.zip(ends)
.map(|((at, piece), end)| {
let length = end - at;
match piece {
Piece::Law(curvature) => ChainPiece2::Intrinsic { curvature, length },
Piece::Cubic(curve) => ChainPiece2::Parametric {
curve,
start: 0.0,
length,
},
}
})
.collect(),
);
if !chain.is_well_formed() {
return Err(malformed());
}
Curve2::Chain(chain)
};
Ok(HorizontalPlan {
curve,
sources: ids,
seams,
})
}
#[derive(Debug, Clone)]
enum Piece {
Law(CurvatureLaw),
Cubic(Curve2),
}
fn check_seam(
before: &HorizontalSegment,
before_piece: &Piece,
next: &HorizontalSegment,
station: f64,
) -> AlignmentResult<HorizontalSeam> {
if let Piece::Law(law) = before_piece {
check_direction(before, law, next)?;
}
check_position(before, next, station)
}
fn segment_piece(
segment: &HorizontalSegment,
cant: Option<&CantLayout>,
station: f64,
) -> AlignmentResult<Piece> {
match &segment.segment_type {
HorizontalSegmentType::Transition(name) if name == CUBIC => {
cubic_curve(segment, local_frame()).map(Piece::Cubic)
}
_ => segment_law(segment, cant, station).map(Piece::Law),
}
}
fn segment_law(
segment: &HorizontalSegment,
cant: Option<&CantLayout>,
station: f64,
) -> AlignmentResult<CurvatureLaw> {
match &segment.segment_type {
HorizontalSegmentType::Line => {
if segment.start_radius != 0.0 || segment.end_radius != 0.0 {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "LINE requires zero start and end radii",
});
}
Ok(CurvatureLaw::straight())
}
HorizontalSegmentType::CircularArc => {
if segment.start_radius == 0.0
|| segment.start_radius != segment.end_radius
|| !segment.start_radius.is_finite()
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CIRCULARARC requires equal, finite, non-zero start and end radii",
});
}
Ok(CurvatureLaw::circular(curvature_of(segment.start_radius)))
}
HorizontalSegmentType::Transition(name) if is_exactly_lowerable(name, cant.is_some()) => {
transition_curvature(segment, name, cant, station)
}
_ => Err(refuse_unlowerable(segment)),
}
}