ifc_lite_export/
error.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Typed export failures, so callers can fail closed instead of shipping an
6//! empty artifact.
7
8use std::fmt;
9
10/// A failure the caller must handle; completion of a `try_export_*` function
11/// implies a non-empty artifact.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ExportError {
14    /// The visible mesh set was empty: the model has no render geometry (or the
15    /// caller's visibility filters removed all of it). The export would not be a
16    /// valid file: an empty GLB has empty `accessors`, `bufferViews`, `meshes`
17    /// and `nodes` arrays (glTF schema `minItems: 1`) and a zero
18    /// `buffers[0].byteLength` (schema `minimum: 1`), and an empty COLLADA
19    /// document has empty libraries its schema requires children in. Some
20    /// downstream tools still accept such a file silently, so this is surfaced
21    /// as an error rather than an artifact.
22    NoRenderGeometry,
23    /// The projected single-GLB output exceeds the glTF 32-bit (4 GiB) container
24    /// / buffer limit — the model is too large for one GLB and must be exported
25    /// as a multi-buffer glTF instead. Carries the projected total container size
26    /// in bytes (from pass 1), so a caller can log it or size a buffer split
27    /// without re-meshing. Replaces the historical 4 GiB `panic!` on the checked
28    /// export paths (issue #1516).
29    TooLarge {
30        /// Projected total GLB container size in bytes (may be a lower bound once
31        /// oversize; see [`crate::GlbSizeProjection::total_bytes`]).
32        bytes: u64,
33    },
34    /// A downstream encoder (Arrow/Parquet columnar writer, ZIP container, or
35    /// JSON serializer) rejected the data it was handed. This is an I/O/library
36    /// failure rather than a business-logic outcome — replaces panicking
37    /// `.unwrap()`/`.expect()` calls on writer/serializer results so a malformed
38    /// or resource-exhausted encode returns an error instead of unwinding the
39    /// whole export (see the `parquet_bos` exporter).
40    Serialization {
41        /// What was being serialized/written (e.g. "entities batch", "zip write_all").
42        stage: &'static str,
43        /// The underlying error's `Display` output.
44        detail: String,
45    },
46    /// A from-meshes writer was handed inconsistent inputs. Two shapes, and
47    /// they fail differently:
48    ///
49    /// * a COUNT that runs past a buffer — the per-mesh `vertex_counts` /
50    ///   `index_counts` sum past the end of the flattened `positions` /
51    ///   `normals` / `indices`. The crate-private infallible GLB assembler
52    ///   silently drops the un-backed tail (a valid GLB missing part of the
53    ///   model, reported as success);
54    /// * an index BLOCK that is not whole triangles, or an index VALUE that is
55    ///   not a vertex of its own mesh. The GLB assembler copies the index
56    ///   buffer verbatim, so either reaches the file and glTF-Validator rejects
57    ///   the result (glTF 2.0 3.7.2.1).
58    ///
59    /// `try_export_glb_from_meshes` and `try_export_collada_from_meshes` surface
60    /// both, through one index-block predicate (#4684), so a caller bug can't
61    /// ship truncated or invalid geometry from either.
62    MalformedMeshInput {
63        /// Which buffer was too short (expected vs actual lengths), or which
64        /// mesh carried which out-of-range index.
65        detail: String,
66    },
67}
68
69impl ExportError {
70    /// Stable machine-readable code, mirrored across the wasm boundary so TS
71    /// callers can match on it without parsing prose.
72    pub fn code(&self) -> &'static str {
73        match self {
74            ExportError::NoRenderGeometry => "NO_RENDER_GEOMETRY",
75            ExportError::TooLarge { .. } => "TOO_LARGE",
76            ExportError::Serialization { .. } => "SERIALIZATION_FAILED",
77            ExportError::MalformedMeshInput { .. } => "MALFORMED_MESH_INPUT",
78        }
79    }
80}
81
82impl fmt::Display for ExportError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            ExportError::NoRenderGeometry => write!(
86                f,
87                "{}: export produced no render geometry (empty model or all meshes filtered out)",
88                self.code()
89            ),
90            ExportError::TooLarge { bytes } => write!(
91                f,
92                "{}: projected GLB is {bytes} bytes, over the glTF 32-bit (4 GiB) limit; \
93                 export as a multi-buffer glTF instead",
94                self.code()
95            ),
96            ExportError::Serialization { stage, detail } => {
97                write!(f, "{}: failed to serialize {stage}: {detail}", self.code())
98            }
99            ExportError::MalformedMeshInput { detail } => {
100                write!(f, "{}: from-meshes inputs are inconsistent: {detail}", self.code())
101            }
102        }
103    }
104}
105
106impl std::error::Error for ExportError {}
107
108/// The one empty-export rule (#7695). Every render-geometry writer (GLB, the
109/// multi-buffer glTF, COLLADA, KMZ, OBJ, USD) calls this with the number of
110/// meshes it actually wrote, after the caller's visibility filters and its own
111/// emittable-mesh gate, before it hands out an artifact.
112///
113/// What counts as exportable depends on the format:
114///
115/// * **Render geometry** (the formats above): at least one emitted mesh. With
116///   none, every one of them is either schema-invalid (glTF `minItems: 1`,
117///   empty COLLADA libraries) or a file with nothing to look at (a header-only
118///   OBJ, a `/World`-only USD stage), so it is [`ExportError::NoRenderGeometry`].
119///   A model that has only non-geometric data is empty here.
120/// * **Data** (CSV, JSON, JSON-LD, IFCX, STEP, BOS, HBJSON, DFJSON): a row,
121///   node, entity or room. Zero of them is still a valid empty document (a
122///   header-only table, an empty `@graph`, a model without rooms), so these
123///   never call this; they report the count instead (the TS `count*`
124///   helpers, `HbjsonStats::rooms`, `StepStats`), and a caller that wants a
125///   non-empty result checks that count.
126///
127/// The cross-language vectors are `tests/fixtures/empty_export_vectors.json`.
128pub(crate) fn require_render_geometry(meshes_written: usize) -> Result<(), ExportError> {
129    if meshes_written == 0 {
130        return Err(ExportError::NoRenderGeometry);
131    }
132    Ok(())
133}