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/// # Errors
49///
50/// Refuses a non-finite distance or offset.
51pub fn point_by_distance(
52    tx: &mut Transaction,
53    distance_along: f64,
54    offsets: (Option<f64>, Option<f64>, Option<f64>),
55    basis_curve: EntityId,
56) -> Result<EntityId, AlignmentError> {
57    finite(
58        "IFCPOINTBYDISTANCEEXPRESSION",
59        "DistanceAlong",
60        distance_along,
61    )?;
62    let (lateral, vertical, longitudinal) = offsets;
63    for (name, value) in [
64        ("OffsetLateral", lateral),
65        ("OffsetVertical", vertical),
66        ("OffsetLongitudinal", longitudinal),
67    ] {
68        if let Some(value) = value {
69            finite("IFCPOINTBYDISTANCEEXPRESSION", name, value)?;
70        }
71    }
72    let mut attrs = vec![Value::Null; slot::point_by_distance::ARITY];
73    attrs[slot::point_by_distance::DISTANCE_ALONG] = Value::Real(distance_along);
74    attrs[slot::point_by_distance::OFFSET_LATERAL] = lateral.map_or(Value::Null, Value::Real);
75    attrs[slot::point_by_distance::OFFSET_VERTICAL] = vertical.map_or(Value::Null, Value::Real);
76    attrs[slot::point_by_distance::OFFSET_LONGITUDINAL] =
77        longitudinal.map_or(Value::Null, Value::Real);
78    attrs[slot::point_by_distance::BASIS_CURVE] = Value::Ref(basis_curve);
79    Ok(tx.create(Entity::new("IFCPOINTBYDISTANCEEXPRESSION", attrs)))
80}
81
82/// Stage an `IfcAxis2PlacementLinear`.
83///
84/// # Errors
85///
86/// Never fails; the signature matches its siblings so callers can
87/// chain the three placement entities with one error type.
88pub fn axis2_placement_linear(
89    tx: &mut Transaction,
90    location: EntityId,
91    axis: Option<EntityId>,
92    ref_direction: Option<EntityId>,
93) -> Result<EntityId, AlignmentError> {
94    let mut attrs = vec![Value::Null; slot::axis2_placement_linear::ARITY];
95    attrs[slot::axis2_placement_linear::LOCATION] = Value::Ref(location);
96    attrs[slot::axis2_placement_linear::AXIS] = axis.map_or(Value::Null, Value::Ref);
97    attrs[slot::axis2_placement_linear::REF_DIRECTION] =
98        ref_direction.map_or(Value::Null, Value::Ref);
99    Ok(tx.create(Entity::new("IFCAXIS2PLACEMENTLINEAR", attrs)))
100}
101
102/// Stage an `IfcLinearPlacement`.
103///
104/// `CartesianPosition` is optional and deliberately left to the
105/// caller: it caches the resolved world position, and this crate will
106/// not compute one, since deriving coordinates from an alignment curve
107/// belongs to the geometry kernel.
108///
109/// # Errors
110///
111/// Never fails; kept fallible for symmetry with its siblings.
112pub fn linear_placement(
113    tx: &mut Transaction,
114    relative_placement: EntityId,
115    placement_rel_to: Option<EntityId>,
116    cartesian_position: Option<EntityId>,
117) -> Result<EntityId, AlignmentError> {
118    let mut attrs = vec![Value::Null; slot::linear_placement::ARITY];
119    attrs[slot::linear_placement::PLACEMENT_REL_TO] =
120        placement_rel_to.map_or(Value::Null, Value::Ref);
121    attrs[slot::linear_placement::RELATIVE_PLACEMENT] = Value::Ref(relative_placement);
122    attrs[slot::linear_placement::CARTESIAN_POSITION] =
123        cartesian_position.map_or(Value::Null, Value::Ref);
124    Ok(tx.create(Entity::new("IFCLINEARPLACEMENT", attrs)))
125}
126
127/// Stage an `IfcReferent`.
128///
129/// # Errors
130///
131/// Refuses a `GlobalId` that is not 22 characters.
132pub fn referent(
133    tx: &mut Transaction,
134    global_id: &str,
135    name: Option<&str>,
136    predefined_type: Option<&str>,
137    placement: Option<EntityId>,
138) -> Result<EntityId, AlignmentError> {
139    let mut attrs = vec![Value::Null; slot::referent::ARITY];
140    attrs[slot::product::GLOBAL_ID] = guid("IFCREFERENT", global_id)?;
141    if let Some(name) = name {
142        attrs[slot::product::NAME] = Value::Text(name.into());
143    }
144    attrs[slot::product::OBJECT_PLACEMENT] = placement.map_or(Value::Null, Value::Ref);
145    attrs[slot::referent::PREDEFINED_TYPE] =
146        predefined_type.map_or(Value::Null, |token| Value::Enum(token.into()));
147    Ok(tx.create(Entity::new("IFCREFERENT", attrs)))
148}
149
150/// Stage the `Pset_Stationing` property set for a referent.
151///
152/// Stationing is carried by a property set, not by an attribute, so a
153/// referent without one is positioned but has no station. The reader
154/// looks for this exact set name and these exact property names,
155/// which is why authoring them is a named helper rather than a
156/// generic property-set call: a typo here produces a referent the
157/// stationing reader silently skips.
158///
159/// Returns the `IfcRelDefinesByProperties` that binds the set to the
160/// referent.
161///
162/// # Errors
163///
164/// Refuses a non-finite station and a `GlobalId` that is not 22
165/// characters.
166pub fn stationing(
167    tx: &mut Transaction,
168    pset_global_id: &str,
169    rel_global_id: &str,
170    referent: EntityId,
171    station: f64,
172    incoming_station: Option<f64>,
173    has_increasing_station: Option<bool>,
174) -> Result<EntityId, AlignmentError> {
175    finite("IFCPROPERTYSET", "Station", station)?;
176    if let Some(value) = incoming_station {
177        finite("IFCPROPERTYSET", "IncomingStation", value)?;
178    }
179    let pset_guid = guid("IFCPROPERTYSET", pset_global_id)?;
180    let rel_guid = guid("IFCRELDEFINESBYPROPERTIES", rel_global_id)?;
181
182    let mut properties = vec![single_value(tx, "Station", Value::Real(station))];
183    if let Some(value) = incoming_station {
184        properties.push(single_value(tx, "IncomingStation", Value::Real(value)));
185    }
186    if let Some(value) = has_increasing_station {
187        properties.push(single_value(tx, "HasIncreasingStation", Value::Bool(value)));
188    }
189
190    let mut pset_attrs = vec![Value::Null; 5];
191    pset_attrs[0] = pset_guid;
192    pset_attrs[2] = Value::Text("Pset_Stationing".into());
193    pset_attrs[4] = Value::List(properties.into_iter().map(Value::Ref).collect());
194    let pset = tx.create(Entity::new("IFCPROPERTYSET", pset_attrs));
195
196    let mut rel_attrs = vec![Value::Null; 6];
197    rel_attrs[0] = rel_guid;
198    rel_attrs[4] = Value::List(vec![Value::Ref(referent)]);
199    rel_attrs[5] = Value::Ref(pset);
200    Ok(tx.create(Entity::new("IFCRELDEFINESBYPROPERTIES", rel_attrs)))
201}
202
203/// Stage an `IfcPropertySingleValue` for the stationing set.
204fn single_value(tx: &mut Transaction, name: &str, value: Value) -> EntityId {
205    let mut attrs = vec![Value::Null; 4];
206    attrs[0] = Value::Text(name.into());
207    attrs[2] = value;
208    tx.create(Entity::new("IFCPROPERTYSINGLEVALUE", attrs))
209}