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}