Skip to main content

ifc_alignment/vertical/
layout.rs

1//! Ordering and seam checks for `IfcAlignmentVertical`.
2//!
3//! The vertical counterpart of `CantLayout::resolve`: given the parent
4//! `IfcAlignmentVertical`, resolve its nested segments in authored order
5//! and check that they form one run. Every segment family is read,
6//! including the ones no exact elevation law exists for yet; whether a
7//! profile can be lowered is `profile_law`'s question, not this one's.
8//!
9//! A grade break at a seam is legal IFC4.3 and is reported in
10//! [`VerticalLayout::seams`], not refused (see `seam.rs`). The zero-length
11//! segment IFC4.3 requires at the end of a layout is kept in
12//! [`VerticalLayout::segments`] and checked for contiguity; it closes the
13//! profile and has no seam of its own in [`VerticalLayout::seams`].
14
15use ifc_model::{EntityId, Model};
16
17use crate::curve::terminal::split_closing;
18use crate::curve::SeamTolerance;
19use crate::error::{AlignmentError, AlignmentResult, ProfileSeam};
20use crate::horizontal::AlignmentUnits;
21use crate::vertical::seam::{VerticalSeam, VerticalSeamKind};
22use crate::vertical::segment::{read_vertical_segment, VerticalSegment};
23use crate::view::AlignmentView;
24
25/// One resolved, ordered vertical profile.
26#[derive(Debug, Clone, PartialEq)]
27#[non_exhaustive]
28pub struct VerticalLayout {
29    /// The `IfcAlignmentVertical` entity this layout was resolved from.
30    pub entity: EntityId,
31    segments: Vec<VerticalSegment>,
32    seams: Vec<VerticalSeam>,
33}
34
35impl VerticalLayout {
36    /// Resolve `IfcAlignmentVertical#entity`'s nested `IfcAlignmentSegment`
37    /// chain into an ordered profile.
38    ///
39    /// Checks what the segments state directly: they are contiguous and
40    /// ascending in `StartDistAlong`, at the [`SeamTolerance`] the model
41    /// declares, and a zero-length segment stands only in last place. The
42    /// height seam needs each segment's elevation law and is checked by
43    /// `profile_law` when the profile is lowered. A grade break is recorded
44    /// in [`Self::seams`], not refused: IFC4.3 does not require vertical
45    /// seams to be tangential.
46    ///
47    /// # Errors
48    ///
49    /// Refuses a model that is not IFC4X3, an entity that is not an
50    /// `IfcAlignmentVertical`, an invalid declared `Precision`, a layout
51    /// nesting no segment, a segment that does not read, a gap or overlap
52    /// ([`AlignmentError::InvalidSegment`]), and a zero-length segment that
53    /// is not last or is the only one ([`AlignmentError::SemanticViolation`]).
54    pub fn resolve(
55        model: &Model,
56        entity: EntityId,
57        units: AlignmentUnits,
58    ) -> AlignmentResult<Self> {
59        let view = AlignmentView::for_model(model)?;
60        view.require(entity, "IfcAlignmentVertical")?;
61        let ids = view.segment_chain(entity, "IfcAlignmentVerticalSegment")?;
62        if ids.is_empty() {
63            return Err(AlignmentError::SemanticViolation {
64                entity: Some(entity),
65                rule: "IfcAlignmentVertical must nest at least one IfcAlignmentSegment",
66            });
67        }
68        // The same rule `vertical_profile_law` applies (#141): length seams
69        // within the model's declared precision, gradients at rounding.
70        let tolerance = SeamTolerance::for_model(model, units)?;
71        let mut segments = Vec::with_capacity(ids.len());
72        for id in ids {
73            segments.push(read_vertical_segment(model, id, units)?);
74        }
75        let (body, _closing) = split_closing(&segments, |s| s.horizontal_length, |s| s.entity)?;
76        let mut seams = Vec::with_capacity(body.len().saturating_sub(1));
77        for (index, pair) in segments.windows(2).enumerate() {
78            let [previous, next] = pair else {
79                unreachable!("windows(2) yields pairs")
80            };
81            let previous_end = previous.start_dist_along + previous.horizontal_length;
82            if !tolerance.same_length(next.start_dist_along, previous_end) {
83                return Err(AlignmentError::InvalidSegment {
84                    entity: next.entity,
85                    detail: "vertical segments must be contiguous and ascending in StartDistAlong",
86                });
87            }
88            // The closing segment ends the profile: there is no grade after
89            // it to break, so it has no seam of its own.
90            if index + 1 < body.len() {
91                let kind = if tolerance.same_gradient(next.start_gradient, previous.end_gradient) {
92                    VerticalSeamKind::Tangential
93                } else {
94                    VerticalSeamKind::GradeBreak
95                };
96                seams.push(VerticalSeam {
97                    previous: previous.entity,
98                    next: next.entity,
99                    distance_along: next.start_dist_along,
100                    incoming_gradient: previous.end_gradient,
101                    outgoing_gradient: next.start_gradient,
102                    kind,
103                });
104            }
105        }
106        Ok(Self {
107            entity,
108            segments,
109            seams,
110        })
111    }
112
113    /// Every seam between consecutive segments of positive length, in
114    /// order, with how the grade continues across it.
115    #[must_use]
116    pub fn seams(&self) -> &[VerticalSeam] {
117        &self.seams
118    }
119
120    /// Demand a tangent profile: refuse the first grade break.
121    ///
122    /// IFC4.3 does not require vertical seams to be tangential, so
123    /// [`Self::resolve`] accepts grade breaks. A consumer whose model needs
124    /// a continuous grade (a design check, say) asks for it here.
125    ///
126    /// # Errors
127    ///
128    /// [`AlignmentError::ProfileDiscontinuity`] with
129    /// [`ProfileSeam::Gradient`] at the first grade break.
130    pub fn require_tangential(&self) -> AlignmentResult<()> {
131        match self
132            .seams
133            .iter()
134            .find(|seam| seam.kind == VerticalSeamKind::GradeBreak)
135        {
136            None => Ok(()),
137            Some(seam) => Err(AlignmentError::ProfileDiscontinuity {
138                entity: seam.next,
139                previous: seam.previous,
140                seam: ProfileSeam::Gradient,
141                expected: seam.incoming_gradient,
142                actual: seam.outgoing_gradient,
143            }),
144        }
145    }
146
147    /// Segments in authored (distance-along) order.
148    #[must_use]
149    pub fn segments(&self) -> &[VerticalSegment] {
150        &self.segments
151    }
152
153    /// Distance along where the profile starts.
154    #[must_use]
155    pub fn start_dist_along(&self) -> f64 {
156        self.segments[0].start_dist_along
157    }
158
159    /// Total distance-along span covered by this profile.
160    #[must_use]
161    pub fn length(&self) -> f64 {
162        let last = &self.segments[self.segments.len() - 1];
163        last.start_dist_along + last.horizontal_length - self.start_dist_along()
164    }
165}