use ifc_model::{EntityId, Model};
use crate::curve::terminal::split_closing;
use crate::curve::SeamTolerance;
use crate::error::{AlignmentError, AlignmentResult, ProfileSeam};
use crate::horizontal::AlignmentUnits;
use crate::vertical::seam::{VerticalSeam, VerticalSeamKind};
use crate::vertical::segment::{read_vertical_segment, VerticalSegment};
use crate::view::AlignmentView;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct VerticalLayout {
pub entity: EntityId,
segments: Vec<VerticalSegment>,
seams: Vec<VerticalSeam>,
}
impl VerticalLayout {
pub fn resolve(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<Self> {
let view = AlignmentView::for_model(model)?;
view.require(entity, "IfcAlignmentVertical")?;
let ids = view.segment_chain(entity, "IfcAlignmentVerticalSegment")?;
if ids.is_empty() {
return Err(AlignmentError::SemanticViolation {
entity: Some(entity),
rule: "IfcAlignmentVertical must nest at least one IfcAlignmentSegment",
});
}
let tolerance = SeamTolerance::for_model(model, units)?;
let mut segments = Vec::with_capacity(ids.len());
for id in ids {
segments.push(read_vertical_segment(model, id, units)?);
}
let (body, _closing) = split_closing(&segments, |s| s.horizontal_length, |s| s.entity)?;
let mut seams = Vec::with_capacity(body.len().saturating_sub(1));
for (index, pair) in segments.windows(2).enumerate() {
let [previous, next] = pair else {
unreachable!("windows(2) yields pairs")
};
let previous_end = previous.start_dist_along + previous.horizontal_length;
if !tolerance.same_length(next.start_dist_along, previous_end) {
return Err(AlignmentError::InvalidSegment {
entity: next.entity,
detail: "vertical segments must be contiguous and ascending in StartDistAlong",
});
}
if index + 1 < body.len() {
let kind = if tolerance.same_gradient(next.start_gradient, previous.end_gradient) {
VerticalSeamKind::Tangential
} else {
VerticalSeamKind::GradeBreak
};
seams.push(VerticalSeam {
previous: previous.entity,
next: next.entity,
distance_along: next.start_dist_along,
incoming_gradient: previous.end_gradient,
outgoing_gradient: next.start_gradient,
kind,
});
}
}
Ok(Self {
entity,
segments,
seams,
})
}
#[must_use]
pub fn seams(&self) -> &[VerticalSeam] {
&self.seams
}
pub fn require_tangential(&self) -> AlignmentResult<()> {
match self
.seams
.iter()
.find(|seam| seam.kind == VerticalSeamKind::GradeBreak)
{
None => Ok(()),
Some(seam) => Err(AlignmentError::ProfileDiscontinuity {
entity: seam.next,
previous: seam.previous,
seam: ProfileSeam::Gradient,
expected: seam.incoming_gradient,
actual: seam.outgoing_gradient,
}),
}
}
#[must_use]
pub fn segments(&self) -> &[VerticalSegment] {
&self.segments
}
#[must_use]
pub fn start_dist_along(&self) -> f64 {
self.segments[0].start_dist_along
}
#[must_use]
pub fn length(&self) -> f64 {
let last = &self.segments[self.segments.len() - 1];
last.start_dist_along + last.horizontal_length - self.start_dist_along()
}
}