Skip to main content

ifc_geometry/authoring/
brep.rs

1//! Boundary representation: vertices, edges, loops, faces and shells.
2//!
3//! The topology types are thin -- most are one or two references -- so
4//! the writer's value is in the two things a careless one gets wrong.
5//!
6//! # `IfcOrientedEdge` restates nothing
7//!
8//! It inherits `EdgeStart` and `EdgeEnd` from `IfcEdge` but **derives**
9//! both from the edge it orients. A derived attribute is written `*` in
10//! STEP, not `$`: the first says "the supertype computes this", the
11//! second says "this is absent". Both decode to a missing value in
12//! most readers, so only the serialized text tells them apart -- and a
13//! file using `$` is not conforming.
14//!
15//! # Orientation is not decoration
16//!
17//! `IfcFaceBound.Orientation` and `IfcOrientedEdge.Orientation` decide
18//! which side of a face is solid and which way a loop runs. Writing the
19//! wrong boolean produces a file that parses, validates, and denotes an
20//! inside-out solid.
21//!
22//! # What this module does not check
23//!
24//! Shell closure, loop planarity and face orientation consistency all
25//! need an evaluator. A caller can build an `IfcClosedShell` that is
26//! not closed and nothing here objects: that is the boundary of what a
27//! kernel-free writer can honestly assert, and it is documented rather
28//! than silently implied.
29
30use ifc_model::{Entity, EntityId, Transaction, Value};
31
32use crate::error::GeometryError;
33use crate::resource::topology::slot;
34
35use super::{invalid, refs};
36
37/// Stage an `IfcVertexPoint`.
38pub fn vertex_point(tx: &mut Transaction, geometry: EntityId) -> EntityId {
39    let mut attrs = vec![Value::Null; 1];
40    attrs[slot::VERTEX_GEOMETRY] = Value::Ref(geometry);
41    tx.create(Entity::new("IFCVERTEXPOINT", attrs))
42}
43
44/// Stage an `IfcEdge` between two vertices.
45pub fn edge(tx: &mut Transaction, start: EntityId, end: EntityId) -> EntityId {
46    let mut attrs = vec![Value::Null; 2];
47    attrs[slot::EDGE_START] = Value::Ref(start);
48    attrs[slot::EDGE_END] = Value::Ref(end);
49    tx.create(Entity::new("IFCEDGE", attrs))
50}
51
52/// Stage an `IfcEdgeCurve`: an edge with curve geometry.
53///
54/// `same_sense` says whether the edge runs along the curve's own
55/// direction. It is not optional and it is not cosmetic.
56pub fn edge_curve(
57    tx: &mut Transaction,
58    start: EntityId,
59    end: EntityId,
60    geometry: EntityId,
61    same_sense: bool,
62) -> EntityId {
63    let mut attrs = vec![Value::Null; 4];
64    attrs[slot::EDGE_START] = Value::Ref(start);
65    attrs[slot::EDGE_END] = Value::Ref(end);
66    attrs[slot::EDGE_GEOMETRY] = Value::Ref(geometry);
67    attrs[slot::EDGE_SAME_SENSE] = Value::Bool(same_sense);
68    tx.create(Entity::new("IFCEDGECURVE", attrs))
69}
70
71/// Stage an `IfcOrientedEdge` over an existing edge.
72///
73/// The inherited `EdgeStart` and `EdgeEnd` are written `Value::Derived`
74/// -- `*` in STEP -- because the schema derives them from
75/// `EdgeElement`. Writing `$` there would claim they are absent, which
76/// is a different and non-conforming statement. See the module note.
77pub fn oriented_edge(tx: &mut Transaction, element: EntityId, orientation: bool) -> EntityId {
78    let mut attrs = vec![Value::Null; 4];
79    attrs[slot::EDGE_START] = Value::Derived;
80    attrs[slot::EDGE_END] = Value::Derived;
81    attrs[slot::EDGE_ELEMENT] = Value::Ref(element);
82    attrs[slot::EDGE_ORIENTATION] = Value::Bool(orientation);
83    tx.create(Entity::new("IFCORIENTEDEDGE", attrs))
84}
85
86/// Stage an `IfcSubedge`.
87///
88/// Unlike an oriented edge, a subedge *does* state its own vertices:
89/// they are a genuine restriction of the parent, not a derivation.
90pub fn subedge(tx: &mut Transaction, start: EntityId, end: EntityId, parent: EntityId) -> EntityId {
91    let mut attrs = vec![Value::Null; 3];
92    attrs[slot::EDGE_START] = Value::Ref(start);
93    attrs[slot::EDGE_END] = Value::Ref(end);
94    attrs[slot::PARENT_EDGE] = Value::Ref(parent);
95    tx.create(Entity::new("IFCSUBEDGE", attrs))
96}
97
98/// Stage an `IfcPolyLoop` through an ordered point list.
99///
100/// # Errors
101///
102/// Refuses fewer than three points: `LIST [3:?]`, because two points
103/// bound no area. The closing point is implicit -- repeating the first
104/// point at the end creates a zero-length edge, so this writer takes
105/// the list as the schema defines it and does not close it for you.
106pub fn poly_loop(tx: &mut Transaction, polygon: &[EntityId]) -> Result<EntityId, GeometryError> {
107    const T: &str = "IFCPOLYLOOP";
108    if polygon.len() < 3 {
109        return Err(invalid(
110            T,
111            "Polygon",
112            format!("expected at least 3 points, got {}", polygon.len()),
113        ));
114    }
115    // LIST [3:?] OF UNIQUE: a repeated vertex is a zero-length edge, and
116    // a loop that closes by repeating its first point is the commonest
117    // way to write one. The closure is implied, never stated.
118    for (i, point) in polygon.iter().enumerate() {
119        if polygon[..i].contains(point) {
120            return Err(invalid(
121                T,
122                "Polygon",
123                "the point list is UNIQUE; a loop closes implicitly, so the \
124                 first point must not be repeated at the end",
125            ));
126        }
127    }
128    let mut attrs = vec![Value::Null; 1];
129    attrs[slot::POLYGON] = refs(polygon);
130    Ok(tx.create(Entity::new(T, attrs)))
131}
132
133/// Stage an `IfcEdgeLoop` over oriented edges.
134///
135/// # Errors
136///
137/// Refuses an empty edge list. Whether the edges actually form a closed
138/// circuit is not checked: that needs to follow vertex identity through
139/// the chain, which is evaluation.
140pub fn edge_loop(tx: &mut Transaction, edges: &[EntityId]) -> Result<EntityId, GeometryError> {
141    const T: &str = "IFCEDGELOOP";
142    if edges.is_empty() {
143        return Err(invalid(T, "EdgeList", "expected at least one edge"));
144    }
145    let mut attrs = vec![Value::Null; 1];
146    attrs[slot::EDGE_LIST] = refs(edges);
147    Ok(tx.create(Entity::new(T, attrs)))
148}
149
150/// Stage an `IfcVertexLoop`: a degenerate loop at a single vertex.
151///
152/// Legal, and occasionally meaningful as a cone apex.
153pub fn vertex_loop(tx: &mut Transaction, vertex: EntityId) -> EntityId {
154    tx.create(Entity::new("IFCVERTEXLOOP", vec![Value::Ref(vertex)]))
155}
156
157/// Stage an `IfcFaceBound`.
158///
159/// `orientation` false means the loop runs opposite to the face's own
160/// sense. Getting it wrong turns a hole into a boundary.
161pub fn face_bound(tx: &mut Transaction, bound: EntityId, orientation: bool) -> EntityId {
162    bound_entity(tx, "IFCFACEBOUND", bound, orientation)
163}
164
165/// Stage an `IfcFaceOuterBound`: the bound enclosing the face's area.
166///
167/// A face has at most one of these. The distinction from a plain
168/// `IfcFaceBound` is the entity type, not an attribute.
169pub fn face_outer_bound(tx: &mut Transaction, bound: EntityId, orientation: bool) -> EntityId {
170    bound_entity(tx, "IFCFACEOUTERBOUND", bound, orientation)
171}
172
173/// The two slots both bound types share.
174fn bound_entity(
175    tx: &mut Transaction,
176    type_name: &'static str,
177    bound: EntityId,
178    orientation: bool,
179) -> EntityId {
180    let mut attrs = vec![Value::Null; 2];
181    attrs[slot::BOUND] = Value::Ref(bound);
182    attrs[slot::ORIENTATION] = Value::Bool(orientation);
183    tx.create(Entity::new(type_name, attrs))
184}
185
186/// Stage an `IfcFace` from its bounds.
187///
188/// # Errors
189///
190/// Refuses an empty bound set: `SET [1:?]`.
191pub fn face(tx: &mut Transaction, bounds: &[EntityId]) -> Result<EntityId, GeometryError> {
192    const T: &str = "IFCFACE";
193    if bounds.is_empty() {
194        return Err(invalid(T, "Bounds", "expected at least one bound"));
195    }
196    let mut attrs = vec![Value::Null; 1];
197    attrs[slot::BOUNDS] = refs(bounds);
198    Ok(tx.create(Entity::new(T, attrs)))
199}
200
201/// Stage an `IfcFaceSurface` or `IfcAdvancedFace`.
202///
203/// The two differ only in type name: an advanced face promises its
204/// surface is one of the analytic or B-spline forms, which is a claim
205/// about the referenced entity rather than about this record.
206///
207/// # Errors
208///
209/// Refuses an empty bound set.
210pub fn face_surface(
211    tx: &mut Transaction,
212    bounds: &[EntityId],
213    surface: EntityId,
214    same_sense: bool,
215    advanced: bool,
216) -> Result<EntityId, GeometryError> {
217    let type_name = if advanced {
218        "IFCADVANCEDFACE"
219    } else {
220        "IFCFACESURFACE"
221    };
222    if bounds.is_empty() {
223        return Err(invalid(type_name, "Bounds", "expected at least one bound"));
224    }
225    let mut attrs = vec![Value::Null; 3];
226    attrs[slot::BOUNDS] = refs(bounds);
227    attrs[slot::FACE_SURFACE] = Value::Ref(surface);
228    attrs[slot::FACE_SAME_SENSE] = Value::Bool(same_sense);
229    Ok(tx.create(Entity::new(type_name, attrs)))
230}
231
232/// Which shell kind a face set forms.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum ShellKind {
235    /// `IfcClosedShell`: claims to bound a volume.
236    ///
237    /// The claim is not verified here -- see the module note on what a
238    /// kernel-free writer cannot check.
239    Closed,
240    /// `IfcOpenShell`: a surface patch that does not enclose anything.
241    Open,
242    /// `IfcConnectedFaceSet`: connected faces, neither open nor closed.
243    Connected,
244}
245
246impl ShellKind {
247    /// The entity type name.
248    fn type_name(self) -> &'static str {
249        match self {
250            Self::Closed => "IFCCLOSEDSHELL",
251            Self::Open => "IFCOPENSHELL",
252            Self::Connected => "IFCCONNECTEDFACESET",
253        }
254    }
255}
256
257/// Stage a shell over a set of faces.
258///
259/// # Errors
260///
261/// Refuses an empty face set: `SET [1:?]` on all three types.
262pub fn shell(
263    tx: &mut Transaction,
264    kind: ShellKind,
265    faces: &[EntityId],
266) -> Result<EntityId, GeometryError> {
267    let type_name = kind.type_name();
268    if faces.is_empty() {
269        return Err(invalid(type_name, "CfsFaces", "expected at least one face"));
270    }
271    let mut attrs = vec![Value::Null; 1];
272    attrs[slot::CFS_FACES] = refs(faces);
273    Ok(tx.create(Entity::new(type_name, attrs)))
274}
275
276/// Which brep flavour to author.
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum BrepKind {
279    /// `IfcFacetedBrep`: every face is planar and polygonally bounded.
280    Faceted,
281    /// `IfcAdvancedBrep`: faces may carry analytic or B-spline surfaces.
282    Advanced,
283}
284
285/// Stage a manifold solid brep, with or without voids.
286///
287/// Passing an empty `voids` slice authors the plain form
288/// (`IfcFacetedBrep` / `IfcAdvancedBrep`); passing voids authors the
289/// `WithVoids` subtype. The schema declares `Voids` as `SET [1:?]`, so
290/// there is no such thing as a `WithVoids` brep with no voids -- the
291/// two are different entity types, not one type with an empty set.
292pub fn manifold_solid_brep(
293    tx: &mut Transaction,
294    kind: BrepKind,
295    outer: EntityId,
296    voids: &[EntityId],
297) -> EntityId {
298    let type_name = match (kind, voids.is_empty()) {
299        (BrepKind::Faceted, true) => "IFCFACETEDBREP",
300        (BrepKind::Faceted, false) => "IFCFACETEDBREPWITHVOIDS",
301        (BrepKind::Advanced, true) => "IFCADVANCEDBREP",
302        (BrepKind::Advanced, false) => "IFCADVANCEDBREPWITHVOIDS",
303    };
304    let mut attrs = vec![Value::Null; if voids.is_empty() { 1 } else { 2 }];
305    attrs[slot::OUTER] = Value::Ref(outer);
306    if !voids.is_empty() {
307        attrs[slot::VOIDS] = refs(voids);
308    }
309    tx.create(Entity::new(type_name, attrs))
310}
311
312/// Stage an `IfcShellBasedSurfaceModel`.
313///
314/// A surface model, not a solid: the shells describe skin, and nothing
315/// claims they enclose a volume.
316///
317/// # Errors
318///
319/// Refuses an empty shell set.
320pub fn shell_based_surface_model(
321    tx: &mut Transaction,
322    shells: &[EntityId],
323) -> Result<EntityId, GeometryError> {
324    const T: &str = "IFCSHELLBASEDSURFACEMODEL";
325    if shells.is_empty() {
326        return Err(invalid(T, "SbsmBoundary", "expected at least one shell"));
327    }
328    Ok(tx.create(Entity::new(T, vec![refs(shells)])))
329}
330
331/// Stage an `IfcFaceBasedSurfaceModel`.
332///
333/// # Errors
334///
335/// Refuses an empty face set.
336pub fn face_based_surface_model(
337    tx: &mut Transaction,
338    face_sets: &[EntityId],
339) -> Result<EntityId, GeometryError> {
340    const T: &str = "IFCFACEBASEDSURFACEMODEL";
341    if face_sets.is_empty() {
342        return Err(invalid(T, "FbsmFaces", "expected at least one face set"));
343    }
344    Ok(tx.create(Entity::new(T, vec![refs(face_sets)])))
345}
346
347/// Stage a bare `IfcLoop` or `IfcVertex`.
348///
349/// Both are concrete in the schema despite having no attributes, and
350/// both are ordinarily written as one of their subtypes --
351/// [`poly_loop`], [`edge_loop`], [`vertex_loop`], [`vertex_point`].
352/// The bare forms exist for a topology that names a connection without
353/// yet describing its geometry, which round-tripping a partial model
354/// requires: refusing to write them would make this library unable to
355/// reproduce a file it can read.
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum BareTopology {
358    /// `IfcLoop`: a closed path with no stated geometry.
359    Loop,
360    /// `IfcVertex`: a point with no stated geometry.
361    Vertex,
362}
363
364/// Stage an attribute-less topological item.
365pub fn bare_topology(tx: &mut Transaction, kind: BareTopology) -> EntityId {
366    let entity = match kind {
367        BareTopology::Loop => "IFCLOOP",
368        BareTopology::Vertex => "IFCVERTEX",
369    };
370    tx.create(Entity::new(entity, Vec::new()))
371}