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