ifc_lite_export/step_api.rs
1// SPDX-License-Identifier: MPL-2.0
2//! The STEP exporter's public vocabulary: what a caller asks for
3//! ([`StepOptions`] and the three mutation kinds) and what it gets told
4//! afterwards ([`StepStats`]).
5//!
6//! Split out of `step.rs` for the module-size ratchet
7//! (`rust/processing/tests/module_size_ratchet.rs`), the same way the text
8//! primitives went to `step_text.rs` and the copy-on-write pass to
9//! `step_cow.rs`. `step.rs` was 399 lines against a 400-line limit, so the next
10//! change to it had to move something out, and these five declaration-only
11//! types were the coherent thing to move: no behaviour, no dependency on the
12//! export loop, and one theme between them.
13//!
14//! No CALLER can see the split, and the header does not claim otherwise:
15//! `step.rs` re-exports all five (so `crate::step::StepOptions` and every other
16//! existing path still resolves) and `lib.rs` already exported them from the
17//! crate root. The benefit is to the ratchet and to whoever next reads
18//! `step.rs`, not to the interface.
19
20/// A single root-attribute edit: replace the top-level attribute at `index` of entity
21/// `express_id` with `value` (already STEP-serialized, e.g. `'New Name'` or `$`).
22/// This is the wasm-bridge form of a `MutablePropertyView` UPDATE_ATTRIBUTE mutation.
23pub struct AttrMutation {
24 pub express_id: u32,
25 pub index: usize,
26 pub value: String,
27}
28
29/// A property create/update: attach (or overwrite) `prop_name` in `pset_name` on
30/// `express_id` with `value` — the STEP-serialized nominal value, e.g. `IFCLABEL('2HR')`
31/// or `IFCREAL(42.)`. The wasm-bridge form of a `MutablePropertyView` CREATE/UPDATE_PROPERTY.
32/// Synthesizes fresh `IfcPropertySingleValue` / `IfcPropertySet` / `IfcRelDefinesByProperties`
33/// entities appended to DATA (new psets; merge-into-existing is a follow-on).
34pub struct PropMutation {
35 pub express_id: u32,
36 pub pset_name: String,
37 pub prop_name: String,
38 pub value: String,
39}
40
41/// Replace one attribute of a record that other records share, by copying the
42/// record and repointing a single referrer at the copy.
43///
44/// The reason this is a writer job rather than a caller one is the id. A copy
45/// needs a number no record holds, and the writer is what knows `max_id`; a
46/// caller that allocates its own has to agree with `PropMutation`'s synthesis
47/// about which numbers are free, and two allocators sharing one space is a
48/// collision waiting for the first export that uses both.
49///
50/// Doing it here also keeps the copy inside the emit path, so it is counted in
51/// [`StepStats::written`] and converted when the export targets another schema.
52/// A record spliced into the output afterwards is neither.
53///
54/// Property sets are the case this exists for. IFC exporters routinely give
55/// each element its own `IfcPropertySet` and point them all at one
56/// `IfcPropertySingleValue` per distinct value, so editing that value in place
57/// changes it for every element sharing it. Copying first changes one.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct CopyOnWriteMutation {
60 /// The record to copy.
61 pub express_id: u32,
62 /// Which attribute of the copy to replace, zero-based.
63 pub index: usize,
64 /// The replacement, STEP-serialized, e.g. `IFCLABEL('2HR')`.
65 pub value: String,
66 /// The record that should point at the copy instead of the original.
67 pub referrer_id: u32,
68 /// Which attribute of the referrer holds that reference. A list attribute
69 /// is rewritten with the one reference substituted and the rest untouched.
70 pub referrer_index: usize,
71}
72
73/// Options for STEP export.
74#[derive(Default)]
75pub struct StepOptions {
76 /// FILE_SCHEMA label to write (e.g. `IFC4`). `None` ⇒ preserve the source schema.
77 /// When `Some` and the target differs, entity types/attributes are converted (P2).
78 pub schema: Option<String>,
79 /// Express ids to include. `None` ⇒ the whole model. When set, the forward
80 /// reference closure is added so every emitted `#ref` resolves.
81 pub included: Option<Vec<u32>>,
82 /// Root-attribute edits to apply during serialization (P3 mutation bridge).
83 pub attribute_mutations: Vec<AttrMutation>,
84 /// Property create/update edits — synthesized as new pset entities appended to DATA.
85 pub property_mutations: Vec<PropMutation>,
86 /// Copy-then-edit mutations for records other records share.
87 pub copy_on_write: Vec<CopyOnWriteMutation>,
88 /// `FILE_DESCRIPTION` item. `None` ⇒ keep the source file's items, and
89 /// fall back to the generic view-definition default only when the source
90 /// carried none.
91 pub description: Option<String>,
92 /// `FILE_NAME` author. `None` ⇒ keep the source file's.
93 pub author: Option<String>,
94 /// `FILE_NAME` organization. `None` ⇒ keep the source file's.
95 pub organization: Option<String>,
96 /// `FILE_NAME` preprocessor_version — the tool writing this file.
97 /// `None` ⇒ `ifc-lite`.
98 pub application: Option<String>,
99 /// `FILE_NAME` name. `None` ⇒ `export.ifc`.
100 pub filename: Option<String>,
101 /// `FILE_NAME` time_stamp. `None` ⇒ the source file's stamp. There is no
102 /// clock fallback: `SystemTime::now` is unavailable on the
103 /// `wasm32-unknown-unknown` target this exporter ships to, so a caller that
104 /// wants "now" states it.
105 pub time_stamp: Option<String>,
106}
107
108/// Coverage stats for a STEP export.
109pub struct StepStats {
110 /// Entities in the source model.
111 pub total: usize,
112 /// Entities written (after filtering + reference closure).
113 pub written: usize,
114 /// Copy-on-write mutations the file could not express, so none was made.
115 /// Non-zero means an edit the caller asked for is not in the output, and
116 /// the caller is the only one who can say what to do about it.
117 pub copies_refused: usize,
118 /// `#<digits>` references above `u32::MAX` refused (issue #3421) while
119 /// resolving a filtered export's reference closure. The referenced record
120 /// could never itself be a real entity, so this excludes nothing
121 /// reachable — it only says the source has an id ifc-lite can't hold (#3752).
122 pub refused_refs: usize,
123 /// Records with a root-attribute edit that could not be applied: the
124 /// record's own argument list did not scan into slots, so the line was
125 /// emitted as the source wrote it (issue #4125), or an edit's `index` is
126 /// past the record's arity, so that edit was dropped and the others kept.
127 /// Non-zero means an edit the caller asked for is not in the output.
128 /// Counted rather than left as an unchanged line, because a refusal that
129 /// looks like a successful no-op is the failure this whole family is
130 /// about: writing by index into a mis-scanned list lands on the wrong
131 /// attribute and reports success.
132 pub attribute_edits_refused: usize,
133 /// Records written to an IFC2X3 downgrade with `$` in `OwnerHistory`,
134 /// which IFC2X3 requires, because the export writes no `IfcOwnerHistory`
135 /// to point it at (#4686). Non-zero means the file is not valid IFC2X3.
136 /// Records the source gave `$` are otherwise pointed at the first owner
137 /// history the export writes; none is invented.
138 pub owner_history_unfilled: usize,
139 /// SLOTS other than `OwnerHistory` written to an IFC2X3 export with `$`
140 /// where IFC2X3 requires a value and the schema offers no default that
141 /// claims nothing (#4714): measures, labels, identifiers, entity
142 /// references, and enums with no `NOTDEFINED` member such as
143 /// `IfcBuildingStorey.CompositionType`. Non-zero means the file is not
144 /// valid IFC2X3. Counted per slot, not per record: one record can leave
145 /// several. Slots whose IFC2X3 declaration DOES offer such a default take
146 /// `.NOTDEFINED.` or `.F.` and are not counted.
147 pub required_slots_unfilled: usize,
148 /// SLOTS written to an IFC4X3/IFC5 -> IFC4 downgrade with `$` where IFC4
149 /// requires a value (#5307, the Rust twin of #5202). The converter never
150 /// invents one: no measure, label, identifier, reference, flag or enum.
151 /// Non-zero means the file is not valid IFC4. Counted per slot, not per
152 /// record. Zero for every other direction, including IFC2X3 -> IFC4.
153 pub ifc4_required_slots_unfilled: usize,
154 /// Enum values the target schema does not define that a conversion wrote
155 /// as `.NOTDEFINED.` or `$`, or as `.USERDEFINED.` without room for the
156 /// member name (#5365). Non-zero means information was lost.
157 pub enum_values_lost: usize,
158 /// Enum values the target schema does not define and offers no valid
159 /// replacement for, so they were kept as written (#5365). Non-zero means
160 /// the file is not valid against its header.
161 pub enum_values_refused: usize,
162}