Skip to main content

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