1use crate::error::GeometryResult;
27use crate::resource::placement::Axis2Placement3D;
28use crate::resource::resolve;
29use crate::slots::Slots;
30use ifc_model::{Entity, EntityId, Model};
31
32pub(crate) mod slot {
37 pub const POSITION: usize = 0;
39 pub const RADIUS: usize = 1;
41 pub const MAJOR_RADIUS: usize = 1;
43 pub const MINOR_RADIUS: usize = 2;
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ParameterKind {
50 Length,
52 Angle,
54}
55
56#[derive(Debug, Clone, Copy)]
62pub struct Plane<'m> {
63 slots: Slots<'m>,
64}
65
66impl<'m> Plane<'m> {
67 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
69 Self {
70 slots: Slots::new(id, entity),
71 }
72 }
73
74 pub fn id(&self) -> EntityId {
76 self.slots.id()
77 }
78
79 pub fn position_ref(&self) -> GeometryResult<EntityId> {
84 self.slots.req_ref(slot::POSITION, "Position")
85 }
86
87 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
95 resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
96 }
97
98 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
100 (ParameterKind::Length, ParameterKind::Length)
101 }
102}
103
104#[derive(Debug, Clone, Copy)]
106pub struct CylindricalSurface<'m> {
107 slots: Slots<'m>,
108}
109
110impl<'m> CylindricalSurface<'m> {
111 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
113 Self {
114 slots: Slots::new(id, entity),
115 }
116 }
117
118 pub fn id(&self) -> EntityId {
120 self.slots.id()
121 }
122
123 pub fn position_ref(&self) -> GeometryResult<EntityId> {
125 self.slots.req_ref(slot::POSITION, "Position")
126 }
127
128 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
136 resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
137 }
138
139 pub fn radius(&self) -> GeometryResult<f64> {
141 positive(&self.slots, slot::RADIUS, "Radius")
142 }
143
144 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
146 (ParameterKind::Angle, ParameterKind::Length)
147 }
148}
149
150#[derive(Debug, Clone, Copy)]
152pub struct SphericalSurface<'m> {
153 slots: Slots<'m>,
154}
155
156impl<'m> SphericalSurface<'m> {
157 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
159 Self {
160 slots: Slots::new(id, entity),
161 }
162 }
163
164 pub fn id(&self) -> EntityId {
166 self.slots.id()
167 }
168
169 pub fn position_ref(&self) -> GeometryResult<EntityId> {
171 self.slots.req_ref(slot::POSITION, "Position")
172 }
173
174 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
182 resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
183 }
184
185 pub fn radius(&self) -> GeometryResult<f64> {
187 positive(&self.slots, slot::RADIUS, "Radius")
188 }
189
190 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
192 (ParameterKind::Angle, ParameterKind::Angle)
193 }
194}
195
196#[derive(Debug, Clone, Copy)]
198pub struct ToroidalSurface<'m> {
199 slots: Slots<'m>,
200}
201
202impl<'m> ToroidalSurface<'m> {
203 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
205 Self {
206 slots: Slots::new(id, entity),
207 }
208 }
209
210 pub fn id(&self) -> EntityId {
212 self.slots.id()
213 }
214
215 pub fn position_ref(&self) -> GeometryResult<EntityId> {
217 self.slots.req_ref(slot::POSITION, "Position")
218 }
219
220 pub fn position<'v>(&self, model: &'v Model) -> GeometryResult<Axis2Placement3D<'v>> {
228 resolve::axis2_placement_3d(model, self.id(), self.position_ref()?)
229 }
230
231 pub fn major_radius(&self) -> GeometryResult<f64> {
233 positive(&self.slots, slot::MAJOR_RADIUS, "MajorRadius")
234 }
235
236 pub fn minor_radius(&self) -> GeometryResult<f64> {
238 positive(&self.slots, slot::MINOR_RADIUS, "MinorRadius")
239 }
240
241 pub fn is_self_intersecting(&self) -> GeometryResult<bool> {
249 Ok(self.minor_radius()? >= self.major_radius()?)
250 }
251
252 pub fn parameter_kinds(&self) -> (ParameterKind, ParameterKind) {
254 (ParameterKind::Angle, ParameterKind::Angle)
255 }
256}
257
258fn positive(slots: &Slots<'_>, index: usize, name: &'static str) -> GeometryResult<f64> {
263 let value = slots.req_f64(index, name)?;
264 if value > 0.0 {
265 Ok(value)
266 } else {
267 Err(slots.degenerate(format!("{name} must be positive, found {value}")))
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use ifc_model::Value;
275
276 fn surface(type_name: &str, radii: &[f64]) -> Entity {
277 let mut attributes = vec![Value::Ref(EntityId(70))];
278 attributes.extend(radii.iter().map(|r| Value::Real(*r)));
279 Entity::new(type_name, attributes)
280 }
281
282 #[test]
283 fn every_elementary_surface_reads_position_from_the_inherited_slot_zero() {
284 let plane = surface("IFCPLANE", &[]);
285 assert_eq!(
286 Plane::new(EntityId(1), &plane).position_ref().unwrap(),
287 EntityId(70)
288 );
289
290 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
291 assert_eq!(
292 CylindricalSurface::new(EntityId(1), &cylinder)
293 .position_ref()
294 .unwrap(),
295 EntityId(70)
296 );
297
298 let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
299 assert_eq!(
300 SphericalSurface::new(EntityId(1), &sphere)
301 .position_ref()
302 .unwrap(),
303 EntityId(70)
304 );
305
306 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
307 assert_eq!(
308 ToroidalSurface::new(EntityId(1), &torus)
309 .position_ref()
310 .unwrap(),
311 EntityId(70)
312 );
313 }
314
315 #[test]
316 fn cylinder_and_sphere_radii_are_read_from_the_slot_after_position() {
317 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.5]);
318 assert_eq!(
319 CylindricalSurface::new(EntityId(1), &cylinder)
320 .radius()
321 .unwrap(),
322 2.5
323 );
324 let sphere = surface("IFCSPHERICALSURFACE", &[4.0]);
325 assert_eq!(
326 SphericalSurface::new(EntityId(1), &sphere)
327 .radius()
328 .unwrap(),
329 4.0
330 );
331 }
332
333 #[test]
334 fn torus_radii_are_read_in_major_then_minor_order() {
335 let e = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
336 let view = ToroidalSurface::new(EntityId(1), &e);
337 assert_eq!(view.major_radius().unwrap(), 5.0);
338 assert_eq!(view.minor_radius().unwrap(), 1.0);
339 assert!(!view.is_self_intersecting().unwrap());
340 }
341
342 #[test]
345 fn a_minor_radius_at_least_the_major_is_reported_as_self_intersecting() {
346 let e = surface("IFCTOROIDALSURFACE", &[1.0, 2.0]);
347 assert!(ToroidalSurface::new(EntityId(1), &e)
348 .is_self_intersecting()
349 .unwrap());
350 }
351
352 #[test]
353 fn a_zero_radius_surface_is_degenerate_and_names_the_attribute() {
354 let cylinder = surface("IFCCYLINDRICALSURFACE", &[0.0]);
355 let err = CylindricalSurface::new(EntityId(8), &cylinder)
356 .radius()
357 .unwrap_err();
358 assert!(err.to_string().contains("#8"), "got: {err}");
359 assert!(err.to_string().contains("Radius"), "got: {err}");
360
361 let torus = surface("IFCTOROIDALSURFACE", &[5.0, -1.0]);
362 let err = ToroidalSurface::new(EntityId(1), &torus)
363 .minor_radius()
364 .unwrap_err();
365 assert!(err.to_string().contains("MinorRadius"), "got: {err}");
366 }
367
368 #[test]
371 fn only_the_plane_has_two_length_parameters() {
372 let plane = surface("IFCPLANE", &[]);
373 assert_eq!(
374 Plane::new(EntityId(1), &plane).parameter_kinds(),
375 (ParameterKind::Length, ParameterKind::Length)
376 );
377
378 let cylinder = surface("IFCCYLINDRICALSURFACE", &[1.0]);
379 assert_eq!(
380 CylindricalSurface::new(EntityId(1), &cylinder).parameter_kinds(),
381 (ParameterKind::Angle, ParameterKind::Length)
382 );
383
384 let sphere = surface("IFCSPHERICALSURFACE", &[1.0]);
385 assert_eq!(
386 SphericalSurface::new(EntityId(1), &sphere).parameter_kinds(),
387 (ParameterKind::Angle, ParameterKind::Angle)
388 );
389
390 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
391 assert_eq!(
392 ToroidalSurface::new(EntityId(1), &torus).parameter_kinds(),
393 (ParameterKind::Angle, ParameterKind::Angle)
394 );
395 }
396
397 #[test]
398 fn a_surface_missing_its_position_reports_the_attribute_by_name() {
399 let e = Entity::new("IFCPLANE", vec![]);
400 let err = Plane::new(EntityId(1), &e).position_ref().unwrap_err();
401 assert!(err.to_string().contains("Position"), "got: {err}");
402 }
403
404 #[test]
409 fn every_elementary_surface_resolves_a_typed_placement_view() {
410 let mut model = Model::new();
411 model.insert(
412 EntityId(60),
413 Entity::new(
414 "IFCCARTESIANPOINT",
415 vec![Value::List(vec![
416 Value::Real(1.0),
417 Value::Real(2.0),
418 Value::Real(3.0),
419 ])],
420 ),
421 );
422 model.insert(
423 EntityId(70),
424 Entity::new("IFCAXIS2PLACEMENT3D", vec![Value::Ref(EntityId(60))]),
425 );
426
427 let plane = surface("IFCPLANE", &[]);
428 let view = Plane::new(EntityId(1), &plane).position(&model).unwrap();
429 assert_eq!(view.id(), EntityId(70));
430 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
431
432 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
433 let view = CylindricalSurface::new(EntityId(2), &cylinder)
434 .position(&model)
435 .unwrap();
436 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
437
438 let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
439 let view = SphericalSurface::new(EntityId(3), &sphere)
440 .position(&model)
441 .unwrap();
442 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
443
444 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
445 let view = ToroidalSurface::new(EntityId(4), &torus)
446 .position(&model)
447 .unwrap();
448 assert_eq!(view.location(&model).unwrap(), [1.0, 2.0, 3.0]);
449 }
450
451 #[test]
453 fn a_dangling_placement_reference_is_reported() {
454 let model = Model::new();
455 let plane = surface("IFCPLANE", &[]);
456 let error = Plane::new(EntityId(1), &plane)
457 .position(&model)
458 .expect_err("placement 70 is not in the model");
459 assert_eq!(error.entity(), Some(EntityId(1)));
460 }
461
462 fn model_with_2d_position() -> Model {
465 let mut model = Model::new();
466 model.insert(
467 EntityId(60),
468 Entity::new(
469 "IFCCARTESIANPOINT",
470 vec![Value::List(vec![Value::Real(1.0), Value::Real(2.0)])],
471 ),
472 );
473 model.insert(
474 EntityId(70),
475 Entity::new("IFCAXIS2PLACEMENT2D", vec![Value::Ref(EntityId(60))]),
476 );
477 model
478 }
479
480 fn assert_wrong_placement(result: GeometryResult<Axis2Placement3D<'_>>) {
481 let error = result.expect_err("#70 is not an IfcAxis2Placement3D");
482 assert!(
483 matches!(
484 error,
485 crate::GeometryError::WrongEntityType {
486 entity: EntityId(70),
487 expected: "IfcAxis2Placement3D",
488 ..
489 }
490 ),
491 "got: {error:?}"
492 );
493 }
494
495 #[test]
496 fn a_plane_rejects_a_position_that_is_not_an_axis2_placement_3d() {
497 let model = model_with_2d_position();
498 let plane = surface("IFCPLANE", &[]);
499 assert_wrong_placement(Plane::new(EntityId(1), &plane).position(&model));
500 }
501
502 #[test]
503 fn a_cylinder_rejects_a_position_that_is_not_an_axis2_placement_3d() {
504 let model = model_with_2d_position();
505 let cylinder = surface("IFCCYLINDRICALSURFACE", &[2.0]);
506 assert_wrong_placement(CylindricalSurface::new(EntityId(2), &cylinder).position(&model));
507 }
508
509 #[test]
510 fn a_sphere_rejects_a_position_that_is_not_an_axis2_placement_3d() {
511 let model = model_with_2d_position();
512 let sphere = surface("IFCSPHERICALSURFACE", &[3.0]);
513 assert_wrong_placement(SphericalSurface::new(EntityId(3), &sphere).position(&model));
514 }
515
516 #[test]
517 fn a_torus_rejects_a_position_that_is_not_an_axis2_placement_3d() {
518 let model = model_with_2d_position();
519 let torus = surface("IFCTOROIDALSURFACE", &[5.0, 1.0]);
520 assert_wrong_placement(ToroidalSurface::new(EntityId(4), &torus).position(&model));
521 }
522}