1use ifc_model::{Entity, EntityId, Value};
9
10use crate::error::{GeometryError, GeometryResult};
11use crate::slots::Slots;
12
13#[non_exhaustive]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LayerSetDirection {
17 Axis1,
19 Axis2,
21 Axis3,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum DirectionSense {
28 Positive,
30 Negative,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct CardinalPoint(u64);
37impl CardinalPoint {
38 pub fn get(self) -> u64 {
40 self.0
41 }
42 pub fn standard(self) -> Option<u8> {
47 u8::try_from(self.0)
48 .ok()
49 .filter(|value| (1..=19).contains(value))
50 }
51}
52
53fn checked<'m>(
54 id: EntityId,
55 entity: &'m Entity,
56 expected: &'static str,
57) -> GeometryResult<Slots<'m>> {
58 if !entity.is_type(expected) {
59 return Err(GeometryError::WrongEntityType {
60 entity: id,
61 actual: entity.type_name.to_string(),
62 expected,
63 });
64 }
65 Ok(Slots::new(id, entity))
66}
67
68fn optional_i64(
69 slots: &Slots<'_>,
70 index: usize,
71 name: &'static str,
72) -> GeometryResult<Option<i64>> {
73 match slots.opt(index) {
74 None => Ok(None),
75 Some(_) => slots.req_i64(index, name).map(Some),
76 }
77}
78
79fn optional_f64(
80 slots: &Slots<'_>,
81 index: usize,
82 name: &'static str,
83) -> GeometryResult<Option<f64>> {
84 match slots.opt(index) {
85 None => Ok(None),
86 Some(_) => slots.req_f64(index, name).map(Some),
87 }
88}
89
90fn cardinal(
91 slots: &Slots<'_>,
92 index: usize,
93 name: &'static str,
94) -> GeometryResult<Option<CardinalPoint>> {
95 let Some(value) = optional_i64(slots, index, name)? else {
96 return Ok(None);
97 };
98 let value = u64::try_from(value)
99 .ok()
100 .filter(|value| *value > 0)
101 .ok_or_else(|| slots.degenerate(format!("{name} must be positive")))?;
102 Ok(Some(CardinalPoint(value)))
103}
104
105fn positive_optional(
106 slots: &Slots<'_>,
107 index: usize,
108 name: &'static str,
109) -> GeometryResult<Option<f64>> {
110 let value = optional_f64(slots, index, name)?;
111 if value.is_some_and(|value| !value.is_finite() || value <= 0.0) {
112 return Err(slots.degenerate(format!("{name} must be finite and positive")));
113 }
114 Ok(value)
115}
116
117fn required_enum<'a>(
118 slots: &'a Slots<'_>,
119 index: usize,
120 name: &'static str,
121) -> GeometryResult<&'a str> {
122 let value = slots.req(index, name)?;
123 match value {
124 Value::Enum(token) => Ok(token),
125 _ => Err(GeometryError::WrongValueKind {
126 entity: slots.id(),
127 type_name: slots.type_name().to_string(),
128 attribute: name,
129 expected: "an enumeration",
130 found: format!("{value:?}"),
131 }),
132 }
133}
134
135#[derive(Debug, Clone, Copy)]
136pub struct MaterialProfileSetUsageGeometry<'m> {
138 slots: Slots<'m>,
139}
140impl<'m> MaterialProfileSetUsageGeometry<'m> {
141 pub fn new(id: EntityId, entity: &'m Entity) -> GeometryResult<Self> {
143 Ok(Self {
144 slots: checked(id, entity, "IFCMATERIALPROFILESETUSAGE")?,
145 })
146 }
147 pub fn profile_set_id(self) -> GeometryResult<EntityId> {
149 self.slots.req_ref(0, "ForProfileSet")
150 }
151 pub fn cardinal_point(self) -> GeometryResult<Option<CardinalPoint>> {
153 cardinal(&self.slots, 1, "CardinalPoint")
154 }
155 pub fn reference_extent(self) -> GeometryResult<Option<f64>> {
157 positive_optional(&self.slots, 2, "ReferenceExtent")
158 }
159}
160
161#[derive(Debug, Clone, Copy)]
162pub struct MaterialProfileSetUsageTaperingGeometry<'m> {
164 slots: Slots<'m>,
165}
166impl<'m> MaterialProfileSetUsageTaperingGeometry<'m> {
167 pub fn new(id: EntityId, entity: &'m Entity) -> GeometryResult<Self> {
169 Ok(Self {
170 slots: checked(id, entity, "IFCMATERIALPROFILESETUSAGETAPERING")?,
171 })
172 }
173 pub fn profile_set_id(self) -> GeometryResult<EntityId> {
175 self.slots.req_ref(0, "ForProfileSet")
176 }
177 pub fn cardinal_point(self) -> GeometryResult<Option<CardinalPoint>> {
179 cardinal(&self.slots, 1, "CardinalPoint")
180 }
181 pub fn reference_extent(self) -> GeometryResult<Option<f64>> {
183 positive_optional(&self.slots, 2, "ReferenceExtent")
184 }
185 pub fn end_profile_set_id(self) -> GeometryResult<EntityId> {
187 self.slots.req_ref(3, "ForProfileEndSet")
188 }
189 pub fn cardinal_end_point(self) -> GeometryResult<Option<CardinalPoint>> {
191 cardinal(&self.slots, 4, "CardinalEndPoint")
192 }
193}
194
195#[derive(Debug, Clone, Copy)]
196pub struct MaterialProfileGeometry<'m> {
198 slots: Slots<'m>,
199}
200impl<'m> MaterialProfileGeometry<'m> {
201 pub fn new(id: EntityId, entity: &'m Entity) -> GeometryResult<Self> {
203 if !entity.is_type("IFCMATERIALPROFILE") && !entity.is_type("IFCMATERIALPROFILEWITHOFFSETS")
204 {
205 return Err(GeometryError::WrongEntityType {
206 entity: id,
207 actual: entity.type_name.to_string(),
208 expected: "IfcMaterialProfile",
209 });
210 }
211 Ok(Self {
212 slots: Slots::new(id, entity),
213 })
214 }
215 pub fn profile_id(self) -> GeometryResult<EntityId> {
217 self.slots.req_ref(3, "Profile")
218 }
219 pub fn offset_values(self) -> GeometryResult<Option<[f64; 2]>> {
221 if !self
222 .slots
223 .type_name()
224 .eq_ignore_ascii_case("IFCMATERIALPROFILEWITHOFFSETS")
225 {
226 return Ok(None);
227 }
228 let values = self.slots.req_f64_list(6, "OffsetValues")?;
229 let values: [f64; 2] = values.try_into().map_err(|_| {
230 self.slots
231 .degenerate("OffsetValues must contain exactly two lengths")
232 })?;
233 if !values.iter().all(|value| value.is_finite()) {
234 return Err(self.slots.degenerate("OffsetValues must be finite"));
235 }
236 Ok(Some(values))
237 }
238}
239
240#[derive(Debug, Clone, Copy)]
241pub struct MaterialLayerSetUsageGeometry<'m> {
243 slots: Slots<'m>,
244}
245impl<'m> MaterialLayerSetUsageGeometry<'m> {
246 pub fn new(id: EntityId, entity: &'m Entity) -> GeometryResult<Self> {
248 Ok(Self {
249 slots: checked(id, entity, "IFCMATERIALLAYERSETUSAGE")?,
250 })
251 }
252 pub fn layer_set_id(self) -> GeometryResult<EntityId> {
254 self.slots.req_ref(0, "ForLayerSet")
255 }
256 pub fn layer_set_direction(self) -> GeometryResult<LayerSetDirection> {
258 match required_enum(&self.slots, 1, "LayerSetDirection")? {
259 token if token.eq_ignore_ascii_case("AXIS1") => Ok(LayerSetDirection::Axis1),
260 token if token.eq_ignore_ascii_case("AXIS2") => Ok(LayerSetDirection::Axis2),
261 token if token.eq_ignore_ascii_case("AXIS3") => Ok(LayerSetDirection::Axis3),
262 _ => Err(self
263 .slots
264 .degenerate("LayerSetDirection must be AXIS1, AXIS2, or AXIS3")),
265 }
266 }
267 pub fn direction_sense(self) -> GeometryResult<DirectionSense> {
269 match required_enum(&self.slots, 2, "DirectionSense")? {
270 token if token.eq_ignore_ascii_case("POSITIVE") => Ok(DirectionSense::Positive),
271 token if token.eq_ignore_ascii_case("NEGATIVE") => Ok(DirectionSense::Negative),
272 _ => Err(self
273 .slots
274 .degenerate("DirectionSense must be POSITIVE or NEGATIVE")),
275 }
276 }
277 pub fn offset_from_reference_line(self) -> GeometryResult<f64> {
279 let value = self.slots.req_f64(3, "OffsetFromReferenceLine")?;
280 if !value.is_finite() {
281 return Err(self
282 .slots
283 .degenerate("OffsetFromReferenceLine must be finite"));
284 }
285 Ok(value)
286 }
287 pub fn reference_extent(self) -> GeometryResult<Option<f64>> {
289 positive_optional(&self.slots, 4, "ReferenceExtent")
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 fn r(id: u64) -> Value {
297 Value::Ref(EntityId(id))
298 }
299
300 #[test]
301 fn profile_usage_reads_absolute_cardinal_and_taper_slots() {
302 let entity = Entity::new(
303 "IFCMATERIALPROFILESETUSAGETAPERING",
304 vec![
305 r(10),
306 Value::Integer(9),
307 Value::Real(4.0),
308 r(11),
309 Value::Integer(5),
310 ],
311 );
312 let view = MaterialProfileSetUsageTaperingGeometry::new(EntityId(1), &entity).unwrap();
313 assert_eq!(view.profile_set_id().unwrap(), EntityId(10));
314 assert_eq!(view.cardinal_point().unwrap().unwrap().standard(), Some(9));
315 assert_eq!(view.reference_extent().unwrap(), Some(4.0));
316 assert_eq!(view.end_profile_set_id().unwrap(), EntityId(11));
317 assert_eq!(view.cardinal_end_point().unwrap().unwrap().get(), 5);
318 }
319
320 #[test]
321 fn layer_usage_preserves_axis_sense_signed_offset_and_extent() {
322 let entity = Entity::new(
323 "IFCMATERIALLAYERSETUSAGE",
324 vec![
325 r(20),
326 Value::Enum("AXIS2".into()),
327 Value::Enum("NEGATIVE".into()),
328 Value::Real(-0.25),
329 Value::Real(8.0),
330 ],
331 );
332 let view = MaterialLayerSetUsageGeometry::new(EntityId(2), &entity).unwrap();
333 assert_eq!(view.layer_set_id().unwrap(), EntityId(20));
334 assert_eq!(
335 view.layer_set_direction().unwrap(),
336 LayerSetDirection::Axis2
337 );
338 assert_eq!(view.direction_sense().unwrap(), DirectionSense::Negative);
339 assert_eq!(view.offset_from_reference_line().unwrap(), -0.25);
340 assert_eq!(view.reference_extent().unwrap(), Some(8.0));
341 }
342
343 #[test]
344 fn profile_offsets_are_kept_as_two_signed_project_lengths() {
345 let entity = Entity::new(
346 "IFCMATERIALPROFILEWITHOFFSETS",
347 vec![
348 Value::Null,
349 Value::Null,
350 Value::Null,
351 r(30),
352 Value::Null,
353 Value::Null,
354 Value::List(vec![Value::Real(-1.0), Value::Real(2.0)]),
355 ],
356 );
357 let view = MaterialProfileGeometry::new(EntityId(3), &entity).unwrap();
358 assert_eq!(view.profile_id().unwrap(), EntityId(30));
359 assert_eq!(view.offset_values().unwrap(), Some([-1.0, 2.0]));
360 }
361
362 #[test]
363 fn invalid_geometry_inputs_report_the_source_entity() {
364 let entity = Entity::new(
365 "IFCMATERIALPROFILESETUSAGE",
366 vec![r(1), Value::Integer(0), Value::Real(-2.0)],
367 );
368 let view = MaterialProfileSetUsageGeometry::new(EntityId(77), &entity).unwrap();
369 for error in [
370 view.cardinal_point().unwrap_err(),
371 view.reference_extent().unwrap_err(),
372 ] {
373 assert_eq!(error.entity(), Some(EntityId(77)));
374 }
375 }
376}