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}