1use super::trim::{read_trim, TrimMeasure};
8use super::{directrix_slot, disk_slot, spine_slot};
9use crate::error::GeometryResult;
10use crate::slots::Slots;
11use crate::solid::swept::area::SweptAreaSolid;
12use ifc_model::{Entity, EntityId};
13
14#[derive(Debug, Clone, Copy)]
26pub struct SurfaceCurveSweptAreaSolid<'m> {
27 slots: Slots<'m>,
28}
29
30impl<'m> SurfaceCurveSweptAreaSolid<'m> {
31 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
33 Self {
34 slots: Slots::new(id, entity),
35 }
36 }
37
38 pub fn id(&self) -> EntityId {
40 self.slots.id()
41 }
42
43 pub fn base(&self) -> SweptAreaSolid<'m> {
45 SweptAreaSolid::from_slots(self.slots)
46 }
47
48 pub fn directrix(&self) -> GeometryResult<EntityId> {
50 self.slots.req_ref(directrix_slot::DIRECTRIX, "Directrix")
51 }
52
53 pub fn start_param(&self) -> GeometryResult<Option<TrimMeasure>> {
62 read_trim(&self.slots, directrix_slot::START_PARAM, "StartParam")
63 }
64
65 pub fn end_param(&self) -> GeometryResult<Option<TrimMeasure>> {
71 read_trim(&self.slots, directrix_slot::END_PARAM, "EndParam")
72 }
73
74 pub fn reference_surface(&self) -> GeometryResult<EntityId> {
76 self.slots
77 .req_ref(directrix_slot::REFERENCE_SURFACE, "ReferenceSurface")
78 }
79}
80
81#[derive(Debug, Clone, Copy)]
89pub struct FixedReferenceSweptAreaSolid<'m> {
90 slots: Slots<'m>,
91}
92
93impl<'m> FixedReferenceSweptAreaSolid<'m> {
94 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
96 Self {
97 slots: Slots::new(id, entity),
98 }
99 }
100
101 pub fn id(&self) -> EntityId {
103 self.slots.id()
104 }
105
106 pub fn base(&self) -> SweptAreaSolid<'m> {
108 SweptAreaSolid::from_slots(self.slots)
109 }
110
111 pub fn directrix(&self) -> GeometryResult<EntityId> {
113 self.slots.req_ref(directrix_slot::DIRECTRIX, "Directrix")
114 }
115
116 pub fn start_param(&self) -> GeometryResult<Option<TrimMeasure>> {
125 read_trim(&self.slots, directrix_slot::START_PARAM, "StartParam")
126 }
127
128 pub fn end_param(&self) -> GeometryResult<Option<TrimMeasure>> {
134 read_trim(&self.slots, directrix_slot::END_PARAM, "EndParam")
135 }
136
137 pub fn fixed_reference(&self) -> GeometryResult<EntityId> {
139 self.slots
140 .req_ref(directrix_slot::FIXED_REFERENCE, "FixedReference")
141 }
142}
143
144#[derive(Debug, Clone, Copy)]
161pub struct SweptDiskSolid<'m> {
162 slots: Slots<'m>,
163}
164
165impl<'m> SweptDiskSolid<'m> {
166 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
168 Self {
169 slots: Slots::new(id, entity),
170 }
171 }
172
173 pub fn id(&self) -> EntityId {
175 self.slots.id()
176 }
177
178 pub fn directrix(&self) -> GeometryResult<EntityId> {
180 self.slots.req_ref(disk_slot::DIRECTRIX, "Directrix")
181 }
182
183 pub fn radius(&self) -> GeometryResult<f64> {
185 self.slots.req_f64(disk_slot::RADIUS, "Radius")
186 }
187
188 pub fn inner_radius(&self) -> Option<f64> {
190 self.slots.opt_f64(disk_slot::INNER_RADIUS)
191 }
192
193 pub fn start_param(&self) -> Option<f64> {
195 self.slots.opt_f64(disk_slot::START_PARAM)
196 }
197
198 pub fn end_param(&self) -> Option<f64> {
200 self.slots.opt_f64(disk_slot::END_PARAM)
201 }
202
203 pub fn checked_radii(&self) -> GeometryResult<(f64, Option<f64>)> {
205 let radius = self.radius()?;
206 if radius <= 0.0 {
207 return Err(self
208 .slots
209 .degenerate(format!("Radius must be positive, found {radius}")));
210 }
211 let inner = self.inner_radius();
212 if let Some(inner) = inner {
213 if inner >= radius {
214 return Err(self.slots.degenerate(format!(
215 "InnerRadius {inner} must be smaller than Radius {radius}"
216 )));
217 }
218 }
219 Ok((radius, inner))
220 }
221}
222
223#[derive(Debug, Clone, Copy)]
230pub struct SweptDiskSolidPolygonal<'m> {
231 slots: Slots<'m>,
232}
233
234impl<'m> SweptDiskSolidPolygonal<'m> {
235 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
237 Self {
238 slots: Slots::new(id, entity),
239 }
240 }
241
242 pub fn id(&self) -> EntityId {
244 self.slots.id()
245 }
246
247 pub fn base(&self) -> SweptDiskSolid<'m> {
249 SweptDiskSolid { slots: self.slots }
250 }
251
252 pub fn fillet_radius(&self) -> Option<f64> {
254 self.slots.opt_f64(disk_slot::FILLET_RADIUS)
255 }
256}
257
258#[derive(Debug, Clone, Copy)]
273pub struct SectionedSpine<'m> {
274 slots: Slots<'m>,
275}
276
277impl<'m> SectionedSpine<'m> {
278 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
280 Self {
281 slots: Slots::new(id, entity),
282 }
283 }
284
285 pub fn id(&self) -> EntityId {
287 self.slots.id()
288 }
289
290 pub fn spine_curve(&self) -> GeometryResult<EntityId> {
292 self.slots.req_ref(spine_slot::SPINE_CURVE, "SpineCurve")
293 }
294
295 pub fn cross_sections(&self) -> GeometryResult<Vec<EntityId>> {
297 self.slots
298 .req_ref_list(spine_slot::CROSS_SECTIONS, "CrossSections")
299 }
300
301 pub fn cross_section_positions(&self) -> GeometryResult<Vec<EntityId>> {
303 self.slots
304 .req_ref_list(spine_slot::CROSS_SECTION_POSITIONS, "CrossSectionPositions")
305 }
306
307 pub fn checked_sections(&self) -> GeometryResult<Vec<(EntityId, EntityId)>> {
309 let sections = self.cross_sections()?;
310 let positions = self.cross_section_positions()?;
311 if sections.len() != positions.len() {
312 return Err(self.slots.degenerate(format!(
313 "CrossSections has {} entries but CrossSectionPositions has {}",
314 sections.len(),
315 positions.len()
316 )));
317 }
318 Ok(sections.into_iter().zip(positions).collect())
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::solid::testkit::{entity, list, n, r};
326 use ifc_model::Value;
327
328 #[test]
329 fn surface_curve_sweep_exposes_directrix_surface_and_optional_trim() {
330 let e = entity(
331 "IFCSURFACECURVESWEPTAREASOLID",
332 vec![r(10), r(20), r(30), n(0.0), n(1.0), r(60)],
333 );
334 let view = SurfaceCurveSweptAreaSolid::new(EntityId(1), &e);
335 assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
336 assert_eq!(view.directrix().unwrap(), EntityId(30));
337 assert_eq!(
338 view.start_param().unwrap(),
339 Some(TrimMeasure::Parameter(0.0))
340 );
341 assert_eq!(view.end_param().unwrap(), Some(TrimMeasure::Parameter(1.0)));
342 assert_eq!(view.reference_surface().unwrap(), EntityId(60));
343 }
344
345 #[test]
346 fn untrimmed_directrix_reports_absent_parameters_rather_than_zero() {
347 let e = entity(
348 "IFCSURFACECURVESWEPTAREASOLID",
349 vec![r(10), r(20), r(30), Value::Null, Value::Null, r(60)],
350 );
351 let view = SurfaceCurveSweptAreaSolid::new(EntityId(1), &e);
352 assert_eq!(view.start_param().unwrap(), None);
353 assert_eq!(view.end_param().unwrap(), None);
354 }
355
356 #[test]
359 fn fixed_reference_sweep_reads_a_direction_where_surface_sweep_reads_a_surface() {
360 let attrs = vec![r(10), r(20), r(30), Value::Null, Value::Null, r(70)];
361 let fixed = entity("IFCFIXEDREFERENCESWEPTAREASOLID", attrs.clone());
362 let surface = entity("IFCSURFACECURVESWEPTAREASOLID", attrs);
363
364 assert_eq!(
365 FixedReferenceSweptAreaSolid::new(EntityId(1), &fixed)
366 .fixed_reference()
367 .unwrap(),
368 EntityId(70)
369 );
370 assert_eq!(
371 SurfaceCurveSweptAreaSolid::new(EntityId(1), &surface)
372 .reference_surface()
373 .unwrap(),
374 EntityId(70)
375 );
376 assert_eq!(
377 FixedReferenceSweptAreaSolid::new(EntityId(1), &fixed)
378 .directrix()
379 .unwrap(),
380 EntityId(30)
381 );
382 }
383
384 #[test]
387 fn swept_disk_directrix_is_slot_zero_with_no_inherited_profile() {
388 let e = entity(
389 "IFCSWEPTDISKSOLID",
390 vec![r(10), n(0.1), n(0.08), n(0.0), n(1.0)],
391 );
392 let view = SweptDiskSolid::new(EntityId(1), &e);
393 assert_eq!(view.directrix().unwrap(), EntityId(10));
394 assert_eq!(view.radius().unwrap(), 0.1);
395 assert_eq!(view.inner_radius(), Some(0.08));
396 assert_eq!(view.start_param(), Some(0.0));
397 assert_eq!(view.end_param(), Some(1.0));
398 }
399
400 #[test]
401 fn inner_radius_not_smaller_than_outer_is_degenerate() {
402 let bad = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1), n(0.1)]);
403 let err = SweptDiskSolid::new(EntityId(5), &bad)
404 .checked_radii()
405 .unwrap_err();
406 assert_eq!(err.entity(), Some(EntityId(5)));
407
408 let tube = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1), n(0.09)]);
409 assert_eq!(
410 SweptDiskSolid::new(EntityId(5), &tube)
411 .checked_radii()
412 .unwrap(),
413 (0.1, Some(0.09))
414 );
415
416 let rod = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1)]);
417 assert_eq!(
418 SweptDiskSolid::new(EntityId(5), &rod)
419 .checked_radii()
420 .unwrap(),
421 (0.1, None)
422 );
423 }
424
425 #[test]
426 fn polygonal_disk_fillet_radius_is_optional() {
427 let with_fillet = entity(
428 "IFCSWEPTDISKSOLIDPOLYGONAL",
429 vec![
430 r(10),
431 n(0.1),
432 Value::Null,
433 Value::Null,
434 Value::Null,
435 n(0.15),
436 ],
437 );
438 let view = SweptDiskSolidPolygonal::new(EntityId(1), &with_fillet);
439 assert_eq!(view.fillet_radius(), Some(0.15));
440 assert_eq!(view.base().radius().unwrap(), 0.1);
441
442 let sharp = entity("IFCSWEPTDISKSOLIDPOLYGONAL", vec![r(10), n(0.1)]);
443 assert_eq!(
444 SweptDiskSolidPolygonal::new(EntityId(1), &sharp).fillet_radius(),
445 None
446 );
447 }
448
449 #[test]
450 fn sectioned_spine_pairs_each_section_with_its_own_placement() {
451 let e = entity(
452 "IFCSECTIONEDSPINE",
453 vec![
454 r(1),
455 list(vec![r(10), r(11), r(12)]),
456 list(vec![r(20), r(21), r(22)]),
457 ],
458 );
459 let view = SectionedSpine::new(EntityId(9), &e);
460 assert_eq!(view.spine_curve().unwrap(), EntityId(1));
461 assert_eq!(
462 view.checked_sections().unwrap(),
463 vec![
464 (EntityId(10), EntityId(20)),
465 (EntityId(11), EntityId(21)),
466 (EntityId(12), EntityId(22)),
467 ]
468 );
469 }
470
471 #[test]
474 fn mismatched_spine_list_lengths_are_reported_not_truncated() {
475 let e = entity(
476 "IFCSECTIONEDSPINE",
477 vec![
478 r(1),
479 list(vec![r(10), r(11), r(12)]),
480 list(vec![r(20), r(21)]),
481 ],
482 );
483 let view = SectionedSpine::new(EntityId(9), &e);
484 let err = view.checked_sections().unwrap_err();
485 assert_eq!(err.entity(), Some(EntityId(9)));
486 assert!(err.to_string().contains('3'));
487 }
488}