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::guid::Guid;
20use ifc_model::{Entity, EntityId, Transaction, Value};
21
22use crate::error::GeometryError;
23use crate::resource::operator::slot;
24
25use super::{invalid, refs, require_finite};
26
27/// The axes and scale shared by every transformation operator.
28///
29/// Every field is optional except the origin: the schema defaults the
30/// axes to the identity frame and the scale to one.
31#[derive(Debug, Default, Clone, Copy)]
32pub struct Transform {
33    /// `Axis1`: the local X direction.
34    pub axis1: Option<EntityId>,
35    /// `Axis2`: the local Y direction.
36    pub axis2: Option<EntityId>,
37    /// `Scale`. Absent means one, which is why `None` is not zero.
38    pub scale: Option<f64>,
39}
40
41/// Check a scale against `ScaleGreaterZero`.
42fn check_scale(
43    type_name: &'static str,
44    attribute: &'static str,
45    scale: Option<f64>,
46) -> Result<(), GeometryError> {
47    let Some(scale) = scale else {
48        // Absent derives to 1.0, which satisfies the rule.
49        return Ok(());
50    };
51    require_finite(type_name, attribute, &[scale])?;
52    if scale <= 0.0 {
53        return Err(invalid(
54            type_name,
55            attribute,
56            format!("expected a scale above zero, got {scale}"),
57        ));
58    }
59    Ok(())
60}
61
62/// Fill the four slots every operator shares.
63fn base(
64    type_name: &'static str,
65    width: usize,
66    local_origin: EntityId,
67    transform: Transform,
68) -> Result<Vec<Value>, GeometryError> {
69    check_scale(type_name, "Scale", transform.scale)?;
70    let mut attrs = vec![Value::Null; width];
71    attrs[slot::AXIS1] = transform.axis1.map_or(Value::Null, Value::Ref);
72    attrs[slot::AXIS2] = transform.axis2.map_or(Value::Null, Value::Ref);
73    attrs[slot::LOCAL_ORIGIN] = Value::Ref(local_origin);
74    attrs[slot::SCALE] = transform.scale.map_or(Value::Null, Value::Real);
75    Ok(attrs)
76}
77
78/// Stage an `IfcCartesianTransformationOperator2D`.
79///
80/// # Errors
81///
82/// Refuses a scale that is zero, negative or non-finite.
83pub fn transformation_operator_2d(
84    tx: &mut Transaction,
85    local_origin: EntityId,
86    transform: Transform,
87) -> Result<EntityId, GeometryError> {
88    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR2D";
89    let attrs = base(T, 4, local_origin, transform)?;
90    Ok(tx.create(Entity::new(T, attrs)))
91}
92
93/// Stage an `IfcCartesianTransformationOperator2DnonUniform`.
94///
95/// `scale2` scales the local Y axis independently. Its slot is 4 here
96/// and 5 on the 3D variant; see the module note.
97///
98/// # Errors
99///
100/// Refuses either scale being zero, negative or non-finite.
101pub fn transformation_operator_2d_non_uniform(
102    tx: &mut Transaction,
103    local_origin: EntityId,
104    transform: Transform,
105    scale2: Option<f64>,
106) -> Result<EntityId, GeometryError> {
107    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM";
108    check_scale(T, "Scale2", scale2)?;
109    let mut attrs = base(T, 5, local_origin, transform)?;
110    attrs[slot::SCALE2_2D] = scale2.map_or(Value::Null, Value::Real);
111    Ok(tx.create(Entity::new(T, attrs)))
112}
113
114/// Stage an `IfcCartesianTransformationOperator3D`.
115///
116/// # Errors
117///
118/// Refuses a scale that is zero, negative or non-finite.
119pub fn transformation_operator_3d(
120    tx: &mut Transaction,
121    local_origin: EntityId,
122    transform: Transform,
123    axis3: Option<EntityId>,
124) -> Result<EntityId, GeometryError> {
125    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR3D";
126    let mut attrs = base(T, 5, local_origin, transform)?;
127    attrs[slot::AXIS3] = axis3.map_or(Value::Null, Value::Ref);
128    Ok(tx.create(Entity::new(T, attrs)))
129}
130
131/// Stage an `IfcCartesianTransformationOperator3DnonUniform`.
132///
133/// `Scale2` and `Scale3` sit at slots 5 and 6, *after* `Axis3` -- not
134/// at 4 as on the 2D non-uniform operator.
135///
136/// # Errors
137///
138/// Refuses any of the three scales being zero, negative or non-finite.
139pub fn transformation_operator_3d_non_uniform(
140    tx: &mut Transaction,
141    local_origin: EntityId,
142    transform: Transform,
143    axis3: Option<EntityId>,
144    scale2: Option<f64>,
145    scale3: Option<f64>,
146) -> Result<EntityId, GeometryError> {
147    const T: &str = "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM";
148    check_scale(T, "Scale2", scale2)?;
149    check_scale(T, "Scale3", scale3)?;
150    let mut attrs = base(T, 7, local_origin, transform)?;
151    attrs[slot::AXIS3] = axis3.map_or(Value::Null, Value::Ref);
152    attrs[slot::SCALE2_3D] = scale2.map_or(Value::Null, Value::Real);
153    attrs[slot::SCALE3_3D] = scale3.map_or(Value::Null, Value::Real);
154    Ok(tx.create(Entity::new(T, attrs)))
155}
156
157/// Stage an `IfcRepresentationMap`: a reusable shape and its origin.
158///
159/// The map is what an `IfcMappedItem` points at, so one map serves
160/// many instances -- that is the whole point of mapping rather than
161/// repeating the geometry.
162pub fn representation_map(
163    tx: &mut Transaction,
164    mapping_origin: EntityId,
165    mapped_representation: EntityId,
166) -> EntityId {
167    let attrs = vec![
168        Value::Ref(mapping_origin),
169        Value::Ref(mapped_representation),
170    ];
171    tx.create(Entity::new("IFCREPRESENTATIONMAP", attrs))
172}
173
174/// Stage an `IfcMappedItem`: one placed instance of a mapped shape.
175///
176/// `mapping_target` is a transformation operator, so the same source
177/// map appears at a different place and scale for each item.
178pub fn mapped_item(
179    tx: &mut Transaction,
180    mapping_source: EntityId,
181    mapping_target: EntityId,
182) -> EntityId {
183    let attrs = vec![Value::Ref(mapping_source), Value::Ref(mapping_target)];
184    tx.create(Entity::new("IFCMAPPEDITEM", attrs))
185}
186
187/// Stage an `IfcTopologyRepresentation`.
188///
189/// A shape representation whose items are topological rather than
190/// geometric -- vertices, edges, faces and shells.
191///
192/// # Errors
193///
194/// Refuses an empty item set: `SET [1:?]`.
195pub fn topology_representation(
196    tx: &mut Transaction,
197    context: EntityId,
198    identifier: Option<&str>,
199    representation_type: Option<&str>,
200    items: &[EntityId],
201) -> Result<EntityId, GeometryError> {
202    const T: &str = "IFCTOPOLOGYREPRESENTATION";
203    if items.is_empty() {
204        return Err(invalid(T, "Items", "expected at least one item"));
205    }
206    let attrs = vec![
207        Value::Ref(context),
208        identifier.map_or(Value::Null, |v| Value::Text(v.into())),
209        representation_type.map_or(Value::Null, |v| Value::Text(v.into())),
210        refs(items),
211    ];
212    Ok(tx.create(Entity::new(T, attrs)))
213}
214
215/// Stage an `IfcShapeAspect`: a named part of a product shape.
216///
217/// This is how a subtype points at one component of a larger
218/// representation -- a varying structural member naming the aspect
219/// that carries its thickness, for instance.
220///
221/// `ProductDefinitional` is an `IfcLogical`, not a boolean: it may be
222/// UNKNOWN, meaning nobody has stated whether the aspect defines the
223/// product shape. `None` is written as that third state rather than
224/// being collapsed to false, which would assert something untrue.
225///
226/// # Errors
227///
228/// Refuses an empty `shape_representations` list, which is bounded
229/// `LIST [1:?]`: an aspect representing nothing names nothing.
230pub fn shape_aspect(
231    tx: &mut Transaction,
232    shape_representations: &[EntityId],
233    name: Option<&str>,
234    description: Option<&str>,
235    product_definitional: Option<bool>,
236    part_of_product_definition_shape: Option<EntityId>,
237) -> Result<EntityId, GeometryError> {
238    const ENTITY: &str = "IFCSHAPEASPECT";
239    if shape_representations.is_empty() {
240        return Err(invalid(
241            ENTITY,
242            "ShapeRepresentations",
243            "expected at least one representation, per LIST [1:?]",
244        ));
245    }
246    let attrs = vec![
247        refs(shape_representations),
248        name.map_or(Value::Null, |v| Value::Text(v.into())),
249        description.map_or(Value::Null, |v| Value::Text(v.into())),
250        product_definitional.map_or(Value::LogicalUnknown, Value::Bool),
251        part_of_product_definition_shape.map_or(Value::Null, Value::Ref),
252    ];
253    Ok(tx.create(Entity::new(ENTITY, attrs)))
254}
255
256/// Stage an `IfcGrid`: the U/V/W axis system a plan is dimensioned
257/// against.
258///
259/// `UAxes` and `VAxes` are `LIST [1:?] OF UNIQUE`, so a grid needs at
260/// least one axis in each direction and may not list the same axis
261/// twice. A repeated axis is not a harmless duplicate: it makes the
262/// grid ambiguous about which intersection a gridline names.
263///
264/// # Errors
265///
266/// Refuses a malformed GlobalId, an empty U or V list, and a repeated
267/// axis within any one list.
268pub fn grid(
269    tx: &mut Transaction,
270    global_id: &str,
271    placement: Option<EntityId>,
272    axes: (&[EntityId], &[EntityId], &[EntityId]),
273    predefined_type: Option<&str>,
274) -> Result<EntityId, GeometryError> {
275    const ENTITY: &str = "IFCGRID";
276    if Guid::parse(global_id).is_none() {
277        return Err(invalid(ENTITY, "GlobalId", global_id));
278    }
279    let (u_axes, v_axes, w_axes) = axes;
280    for (values, attribute) in [(u_axes, "UAxes"), (v_axes, "VAxes")] {
281        if values.is_empty() {
282            return Err(invalid(
283                ENTITY,
284                attribute,
285                "expected at least one axis, per LIST [1:?]",
286            ));
287        }
288    }
289    for (values, attribute) in [(u_axes, "UAxes"), (v_axes, "VAxes"), (w_axes, "WAxes")] {
290        let mut seen = std::collections::HashSet::new();
291        if let Some(repeat) = values.iter().find(|axis| !seen.insert(**axis)) {
292            return Err(invalid(
293                ENTITY,
294                attribute,
295                format!("axis {repeat:?} appears twice in a UNIQUE list"),
296            ));
297        }
298    }
299    let mut attrs = vec![Value::Null; 11];
300    attrs[0] = Value::Text(global_id.into());
301    attrs[5] = placement.map_or(Value::Null, Value::Ref);
302    attrs[7] = refs(u_axes);
303    attrs[8] = refs(v_axes);
304    if !w_axes.is_empty() {
305        attrs[9] = refs(w_axes);
306    }
307    attrs[10] = predefined_type.map_or(Value::Null, |t| Value::Enum(t.into()));
308    Ok(tx.create(Entity::new(ENTITY, attrs)))
309}