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> {
49 self.slots.req_ref(swept_area_slot::SWEPT_AREA, "SweptArea")
50 }
51
52 pub fn position(&self) -> Option<EntityId> {
57 self.slots.opt_ref(swept_area_slot::POSITION)
58 }
59}
60
61#[derive(Debug, Clone, Copy)]
77pub struct ExtrudedAreaSolid<'m> {
78 slots: Slots<'m>,
79}
80
81impl<'m> ExtrudedAreaSolid<'m> {
82 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
84 Self {
85 slots: Slots::new(id, entity),
86 }
87 }
88
89 pub fn id(&self) -> EntityId {
91 self.slots.id()
92 }
93
94 pub fn base(&self) -> SweptAreaSolid<'m> {
96 SweptAreaSolid::from_slots(self.slots)
97 }
98
99 pub fn extruded_direction(&self) -> GeometryResult<EntityId> {
104 self.slots
105 .req_ref(extruded_slot::EXTRUDED_DIRECTION, "ExtrudedDirection")
106 }
107
108 pub fn depth(&self) -> GeometryResult<f64> {
110 self.slots.req_f64(extruded_slot::DEPTH, "Depth")
111 }
112
113 pub fn checked_depth(&self) -> GeometryResult<f64> {
119 let depth = self.depth()?;
120 if depth > 0.0 {
121 Ok(depth)
122 } else {
123 Err(self
124 .slots
125 .degenerate(format!("Depth must be positive, found {depth}")))
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy)]
137pub struct ExtrudedAreaSolidTapered<'m> {
138 slots: Slots<'m>,
139}
140
141impl<'m> ExtrudedAreaSolidTapered<'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 base(&self) -> ExtrudedAreaSolid<'m> {
156 ExtrudedAreaSolid { slots: self.slots }
157 }
158
159 pub fn end_swept_area(&self) -> GeometryResult<EntityId> {
161 self.slots
162 .req_ref(extruded_slot::END_SWEPT_AREA, "EndSweptArea")
163 }
164}
165
166#[derive(Debug, Clone, Copy)]
178pub struct RevolvedAreaSolid<'m> {
179 slots: Slots<'m>,
180}
181
182impl<'m> RevolvedAreaSolid<'m> {
183 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
185 Self {
186 slots: Slots::new(id, entity),
187 }
188 }
189
190 pub fn id(&self) -> EntityId {
192 self.slots.id()
193 }
194
195 pub fn base(&self) -> SweptAreaSolid<'m> {
197 SweptAreaSolid::from_slots(self.slots)
198 }
199
200 pub fn axis(&self) -> GeometryResult<EntityId> {
205 self.slots.req_ref(revolved_slot::AXIS, "Axis")
206 }
207
208 pub fn angle_raw(&self) -> GeometryResult<f64> {
210 self.slots.req_f64(revolved_slot::ANGLE, "Angle")
211 }
212}
213
214#[derive(Debug, Clone, Copy)]
219pub struct RevolvedAreaSolidTapered<'m> {
220 slots: Slots<'m>,
221}
222
223impl<'m> RevolvedAreaSolidTapered<'m> {
224 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
226 Self {
227 slots: Slots::new(id, entity),
228 }
229 }
230
231 pub fn id(&self) -> EntityId {
233 self.slots.id()
234 }
235
236 pub fn base(&self) -> RevolvedAreaSolid<'m> {
238 RevolvedAreaSolid { slots: self.slots }
239 }
240
241 pub fn end_swept_area(&self) -> GeometryResult<EntityId> {
243 self.slots
244 .req_ref(revolved_slot::END_SWEPT_AREA, "EndSweptArea")
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::solid::testkit::{entity, n, r};
252 use ifc_model::Value;
253
254 fn extrusion(attrs: Vec<Value>) -> Entity {
255 entity("IFCEXTRUDEDAREASOLID", attrs)
256 }
257
258 #[test]
261 fn inherited_swept_area_slots_precede_the_subtype_own_slots() {
262 let e = extrusion(vec![r(10), r(20), r(30), n(3.0)]);
263 let view = ExtrudedAreaSolid::new(EntityId(1), &e);
264 assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
265 assert_eq!(view.base().position(), Some(EntityId(20)));
266 assert_eq!(view.extruded_direction().unwrap(), EntityId(30));
267 assert_eq!(view.depth().unwrap(), 3.0);
268 }
269
270 #[test]
272 fn absent_position_is_reported_as_none_not_as_a_failure() {
273 let e = extrusion(vec![r(10), Value::Null, r(30), n(3.0)]);
274 let view = ExtrudedAreaSolid::new(EntityId(1), &e);
275 assert_eq!(view.base().position(), None);
276 assert!(view.base().swept_area().is_ok());
277 }
278
279 #[test]
282 fn extruded_direction_is_a_reference_and_never_defaulted_to_z() {
283 let e = extrusion(vec![r(10), r(20), r(99), n(1.0)]);
284 assert_eq!(
285 ExtrudedAreaSolid::new(EntityId(1), &e)
286 .extruded_direction()
287 .unwrap(),
288 EntityId(99)
289 );
290
291 let missing = extrusion(vec![r(10), r(20), Value::Null, n(1.0)]);
292 assert!(ExtrudedAreaSolid::new(EntityId(1), &missing)
293 .extruded_direction()
294 .is_err());
295 }
296
297 #[test]
298 fn non_positive_depth_is_rejected_as_degenerate() {
299 for bad in [0.0, -2.5] {
300 let e = extrusion(vec![r(10), r(20), r(30), n(bad)]);
301 let view = ExtrudedAreaSolid::new(EntityId(7), &e);
302 let err = view.checked_depth().unwrap_err();
303 assert_eq!(err.entity(), Some(EntityId(7)));
304 assert!(view.depth().is_ok(), "raw depth stays readable");
305 }
306 let good = extrusion(vec![r(10), r(20), r(30), n(2.5)]);
307 assert_eq!(
308 ExtrudedAreaSolid::new(EntityId(7), &good)
309 .checked_depth()
310 .unwrap(),
311 2.5
312 );
313 }
314
315 #[test]
316 fn tapered_extrusion_keeps_both_profiles_addressable() {
317 let e = entity(
318 "IFCEXTRUDEDAREASOLIDTAPERED",
319 vec![r(10), r(20), r(30), n(3.0), r(40)],
320 );
321 let view = ExtrudedAreaSolidTapered::new(EntityId(1), &e);
322 assert_eq!(view.base().base().swept_area().unwrap(), EntityId(10));
323 assert_eq!(view.base().depth().unwrap(), 3.0);
324 assert_eq!(view.end_swept_area().unwrap(), EntityId(40));
325 }
326
327 #[test]
330 fn revolution_angle_is_returned_raw_without_unit_conversion() {
331 let e = entity("IFCREVOLVEDAREASOLID", vec![r(10), r(20), r(30), n(90.0)]);
332 let view = RevolvedAreaSolid::new(EntityId(1), &e);
333 assert_eq!(view.angle_raw().unwrap(), 90.0);
334 assert_eq!(view.axis().unwrap(), EntityId(30));
335 assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
336 }
337
338 #[test]
339 fn tapered_revolution_exposes_its_end_profile() {
340 let e = entity(
341 "IFCREVOLVEDAREASOLIDTAPERED",
342 vec![r(10), r(20), r(30), n(45.0), r(50)],
343 );
344 let view = RevolvedAreaSolidTapered::new(EntityId(1), &e);
345 assert_eq!(view.base().angle_raw().unwrap(), 45.0);
346 assert_eq!(view.end_swept_area().unwrap(), EntityId(50));
347 }
348
349 #[test]
351 fn abstract_view_reports_the_concrete_subtype_name() {
352 let e = extrusion(vec![r(10), r(20), r(30), n(1.0)]);
353 let view = SweptAreaSolid::new(EntityId(1), &e);
354 assert_eq!(view.type_name(), "IFCEXTRUDEDAREASOLID");
355 assert_eq!(view.id(), EntityId(1));
356 }
357}