Skip to main content

ifc_geometry/solid/
csg.rs

1//! CSG: analytic primitives and the solid that roots a CSG tree.
2//!
3//! # The shape of a CSG solid
4//!
5//! `IfcCsgSolid` holds a single `TreeRootExpression`, an `IfcCsgSelect` which
6//! is either an `IfcBooleanResult` or an `IfcCsgPrimitive3D`. The tree lives in
7//! the boolean results, so a CSG solid is a one-attribute wrapper, not a tree
8//! node itself.
9//!
10//! # Where a primitive sits
11//!
12//! Every `IfcCsgPrimitive3D` has a **required** `Position`
13//! (`IfcAxis2Placement3D`) at slot 0. Unlike `IfcSweptAreaSolid.Position` it is
14//! not optional, so there is no identity default to fall back on.
15//!
16//! # Origin conventions differ per primitive
17//!
18//! This is the trap. The placement origin is the **corner** of a block and of a
19//! pyramid's base, but the **centre** of the base circle for a cylinder and a
20//! cone, and the **centre** of a sphere. Applying one convention uniformly
21//! offsets half the primitives by half their size.
22
23use crate::error::GeometryResult;
24use crate::slots::Slots;
25use ifc_model::{Entity, EntityId};
26
27/// `IfcCsgSolid` slots.
28///
29/// EXPRESS (IFC4 ADD2 TC1): subtypes `IfcSolidModel`, which declares no
30/// explicit attributes, so `TreeRootExpression` is absolute slot 0.
31pub(crate) mod csg_solid_slot {
32    /// `TreeRootExpression : IfcCsgSelect`.
33    pub const TREE_ROOT_EXPRESSION: usize = 0;
34}
35
36/// `IfcCsgPrimitive3D` slots, inherited by every primitive.
37///
38/// EXPRESS: `Position : IfcAxis2Placement3D` is declared on
39/// `IfcCsgPrimitive3D`, whose supertype `IfcGeometricRepresentationItem` has no
40/// explicit attributes -- so it is absolute slot 0 and each primitive's own
41/// dimensions start at slot 1.
42pub(crate) mod primitive_slot {
43    /// `Position : IfcAxis2Placement3D`, required.
44    pub const POSITION: usize = 0;
45    /// First dimension attribute of the concrete primitive.
46    pub const DIM_0: usize = 1;
47    /// Second dimension attribute, where the primitive has one.
48    pub const DIM_1: usize = 2;
49    /// Third dimension attribute, where the primitive has one.
50    pub const DIM_2: usize = 3;
51}
52
53/// `IfcCsgSolid`: a solid defined by a CSG expression tree.
54///
55/// The root reference is returned unresolved: it may be an `IfcBooleanResult`
56/// or an `IfcCsgPrimitive3D`, and deciding which is the caller's dispatch, not
57/// this view's.
58#[derive(Debug, Clone, Copy)]
59pub struct CsgSolid<'m> {
60    slots: Slots<'m>,
61}
62
63impl<'m> CsgSolid<'m> {
64    /// Wrap an entity assumed to be an `IfcCsgSolid`.
65    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
66        Self {
67            slots: Slots::new(id, entity),
68        }
69    }
70
71    /// The entity id.
72    pub fn id(&self) -> EntityId {
73        self.slots.id()
74    }
75
76    /// The `IfcCsgSelect` root: an `IfcBooleanResult` or `IfcCsgPrimitive3D`.
77    pub fn tree_root_expression(&self) -> GeometryResult<EntityId> {
78        self.slots
79            .req_ref(csg_solid_slot::TREE_ROOT_EXPRESSION, "TreeRootExpression")
80    }
81}
82
83/// `IfcCsgPrimitive3D`: the abstract primitive, giving access to `Position`.
84///
85/// Every concrete primitive delegates here for its placement, since it is at
86/// the same absolute slot 0 in all five.
87#[derive(Debug, Clone, Copy)]
88pub struct CsgPrimitive3D<'m> {
89    slots: Slots<'m>,
90}
91
92impl<'m> CsgPrimitive3D<'m> {
93    /// Wrap an entity assumed to be an `IfcCsgPrimitive3D` subtype.
94    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
95        Self {
96            slots: Slots::new(id, entity),
97        }
98    }
99
100    /// The entity id.
101    pub fn id(&self) -> EntityId {
102        self.slots.id()
103    }
104
105    /// The IFC type name, naming the concrete primitive.
106    pub fn type_name(&self) -> &'m str {
107        self.slots.type_name()
108    }
109
110    /// The `IfcAxis2Placement3D` reference. Required, with no default.
111    pub fn position(&self) -> GeometryResult<EntityId> {
112        self.slots.req_ref(primitive_slot::POSITION, "Position")
113    }
114}
115
116/// Emit an accessor-bearing primitive newtype with the shared shape.
117///
118/// Written as a macro because the five primitives differ only in which of the
119/// three dimension slots they use and what they are called; hand-writing them
120/// would be 200 lines whose only content is a slot number, and a typo in one of
121/// them would be invisible on review.
122macro_rules! csg_primitive {
123    (
124        $(#[$meta:meta])*
125        $name:ident { $( $(#[$field_meta:meta])* $method:ident => $slot:expr , $express:literal );+ $(;)? }
126    ) => {
127        $(#[$meta])*
128        #[derive(Debug, Clone, Copy)]
129        pub struct $name<'m> {
130            slots: Slots<'m>,
131        }
132
133        impl<'m> $name<'m> {
134            /// Wrap an entity assumed to be of this primitive's type.
135            pub fn new(id: EntityId, entity: &'m Entity) -> Self {
136                Self { slots: Slots::new(id, entity) }
137            }
138
139            /// The entity id.
140            pub fn id(&self) -> EntityId {
141                self.slots.id()
142            }
143
144            /// The inherited `IfcCsgPrimitive3D` attributes.
145            pub fn base(&self) -> CsgPrimitive3D<'m> {
146                CsgPrimitive3D { slots: self.slots }
147            }
148
149            $(
150                $(#[$field_meta])*
151                pub fn $method(&self) -> GeometryResult<f64> {
152                    self.slots.req_f64($slot, $express)
153                }
154            )+
155        }
156    };
157}
158
159csg_primitive! {
160    /// `IfcBlock`: an axis-aligned box.
161    ///
162    /// The placement origin is the box **corner**, and the box extends along
163    /// +X, +Y and +Z of `Position`. It is not centred, so treating it as a
164    /// centred box offsets it by half its size in all three axes.
165    Block {
166        /// `XLength`, extent along the placement's X axis.
167        x_length => primitive_slot::DIM_0, "XLength";
168        /// `YLength`, extent along the placement's Y axis.
169        y_length => primitive_slot::DIM_1, "YLength";
170        /// `ZLength`, extent along the placement's Z axis.
171        z_length => primitive_slot::DIM_2, "ZLength";
172    }
173}
174
175csg_primitive! {
176    /// `IfcRectangularPyramid`: a pyramid on a rectangular base.
177    ///
178    /// The base rectangle's **corner** is at the placement origin, extending
179    /// along +X and +Y; the apex sits above the base centre at `Height`.
180    RectangularPyramid {
181        /// `XLength`, base extent along the placement's X axis.
182        x_length => primitive_slot::DIM_0, "XLength";
183        /// `YLength`, base extent along the placement's Y axis.
184        y_length => primitive_slot::DIM_1, "YLength";
185        /// `Height`, apex height above the base plane.
186        height => primitive_slot::DIM_2, "Height";
187    }
188}
189
190csg_primitive! {
191    /// `IfcRightCircularCone`: a cone standing on the placement XY plane.
192    ///
193    /// The base circle is **centred** on the placement origin (unlike
194    /// [`Block`]), and the apex is at `Height` along +Z. There is no top
195    /// radius: this is always a full cone, never a frustum.
196    RightCircularCone {
197        /// `Height`, apex height above the base plane.
198        height => primitive_slot::DIM_0, "Height";
199        /// `BottomRadius`, radius of the base circle.
200        bottom_radius => primitive_slot::DIM_1, "BottomRadius";
201    }
202}
203
204csg_primitive! {
205    /// `IfcRightCircularCylinder`: a cylinder standing on the placement XY
206    /// plane.
207    ///
208    /// The base circle is **centred** on the placement origin and the axis is
209    /// +Z. Note the attribute order is `Height` then `Radius`, the opposite of
210    /// how most APIs spell a cylinder.
211    RightCircularCylinder {
212        /// `Height`, extent along the placement's Z axis.
213        height => primitive_slot::DIM_0, "Height";
214        /// `Radius`, radius of the circular section.
215        radius => primitive_slot::DIM_1, "Radius";
216    }
217}
218
219csg_primitive! {
220    /// `IfcSphere`: a sphere **centred** on the placement origin.
221    ///
222    /// The only primitive whose placement orientation is geometrically
223    /// irrelevant; only the origin matters.
224    Sphere {
225        /// `Radius`.
226        radius => primitive_slot::DIM_0, "Radius";
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::solid::testkit::{entity, n, r};
234
235    #[test]
236    fn csg_solid_exposes_its_tree_root_without_resolving_it() {
237        let e = entity("IFCCSGSOLID", vec![r(55)]);
238        let view = CsgSolid::new(EntityId(1), &e);
239        assert_eq!(view.tree_root_expression().unwrap(), EntityId(55));
240    }
241
242    #[test]
243    fn a_csg_solid_without_a_root_expression_is_an_error_not_an_empty_tree() {
244        let e = entity("IFCCSGSOLID", vec![]);
245        let err = CsgSolid::new(EntityId(3), &e)
246            .tree_root_expression()
247            .unwrap_err();
248        assert_eq!(err.entity(), Some(EntityId(3)));
249    }
250
251    /// Position is slot 0 for every primitive, so each primitive's own
252    /// dimensions begin at slot 1.
253    #[test]
254    fn primitive_position_precedes_the_dimension_attributes() {
255        let e = entity("IFCBLOCK", vec![r(9), n(1.0), n(2.0), n(3.0)]);
256        let block = Block::new(EntityId(1), &e);
257        assert_eq!(block.base().position().unwrap(), EntityId(9));
258        assert_eq!(block.x_length().unwrap(), 1.0);
259        assert_eq!(block.y_length().unwrap(), 2.0);
260        assert_eq!(block.z_length().unwrap(), 3.0);
261    }
262
263    /// Position is required on IfcCsgPrimitive3D; unlike a swept solid there
264    /// is no identity default.
265    #[test]
266    fn primitive_position_is_required_and_has_no_identity_default() {
267        let e = entity("IFCSPHERE", vec![ifc_model::Value::Null, n(2.0)]);
268        let sphere = Sphere::new(EntityId(4), &e);
269        assert!(sphere.base().position().is_err());
270        assert_eq!(sphere.radius().unwrap(), 2.0);
271    }
272
273    #[test]
274    fn pyramid_reads_height_where_block_reads_z_length() {
275        let attrs = vec![r(9), n(4.0), n(5.0), n(6.0)];
276        let pyramid = entity("IFCRECTANGULARPYRAMID", attrs.clone());
277        let block = entity("IFCBLOCK", attrs);
278        assert_eq!(
279            RectangularPyramid::new(EntityId(1), &pyramid)
280                .height()
281                .unwrap(),
282            6.0
283        );
284        assert_eq!(Block::new(EntityId(1), &block).z_length().unwrap(), 6.0);
285    }
286
287    /// The cylinder and cone both spell Height BEFORE their radius, which is
288    /// the reverse of the usual API convention.
289    #[test]
290    fn cylinder_and_cone_declare_height_before_radius() {
291        let cyl = entity("IFCRIGHTCIRCULARCYLINDER", vec![r(9), n(10.0), n(0.5)]);
292        let view = RightCircularCylinder::new(EntityId(1), &cyl);
293        assert_eq!(view.height().unwrap(), 10.0);
294        assert_eq!(view.radius().unwrap(), 0.5);
295
296        let cone = entity("IFCRIGHTCIRCULARCONE", vec![r(9), n(10.0), n(0.5)]);
297        let view = RightCircularCone::new(EntityId(1), &cone);
298        assert_eq!(view.height().unwrap(), 10.0);
299        assert_eq!(view.bottom_radius().unwrap(), 0.5);
300    }
301
302    #[test]
303    fn every_primitive_reports_its_concrete_type_name() {
304        for name in [
305            "IFCBLOCK",
306            "IFCRECTANGULARPYRAMID",
307            "IFCRIGHTCIRCULARCONE",
308            "IFCRIGHTCIRCULARCYLINDER",
309            "IFCSPHERE",
310        ] {
311            let e = entity(name, vec![r(1), n(1.0), n(1.0), n(1.0)]);
312            assert_eq!(CsgPrimitive3D::new(EntityId(1), &e).type_name(), name);
313        }
314    }
315}