Skip to main content

ifc_properties/quantity/
edit.rs

1//! Staging authored quantity updates onto a transaction.
2//!
3//! # This module stages; it does not commit
4//!
5//! Every function here takes a `&mut Transaction` and adds edits to it. None
6//! of them touch the model. That is deliberate: a takeoff run updates areas on
7//! two hundred elements, and those either all land or none do. If each helper
8//! committed, a failure halfway through would leave a file whose quantities
9//! disagree with each other, which is worse than a file that was never
10//! updated.
11//!
12//! The caller decides the boundary:
13//!
14//! ```
15//! use ifc_model::{Model, Transaction};
16//! use ifc_properties::{set_quantity_value, QuantityKind};
17//!
18//! # fn run(model: &mut Model, area: ifc_model::EntityId) -> Result<(), Box<dyn std::error::Error>> {
19//! let mut tx = Transaction::new(model);
20//! set_quantity_value(&mut tx, model, area, 12.5)?;
21//! // ... stage the rest of the takeoff ...
22//! tx.commit(model).map_err(|c| format!("{c:?}"))?;
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! # The value is written bare
28//!
29//! A quantity's value slot is typed by its declaration: `IfcQuantityArea`
30//! declares `AreaValue : IfcAreaMeasure`, a defined type and not a SELECT.
31//! The measure is therefore already stated by the schema, and ISO 10303-21
32//! writes the simple value there (`12.5`); a typed parameter such as
33//! `IFCAREAMEASURE(12.5)` belongs only in a SELECT slot, where the type is
34//! otherwise ambiguous (#190). The readers accept both forms, because files
35//! in the wild carry both. These helpers refuse when the target is not the
36//! kind of quantity the caller thinks it is, rather than coerce it.
37
38use ifc_model::{EntityId, Model, Transaction, Value};
39
40use crate::error::PropertyError;
41use crate::quantity::release::bind;
42use crate::quantity::set::QuantityKind;
43
44/// Stage a new value for an existing simple quantity.
45///
46/// The value is written into the slot the model's declared release gives
47/// the quantity's value attribute (`release.rs` has the binding), as a bare
48/// number: the schema fixes which measure each subtype carries, so no
49/// wrapper is written. A value read in the typed form
50/// (`IFCAREAMEASURE(12.5)`) is replaced by the bare one. This also repairs
51/// a quantity read as [`Quantity::Unresolved`](crate::Quantity::Unresolved).
52///
53/// # Errors
54///
55/// Nothing is staged on an error:
56/// [`PropertyError::MissingEntity`] if `id` is not in the model,
57/// [`PropertyError::NotAQuantity`] if it is not a simple quantity,
58/// [`PropertyError::MultipleSchemas`] or [`PropertyError::UnsupportedSchema`]
59/// if the model binds no single release,
60/// [`PropertyError::EntityNotInSchema`] if that release does not declare the
61/// quantity's entity, [`PropertyError::MalformedEntitySlots`] if the record
62/// does not have the release's attribute count, and
63/// [`PropertyError::AuthoringInvalid`] for a value its measure cannot hold.
64pub fn set_quantity_value(
65    tx: &mut Transaction,
66    model: &Model,
67    id: EntityId,
68    value: f64,
69) -> Result<(), PropertyError> {
70    let entity = model.get(id).ok_or(PropertyError::MissingEntity { id })?;
71    let kind = QuantityKind::from_type_name(&entity.type_name).ok_or_else(|| {
72        PropertyError::NotAQuantity {
73            id,
74            type_name: entity.type_name.to_string(),
75        }
76    })?;
77    let layout = bind(model)?;
78    let name = layout.entity(kind)?;
79    let expected = layout.arity(&name);
80    if entity.attributes.len() != expected {
81        return Err(PropertyError::MalformedEntitySlots {
82            id,
83            type_name: entity.type_name.to_string(),
84            expected,
85            actual: entity.attributes.len(),
86        });
87    }
88    let slot = layout
89        .slot(&name, kind.value_attribute())
90        .expect("every release that declares a quantity declares its value");
91    tx.set_attribute(id, slot, layout.scalar(kind, value)?);
92    Ok(())
93}
94
95/// Stage a new `Name` for a quantity or property.
96///
97/// Slot 0 on both `IfcPhysicalQuantity` and `IfcProperty`.
98///
99/// # Errors
100///
101/// [`PropertyError::MissingEntity`] if `id` is not in the model.
102pub fn set_name(
103    tx: &mut Transaction,
104    model: &Model,
105    id: EntityId,
106    name: &str,
107) -> Result<(), PropertyError> {
108    if model.get(id).is_none() {
109        return Err(PropertyError::MissingEntity { id });
110    }
111    tx.set_attribute(id, 0, Value::Text(name.into()));
112    Ok(())
113}
114
115/// Stage a new `Description`, or clear it with `None`.
116///
117/// Slot 1 on both `IfcPhysicalQuantity` and `IfcProperty`. `None` writes
118/// `Value::Null`, which is STEP's `$` -- the attribute is genuinely unset,
119/// not set to an empty string. The distinction survives a round trip and a
120/// consumer can tell "no description" from "description is blank".
121///
122/// # Errors
123///
124/// [`PropertyError::MissingEntity`] if `id` is not in the model.
125pub fn set_description(
126    tx: &mut Transaction,
127    model: &Model,
128    id: EntityId,
129    description: Option<&str>,
130) -> Result<(), PropertyError> {
131    if model.get(id).is_none() {
132        return Err(PropertyError::MissingEntity { id });
133    }
134    let value = match description {
135        Some(text) => Value::Text(text.into()),
136        None => Value::Null,
137    };
138    tx.set_attribute(id, 1, value);
139    Ok(())
140}
141
142/// Stage a brand-new simple quantity in `model`'s release, returning the id
143/// reserved for it.
144///
145/// The entity is created with its value written bare in the slot whose
146/// declared measure its kind implies, and no unit, meaning "the project
147/// default applies" -- which is what most authored quantities mean. Attach
148/// it to a set with [`add_quantity_to_set`].
149///
150/// # Errors
151///
152/// As [`create_quantity_with`].
153pub fn create_quantity(
154    tx: &mut Transaction,
155    model: &Model,
156    kind: QuantityKind,
157    name: &str,
158    value: f64,
159) -> Result<EntityId, PropertyError> {
160    create_quantity_with(tx, model, kind, name, value, QuantityExtras::default())
161}
162
163/// The optional attributes of an `IfcPhysicalSimpleQuantity`.
164///
165/// Separated from [`create_quantity`] so the common call stays short,
166/// while `Description`, `Unit` and `Formula` remain reachable. They are
167/// attributes of the entity, not decoration: the reader in this crate
168/// resolves all three.
169#[derive(Debug, Clone, Copy, Default)]
170pub struct QuantityExtras<'a> {
171    /// `IfcPhysicalQuantity.Description`.
172    pub description: Option<&'a str>,
173    /// `IfcPhysicalSimpleQuantity.Unit`, an `IfcNamedUnit` reference.
174    ///
175    /// Left unset the quantity is read in the project's default unit for
176    /// its measure, which is usually what a take-off wants.
177    pub unit: Option<EntityId>,
178    /// `Formula`, how the quantity was derived. IFC4 and IFC4X3 only: an
179    /// IFC2X3 quantity has no `Formula`, and one given for it is refused.
180    pub formula: Option<&'a str>,
181}
182
183/// Stage a simple quantity with its optional attributes, in `model`'s
184/// release.
185///
186/// # The record has the release's layout
187///
188/// Attributes are placed by name in the declared release's table. IFC4 and
189/// IFC4X3 quantities have five attributes (`Name`, `Description`, `Unit`,
190/// the value, `Formula`); IFC2X3 ones have four, with no `Formula`. A
191/// model without `FILE_SCHEMA` binds IFC4. The value is written bare
192/// (`IFCQUANTITYAREA('A',$,$,12.5,$)`), since its declared type is a
193/// defined measure and not a SELECT.
194///
195/// # Errors
196///
197/// Nothing is staged on an error:
198/// - [`PropertyError::MultipleSchemas`] or
199///   [`PropertyError::UnsupportedSchema`]: the model binds no single release.
200/// - [`PropertyError::EntityNotInSchema`]: the release does not declare the
201///   kind, as `IfcQuantityNumber` outside IFC4X3.
202/// - [`PropertyError::AuthoringNotInSchema`]: a `Formula` for an IFC2X3
203///   model, which has none.
204/// - [`PropertyError::AuthoringInvalid`]: a non-finite value, or a
205///   fractional count where `IfcCountMeasure` is `INTEGER` (IFC4X3). In
206///   IFC2X3 and IFC4 it is `NUMBER`, so a fractional count is written as a
207///   real, and never truncated.
208pub fn create_quantity_with(
209    tx: &mut Transaction,
210    model: &Model,
211    kind: QuantityKind,
212    name: &str,
213    value: f64,
214    extras: QuantityExtras<'_>,
215) -> Result<EntityId, PropertyError> {
216    let layout = bind(model)?;
217    let entity = layout.entity(kind)?;
218    let numeric = layout.scalar(kind, value)?;
219    let text = |text: Option<&str>| text.map_or(Value::Null, |text| Value::Text(text.into()));
220    let record = layout.record(
221        kind,
222        &entity,
223        vec![
224            ("Name", Value::Text(name.into())),
225            ("Description", text(extras.description)),
226            ("Unit", extras.unit.map_or(Value::Null, Value::Ref)),
227            // Bare: the declared type is a defined measure, not a SELECT.
228            (kind.value_attribute(), numeric),
229            ("Formula", text(extras.formula)),
230        ],
231    )?;
232    Ok(tx.create(record))
233}
234
235/// Stage adding a quantity to an existing `IfcElementQuantity`.
236///
237/// Reads the set's current contents and writes back the extended list, so
238/// this is a read-modify-write against the model as it stands. Two calls
239/// adding to the same set in one transaction would each be computed from the
240/// same starting list and the second would win -- add both in one call
241/// instead.
242///
243/// # Errors
244///
245/// [`PropertyError::MissingEntity`] if the set is absent, and
246/// [`PropertyError::NotAQuantitySet`] if it is not an `IfcElementQuantity`.
247pub fn add_quantity_to_set(
248    tx: &mut Transaction,
249    model: &Model,
250    set: EntityId,
251    quantities: &[EntityId],
252) -> Result<(), PropertyError> {
253    let entity = model
254        .get(set)
255        .ok_or(PropertyError::MissingEntity { id: set })?;
256    if !entity.type_name.eq_ignore_ascii_case("IFCELEMENTQUANTITY") {
257        return Err(PropertyError::NotAQuantitySet {
258            id: set,
259            type_name: entity.type_name.to_string(),
260        });
261    }
262
263    /// `Quantities` slot on `IfcElementQuantity`: `IfcRoot` contributes four
264    /// attributes, then `MethodOfMeasurement` at 4.
265    const QUANTITIES_SLOT: usize = 5;
266
267    let mut members: Vec<Value> = entity
268        .attribute(QUANTITIES_SLOT)
269        .and_then(Value::as_list)
270        .map(<[Value]>::to_vec)
271        .unwrap_or_default();
272    let existing: Vec<EntityId> = members.iter().filter_map(Value::as_ref_id).collect();
273    for id in quantities {
274        // A set naming the same quantity twice is malformed; adding one that
275        // is already there is a no-op rather than a duplicate.
276        if !existing.contains(id) {
277            members.push(Value::Ref(*id));
278        }
279    }
280    tx.set_attribute(set, QUANTITIES_SLOT, Value::List(members));
281    Ok(())
282}