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}