ifc_geometry/solid/swept/
directrix.rs1use super::{directrix_slot, disk_slot, spine_slot};
8use crate::error::GeometryResult;
9use crate::slots::Slots;
10use crate::solid::swept::area::SweptAreaSolid;
11use ifc_model::{Entity, EntityId};
12
13#[derive(Debug, Clone, Copy)]
24pub struct SurfaceCurveSweptAreaSolid<'m> {
25 slots: Slots<'m>,
26}
27
28impl<'m> SurfaceCurveSweptAreaSolid<'m> {
29 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
31 Self {
32 slots: Slots::new(id, entity),
33 }
34 }
35
36 pub fn id(&self) -> EntityId {
38 self.slots.id()
39 }
40
41 pub fn base(&self) -> SweptAreaSolid<'m> {
43 SweptAreaSolid::from_slots(self.slots)
44 }
45
46 pub fn directrix(&self) -> GeometryResult<EntityId> {
48 self.slots.req_ref(directrix_slot::DIRECTRIX, "Directrix")
49 }
50
51 pub fn start_param(&self) -> Option<f64> {
53 self.slots.opt_f64(directrix_slot::START_PARAM)
54 }
55
56 pub fn end_param(&self) -> Option<f64> {
58 self.slots.opt_f64(directrix_slot::END_PARAM)
59 }
60
61 pub fn reference_surface(&self) -> GeometryResult<EntityId> {
63 self.slots
64 .req_ref(directrix_slot::REFERENCE_SURFACE, "ReferenceSurface")
65 }
66}
67
68#[derive(Debug, Clone, Copy)]
76pub struct FixedReferenceSweptAreaSolid<'m> {
77 slots: Slots<'m>,
78}
79
80impl<'m> FixedReferenceSweptAreaSolid<'m> {
81 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
83 Self {
84 slots: Slots::new(id, entity),
85 }
86 }
87
88 pub fn id(&self) -> EntityId {
90 self.slots.id()
91 }
92
93 pub fn base(&self) -> SweptAreaSolid<'m> {
95 SweptAreaSolid::from_slots(self.slots)
96 }
97
98 pub fn directrix(&self) -> GeometryResult<EntityId> {
100 self.slots.req_ref(directrix_slot::DIRECTRIX, "Directrix")
101 }
102
103 pub fn start_param(&self) -> Option<f64> {
105 self.slots.opt_f64(directrix_slot::START_PARAM)
106 }
107
108 pub fn end_param(&self) -> Option<f64> {
110 self.slots.opt_f64(directrix_slot::END_PARAM)
111 }
112
113 pub fn fixed_reference(&self) -> GeometryResult<EntityId> {
115 self.slots
116 .req_ref(directrix_slot::FIXED_REFERENCE, "FixedReference")
117 }
118}
119
120#[derive(Debug, Clone, Copy)]
137pub struct SweptDiskSolid<'m> {
138 slots: Slots<'m>,
139}
140
141impl<'m> SweptDiskSolid<'m> {
142 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
144 Self {
145 slots: Slots::new(id, entity),
146 }
147 }
148
149 pub fn id(&self) -> EntityId {
151 self.slots.id()
152 }
153
154 pub fn directrix(&self) -> GeometryResult<EntityId> {
156 self.slots.req_ref(disk_slot::DIRECTRIX, "Directrix")
157 }
158
159 pub fn radius(&self) -> GeometryResult<f64> {
161 self.slots.req_f64(disk_slot::RADIUS, "Radius")
162 }
163
164 pub fn inner_radius(&self) -> Option<f64> {
166 self.slots.opt_f64(disk_slot::INNER_RADIUS)
167 }
168
169 pub fn start_param(&self) -> Option<f64> {
171 self.slots.opt_f64(disk_slot::START_PARAM)
172 }
173
174 pub fn end_param(&self) -> Option<f64> {
176 self.slots.opt_f64(disk_slot::END_PARAM)
177 }
178
179 pub fn checked_radii(&self) -> GeometryResult<(f64, Option<f64>)> {
181 let radius = self.radius()?;
182 if radius <= 0.0 {
183 return Err(self
184 .slots
185 .degenerate(format!("Radius must be positive, found {radius}")));
186 }
187 let inner = self.inner_radius();
188 if let Some(inner) = inner {
189 if inner >= radius {
190 return Err(self.slots.degenerate(format!(
191 "InnerRadius {inner} must be smaller than Radius {radius}"
192 )));
193 }
194 }
195 Ok((radius, inner))
196 }
197}
198
199#[derive(Debug, Clone, Copy)]
206pub struct SweptDiskSolidPolygonal<'m> {
207 slots: Slots<'m>,
208}
209
210impl<'m> SweptDiskSolidPolygonal<'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 base(&self) -> SweptDiskSolid<'m> {
225 SweptDiskSolid { slots: self.slots }
226 }
227
228 pub fn fillet_radius(&self) -> Option<f64> {
230 self.slots.opt_f64(disk_slot::FILLET_RADIUS)
231 }
232}
233
234#[derive(Debug, Clone, Copy)]
249pub struct SectionedSpine<'m> {
250 slots: Slots<'m>,
251}
252
253impl<'m> SectionedSpine<'m> {
254 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
256 Self {
257 slots: Slots::new(id, entity),
258 }
259 }
260
261 pub fn id(&self) -> EntityId {
263 self.slots.id()
264 }
265
266 pub fn spine_curve(&self) -> GeometryResult<EntityId> {
268 self.slots.req_ref(spine_slot::SPINE_CURVE, "SpineCurve")
269 }
270
271 pub fn cross_sections(&self) -> GeometryResult<Vec<EntityId>> {
273 self.slots
274 .req_ref_list(spine_slot::CROSS_SECTIONS, "CrossSections")
275 }
276
277 pub fn cross_section_positions(&self) -> GeometryResult<Vec<EntityId>> {
279 self.slots
280 .req_ref_list(spine_slot::CROSS_SECTION_POSITIONS, "CrossSectionPositions")
281 }
282
283 pub fn checked_sections(&self) -> GeometryResult<Vec<(EntityId, EntityId)>> {
285 let sections = self.cross_sections()?;
286 let positions = self.cross_section_positions()?;
287 if sections.len() != positions.len() {
288 return Err(self.slots.degenerate(format!(
289 "CrossSections has {} entries but CrossSectionPositions has {}",
290 sections.len(),
291 positions.len()
292 )));
293 }
294 Ok(sections.into_iter().zip(positions).collect())
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::solid::testkit::{entity, list, n, r};
302 use ifc_model::Value;
303
304 #[test]
305 fn surface_curve_sweep_exposes_directrix_surface_and_optional_trim() {
306 let e = entity(
307 "IFCSURFACECURVESWEPTAREASOLID",
308 vec![r(10), r(20), r(30), n(0.0), n(1.0), r(60)],
309 );
310 let view = SurfaceCurveSweptAreaSolid::new(EntityId(1), &e);
311 assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
312 assert_eq!(view.directrix().unwrap(), EntityId(30));
313 assert_eq!(view.start_param(), Some(0.0));
314 assert_eq!(view.end_param(), Some(1.0));
315 assert_eq!(view.reference_surface().unwrap(), EntityId(60));
316 }
317
318 #[test]
319 fn untrimmed_directrix_reports_absent_parameters_rather_than_zero() {
320 let e = entity(
321 "IFCSURFACECURVESWEPTAREASOLID",
322 vec![r(10), r(20), r(30), Value::Null, Value::Null, r(60)],
323 );
324 let view = SurfaceCurveSweptAreaSolid::new(EntityId(1), &e);
325 assert_eq!(view.start_param(), None);
326 assert_eq!(view.end_param(), None);
327 }
328
329 #[test]
332 fn fixed_reference_sweep_reads_a_direction_where_surface_sweep_reads_a_surface() {
333 let attrs = vec![r(10), r(20), r(30), Value::Null, Value::Null, r(70)];
334 let fixed = entity("IFCFIXEDREFERENCESWEPTAREASOLID", attrs.clone());
335 let surface = entity("IFCSURFACECURVESWEPTAREASOLID", attrs);
336
337 assert_eq!(
338 FixedReferenceSweptAreaSolid::new(EntityId(1), &fixed)
339 .fixed_reference()
340 .unwrap(),
341 EntityId(70)
342 );
343 assert_eq!(
344 SurfaceCurveSweptAreaSolid::new(EntityId(1), &surface)
345 .reference_surface()
346 .unwrap(),
347 EntityId(70)
348 );
349 assert_eq!(
350 FixedReferenceSweptAreaSolid::new(EntityId(1), &fixed)
351 .directrix()
352 .unwrap(),
353 EntityId(30)
354 );
355 }
356
357 #[test]
360 fn swept_disk_directrix_is_slot_zero_with_no_inherited_profile() {
361 let e = entity(
362 "IFCSWEPTDISKSOLID",
363 vec![r(10), n(0.1), n(0.08), n(0.0), n(1.0)],
364 );
365 let view = SweptDiskSolid::new(EntityId(1), &e);
366 assert_eq!(view.directrix().unwrap(), EntityId(10));
367 assert_eq!(view.radius().unwrap(), 0.1);
368 assert_eq!(view.inner_radius(), Some(0.08));
369 assert_eq!(view.start_param(), Some(0.0));
370 assert_eq!(view.end_param(), Some(1.0));
371 }
372
373 #[test]
374 fn inner_radius_not_smaller_than_outer_is_degenerate() {
375 let bad = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1), n(0.1)]);
376 let err = SweptDiskSolid::new(EntityId(5), &bad)
377 .checked_radii()
378 .unwrap_err();
379 assert_eq!(err.entity(), Some(EntityId(5)));
380
381 let tube = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1), n(0.09)]);
382 assert_eq!(
383 SweptDiskSolid::new(EntityId(5), &tube)
384 .checked_radii()
385 .unwrap(),
386 (0.1, Some(0.09))
387 );
388
389 let rod = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1)]);
390 assert_eq!(
391 SweptDiskSolid::new(EntityId(5), &rod)
392 .checked_radii()
393 .unwrap(),
394 (0.1, None)
395 );
396 }
397
398 #[test]
399 fn polygonal_disk_fillet_radius_is_optional() {
400 let with_fillet = entity(
401 "IFCSWEPTDISKSOLIDPOLYGONAL",
402 vec![
403 r(10),
404 n(0.1),
405 Value::Null,
406 Value::Null,
407 Value::Null,
408 n(0.15),
409 ],
410 );
411 let view = SweptDiskSolidPolygonal::new(EntityId(1), &with_fillet);
412 assert_eq!(view.fillet_radius(), Some(0.15));
413 assert_eq!(view.base().radius().unwrap(), 0.1);
414
415 let sharp = entity("IFCSWEPTDISKSOLIDPOLYGONAL", vec![r(10), n(0.1)]);
416 assert_eq!(
417 SweptDiskSolidPolygonal::new(EntityId(1), &sharp).fillet_radius(),
418 None
419 );
420 }
421
422 #[test]
423 fn sectioned_spine_pairs_each_section_with_its_own_placement() {
424 let e = entity(
425 "IFCSECTIONEDSPINE",
426 vec![
427 r(1),
428 list(vec![r(10), r(11), r(12)]),
429 list(vec![r(20), r(21), r(22)]),
430 ],
431 );
432 let view = SectionedSpine::new(EntityId(9), &e);
433 assert_eq!(view.spine_curve().unwrap(), EntityId(1));
434 assert_eq!(
435 view.checked_sections().unwrap(),
436 vec![
437 (EntityId(10), EntityId(20)),
438 (EntityId(11), EntityId(21)),
439 (EntityId(12), EntityId(22)),
440 ]
441 );
442 }
443
444 #[test]
447 fn mismatched_spine_list_lengths_are_reported_not_truncated() {
448 let e = entity(
449 "IFCSECTIONEDSPINE",
450 vec![
451 r(1),
452 list(vec![r(10), r(11), r(12)]),
453 list(vec![r(20), r(21)]),
454 ],
455 );
456 let view = SectionedSpine::new(EntityId(9), &e);
457 let err = view.checked_sections().unwrap_err();
458 assert_eq!(err.entity(), Some(EntityId(9)));
459 assert!(err.to_string().contains('3'));
460 }
461}