ifc_lite_export/model_options.rs
1// SPDX-License-Identifier: MPL-2.0
2//! What the attribute export can be asked to resolve, and the placement it
3//! resolves.
4//!
5//! Split from `model.rs` under the house rule (AGENTS.md). Keeping `Placement`
6//! here also keeps its frame documentation — the part that is easy to get wrong
7//! and expensive to get wrong — beside the type rather than buried in the
8//! streaming pass.
9
10/// A product's placement, composed from its `IfcLocalPlacement` chain.
11///
12/// **Which frame this is in**, because the crate contains three and a bare
13/// `[f64; 16]` cannot tell them apart:
14///
15/// * **Column-major** 4×4 (nalgebra's `as_slice` order). Translation is at
16/// indices 12, 13, 14 — not 3, 7, 11. The instancing path uses ROW-major
17/// (`mat4_to_row_major`); this is not that.
18/// * Translation is in **metres**: the file's length unit scale is applied. The
19/// basis is unitless.
20/// * **No RTC rebase.** Georeferenced models are shifted toward the origin for
21/// f32 precision before meshing; this is not shifted.
22/// * **No Z-up → Y-up conversion.** The glTF and OBJ exporters map
23/// `(x, y, z) → (x, z, −y)`; this does not.
24///
25/// So: the IFC world frame, which is the frame the file itself is in and not the
26/// frame any exporter emits. A consumer combining this with exported geometry
27/// has to account for the last two itself.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct Placement {
30 /// Column-major 4×4.
31 pub matrix: [f64; 16],
32}
33
34impl Placement {
35 /// The translation, in metres.
36 pub fn translation(&self) -> [f64; 3] {
37 [self.matrix[12], self.matrix[13], self.matrix[14]]
38 }
39}
40
41/// What the export should resolve beyond the attributes it always reads.
42///
43/// ```
44/// # use ifc_lite_export::ModelOptions;
45/// let opts = ModelOptions::default().with_placements(true);
46/// ```
47///
48/// `#[non_exhaustive]` plus builders, rather than a plain struct: this will
49/// grow, and `non_exhaustive` forbids EVERY struct expression from outside this
50/// crate — including `..Default::default()`, which is the one people reach for
51/// to survive new fields and which does not compile here. Builders are the
52/// shape that actually works for an external caller and keeps working when a
53/// field is added.
54#[derive(Debug, Clone, Default)]
55#[non_exhaustive]
56pub struct ModelOptions {
57 /// Resolve each product's `ObjectPlacement` into [`EntityRow::placement`].
58 ///
59 /// Off by default because it is not free: the placement memo is ~128 B per
60 /// distinct placement — one per product in most files, so `O(products)` on
61 /// top of the entity index — and each product costs a few extra decodes of
62 /// its own chain after the shared upper levels are memoized.
63 pub placements: bool,
64 /// Merge the property/quantity sets an occurrence inherits from its
65 /// `IfcTypeObject` into [`EntityRow::property_sets`] and
66 /// [`EntityRow::quantity_sets`], per property, occurrence wins.
67 ///
68 /// Off by default, which is NOT a claim that off is more correct. It is
69 /// not: authoring tools put a great deal on types, a type carrying
70 /// `Pset_WallCommon` yields no row of its own here (it only gets one when
71 /// it also has orphan geometry), and the browser has resolved inheritance
72 /// this way since #1913. Off is the default only because this changes what
73 /// every existing CSV/JSON/IFCX/Parquet export contains, and that is a
74 /// decision to take deliberately rather than as a side effect of adding
75 /// the capability.
76 ///
77 /// It costs one extra `IfcRelDefinesByType` map in pass 1 (`O(typed
78 /// occurrences)`) and one resolve per distinct type, memoized.
79 ///
80 /// [`EntityRow::property_sets`]: crate::model::EntityRow::property_sets
81 /// [`EntityRow::quantity_sets`]: crate::model::EntityRow::quantity_sets
82 pub inherit_type_properties: bool,
83 /// Render the attributes each entity's own IFC class declares, into
84 /// [`EntityRow::attributes`].
85 ///
86 /// Off by default for the same reason as the others: it adds columns to
87 /// every flattened export. It is cheap when on, though, since the row's
88 /// entity is already decoded and this only formats fields already in hand.
89 ///
90 /// [`EntityRow::attributes`]: crate::model::EntityRow::attributes
91 pub attributes: bool,
92}
93
94impl ModelOptions {
95 /// Resolve each product's `ObjectPlacement`. See [`ModelOptions::placements`].
96 #[must_use]
97 pub fn with_placements(mut self, yes: bool) -> Self {
98 self.placements = yes;
99 self
100 }
101
102 /// Merge type-inherited property/quantity sets into each occurrence. See
103 /// [`ModelOptions::inherit_type_properties`].
104 #[must_use]
105 pub fn with_inherit_type_properties(mut self, yes: bool) -> Self {
106 self.inherit_type_properties = yes;
107 self
108 }
109
110 /// Render each entity class's declared attributes into every row. See
111 /// [`ModelOptions::attributes`].
112 #[must_use]
113 pub fn with_attributes(mut self, yes: bool) -> Self {
114 self.attributes = yes;
115 self
116 }
117}