ifc_geometry/solid/swept/
area.rs1use super::{extruded_slot, revolved_slot, swept_area_slot};
6use crate::error::GeometryResult;
7use crate::slots::Slots;
8use ifc_model::{Entity, EntityId};
9
10#[derive(Debug, Clone, Copy)]
16pub struct SweptAreaSolid<'m> {
17 slots: Slots<'m>,
18}
19
20impl<'m> SweptAreaSolid<'m> {
21 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
23 Self {
24 slots: Slots::new(id, entity),
25 }
26 }
27
28 pub(super) fn from_slots(slots: Slots<'m>) -> Self {
30 Self { slots }
31 }
32
33 pub fn id(&self) -> EntityId {
35 self.slots.id()
36 }
37
38 pub fn type_name(&self) -> &'m str {
40 self.slots.type_name()
41 }
42
43 pub fn swept_area(&self) -> GeometryResult<EntityId> {
48 self.slots.req_ref(swept_area_slot::SWEPT_AREA, "SweptArea")
49 }
50
51 pub fn position(&self) -> Option<EntityId> {
56 self.slots.opt_ref(swept_area_slot::POSITION)
57 }
58}
59
60#[derive(Debug, Clone, Copy)]
76pub struct ExtrudedAreaSolid<'m> {
77 slots: Slots<'m>,
78}
79
80impl<'m> ExtrudedAreaSolid<'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 extruded_direction(&self) -> GeometryResult<EntityId> {
103 self.slots
104 .req_ref(extruded_slot::EXTRUDED_DIRECTION, "ExtrudedDirection")
105 }
106
107 pub fn depth(&self) -> GeometryResult<f64> {
109 self.slots.req_f64(extruded_slot::DEPTH, "Depth")
110 }
111
112 pub fn checked_depth(&self) -> GeometryResult<f64> {
118 let depth = self.depth()?;
119 if depth > 0.0 {
120 Ok(depth)
121 } else {
122 Err(self
123 .slots
124 .degenerate(format!("Depth must be positive, found {depth}")))
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy)]
136pub struct ExtrudedAreaSolidTapered<'m> {
137 slots: Slots<'m>,
138}
139
140impl<'m> ExtrudedAreaSolidTapered<'m> {
141 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
143 Self {
144 slots: Slots::new(id, entity),
145 }
146 }
147
148 pub fn id(&self) -> EntityId {
150 self.slots.id()
151 }
152
153 pub fn base(&self) -> ExtrudedAreaSolid<'m> {
155 ExtrudedAreaSolid { slots: self.slots }
156 }
157
158 pub fn end_swept_area(&self) -> GeometryResult<EntityId> {
160 self.slots
161 .req_ref(extruded_slot::END_SWEPT_AREA, "EndSweptArea")
162 }
163}
164
165#[derive(Debug, Clone, Copy)]
177pub struct RevolvedAreaSolid<'m> {
178 slots: Slots<'m>,
179}
180
181impl<'m> RevolvedAreaSolid<'m> {
182 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
184 Self {
185 slots: Slots::new(id, entity),
186 }
187 }
188
189 pub fn id(&self) -> EntityId {
191 self.slots.id()
192 }
193
194 pub fn base(&self) -> SweptAreaSolid<'m> {
196 SweptAreaSolid::from_slots(self.slots)
197 }
198
199 pub fn axis(&self) -> GeometryResult<EntityId> {
204 self.slots.req_ref(revolved_slot::AXIS, "Axis")
205 }
206
207 pub fn angle_raw(&self) -> GeometryResult<f64> {
209 self.slots.req_f64(revolved_slot::ANGLE, "Angle")
210 }
211}
212
213#[derive(Debug, Clone, Copy)]
218pub struct RevolvedAreaSolidTapered<'m> {
219 slots: Slots<'m>,
220}
221
222impl<'m> RevolvedAreaSolidTapered<'m> {
223 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
225 Self {
226 slots: Slots::new(id, entity),
227 }
228 }
229
230 pub fn id(&self) -> EntityId {
232 self.slots.id()
233 }
234
235 pub fn base(&self) -> RevolvedAreaSolid<'m> {
237 RevolvedAreaSolid { slots: self.slots }
238 }
239
240 pub fn end_swept_area(&self) -> GeometryResult<EntityId> {
242 self.slots
243 .req_ref(revolved_slot::END_SWEPT_AREA, "EndSweptArea")
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::solid::testkit::{entity, n, r};
251 use ifc_model::Value;
252
253 fn extrusion(attrs: Vec<Value>) -> Entity {
254 entity("IFCEXTRUDEDAREASOLID", attrs)
255 }
256
257 #[test]
260 fn inherited_swept_area_slots_precede_the_subtype_own_slots() {
261 let e = extrusion(vec![r(10), r(20), r(30), n(3.0)]);
262 let view = ExtrudedAreaSolid::new(EntityId(1), &e);
263 assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
264 assert_eq!(view.base().position(), Some(EntityId(20)));
265 assert_eq!(view.extruded_direction().unwrap(), EntityId(30));
266 assert_eq!(view.depth().unwrap(), 3.0);
267 }
268
269 #[test]
271 fn absent_position_is_reported_as_none_not_as_a_failure() {
272 let e = extrusion(vec![r(10), Value::Null, r(30), n(3.0)]);
273 let view = ExtrudedAreaSolid::new(EntityId(1), &e);
274 assert_eq!(view.base().position(), None);
275 assert!(view.base().swept_area().is_ok());
276 }
277
278 #[test]
281 fn extruded_direction_is_a_reference_and_never_defaulted_to_z() {
282 let e = extrusion(vec![r(10), r(20), r(99), n(1.0)]);
283 assert_eq!(
284 ExtrudedAreaSolid::new(EntityId(1), &e)
285 .extruded_direction()
286 .unwrap(),
287 EntityId(99)
288 );
289
290 let missing = extrusion(vec![r(10), r(20), Value::Null, n(1.0)]);
291 assert!(ExtrudedAreaSolid::new(EntityId(1), &missing)
292 .extruded_direction()
293 .is_err());
294 }
295
296 #[test]
297 fn non_positive_depth_is_rejected_as_degenerate() {
298 for bad in [0.0, -2.5] {
299 let e = extrusion(vec![r(10), r(20), r(30), n(bad)]);
300 let view = ExtrudedAreaSolid::new(EntityId(7), &e);
301 let err = view.checked_depth().unwrap_err();
302 assert_eq!(err.entity(), Some(EntityId(7)));
303 assert!(view.depth().is_ok(), "raw depth stays readable");
304 }
305 let good = extrusion(vec![r(10), r(20), r(30), n(2.5)]);
306 assert_eq!(
307 ExtrudedAreaSolid::new(EntityId(7), &good)
308 .checked_depth()
309 .unwrap(),
310 2.5
311 );
312 }
313
314 #[test]
315 fn tapered_extrusion_keeps_both_profiles_addressable() {
316 let e = entity(
317 "IFCEXTRUDEDAREASOLIDTAPERED",
318 vec![r(10), r(20), r(30), n(3.0), r(40)],
319 );
320 let view = ExtrudedAreaSolidTapered::new(EntityId(1), &e);
321 assert_eq!(view.base().base().swept_area().unwrap(), EntityId(10));
322 assert_eq!(view.base().depth().unwrap(), 3.0);
323 assert_eq!(view.end_swept_area().unwrap(), EntityId(40));
324 }
325
326 #[test]
329 fn revolution_angle_is_returned_raw_without_unit_conversion() {
330 let e = entity("IFCREVOLVEDAREASOLID", vec![r(10), r(20), r(30), n(90.0)]);
331 let view = RevolvedAreaSolid::new(EntityId(1), &e);
332 assert_eq!(view.angle_raw().unwrap(), 90.0);
333 assert_eq!(view.axis().unwrap(), EntityId(30));
334 assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
335 }
336
337 #[test]
338 fn tapered_revolution_exposes_its_end_profile() {
339 let e = entity(
340 "IFCREVOLVEDAREASOLIDTAPERED",
341 vec![r(10), r(20), r(30), n(45.0), r(50)],
342 );
343 let view = RevolvedAreaSolidTapered::new(EntityId(1), &e);
344 assert_eq!(view.base().angle_raw().unwrap(), 45.0);
345 assert_eq!(view.end_swept_area().unwrap(), EntityId(50));
346 }
347
348 #[test]
350 fn abstract_view_reports_the_concrete_subtype_name() {
351 let e = extrusion(vec![r(10), r(20), r(30), n(1.0)]);
352 let view = SweptAreaSolid::new(EntityId(1), &e);
353 assert_eq!(view.type_name(), "IFCEXTRUDEDAREASOLID");
354 assert_eq!(view.id(), EntityId(1));
355 }
356}