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}