ifc_geometry/input/body/mod.rs
1//! What a product's Body representation is made of, without lowering it.
2//!
3//! # Why a description and not a mesh
4//!
5//! Rule checks on structural members and walls ask how a body is modelled:
6//! "is this beam an extrusion of an I-section, how deep is the web, which way
7//! does it run". A mesh has lost every one of those answers. This module
8//! reads them from the representation items directly, in SI units and world
9//! coordinates, and links no geometry kernel.
10//!
11//! # One entry per item, never a merged answer
12//!
13//! A Body representation may hold several items, and a mapped item may map
14//! several more. [`body_description`] reports one [`BodyItem`] per resolved
15//! geometric item, in authored order, rather than refusing or picking one:
16//! a column with a base plate is two extrusions, and both are facts a rule
17//! check may need. [`BodyDescription::sole_item`] is the convenience for the
18//! common single-item case.
19//!
20//! # All or nothing
21//!
22//! If any item cannot be described exactly (an unsupported family, a dangling
23//! reference, a mapping that scales a swept solid), the whole call fails with
24//! a typed error naming the entity. A partial list would let a caller treat a
25//! body as fully checked when part of it was never read.
26//!
27//! # Frames
28//!
29//! Items are placed exactly as lowering places them: the representation
30//! context's `WorldCoordinateSystem`, then the product's placement chain, then
31//! for a mapped item `MappingTarget` and `MappingOrigin`, then the item's own
32//! `Position`. Mapped geometry therefore resolves to the same answer as the
33//! same geometry authored in place.
34
35mod kind;
36mod sweep;
37
38pub use kind::BodyKind;
39
40use ifc_model::{EntityId, Model};
41
42use super::context::product_representation_frame;
43use super::profile::ProfileDescription;
44use super::representation::{select_shape_representation, Representation, RepresentationPurpose};
45use crate::error::{GeometryError, GeometryResult};
46use crate::resource::mapped::MappingWalker;
47use crate::resource::operator::operator_transform;
48use crate::resource::placement::axis_placement_transform;
49use crate::slots::Slots;
50use crate::transform::Transform;
51use crate::units::UnitScale;
52
53/// The Body representation of one product, one entry per geometric item.
54#[derive(Debug, Clone, PartialEq)]
55#[non_exhaustive]
56pub struct BodyDescription {
57 /// The product described.
58 pub product: EntityId,
59 /// The representation selected as the body
60 /// ([`crate::select_shape_representation`]).
61 pub representation: EntityId,
62 /// One entry per geometric item, mapped items resolved, in authored order.
63 pub items: Vec<BodyItem>,
64}
65
66impl BodyDescription {
67 /// The only item, when the body has exactly one.
68 ///
69 /// `None` for an empty body and for a body of several items; a caller
70 /// that needs every item reads [`Self::items`].
71 pub fn sole_item(&self) -> Option<&BodyItem> {
72 match self.items.as_slice() {
73 [item] => Some(item),
74 _ => None,
75 }
76 }
77}
78
79/// One geometric representation item of a body.
80#[derive(Debug, Clone, PartialEq)]
81#[non_exhaustive]
82pub struct BodyItem {
83 /// The geometric item itself, never an `IfcMappedItem`.
84 pub item: EntityId,
85 /// Its concrete IFC type in upper case.
86 pub type_name: String,
87 /// The `IfcMappedItem`s this item was reached through, outermost first.
88 /// Empty when the item is authored directly in the body.
89 pub mapped_by: Vec<EntityId>,
90 /// How the item models its shape.
91 pub kind: BodyKind,
92 /// Profile and path, for the swept-area families
93 /// ([`BodyKind::is_swept_area`]); `None` for every other kind.
94 pub swept: Option<SweptSolid>,
95}
96
97/// A swept-area solid: its profile, where it sits, and the path it follows.
98#[derive(Debug, Clone, PartialEq)]
99#[non_exhaustive]
100pub struct SweptSolid {
101 /// `SweptArea`, in metres and radians.
102 pub profile: ProfileDescription,
103 /// `EndSweptArea` of a tapered sweep; `None` otherwise.
104 pub end_profile: Option<ProfileDescription>,
105 /// The solid's `Position` composed into world coordinates, in metres.
106 ///
107 /// The profile lies in this frame's XY plane. Rigid by construction: a
108 /// mapping that scales or mirrors a swept solid is refused, because its
109 /// profile parameters would no longer be the authored ones.
110 pub placement_world: Transform,
111 /// The path the profile follows.
112 pub path: SweepPath,
113}
114
115/// The path of a swept-area solid, in world coordinates.
116#[derive(Debug, Clone, PartialEq)]
117#[non_exhaustive]
118pub enum SweepPath {
119 /// A straight extrusion (plain or tapered).
120 #[non_exhaustive]
121 Extrusion {
122 /// `ExtrudedDirection` in world coordinates, unit length.
123 direction_world: [f64; 3],
124 /// `Depth`, in metres, measured along `direction_world`.
125 depth: f64,
126 },
127 /// A revolution about an axis (plain or tapered).
128 #[non_exhaustive]
129 Revolution {
130 /// The axis origin in world coordinates, in metres.
131 axis_origin_world: [f64; 3],
132 /// The axis direction in world coordinates, unit length.
133 axis_direction_world: [f64; 3],
134 /// `Angle`, in radians.
135 angle: f64,
136 },
137 /// A sweep along a directrix curve, reported by reference.
138 ///
139 /// The curve and its `StartParam`/`EndParam` live in the directrix's own
140 /// parameterisation; reading them is a curve question, not a body one.
141 #[non_exhaustive]
142 Directrix {
143 /// The `Directrix` curve.
144 directrix: EntityId,
145 },
146}
147
148/// Describe the Body representation of `product`.
149///
150/// Returns `Ok(None)` when the product has no body representation (only an
151/// Axis or FootPrint, or none at all): that is an answer, not a failure. A
152/// body whose representation lists no items yields an empty `items`.
153///
154/// Every item is described or the call fails; see the module docs. Kernel-free:
155/// available with `--no-default-features`.
156///
157/// ```no_run
158/// # use ifc_model::{EntityId, Model};
159/// # use ifc_geometry::{body_description, units, BodyKind, SweepPath};
160/// # fn demo(model: &Model, beam: EntityId) {
161/// let scale = units::resolve(model);
162/// let body = body_description(model, &scale, beam).unwrap().expect("a body");
163/// let item = body.sole_item().expect("one item");
164/// if item.kind == BodyKind::Extrusion {
165/// let swept = item.swept.as_ref().unwrap();
166/// if let SweepPath::Extrusion { direction_world, depth, .. } = swept.path {
167/// let _ = (swept.profile.type_name.as_str(), direction_world, depth);
168/// }
169/// }
170/// # }
171/// ```
172pub fn body_description(
173 model: &Model,
174 units: &UnitScale,
175 product: EntityId,
176) -> GeometryResult<Option<BodyDescription>> {
177 let Some(representation) = select_shape_representation(model, product)? else {
178 return Ok(None);
179 };
180 // The same frame lowering places the body's items in.
181 let Some(world) =
182 product_representation_frame(model, units, product, RepresentationPurpose::Body)?
183 else {
184 return Ok(None);
185 };
186
187 let mut walk = Walk {
188 model,
189 units,
190 walker: MappingWalker::new(),
191 mapped_by: Vec::new(),
192 items: Vec::new(),
193 };
194 walk.representation(product, representation, world)?;
195 Ok(Some(BodyDescription {
196 product,
197 representation,
198 items: walk.items,
199 }))
200}
201
202/// State for one body walk: the mapped-item stack and the collected items.
203struct Walk<'m> {
204 model: &'m Model,
205 units: &'m UnitScale,
206 walker: MappingWalker,
207 mapped_by: Vec<EntityId>,
208 items: Vec<BodyItem>,
209}
210
211impl Walk<'_> {
212 /// Describe every item of one `IfcRepresentation` under `frame`.
213 fn representation(
214 &mut self,
215 referrer: EntityId,
216 representation: EntityId,
217 frame: Transform,
218 ) -> GeometryResult<()> {
219 let entity = self
220 .model
221 .get(representation)
222 .ok_or(GeometryError::MissingEntity {
223 referrer,
224 missing: representation,
225 })?;
226 for item in Representation::new(representation, entity).items()? {
227 self.item(representation, item, frame)?;
228 }
229 Ok(())
230 }
231
232 /// Describe one item, resolving a mapped item to what it maps.
233 fn item(&mut self, referrer: EntityId, item: EntityId, frame: Transform) -> GeometryResult<()> {
234 let entity = self.model.get(item).ok_or(GeometryError::MissingEntity {
235 referrer,
236 missing: item,
237 })?;
238 let type_name = entity.type_name.to_ascii_uppercase();
239 if type_name == "IFCMAPPEDITEM" {
240 return self.mapped(item, frame);
241 }
242 let kind = BodyKind::classify(&type_name).ok_or_else(|| {
243 Slots::new(item, entity).unsupported("representation item family is not described")
244 })?;
245 let swept = if kind.is_swept_area() {
246 // The innermost mapping is what scaled the frame, if anything did.
247 let culprit = self.mapped_by.last().copied().unwrap_or(item);
248 Some(sweep::describe(
249 self.model, self.units, item, entity, frame, culprit,
250 )?)
251 } else {
252 None
253 };
254 self.items.push(BodyItem {
255 item,
256 type_name,
257 mapped_by: self.mapped_by.clone(),
258 kind,
259 swept,
260 });
261 Ok(())
262 }
263
264 /// Resolve an `IfcMappedItem`: `frame o MappingTarget o MappingOrigin`.
265 ///
266 /// The same composition as `lower::mapped`, so a mapped body and the same
267 /// body authored in place describe identically.
268 fn mapped(&mut self, item: EntityId, frame: Transform) -> GeometryResult<()> {
269 self.walker.enter(item)?;
270 let result = self.mapped_inner(item, frame);
271 self.walker.exit();
272 result
273 }
274
275 fn mapped_inner(&mut self, item: EntityId, frame: Transform) -> GeometryResult<()> {
276 let instance = self.walker.resolve(self.model, item)?;
277 let target_entity =
278 self.model
279 .get(instance.mapping_target)
280 .ok_or(GeometryError::MissingEntity {
281 referrer: item,
282 missing: instance.mapping_target,
283 })?;
284 // Both frames carry file-unit coordinates; convert exactly once here.
285 let target = operator_transform(self.model, instance.mapping_target, target_entity)?
286 .to_metres(self.units);
287 let origin_entity =
288 self.model
289 .get(instance.mapping_origin)
290 .ok_or(GeometryError::MissingEntity {
291 referrer: item,
292 missing: instance.mapping_origin,
293 })?;
294 let origin = axis_placement_transform(self.model, instance.mapping_origin, origin_entity)?
295 .to_metres(self.units);
296 let inner = frame.compose(&target).compose(&origin);
297
298 self.mapped_by.push(item);
299 let result = self.representation(item, instance.mapped_representation, inner);
300 self.mapped_by.pop();
301 result
302 }
303}