ifc_alignment/cant/
layout.rs1use ifc_model::{EntityId, Model};
11
12use crate::cant::evaluate::{cant_at, CantAtStation};
13use crate::cant::segment::{read_cant_segment, CantSegment};
14use crate::error::{AlignmentError, AlignmentResult};
15use crate::horizontal::AlignmentUnits;
16use crate::view::AlignmentView;
17
18#[derive(Debug, Clone, PartialEq)]
20#[non_exhaustive]
21pub struct CantLayout {
22 pub entity: EntityId,
24 pub rail_head_distance: f64,
27 segments: Vec<CantSegment>,
28}
29
30impl CantLayout {
31 pub fn resolve(
34 model: &Model,
35 entity: EntityId,
36 units: AlignmentUnits,
37 ) -> AlignmentResult<Self> {
38 let view = AlignmentView::for_model(model)?;
39 let cant_entity = model
40 .get(entity)
41 .ok_or(AlignmentError::MissingEntity { entity })?;
42 if !view.schema.is_a(&cant_entity.type_name, "IfcAlignmentCant") {
43 return Err(AlignmentError::WrongType {
44 entity,
45 expected: "IfcAlignmentCant",
46 actual: cant_entity.type_name.to_string(),
47 });
48 }
49 let rail_head_distance = cant_entity
53 .attributes
54 .last()
55 .and_then(|value| value.unwrap_typed().as_f64())
56 .ok_or(AlignmentError::InvalidAttribute {
57 entity,
58 index: cant_entity.attributes.len().saturating_sub(1),
59 name: "RailHeadDistance",
60 })?
61 * units.length_to_metres;
62 if !(rail_head_distance.is_finite() && rail_head_distance > 0.0) {
63 return Err(AlignmentError::InvalidAttribute {
64 entity,
65 index: cant_entity.attributes.len().saturating_sub(1),
66 name: "RailHeadDistance",
67 });
68 }
69
70 let ids = view.segment_chain(entity, "IfcAlignmentCantSegment")?;
71 if ids.is_empty() {
72 return Err(AlignmentError::SemanticViolation {
73 entity: Some(entity),
74 rule: "IfcAlignmentCant must nest at least one IfcAlignmentSegment",
75 });
76 }
77 let mut segments = Vec::with_capacity(ids.len());
78 for id in ids {
79 segments.push(read_cant_segment(model, id, units)?);
80 }
81
82 for pair in segments.windows(2) {
87 let [previous, next] = pair else {
88 unreachable!()
89 };
90 let previous_end = previous.start_dist_along + previous.horizontal_length;
91 if (next.start_dist_along - previous_end).abs() > 1e-6 {
92 return Err(AlignmentError::SemanticViolation {
93 entity: Some(next.entity),
94 rule: "cant segments must be contiguous in StartDistAlong order",
95 });
96 }
97 let previous_end_left = previous.end_cant_left.unwrap_or(previous.start_cant_left);
98 let previous_end_right = previous.end_cant_right.unwrap_or(previous.start_cant_right);
99 if (previous_end_left - next.start_cant_left).abs() > 1e-9
100 || (previous_end_right - next.start_cant_right).abs() > 1e-9
101 {
102 return Err(AlignmentError::SemanticViolation {
103 entity: Some(next.entity),
104 rule: "cant segments must be C0 continuous: end cant must equal the next segment's start cant",
105 });
106 }
107 }
108
109 Ok(Self {
110 entity,
111 rail_head_distance,
112 segments,
113 })
114 }
115
116 #[must_use]
118 pub fn segments(&self) -> &[CantSegment] {
119 &self.segments
120 }
121
122 #[must_use]
124 pub fn length(&self) -> f64 {
125 self.segments
126 .last()
127 .map(|last| {
128 last.start_dist_along + last.horizontal_length - self.segments[0].start_dist_along
129 })
130 .unwrap_or(0.0)
131 }
132
133 pub fn cant_at_distance(&self, distance_along: f64) -> AlignmentResult<CantAtStation> {
135 if !distance_along.is_finite() {
136 return Err(AlignmentError::InvalidUnits {
137 detail: "distance along must be finite",
138 });
139 }
140 let segment = self
141 .segments
142 .iter()
143 .find(|segment| {
144 let start = segment.start_dist_along;
145 let end = start + segment.horizontal_length;
146 distance_along >= start - 1e-9 && distance_along <= end + 1e-9
147 })
148 .ok_or(AlignmentError::InvalidUnits {
149 detail: "distance along lies outside the cant profile's span",
150 })?;
151 let xi = if segment.horizontal_length > 0.0 {
152 ((distance_along - segment.start_dist_along) / segment.horizontal_length)
153 .clamp(0.0, 1.0)
154 } else {
155 0.0
156 };
157 cant_at(segment, xi, Some(self.rail_head_distance))
158 }
159}