Skip to main content

ifc_geometry/authoring/
connection.rs

1//! Placement, connection geometry, grids and geometric sets.
2//!
3//! # A clipping result is always a difference
4//!
5//! `IfcBooleanClippingResult` fixes `Operator = DIFFERENCE` and
6//! requires the second operand be a half space. There is no operator
7//! to choose, so the writer does not offer one -- a union of a solid
8//! and a half space is not a clip, it is the universe.
9//!
10//! # A spine pairs sections with positions
11//!
12//! `IfcSectionedSpine` carries `CrossSections` and
13//! `CrossSectionPositions` as two independent `LIST [2:?]`s. Nothing
14//! in the schema ties their lengths together, but a section without a
15//! position (or the reverse) describes no solid, so this writer
16//! requires they match.
17
18use ifc_model::{Entity, EntityId, Transaction, Value};
19
20use crate::error::GeometryError;
21use crate::solid::swept::spine_slot;
22
23use super::{invalid, refs, require_finite};
24
25/// Stage an `IfcLocalPlacement`.
26///
27/// `placement_rel_to` absent means the placement is absolute, in the
28/// project's own coordinate system. A chain of these is how IFC nests
29/// a component inside an assembly inside a storey.
30pub fn local_placement(
31    tx: &mut Transaction,
32    placement_rel_to: Option<EntityId>,
33    relative_placement: EntityId,
34) -> EntityId {
35    let attrs = vec![
36        placement_rel_to.map_or(Value::Null, Value::Ref),
37        Value::Ref(relative_placement),
38    ];
39    tx.create(Entity::new("IFCLOCALPLACEMENT", attrs))
40}
41
42/// Stage an `IfcGridAxis`.
43///
44/// `same_sense` says whether the axis runs along its curve's own
45/// direction; it decides which way offsets are measured at an
46/// intersection.
47pub fn grid_axis(
48    tx: &mut Transaction,
49    axis_tag: Option<&str>,
50    axis_curve: EntityId,
51    same_sense: bool,
52) -> EntityId {
53    let attrs = vec![
54        axis_tag.map_or(Value::Null, |t| Value::Text(t.into())),
55        Value::Ref(axis_curve),
56        Value::Bool(same_sense),
57    ];
58    tx.create(Entity::new("IFCGRIDAXIS", attrs))
59}
60
61/// Stage an `IfcVirtualGridIntersection`.
62///
63/// # Errors
64///
65/// Refuses anything but exactly two axes -- `LIST [2:2]` -- the same
66/// axis twice, or an offset list outside `LIST [2:3]`. Two identical
67/// axes do not intersect anywhere in particular.
68pub fn virtual_grid_intersection(
69    tx: &mut Transaction,
70    intersecting_axes: &[EntityId],
71    offset_distances: &[f64],
72) -> Result<EntityId, GeometryError> {
73    const T: &str = "IFCVIRTUALGRIDINTERSECTION";
74    if intersecting_axes.len() != 2 {
75        return Err(invalid(
76            T,
77            "IntersectingAxes",
78            format!("expected exactly 2 axes, got {}", intersecting_axes.len()),
79        ));
80    }
81    if intersecting_axes[0] == intersecting_axes[1] {
82        return Err(invalid(
83            T,
84            "IntersectingAxes",
85            "the list is UNIQUE; an axis does not intersect itself",
86        ));
87    }
88    if offset_distances.len() < 2 || offset_distances.len() > 3 {
89        return Err(invalid(
90            T,
91            "OffsetDistances",
92            format!("expected 2 or 3 offsets, got {}", offset_distances.len()),
93        ));
94    }
95    require_finite(T, "OffsetDistances", offset_distances)?;
96    let attrs = vec![
97        refs(intersecting_axes),
98        Value::List(offset_distances.iter().copied().map(Value::Real).collect()),
99    ];
100    Ok(tx.create(Entity::new(T, attrs)))
101}
102
103/// Stage an `IfcGridPlacement`: a placement located on a grid.
104pub fn grid_placement(
105    tx: &mut Transaction,
106    placement_rel_to: Option<EntityId>,
107    placement_location: EntityId,
108    placement_ref_direction: Option<EntityId>,
109) -> EntityId {
110    let attrs = vec![
111        placement_rel_to.map_or(Value::Null, Value::Ref),
112        Value::Ref(placement_location),
113        placement_ref_direction.map_or(Value::Null, Value::Ref),
114    ];
115    tx.create(Entity::new("IFCGRIDPLACEMENT", attrs))
116}
117
118/// Which connection geometry form to author.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum ConnectionKind {
121    /// `IfcConnectionPointGeometry`: a point or vertex point.
122    Point,
123    /// `IfcConnectionCurveGeometry`: a curve or edge curve.
124    Curve,
125    /// `IfcConnectionSurfaceGeometry`: a surface or face surface.
126    Surface,
127    /// `IfcConnectionVolumeGeometry`: a solid or shell.
128    Volume,
129}
130
131impl ConnectionKind {
132    /// The entity type name.
133    fn type_name(self) -> &'static str {
134        match self {
135            Self::Point => "IFCCONNECTIONPOINTGEOMETRY",
136            Self::Curve => "IFCCONNECTIONCURVEGEOMETRY",
137            Self::Surface => "IFCCONNECTIONSURFACEGEOMETRY",
138            Self::Volume => "IFCCONNECTIONVOLUMEGEOMETRY",
139        }
140    }
141}
142
143/// Stage a connection geometry.
144///
145/// The two slots are the geometry as seen from each side of the
146/// connection. `on_related` absent means both elements agree on the
147/// same geometry -- which is the common case, and is why omitting it
148/// is not the same as repeating the first reference.
149pub fn connection_geometry(
150    tx: &mut Transaction,
151    kind: ConnectionKind,
152    on_relating: EntityId,
153    on_related: Option<EntityId>,
154) -> EntityId {
155    let attrs = vec![
156        Value::Ref(on_relating),
157        on_related.map_or(Value::Null, Value::Ref),
158    ];
159    tx.create(Entity::new(kind.type_name(), attrs))
160}
161
162/// Stage an `IfcConnectionPointEccentricity`.
163///
164/// The eccentricities offset the connection from the stated point --
165/// how a beam meets a column off its centreline. All three are
166/// optional and signed: `IfcLengthMeasure`, not the positive form.
167///
168/// # Errors
169///
170/// Refuses a non-finite eccentricity.
171pub fn connection_point_eccentricity(
172    tx: &mut Transaction,
173    on_relating: EntityId,
174    on_related: Option<EntityId>,
175    eccentricity: [Option<f64>; 3],
176) -> Result<EntityId, GeometryError> {
177    const T: &str = "IFCCONNECTIONPOINTECCENTRICITY";
178    const NAMES: [&str; 3] = ["EccentricityInX", "EccentricityInY", "EccentricityInZ"];
179    let mut attrs = vec![Value::Null; 5];
180    attrs[0] = Value::Ref(on_relating);
181    attrs[1] = on_related.map_or(Value::Null, Value::Ref);
182    for (offset, value) in eccentricity.iter().enumerate() {
183        if let Some(value) = value {
184            require_finite(T, NAMES[offset], &[*value])?;
185            attrs[2 + offset] = Value::Real(*value);
186        }
187    }
188    Ok(tx.create(Entity::new(T, attrs)))
189}
190
191/// Stage an `IfcPointOnCurve`.
192///
193/// # Errors
194///
195/// Refuses a non-finite parameter.
196pub fn point_on_curve(
197    tx: &mut Transaction,
198    basis_curve: EntityId,
199    point_parameter: f64,
200) -> Result<EntityId, GeometryError> {
201    const T: &str = "IFCPOINTONCURVE";
202    require_finite(T, "PointParameter", &[point_parameter])?;
203    let attrs = vec![Value::Ref(basis_curve), parameter(point_parameter)];
204    Ok(tx.create(Entity::new(T, attrs)))
205}
206
207/// Stage an `IfcPointOnSurface`.
208///
209/// # Errors
210///
211/// Refuses a non-finite parameter.
212pub fn point_on_surface(
213    tx: &mut Transaction,
214    basis_surface: EntityId,
215    u: f64,
216    v: f64,
217) -> Result<EntityId, GeometryError> {
218    const T: &str = "IFCPOINTONSURFACE";
219    require_finite(T, "PointParameterU", &[u, v])?;
220    let attrs = vec![Value::Ref(basis_surface), parameter(u), parameter(v)];
221    Ok(tx.create(Entity::new(T, attrs)))
222}
223
224/// An `IfcParameterValue` in `PointParameter`/`PointParameterU`/`V`.
225///
226/// Those are declared with the defined type `IfcParameterValue`, not a
227/// SELECT, in IFC2X3, IFC4 and IFC4X3, so the value is written bare (#200).
228fn parameter(value: f64) -> Value {
229    Value::Real(value)
230}
231
232/// Stage an `IfcGeometricSet` or `IfcGeometricCurveSet`.
233///
234/// The curve-set form restricts its elements to curves; that is a
235/// claim about the referenced entities, which this writer does not
236/// resolve, so the caller chooses the type and the validator checks it.
237///
238/// # Errors
239///
240/// Refuses an empty element set: `SET [1:?]`.
241pub fn geometric_set(
242    tx: &mut Transaction,
243    curves_only: bool,
244    elements: &[EntityId],
245) -> Result<EntityId, GeometryError> {
246    let type_name = if curves_only {
247        "IFCGEOMETRICCURVESET"
248    } else {
249        "IFCGEOMETRICSET"
250    };
251    if elements.is_empty() {
252        return Err(invalid(
253            type_name,
254            "Elements",
255            "expected at least one element",
256        ));
257    }
258    Ok(tx.create(Entity::new(type_name, vec![refs(elements)])))
259}
260
261/// Stage an `IfcPath`: an ordered run of oriented edges.
262///
263/// # Errors
264///
265/// Refuses an empty edge list, or a repeated edge: the list is
266/// `LIST [1:?] OF UNIQUE`.
267pub fn path(tx: &mut Transaction, edge_list: &[EntityId]) -> Result<EntityId, GeometryError> {
268    const T: &str = "IFCPATH";
269    if edge_list.is_empty() {
270        return Err(invalid(T, "EdgeList", "expected at least one edge"));
271    }
272    let mut seen = edge_list.to_vec();
273    seen.sort_unstable();
274    seen.dedup();
275    if seen.len() != edge_list.len() {
276        return Err(invalid(
277            T,
278            "EdgeList",
279            "the edge list is UNIQUE; a path does not repeat an edge",
280        ));
281    }
282    Ok(tx.create(Entity::new(T, vec![refs(edge_list)])))
283}
284
285/// Stage an `IfcBooleanClippingResult`.
286///
287/// `Operator` is fixed to `DIFFERENCE` by `OperatorType`, so it is not
288/// an argument: a clip that unions is not a clip. The second operand
289/// must be a half space, which the validator checks by type.
290pub fn boolean_clipping_result(
291    tx: &mut Transaction,
292    first_operand: EntityId,
293    second_operand: EntityId,
294) -> EntityId {
295    let attrs = vec![
296        Value::Enum("DIFFERENCE".into()),
297        Value::Ref(first_operand),
298        Value::Ref(second_operand),
299    ];
300    tx.create(Entity::new("IFCBOOLEANCLIPPINGRESULT", attrs))
301}
302
303/// Stage an `IfcSectionedSpine`.
304///
305/// # Errors
306///
307/// Refuses fewer than two cross sections or positions -- both are
308/// `LIST [2:?]` -- or lists of differing length. The schema does not
309/// relate the two lengths, but a section with no position places
310/// nothing.
311pub fn sectioned_spine(
312    tx: &mut Transaction,
313    spine_curve: EntityId,
314    cross_sections: &[EntityId],
315    cross_section_positions: &[EntityId],
316) -> Result<EntityId, GeometryError> {
317    const T: &str = "IFCSECTIONEDSPINE";
318    if cross_sections.len() < 2 {
319        return Err(invalid(
320            T,
321            "CrossSections",
322            format!(
323                "expected at least 2 cross sections, got {}",
324                cross_sections.len()
325            ),
326        ));
327    }
328    if cross_sections.len() != cross_section_positions.len() {
329        return Err(invalid(
330            T,
331            "CrossSectionPositions",
332            format!(
333                "{} positions for {} cross sections",
334                cross_section_positions.len(),
335                cross_sections.len()
336            ),
337        ));
338    }
339    let mut attrs = vec![Value::Null; 3];
340    attrs[spine_slot::SPINE_CURVE] = Value::Ref(spine_curve);
341    attrs[spine_slot::CROSS_SECTIONS] = refs(cross_sections);
342    attrs[spine_slot::CROSS_SECTION_POSITIONS] = refs(cross_section_positions);
343    Ok(tx.create(Entity::new(T, attrs)))
344}