Skip to main content

ifc_alignment/authoring/
referent.rs

1//! Referents and the linear placements that position them.
2//!
3//! A referent marks a station along an alignment. Positioning it needs
4//! three entities: a point expressed as a distance along the basis
5//! curve, a linear axis placement at that point, and the placement
6//! itself. They are authored separately because each is referenced on
7//! its own elsewhere in a file.
8
9use ifc_model::{Entity, EntityId, Transaction, Value};
10
11use super::{finite, guid, invalid};
12use crate::error::AlignmentError;
13use crate::slot;
14
15/// Stage an `IfcCartesianPoint`.
16///
17/// # Errors
18///
19/// Refuses coordinates outside `LIST [1:3]` and any non-finite value.
20pub fn cartesian_point(
21    tx: &mut Transaction,
22    coordinates: &[f64],
23) -> Result<EntityId, AlignmentError> {
24    if coordinates.is_empty() || coordinates.len() > 3 {
25        return Err(invalid(
26            "IFCCARTESIANPOINT",
27            "Coordinates",
28            format!("{} coordinates", coordinates.len()),
29        ));
30    }
31    for value in coordinates {
32        finite("IFCCARTESIANPOINT", "Coordinates", *value)?;
33    }
34    Ok(tx.create(Entity::new(
35        "IFCCARTESIANPOINT",
36        vec![Value::List(
37            coordinates.iter().copied().map(Value::Real).collect(),
38        )],
39    )))
40}
41
42/// Stage an `IfcPointByDistanceExpression`.
43///
44/// A position stated as a distance along a curve rather than as
45/// coordinates, which is what keeps a referent attached to the
46/// alignment when the geometry is re-fitted.
47///
48/// `distance_along` is a length along `basis_curve`. `DistanceAlong` is
49/// declared `IfcCurveMeasureSelect = SELECT (IfcLengthMeasure,
50/// IfcParameterValue)`, so it is written `IFCLENGTHMEASURE(..)`: the wrapper
51/// is what tells a length from a curve parameter (#201). The offsets are
52/// plain `IfcLengthMeasure` and stay bare.
53///
54/// # Errors
55///
56/// Refuses a non-finite distance or offset.
57pub fn point_by_distance(
58    tx: &mut Transaction,
59    distance_along: f64,
60    offsets: (Option<f64>, Option<f64>, Option<f64>),
61    basis_curve: EntityId,
62) -> Result<EntityId, AlignmentError> {
63    finite(
64        "IFCPOINTBYDISTANCEEXPRESSION",
65        "DistanceAlong",
66        distance_along,
67    )?;
68    let (lateral, vertical, longitudinal) = offsets;
69    for (name, value) in [
70        ("OffsetLateral", lateral),
71        ("OffsetVertical", vertical),
72        ("OffsetLongitudinal", longitudinal),
73    ] {
74        if let Some(value) = value {
75            finite("IFCPOINTBYDISTANCEEXPRESSION", name, value)?;
76        }
77    }
78    let mut attrs = vec![Value::Null; slot::point_by_distance::ARITY];
79    attrs[slot::point_by_distance::DISTANCE_ALONG] =
80        typed("IFCLENGTHMEASURE", Value::Real(distance_along));
81    attrs[slot::point_by_distance::OFFSET_LATERAL] = lateral.map_or(Value::Null, Value::Real);
82    attrs[slot::point_by_distance::OFFSET_VERTICAL] = vertical.map_or(Value::Null, Value::Real);
83    attrs[slot::point_by_distance::OFFSET_LONGITUDINAL] =
84        longitudinal.map_or(Value::Null, Value::Real);
85    attrs[slot::point_by_distance::BASIS_CURVE] = Value::Ref(basis_curve);
86    Ok(tx.create(Entity::new("IFCPOINTBYDISTANCEEXPRESSION", attrs)))
87}
88
89/// Stage an `IfcAxis2PlacementLinear`.
90///
91/// # Errors
92///
93/// Never fails; the signature matches its siblings so callers can
94/// chain the three placement entities with one error type.
95pub fn axis2_placement_linear(
96    tx: &mut Transaction,
97    location: EntityId,
98    axis: Option<EntityId>,
99    ref_direction: Option<EntityId>,
100) -> Result<EntityId, AlignmentError> {
101    let mut attrs = vec![Value::Null; slot::axis2_placement_linear::ARITY];
102    attrs[slot::axis2_placement_linear::LOCATION] = Value::Ref(location);
103    attrs[slot::axis2_placement_linear::AXIS] = axis.map_or(Value::Null, Value::Ref);
104    attrs[slot::axis2_placement_linear::REF_DIRECTION] =
105        ref_direction.map_or(Value::Null, Value::Ref);
106    Ok(tx.create(Entity::new("IFCAXIS2PLACEMENTLINEAR", attrs)))
107}
108
109/// Stage an `IfcLinearPlacement`.
110///
111/// `CartesianPosition` is optional and deliberately left to the
112/// caller: it caches the resolved world position, and this crate will
113/// not compute one, since deriving coordinates from an alignment curve
114/// belongs to the geometry kernel.
115///
116/// # Errors
117///
118/// Never fails; kept fallible for symmetry with its siblings.
119pub fn linear_placement(
120    tx: &mut Transaction,
121    relative_placement: EntityId,
122    placement_rel_to: Option<EntityId>,
123    cartesian_position: Option<EntityId>,
124) -> Result<EntityId, AlignmentError> {
125    let mut attrs = vec![Value::Null; slot::linear_placement::ARITY];
126    attrs[slot::linear_placement::PLACEMENT_REL_TO] =
127        placement_rel_to.map_or(Value::Null, Value::Ref);
128    attrs[slot::linear_placement::RELATIVE_PLACEMENT] = Value::Ref(relative_placement);
129    attrs[slot::linear_placement::CARTESIAN_POSITION] =
130        cartesian_position.map_or(Value::Null, Value::Ref);
131    Ok(tx.create(Entity::new("IFCLINEARPLACEMENT", attrs)))
132}
133
134/// Stage an `IfcReferent`.
135///
136/// # Errors
137///
138/// Refuses a `GlobalId` that is not 22 characters.
139pub fn referent(
140    tx: &mut Transaction,
141    global_id: &str,
142    name: Option<&str>,
143    predefined_type: Option<&str>,
144    placement: Option<EntityId>,
145) -> Result<EntityId, AlignmentError> {
146    let mut attrs = vec![Value::Null; slot::referent::ARITY];
147    attrs[slot::product::GLOBAL_ID] = guid("IFCREFERENT", global_id)?;
148    if let Some(name) = name {
149        attrs[slot::product::NAME] = Value::Text(name.into());
150    }
151    attrs[slot::product::OBJECT_PLACEMENT] = placement.map_or(Value::Null, Value::Ref);
152    attrs[slot::referent::PREDEFINED_TYPE] =
153        predefined_type.map_or(Value::Null, |token| Value::Enum(token.into()));
154    Ok(tx.create(Entity::new("IFCREFERENT", attrs)))
155}
156
157/// Stage the `Pset_Stationing` property set for a referent.
158///
159/// Stationing is carried by a property set, not by an attribute, so a
160/// referent without one is positioned but has no station. The reader
161/// looks for this exact set name and these exact property names,
162/// which is why authoring them is a named helper rather than a
163/// generic property-set call: a typo here produces a referent the
164/// stationing reader silently skips.
165///
166/// Returns the `IfcRelDefinesByProperties` that binds the set to the
167/// referent.
168///
169/// # Errors
170///
171/// Refuses a non-finite station and a `GlobalId` that is not 22
172/// characters.
173pub fn stationing(
174    tx: &mut Transaction,
175    pset_global_id: &str,
176    rel_global_id: &str,
177    referent: EntityId,
178    station: f64,
179    incoming_station: Option<f64>,
180    has_increasing_station: Option<bool>,
181) -> Result<EntityId, AlignmentError> {
182    finite("IFCPROPERTYSET", "Station", station)?;
183    if let Some(value) = incoming_station {
184        finite("IFCPROPERTYSET", "IncomingStation", value)?;
185    }
186    let pset_guid = guid("IFCPROPERTYSET", pset_global_id)?;
187    let rel_guid = guid("IFCRELDEFINESBYPROPERTIES", rel_global_id)?;
188
189    let length = |value| typed("IFCLENGTHMEASURE", Value::Real(value));
190    let mut properties = vec![single_value(tx, "Station", length(station))];
191    if let Some(value) = incoming_station {
192        properties.push(single_value(tx, "IncomingStation", length(value)));
193    }
194    if let Some(value) = has_increasing_station {
195        let flag = typed("IFCBOOLEAN", Value::Bool(value));
196        properties.push(single_value(tx, "HasIncreasingStation", flag));
197    }
198
199    let mut pset_attrs = vec![Value::Null; 5];
200    pset_attrs[0] = pset_guid;
201    pset_attrs[2] = Value::Text("Pset_Stationing".into());
202    pset_attrs[4] = Value::List(properties.into_iter().map(Value::Ref).collect());
203    let pset = tx.create(Entity::new("IFCPROPERTYSET", pset_attrs));
204
205    let mut rel_attrs = vec![Value::Null; 6];
206    rel_attrs[0] = rel_guid;
207    rel_attrs[4] = Value::List(vec![Value::Ref(referent)]);
208    rel_attrs[5] = Value::Ref(pset);
209    Ok(tx.create(Entity::new("IFCRELDEFINESBYPROPERTIES", rel_attrs)))
210}
211
212/// `value` as the typed parameter of the SELECT member `member`.
213fn typed(member: &str, value: Value) -> Value {
214    Value::Typed {
215        type_name: member.into(),
216        value: Box::new(value),
217    }
218}
219
220/// Stage an `IfcPropertySingleValue` for the stationing set.
221///
222/// `NominalValue` is declared `IfcValue`, a SELECT, so `value` arrives as the
223/// typed parameter of its member (#201): `Station` and `IncomingStation`
224/// are `IfcLengthMeasure` (their `Pset_Stationing` template type) and
225/// `HasIncreasingStation` is an `IfcBoolean`.
226fn single_value(tx: &mut Transaction, name: &str, value: Value) -> EntityId {
227    let mut attrs = vec![Value::Null; 4];
228    attrs[0] = Value::Text(name.into());
229    attrs[2] = value;
230    tx.create(Entity::new("IFCPROPERTYSINGLEVALUE", attrs))
231}