1use crate::error::{GeometryError, GeometryResult};
27use crate::resource::placement::Axis2Placement3D;
28use crate::slots::Slots;
29use ifc_model::{Entity, EntityId, Model};
30
31pub(crate) mod slot {
36 pub const POSITION: usize = 0;
38 pub const RADIUS: usize = 1;
40 pub const MAJOR_RADIUS: usize = 1;
42 pub const MINOR_RADIUS: usize = 2;
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ParameterKind {
49 Length,
51 Angle,
53}
54
55#[derive(Debug, Clone, Copy)]
61pub struct Plane<'m> {
62 slots: Slots<'m>,
63}
64
65impl<'m> Plane<'m> {
66 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
68 Self {
69 slots: Slots::new(id, entity),
70 }
71 }
72
73 pub fn id(&self) -> EntityId {
75 self.slots.id()
76 }
77
78 pub fn position_ref(&self) -> GeometryResult<EntityId> {
83 self.slots.req_ref(slot::POSITION, "Position")
84 }
85
86 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
92 let id = self.position_ref()?;
93 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
94 referrer: self.id(),
95 missing: id,
96 })?;
97 Ok(Axis2Placement3D::new(id, entity))
98 }
99
100 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
102 (ParameterKind::Length, ParameterKind::Length)
103 }
104}
105
106#[derive(Debug, Clone, Copy)]
108pub struct CylindricalSurface<'m> {
109 slots: Slots<'m>,
110}
111
112impl<'m> CylindricalSurface<'m> {
113 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
115 Self {
116 slots: Slots::new(id, entity),
117 }
118 }
119
120 pub fn id(&self) -> EntityId {
122 self.slots.id()
123 }
124
125 pub fn position_ref(&self) -> GeometryResult<EntityId> {
127 self.slots.req_ref(slot::POSITION, "Position")
128 }
129
130 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
136 let id = self.position_ref()?;
137 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
138 referrer: self.id(),
139 missing: id,
140 })?;
141 Ok(Axis2Placement3D::new(id, entity))
142 }
143
144 pub fn radius(&self) -> GeometryResult<f64> {
146 positive(&self.slots, slot::RADIUS, "Radius")
147 }
148
149 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
151 (ParameterKind::Angle, ParameterKind::Length)
152 }
153}
154
155#[derive(Debug, Clone, Copy)]
157pub struct SphericalSurface<'m> {
158 slots: Slots<'m>,
159}
160
161impl<'m> SphericalSurface<'m> {
162 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
164 Self {
165 slots: Slots::new(id, entity),
166 }
167 }
168
169 pub fn id(&self) -> EntityId {
171 self.slots.id()
172 }
173
174 pub fn position_ref(&self) -> GeometryResult<EntityId> {
176 self.slots.req_ref(slot::POSITION, "Position")
177 }
178
179 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
185 let id = self.position_ref()?;
186 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
187 referrer: self.id(),
188 missing: id,
189 })?;
190 Ok(Axis2Placement3D::new(id, entity))
191 }
192
193 pub fn radius(&self) -> GeometryResult<f64> {
195 positive(&self.slots, slot::RADIUS, "Radius")
196 }
197
198 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
200 (ParameterKind::Angle, ParameterKind::Angle)
201 }
202}
203
204#[derive(Debug, Clone, Copy)]
206pub struct ToroidalSurface<'m> {
207 slots: Slots<'m>,
208}
209
210impl<'m> ToroidalSurface<'m> {
211 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
213 Self {
214 slots: Slots::new(id, entity),
215 }
216 }
217
218 pub fn id(&self) -> EntityId {
220 self.slots.id()
221 }
222
223 pub fn position_ref(&self) -> GeometryResult<EntityId> {
225 self.slots.req_ref(slot::POSITION, "Position")
226 }
227
228 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
234 let id = self.position_ref()?;
235 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
236 referrer: self.id(),
237 missing: id,
238 })?;
239 Ok(Axis2Placement3D::new(id, entity))
240 }
241
242 pub fn major_radius(&self) -> GeometryResult<f64> {
244 positive(&self.slots, slot::MAJOR_RADIUS, "MajorRadius")
245 }
246
247 pub fn minor_radius(&self) -> GeometryResult<f64> {
249 positive(&self.slots, slot::MINOR_RADIUS, "MinorRadius")
250 }
251
252 pub fn is_self_intersecting(&self) -> GeometryResult<bool> {
260 Ok(self.minor_radius()? >= self.major_radius()?)
261 }
262
263 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
265 (ParameterKind::Angle, ParameterKind::Angle)
266 }
267}
268
269fn positive(slots: &Slots<'_>, index: usize, name: &'static str) -> GeometryResult<f64> {
274 let value = slots.req_f64(index, name)?;
275 if value > 0.0 {
276 Ok(value)
277 } else {
278 Err(slots.degenerate(format!("{name} must be positive, found {value}")))
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use ifc_model::Value;
286
287 fn surface(type_name: &str, radii: &[f64]) -> Entity {
288 let mut attributes = vec![Value::Ref(EntityId(70))];
289 attributes.extend(radii.iter().map(|r| Value::Real(*r)));
290 Entity::new(type_name, attributes)
291 }
292
293 #[test]
294 fn every_elementary_surface_reads_position_from_the_inherited_slot_zero() {
295 let plane = surface("IFCPLANE", &[]);
296 assert_eq!(
297 Plane::new(EntityId(1), &plane).position_ref().unwrap(),
298 EntityId(70)
299 );
300
301 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
302 assert_eq!(
303 CylindricalSurface::new(EntityId(1), &cylinder)
304 .position_ref()
305 .unwrap(),
306 EntityId(70)
307 );
308
309 let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
310 assert_eq!(
311 SphericalSurface::new(EntityId(1), &sphere)
312 .position_ref()
313 .unwrap(),
314 EntityId(70)
315 );
316
317 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
318 assert_eq!(
319 ToroidalSurface::new(EntityId(1), &torus)
320 .position_ref()
321 .unwrap(),
322 EntityId(70)
323 );
324 }
325
326 #[test]
327 fn cylinder_and_sphere_radii_are_read_from_the_slot_after_position() {
328 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.5]);
329 assert_eq!(
330 CylindricalSurface::new(EntityId(1), &cylinder)
331 .radius()
332 .unwrap(),
333 2.5
334 );
335 let sphere = surface("IFCSPHERICALSURFACE", &[4.0]);
336 assert_eq!(
337 SphericalSurface::new(EntityId(1), &sphere)
338 .radius()
339 .unwrap(),
340 4.0
341 );
342 }
343
344 #[test]
345 fn torus_radii_are_read_in_major_then_minor_order() {
346 let e = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
347 let view = ToroidalSurface::new(EntityId(1), &e);
348 assert_eq!(view.major_radius().unwrap(), 5.0);
349 assert_eq!(view.minor_radius().unwrap(), 1.0);
350 assert!(!view.is_self_intersecting().unwrap());
351 }
352
353 #[test]
356 fn a_minor_radius_at_least_the_major_is_reported_as_self_intersecting() {
357 let e = surface("IFCTOROIDALSURFACE", &[1.0, 2.0]);
358 assert!(ToroidalSurface::new(EntityId(1), &e)
359 .is_self_intersecting()
360 .unwrap());
361 }
362
363 #[test]
364 fn a_zero_radius_surface_is_degenerate_and_names_the_attribute() {
365 let cylinder = surface("IFCCYLINDRICALSURFACE", &[0.0]);
366 let err = CylindricalSurface::new(EntityId(8), &cylinder)
367 .radius()
368 .unwrap_err();
369 assert!(err.to_string().contains("#8"), "got: {err}");
370 assert!(err.to_string().contains("Radius"), "got: {err}");
371
372 let torus = surface("IFCTOROIDALSURFACE", &[5.0, -1.0]);
373 let err = ToroidalSurface::new(EntityId(1), &torus)
374 .minor_radius()
375 .unwrap_err();
376 assert!(err.to_string().contains("MinorRadius"), "got: {err}");
377 }
378
379 #[test]
382 fn only_the_plane_has_two_length_parameters() {
383 let plane = surface("IFCPLANE", &[]);
384 assert_eq!(
385 Plane::new(EntityId(1), &plane).parameter_kinds(),
386 (ParameterKind::Length, ParameterKind::Length)
387 );
388
389 let cylinder = surface("IFCCYLINDRICALSURFACE", &[1.0]);
390 assert_eq!(
391 CylindricalSurface::new(EntityId(1), &cylinder).parameter_kinds(),
392 (ParameterKind::Angle, ParameterKind::Length)
393 );
394
395 let sphere = surface("IFCSPHERICALSURFACE", &[1.0]);
396 assert_eq!(
397 SphericalSurface::new(EntityId(1), &sphere).parameter_kinds(),
398 (ParameterKind::Angle, ParameterKind::Angle)
399 );
400
401 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
402 assert_eq!(
403 ToroidalSurface::new(EntityId(1), &torus).parameter_kinds(),
404 (ParameterKind::Angle, ParameterKind::Angle)
405 );
406 }
407
408 #[test]
409 fn a_surface_missing_its_position_reports_the_attribute_by_name() {
410 let e = Entity::new("IFCPLANE", vec![]);
411 let err = Plane::new(EntityId(1), &e).position_ref().unwrap_err();
412 assert!(err.to_string().contains("Position"), "got: {err}");
413 }
414
415 #[test]
420 fn every_elementary_surface_resolves_a_typed_placement_view() {
421 let mut model = Model::new();
422 model.insert(
423 EntityId(60),
424 Entity::new(
425 "IFCCARTESIANPOINT",
426 vec![Value::List(vec![
427 Value::Real(1.0),
428 Value::Real(2.0),
429 Value::Real(3.0),
430 ])],
431 ),
432 );
433 model.insert(
434 EntityId(70),
435 Entity::new("IFCAXIS2PLACEMENT3D", vec![Value::Ref(EntityId(60))]),
436 );
437
438 let plane = surface("IFCPLANE", &[]);
439 let view = Plane::new(EntityId(1), &plane).position(&model).unwrap();
440 assert_eq!(view.id(), EntityId(70));
441 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
442
443 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
444 let view = CylindricalSurface::new(EntityId(2), &cylinder)
445 .position(&model)
446 .unwrap();
447 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
448
449 let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
450 let view = SphericalSurface::new(EntityId(3), &sphere)
451 .position(&model)
452 .unwrap();
453 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
454
455 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
456 let view = ToroidalSurface::new(EntityId(4), &torus)
457 .position(&model)
458 .unwrap();
459 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
460 }
461
462 #[test]
464 fn a_dangling_placement_reference_is_reported() {
465 let model = Model::new();
466 let plane = surface("IFCPLANE", &[]);
467 let error = Plane::new(EntityId(1), &plane)
468 .position(&model)
469 .expect_err("placement 70 is not in the model");
470 assert_eq!(error.entity(), Some(EntityId(1)));
471 }
472}