Skip to main content

ifc_geometry/authoring/
transform.rs

1//! Transformation operators, representation maps and mapped items.
2//!
3//! # `Scale2` is not at the same slot in both branches
4//!
5//! `IfcCartesianTransformationOperator3D` inserts `Axis3` at slot 4,
6//! so the 3D non-uniform subtype carries `Scale2` at 5 and `Scale3` at
7//! 6, while the 2D non-uniform one has `Scale2` at 4. An operator
8//! written with the 2D layout into a 3D entity puts a scale where a
9//! direction belongs, which parses as a type error only if a validator
10//! looks. The slot constants in [`crate::resource::operator`] record
11//! both, and this module uses them rather than counting.
12//!
13//! # A scale of zero collapses everything
14//!
15//! The supertype derives `Scl := NVL(Scale, 1.0)` and requires
16//! `Scl > 0.0`. Absent means one, not zero, so `None` is safe -- but an
17//! explicit zero or negative scale is refused here.
18
19use ifc_model::{Entity, EntityId, Transaction, Value};
20
21use crate::error::GeometryError;
22use crate::resource::operator::slot;
23
24use super::{invalid, refs, require_finite};
25
26/// The axes and scale shared by every transformation operator.
27///
28/// Every field is optional except the origin: the schema defaults the
29/// axes to the identity frame and the scale to one.
30#[derive(Debug, Default, Clone, Copy)]
31pub struct Transform {
32    /// `Axis1`: the local X direction.
33    pub axis1: Option<EntityId>,
34    /// `Axis2`: the local Y direction.
35    pub axis2: Option<EntityId>,
36    /// `Scale`. Absent means one, which is why `None` is not zero.
37    pub scale: Option<f64>,
38}
39
40/// Check a scale against `ScaleGreaterZero`.
41fn check_scale(
42    type_name: &'static str,
43    attribute: &'static str,
44    scale: Option<f64>,
45) -> Result<(), GeometryError> {
46    let Some(scale) = scale else {
47        // Absent derives to 1.0, which satisfies the rule.
48        return Ok(());
49    };
50    require_finite(type_name, attribute, &[scale])?;
51    if scale <= 0.0 {
52        return Err(invalid(
53            type_name,
54            attribute,
55            format!("expected a scale above zero, got {scale}"),
56        ));
57    }
58    Ok(())
59}
60
61/// Fill the four slots every operator shares.
62fn base(
63    type_name: &'static str,
64    width: usize,
65    local_origin: EntityId,
66    transform: Transform,
67) -> Result<Vec<Value>, GeometryError> {
68    check_scale(type_name, "Scale", transform.scale)?;
69    let mut attrs = vec![Value::Null; width];
70    attrs[slot::AXIS1] = transform.axis1.map_or(Value::Null, Value::Ref);
71    attrs[slot::AXIS2] = transform.axis2.map_or(Value::Null, Value::Ref);
72    attrs[slot::LOCAL_ORIGIN] = Value::Ref(local_origin);
73    attrs[slot::SCALE] = transform.scale.map_or(Value::Null, Value::Real);
74    Ok(attrs)
75}
76
77/// Stage an `IfcCartesianTransformationOperator2D`.
78///
79/// # Errors
80///
81/// Refuses a scale that is zero, negative or non-finite.
82pub fn transformation_operator_2d(
83    tx: &mut Transaction,
84    local_origin: EntityId,
85    transform: Transform,
86) -> Result<EntityId, GeometryError> {
87    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR2D";
88    let attrs = base(T, 4, local_origin, transform)?;
89    Ok(tx.create(Entity::new(T, attrs)))
90}
91
92/// Stage an `IfcCartesianTransformationOperator2DnonUniform`.
93///
94/// `scale2` scales the local Y axis independently. Its slot is 4 here
95/// and 5 on the 3D variant; see the module note.
96///
97/// # Errors
98///
99/// Refuses either scale being zero, negative or non-finite.
100pub fn transformation_operator_2d_non_uniform(
101    tx: &mut Transaction,
102    local_origin: EntityId,
103    transform: Transform,
104    scale2: Option<f64>,
105) -> Result<EntityId, GeometryError> {
106    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM";
107    check_scale(T, "Scale2", scale2)?;
108    let mut attrs = base(T, 5, local_origin, transform)?;
109    attrs[slot::SCALE2_2D] = scale2.map_or(Value::Null, Value::Real);
110    Ok(tx.create(Entity::new(T, attrs)))
111}
112
113/// Stage an `IfcCartesianTransformationOperator3D`.
114///
115/// # Errors
116///
117/// Refuses a scale that is zero, negative or non-finite.
118pub fn transformation_operator_3d(
119    tx: &mut Transaction,
120    local_origin: EntityId,
121    transform: Transform,
122    axis3: Option<EntityId>,
123) -> Result<EntityId, GeometryError> {
124    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR3D";
125    let mut attrs = base(T, 5, local_origin, transform)?;
126    attrs[slot::AXIS3] = axis3.map_or(Value::Null, Value::Ref);
127    Ok(tx.create(Entity::new(T, attrs)))
128}
129
130/// Stage an `IfcCartesianTransformationOperator3DnonUniform`.
131///
132/// `Scale2` and `Scale3` sit at slots 5 and 6, *after* `Axis3` -- not
133/// at 4 as on the 2D non-uniform operator.
134///
135/// # Errors
136///
137/// Refuses any of the three scales being zero, negative or non-finite.
138pub fn transformation_operator_3d_non_uniform(
139    tx: &mut Transaction,
140    local_origin: EntityId,
141    transform: Transform,
142    axis3: Option<EntityId>,
143    scale2: Option<f64>,
144    scale3: Option<f64>,
145) -> Result<EntityId, GeometryError> {
146    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM";
147    check_scale(T, "Scale2", scale2)?;
148    check_scale(T, "Scale3", scale3)?;
149    let mut attrs = base(T, 7, local_origin, transform)?;
150    attrs[slot::AXIS3] = axis3.map_or(Value::Null, Value::Ref);
151    attrs[slot::SCALE2_3D] = scale2.map_or(Value::Null, Value::Real);
152    attrs[slot::SCALE3_3D] = scale3.map_or(Value::Null, Value::Real);
153    Ok(tx.create(Entity::new(T, attrs)))
154}
155
156/// Stage an `IfcRepresentationMap`: a reusable shape and its origin.
157///
158/// The map is what an `IfcMappedItem` points at, so one map serves
159/// many instances -- that is the whole point of mapping rather than
160/// repeating the geometry.
161pub fn representation_map(
162    tx: &mut Transaction,
163    mapping_origin: EntityId,
164    mapped_representation: EntityId,
165) -> EntityId {
166    let attrs = vec![
167        Value::Ref(mapping_origin),
168        Value::Ref(mapped_representation),
169    ];
170    tx.create(Entity::new("IFCREPRESENTATIONMAP", attrs))
171}
172
173/// Stage an `IfcMappedItem`: one placed instance of a mapped shape.
174///
175/// `mapping_target` is a transformation operator, so the same source
176/// map appears at a different place and scale for each item.
177pub fn mapped_item(
178    tx: &mut Transaction,
179    mapping_source: EntityId,
180    mapping_target: EntityId,
181) -> EntityId {
182    let attrs = vec![Value::Ref(mapping_source), Value::Ref(mapping_target)];
183    tx.create(Entity::new("IFCMAPPEDITEM", attrs))
184}
185
186/// Stage an `IfcTopologyRepresentation`.
187///
188/// A shape representation whose items are topological rather than
189/// geometric -- vertices, edges, faces and shells.
190///
191/// # Errors
192///
193/// Refuses an empty item set: `SET [1:?]`.
194pub fn topology_representation(
195    tx: &mut Transaction,
196    context: EntityId,
197    identifier: Option<&str>,
198    representation_type: Option<&str>,
199    items: &[EntityId],
200) -> Result<EntityId, GeometryError> {
201    const T: &str = "IFCTOPOLOGYREPRESENTATION";
202    if items.is_empty() {
203        return Err(invalid(T, "Items", "expected at least one item"));
204    }
205    let attrs = vec![
206        Value::Ref(context),
207        identifier.map_or(Value::Null, |v| Value::Text(v.into())),
208        representation_type.map_or(Value::Null, |v| Value::Text(v.into())),
209        refs(items),
210    ];
211    Ok(tx.create(Entity::new(T, attrs)))
212}
213
214/// Stage an `IfcShapeAspect`: a named part of a product shape.
215///
216/// This is how a subtype points at one component of a larger
217/// representation -- a varying structural member naming the aspect
218/// that carries its thickness, for instance.
219///
220/// `ProductDefinitional` is an `IfcLogical`, not a boolean: it may be
221/// UNKNOWN, meaning nobody has stated whether the aspect defines the
222/// product shape. `None` is written as that third state rather than
223/// being collapsed to false, which would assert something untrue.
224///
225/// # Errors
226///
227/// Refuses an empty `shape_representations` list, which is bounded
228/// `LIST [1:?]`: an aspect representing nothing names nothing.
229pub fn shape_aspect(
230    tx: &mut Transaction,
231    shape_representations: &[EntityId],
232    name: Option<&str>,
233    description: Option<&str>,
234    product_definitional: Option<bool>,
235    part_of_product_definition_shape: Option<EntityId>,
236) -> Result<EntityId, GeometryError> {
237    const ENTITY: &str = "IFCSHAPEASPECT";
238    if shape_representations.is_empty() {
239        return Err(invalid(
240            ENTITY,
241            "ShapeRepresentations",
242            "expected at least one representation, per LIST [1:?]",
243        ));
244    }
245    let attrs = vec![
246        refs(shape_representations),
247        name.map_or(Value::Null, |v| Value::Text(v.into())),
248        description.map_or(Value::Null, |v| Value::Text(v.into())),
249        product_definitional.map_or(Value::LogicalUnknown, Value::Bool),
250        part_of_product_definition_shape.map_or(Value::Null, Value::Ref),
251    ];
252    Ok(tx.create(Entity::new(ENTITY, attrs)))
253}