1use axiolid_curve::{Curve2, Curve3, Elevated3};
18use axiolid_model::{CurveRelation, GeometryGraphBuilder, GeometryNode};
19use ifc_model::{EntityId, Model};
20
21use super::assemble::{finish, LoweredAlignmentCurve};
22use super::elevation::profile_law;
23use crate::cant::CantLayout;
24use crate::error::{AlignmentError, AlignmentResult};
25use crate::horizontal::AlignmentUnits;
26use crate::vertical::read_vertical_segment;
27use crate::view::AlignmentView;
28
29pub fn lower_gradient_curve(
41 model: &Model,
42 entity: EntityId,
43 units: AlignmentUnits,
44) -> AlignmentResult<LoweredAlignmentCurve> {
45 let view = AlignmentView::for_model(model)?;
46 let alignment = model
47 .get(entity)
48 .ok_or(AlignmentError::MissingEntity { entity })?;
49 if !view.schema.is_a(&alignment.type_name, "IfcAlignment") {
50 return Err(AlignmentError::WrongType {
51 entity,
52 expected: "IfcAlignment",
53 actual: alignment.type_name.to_string(),
54 });
55 }
56
57 let horizontal = sole_layout(&view, entity, "IfcAlignmentHorizontal")?;
58 let vertical = sole_layout(&view, entity, "IfcAlignmentVertical")?;
59
60 let cant = optional_cant(model, &view, entity, units)?;
62 let (curve, ids) = compose(model, &view, horizontal, vertical, units, cant.as_ref())?;
63
64 let mut builder = GeometryGraphBuilder::new();
65 let root =
66 builder
67 .push(GeometryNode::Curve3(curve))
68 .map_err(|error| AlignmentError::Graph {
69 detail: error.to_string(),
70 })?;
71
72 let mut sources = vec![horizontal, vertical];
73 sources.extend(ids);
74 finish(builder, root, sources)
75}
76
77fn sole_layout(
82 view: &AlignmentView,
83 alignment: EntityId,
84 expected: &'static str,
85) -> AlignmentResult<EntityId> {
86 let children = view.nested_children(alignment, expected)?;
87 match children.as_slice() {
88 [only] => Ok(*only),
89 [] => Err(AlignmentError::SemanticViolation {
90 entity: Some(alignment),
91 rule: "a gradient curve needs both a horizontal and a vertical layout",
92 }),
93 _ => Err(AlignmentError::SemanticViolation {
94 entity: Some(alignment),
95 rule: "an alignment with several layouts of one kind is ambiguous to compose",
96 }),
97 }
98}
99
100fn sole_plan_curve(
108 model: &Model,
109 horizontal: EntityId,
110 units: AlignmentUnits,
111 cant: Option<&CantLayout>,
112) -> AlignmentResult<Curve2> {
113 let lowered = super::assemble::lower_horizontal_layout(model, horizontal, units, cant)?;
114 let Some(GeometryNode::CurveRelation(CurveRelation::Composite { segments })) =
118 lowered.graph.get(lowered.root)
119 else {
120 return Err(AlignmentError::Unsupported {
121 entity: horizontal,
122 type_name: "IfcAlignmentHorizontal".to_owned(),
123 detail: "plan did not lower to a composite curve",
124 });
125 };
126 let [only] = segments.as_slice() else {
127 return Err(AlignmentError::Unsupported {
128 entity: horizontal,
129 type_name: "IfcAlignmentHorizontal".to_owned(),
130 detail: "a multi-segment plan has no single Curve2 to elevate; the neutral vocabulary has no composite Curve2",
131 });
132 };
133 if let Some(GeometryNode::CurveRelation(CurveRelation::Trimmed { basis, .. })) =
135 lowered.graph.get(only.curve)
136 {
137 if let Some(GeometryNode::Curve2(curve)) = lowered.graph.get(*basis) {
138 return Ok(curve.clone());
139 }
140 }
141 Err(AlignmentError::Unsupported {
142 entity: horizontal,
143 type_name: "IfcAlignmentHorizontal".to_owned(),
144 detail: "plan has no single basis curve to elevate",
145 })
146}
147
148fn compose(
153 model: &Model,
154 view: &AlignmentView<'_>,
155 horizontal: EntityId,
156 vertical: EntityId,
157 units: AlignmentUnits,
158 cant: Option<&CantLayout>,
159) -> AlignmentResult<(Curve3, Vec<EntityId>)> {
160 let plan = sole_plan_curve(model, horizontal, units, cant)?;
161 let ids = view.segment_chain(vertical, "IfcAlignmentVerticalSegment")?;
162 let mut segments = Vec::with_capacity(ids.len());
163 for id in &ids {
164 segments.push(read_vertical_segment(model, *id, units)?);
165 }
166 let elevation = profile_law(&segments)?;
167 Ok((Curve3::Elevated(Elevated3::new(plan, elevation)), ids))
168}
169
170pub fn gradient_curve3(
177 model: &Model,
178 entity: EntityId,
179 units: AlignmentUnits,
180) -> AlignmentResult<Curve3> {
181 let view = AlignmentView::for_model(model)?;
182 let alignment = model
183 .get(entity)
184 .ok_or(AlignmentError::MissingEntity { entity })?;
185 if !view.schema.is_a(&alignment.type_name, "IfcAlignment") {
186 return Err(AlignmentError::WrongType {
187 entity,
188 expected: "IfcAlignment",
189 actual: alignment.type_name.to_string(),
190 });
191 }
192 let horizontal = sole_layout(&view, entity, "IfcAlignmentHorizontal")?;
193 let vertical = sole_layout(&view, entity, "IfcAlignmentVertical")?;
194 let cant = optional_cant(model, &view, entity, units)?;
195 let (curve, _) = compose(model, &view, horizontal, vertical, units, cant.as_ref())?;
196 Ok(curve)
197}
198
199fn optional_cant(
204 model: &Model,
205 view: &AlignmentView,
206 alignment: EntityId,
207 units: AlignmentUnits,
208) -> AlignmentResult<Option<CantLayout>> {
209 let children = view.nested_children(alignment, "IfcAlignmentCant")?;
210 match children.as_slice() {
211 [] => Ok(None),
212 [only] => CantLayout::resolve(model, *only, units).map(Some),
213 _ => Err(AlignmentError::SemanticViolation {
214 entity: Some(alignment),
215 rule: "an alignment with several cant layouts is ambiguous to compose",
216 }),
217 }
218}