1use ifc_model::value::Value;
10use ifc_model::{EntityId, Model};
11
12use crate::error::{AlignmentError, AlignmentResult};
13use crate::horizontal::AlignmentUnits;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum CurveMeasure {
19 Length(f64),
21 Parameter(f64),
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct PointByDistance {
28 pub entity: EntityId,
30 pub distance_along: CurveMeasure,
33 pub offset_lateral: Option<f64>,
35 pub offset_vertical: Option<f64>,
37 pub offset_longitudinal: Option<f64>,
39 pub basis_curve: EntityId,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45#[non_exhaustive]
46pub struct LinearPlacement {
47 pub entity: EntityId,
49 pub relative_placement: PointByDistance,
51 pub has_explicit_axes: bool,
54}
55
56pub fn resolve_point_by_distance(
61 model: &Model,
62 id: EntityId,
63 units: AlignmentUnits,
64) -> AlignmentResult<PointByDistance> {
65 let entity = model
66 .get(id)
67 .ok_or(AlignmentError::MissingEntity { entity: id })?;
68 if !entity.is_type("IFCPOINTBYDISTANCEEXPRESSION") {
69 return Err(AlignmentError::WrongType {
70 entity: id,
71 expected: "IFCPOINTBYDISTANCEEXPRESSION",
72 actual: entity.type_name.to_string(),
73 });
74 }
75 let values = &entity.attributes;
80 let distance_along = curve_measure(values, id, 0, "DistanceAlong", units)?;
81 let offset_lateral = optional_number(values, id, 1, "OffsetLateral")?.map(|v| length(v, units));
82 let offset_vertical =
83 optional_number(values, id, 2, "OffsetVertical")?.map(|v| length(v, units));
84 let offset_longitudinal =
85 optional_number(values, id, 3, "OffsetLongitudinal")?.map(|v| length(v, units));
86 let basis_curve = reference(values, id, 4, "BasisCurve")?;
87 if model.get(basis_curve).is_none() {
91 return Err(AlignmentError::DanglingReference {
92 entity: id,
93 attribute: "BasisCurve",
94 target: basis_curve,
95 });
96 }
97
98 Ok(PointByDistance {
99 entity: id,
100 distance_along,
101 offset_lateral,
102 offset_vertical,
103 offset_longitudinal,
104 basis_curve,
105 })
106}
107
108pub fn resolve_linear_placement(
116 model: &Model,
117 id: EntityId,
118 units: AlignmentUnits,
119) -> AlignmentResult<LinearPlacement> {
120 let entity = model
121 .get(id)
122 .ok_or(AlignmentError::MissingEntity { entity: id })?;
123 if !entity.is_type("IFCLINEARPLACEMENT") {
124 return Err(AlignmentError::WrongType {
125 entity: id,
126 expected: "IFCLINEARPLACEMENT",
127 actual: entity.type_name.to_string(),
128 });
129 }
130 let values = &entity.attributes;
133 let relative_placement_id = reference(values, id, 1, "RelativePlacement")?;
134 let placement_entity =
135 model
136 .get(relative_placement_id)
137 .ok_or(AlignmentError::DanglingReference {
138 entity: id,
139 attribute: "RelativePlacement",
140 target: relative_placement_id,
141 })?;
142 if !placement_entity.is_type("IFCAXIS2PLACEMENTLINEAR") {
143 return Err(AlignmentError::WrongType {
144 entity: relative_placement_id,
145 expected: "IFCAXIS2PLACEMENTLINEAR",
146 actual: placement_entity.type_name.to_string(),
147 });
148 }
149 let placement_values = &placement_entity.attributes;
153 let location_id = reference(placement_values, relative_placement_id, 0, "Location")?;
154 let relative_placement = resolve_point_by_distance(model, location_id, units)?;
155 let has_explicit_axes = matches!(placement_values.get(1), Some(Value::Ref(_)))
156 || matches!(placement_values.get(2), Some(Value::Ref(_)));
157 let axis_present = matches!(placement_values.get(1), Some(Value::Ref(_)));
159 let ref_direction_present = matches!(placement_values.get(2), Some(Value::Ref(_)));
160 if axis_present != ref_direction_present {
161 return Err(AlignmentError::InvalidSegment {
162 entity: relative_placement_id,
163 detail: "IfcAxis2PlacementLinear.Axis and RefDirection must both be present or both absent (WR2)",
164 });
165 }
166
167 Ok(LinearPlacement {
168 entity: id,
169 relative_placement,
170 has_explicit_axes,
171 })
172}
173
174fn curve_measure(
175 values: &[Value],
176 id: EntityId,
177 slot: usize,
178 name: &'static str,
179 units: AlignmentUnits,
180) -> AlignmentResult<CurveMeasure> {
181 match values.get(slot) {
186 Some(Value::Typed { type_name, value }) => {
187 let raw = value.as_f64().ok_or(AlignmentError::InvalidAttribute {
188 entity: id,
189 index: slot,
190 name,
191 })?;
192 if type_name.eq_ignore_ascii_case("IFCLENGTHMEASURE") {
193 Ok(CurveMeasure::Length(raw * units.length_to_metres))
194 } else if type_name.eq_ignore_ascii_case("IFCPARAMETERVALUE") {
195 Ok(CurveMeasure::Parameter(raw))
196 } else {
197 Err(AlignmentError::InvalidAttribute {
198 entity: id,
199 index: slot,
200 name,
201 })
202 }
203 }
204 Some(Value::Real(value)) => Ok(CurveMeasure::Length(value * units.length_to_metres)),
205 Some(Value::Integer(value)) => {
206 Ok(CurveMeasure::Length(*value as f64 * units.length_to_metres))
207 }
208 _ => Err(AlignmentError::InvalidAttribute {
209 entity: id,
210 index: slot,
211 name,
212 }),
213 }
214}
215
216fn optional_number(
217 values: &[Value],
218 id: EntityId,
219 slot: usize,
220 name: &'static str,
221) -> AlignmentResult<Option<f64>> {
222 match values.get(slot) {
223 Some(Value::Null) => Ok(None),
224 Some(value) => {
225 value
226 .unwrap_typed()
227 .as_f64()
228 .map(Some)
229 .ok_or(AlignmentError::InvalidAttribute {
230 entity: id,
231 index: slot,
232 name,
233 })
234 }
235 None => Err(AlignmentError::InvalidAttribute {
236 entity: id,
237 index: slot,
238 name,
239 }),
240 }
241}
242
243fn reference(
244 values: &[Value],
245 id: EntityId,
246 slot: usize,
247 name: &'static str,
248) -> AlignmentResult<EntityId> {
249 values
250 .get(slot)
251 .and_then(|value| value.as_ref_id())
252 .ok_or(AlignmentError::InvalidAttribute {
253 entity: id,
254 index: slot,
255 name,
256 })
257}
258
259fn length(value: f64, units: AlignmentUnits) -> f64 {
260 value * units.length_to_metres
261}