Skip to main content

ifc_geometry/authoring/
csg.rs

1//! CSG primitives and half spaces: solids given by parameters, not faces.
2//!
3//! A primitive is a placement plus a handful of lengths. There is no
4//! mesh to build and nothing to evaluate, so the writer is a bounds
5//! check and a slot fill. The interesting part is what a primitive
6//! *denotes*: an `IfcBlock` is a solid, an `IfcHalfSpaceSolid` is an
7//! infinite region, and a caller that confuses them gets a boolean
8//! result the size of the universe.
9//!
10//! # Half spaces are infinite on purpose
11//!
12//! `AgreementFlag` picks which side of `BaseSurface` is solid. Neither
13//! side is bounded. `IfcBoxedHalfSpace` adds an `Enclosure` box, but
14//! that is a *hint for display*, not a trim: the half space stays
15//! infinite and the box only tells a viewer where to stop drawing.
16//! `IfcPolygonalBoundedHalfSpace` is the one that genuinely bounds,
17//! via a closed polygonal boundary prism.
18
19use ifc_model::{Entity, EntityId, Transaction, Value};
20
21use crate::error::GeometryError;
22use crate::solid::csg::{csg_solid_slot, primitive_slot};
23use crate::solid::halfspace::slot as half_slot;
24
25use super::std_profile::positive;
26
27/// Stage one of the five `IfcCsgPrimitive3D` subtypes.
28///
29/// `dims` are written from absolute slot 1 upward, in schema order.
30fn primitive(
31    tx: &mut Transaction,
32    type_name: &'static str,
33    names: &[&'static str],
34    position: EntityId,
35    dims: &[f64],
36) -> Result<EntityId, GeometryError> {
37    debug_assert_eq!(names.len(), dims.len());
38    for (name, value) in names.iter().zip(dims) {
39        positive(type_name, name, *value)?;
40    }
41    let mut attrs = vec![Value::Null; 1 + dims.len()];
42    attrs[primitive_slot::POSITION] = Value::Ref(position);
43    for (offset, value) in dims.iter().enumerate() {
44        attrs[primitive_slot::DIM_0 + offset] = Value::Real(*value);
45    }
46    Ok(tx.create(Entity::new(type_name, attrs)))
47}
48
49/// Stage an `IfcBlock`: a box with a corner at `position`.
50///
51/// # Errors
52///
53/// Refuses a non-positive or non-finite edge length.
54pub fn block(
55    tx: &mut Transaction,
56    position: EntityId,
57    x_length: f64,
58    y_length: f64,
59    z_length: f64,
60) -> Result<EntityId, GeometryError> {
61    primitive(
62        tx,
63        "IFCBLOCK",
64        &["XLength", "YLength", "ZLength"],
65        position,
66        &[x_length, y_length, z_length],
67    )
68}
69
70/// Stage an `IfcSphere`.
71///
72/// # Errors
73///
74/// Refuses a non-positive or non-finite radius.
75pub fn sphere(
76    tx: &mut Transaction,
77    position: EntityId,
78    radius: f64,
79) -> Result<EntityId, GeometryError> {
80    primitive(tx, "IFCSPHERE", &["Radius"], position, &[radius])
81}
82
83/// Stage an `IfcRightCircularCylinder`.
84///
85/// # Errors
86///
87/// Refuses a non-positive or non-finite height or radius.
88pub fn cylinder(
89    tx: &mut Transaction,
90    position: EntityId,
91    height: f64,
92    radius: f64,
93) -> Result<EntityId, GeometryError> {
94    primitive(
95        tx,
96        "IFCRIGHTCIRCULARCYLINDER",
97        &["Height", "Radius"],
98        position,
99        &[height, radius],
100    )
101}
102
103/// Stage an `IfcRightCircularCone`.
104///
105/// # Errors
106///
107/// Refuses a non-positive or non-finite height or bottom radius.
108pub fn cone(
109    tx: &mut Transaction,
110    position: EntityId,
111    height: f64,
112    bottom_radius: f64,
113) -> Result<EntityId, GeometryError> {
114    primitive(
115        tx,
116        "IFCRIGHTCIRCULARCONE",
117        &["Height", "BottomRadius"],
118        position,
119        &[height, bottom_radius],
120    )
121}
122
123/// Stage an `IfcRectangularPyramid`.
124///
125/// # Errors
126///
127/// Refuses a non-positive or non-finite dimension.
128pub fn rectangular_pyramid(
129    tx: &mut Transaction,
130    position: EntityId,
131    x_length: f64,
132    y_length: f64,
133    height: f64,
134) -> Result<EntityId, GeometryError> {
135    primitive(
136        tx,
137        "IFCRECTANGULARPYRAMID",
138        &["XLength", "YLength", "Height"],
139        position,
140        &[x_length, y_length, height],
141    )
142}
143
144/// Stage an `IfcCsgSolid` over a tree root.
145///
146/// The root is an `IfcCsgSelect`: either an `IfcBooleanResult` or an
147/// `IfcCsgPrimitive3D`. Both are entity references, so the writer cannot
148/// tell them apart and does not try -- a wrong root is a schema error the
149/// validator reports, not something to guess at here.
150pub fn csg_solid(tx: &mut Transaction, tree_root: EntityId) -> EntityId {
151    let mut attrs = vec![Value::Null; 1];
152    attrs[csg_solid_slot::TREE_ROOT_EXPRESSION] = Value::Ref(tree_root);
153    tx.create(Entity::new("IFCCSGSOLID", attrs))
154}
155
156/// Stage an `IfcHalfSpaceSolid`: everything on one side of a surface.
157///
158/// `agreement` true means the solid is the side the surface normal points
159/// *away* from, per the schema's definition. The result is unbounded.
160pub fn half_space(tx: &mut Transaction, base_surface: EntityId, agreement: bool) -> EntityId {
161    let mut attrs = vec![Value::Null; 2];
162    attrs[half_slot::BASE_SURFACE] = Value::Ref(base_surface);
163    attrs[half_slot::AGREEMENT_FLAG] = Value::Bool(agreement);
164    tx.create(Entity::new("IFCHALFSPACESOLID", attrs))
165}
166
167/// Stage an `IfcBoxedHalfSpace`.
168///
169/// `enclosure` is a *display hint*, not a trim: the half space remains
170/// infinite and a consumer that treats the box as the solid's extent is
171/// reading the schema wrong. Authoring it anyway, because files in the
172/// wild carry it and round-tripping must not drop it.
173pub fn boxed_half_space(
174    tx: &mut Transaction,
175    base_surface: EntityId,
176    agreement: bool,
177    enclosure: EntityId,
178) -> EntityId {
179    let mut attrs = vec![Value::Null; 3];
180    attrs[half_slot::BASE_SURFACE] = Value::Ref(base_surface);
181    attrs[half_slot::AGREEMENT_FLAG] = Value::Bool(agreement);
182    attrs[half_slot::ENCLOSURE] = Value::Ref(enclosure);
183    tx.create(Entity::new("IFCBOXEDHALFSPACE", attrs))
184}
185
186/// Stage an `IfcPolygonalBoundedHalfSpace`.
187///
188/// Unlike the other two this one is genuinely bounded: the polygonal
189/// boundary sweeps a prism, and the solid is the intersection of that
190/// prism with the half space.
191///
192/// # Errors
193///
194/// The boundary must be a closed curve. This writer cannot verify
195/// closure without evaluating the curve, so it takes the reference as
196/// given -- the same position the profile writers take for
197/// `IfcArbitraryClosedProfileDef`.
198pub fn polygonal_bounded_half_space(
199    tx: &mut Transaction,
200    base_surface: EntityId,
201    agreement: bool,
202    position: EntityId,
203    polygonal_boundary: EntityId,
204) -> EntityId {
205    let mut attrs = vec![Value::Null; 4];
206    attrs[half_slot::BASE_SURFACE] = Value::Ref(base_surface);
207    attrs[half_slot::AGREEMENT_FLAG] = Value::Bool(agreement);
208    attrs[half_slot::POSITION] = Value::Ref(position);
209    attrs[half_slot::POLYGONAL_BOUNDARY] = Value::Ref(polygonal_boundary);
210    tx.create(Entity::new("IFCPOLYGONALBOUNDEDHALFSPACE", attrs))
211}
212
213/// Stage an `IfcBoundingBox`.
214///
215/// # Errors
216///
217/// Refuses a non-positive or non-finite extent: the schema declares all
218/// three as `IfcPositiveLengthMeasure`, so a flat box is not expressible.
219pub fn bounding_box(
220    tx: &mut Transaction,
221    corner: EntityId,
222    x_dim: f64,
223    y_dim: f64,
224    z_dim: f64,
225) -> Result<EntityId, GeometryError> {
226    const T: &str = "IFCBOUNDINGBOX";
227    positive(T, "XDim", x_dim)?;
228    positive(T, "YDim", y_dim)?;
229    positive(T, "ZDim", z_dim)?;
230    let attrs = vec![
231        Value::Ref(corner),
232        Value::Real(x_dim),
233        Value::Real(y_dim),
234        Value::Real(z_dim),
235    ];
236    Ok(tx.create(Entity::new(T, attrs)))
237}