ifc_lite_landxml/
model.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
5use serde::{Deserialize, Serialize};
6use std::{collections::BTreeMap, fmt};
7
8pub type LandXmlProperties = BTreeMap<String, String>;
9
10/// Stable, machine-readable reason for an ingestion refusal or invalid source.
11#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub enum LandXmlDiagnosticCode {
13    InputTooLarge,
14    DtdForbidden,
15    EntityForbidden,
16    LimitExceeded,
17    Cancelled,
18    InvalidXml,
19    UnsupportedNamespace,
20    UnsupportedVersion,
21    InvalidSemantic,
22}
23
24impl LandXmlDiagnosticCode {
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            Self::InputTooLarge => "LXML001",
28            Self::DtdForbidden => "LXML002",
29            Self::EntityForbidden => "LXML003",
30            Self::LimitExceeded => "LXML004",
31            Self::Cancelled => "LXML005",
32            Self::InvalidXml => "LXML006",
33            Self::UnsupportedNamespace => "LXML007",
34            Self::UnsupportedVersion => "LXML008",
35            Self::InvalidSemantic => "LXML009",
36        }
37    }
38}
39
40/// Parse failure with a stable code suitable for native/server/wasm mapping.
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct LandXmlError {
43    pub code: LandXmlDiagnosticCode,
44    pub message: String,
45}
46
47impl LandXmlError {
48    pub fn new(code: LandXmlDiagnosticCode, message: impl Into<String>) -> Self {
49        Self {
50            code,
51            message: message.into(),
52        }
53    }
54}
55
56impl fmt::Display for LandXmlError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "{}: {}", self.code.as_str(), self.message)
59    }
60}
61
62impl std::error::Error for LandXmlError {}
63
64/// Capability selected from the root LandXML namespace and version attribute.
65///
66/// The namespace selects the element grammar. A declared version remains
67/// source provenance: known mismatches are accepted with a diagnostic rather
68/// than silently reclassifying the source.
69#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
70#[serde(rename_all = "snake_case")]
71pub enum LandXmlVersionCapability {
72    NotLandXml,
73    /// A 1.0 namespace without a known LandXML version declaration.
74    LandXml10Unsupported,
75    /// A 1.1 namespace without a known LandXML version declaration.
76    LandXml11Unsupported,
77    LandXml10Tin,
78    LandXml11Tin,
79    LandXml12Tin,
80    LandXml10VersionMismatch,
81    LandXml11VersionMismatch,
82    LandXml12VersionMismatch,
83    /// A 1.2 namespace without a known LandXML version declaration.
84    LandXml12Unsupported,
85}
86
87impl LandXmlVersionCapability {
88    pub const fn supports_tin_ingestion(self) -> bool {
89        matches!(
90            self,
91            Self::LandXml10Tin
92                | Self::LandXml11Tin
93                | Self::LandXml12Tin
94                | Self::LandXml10VersionMismatch
95                | Self::LandXml11VersionMismatch
96                | Self::LandXml12VersionMismatch
97        )
98    }
99
100    pub const fn schema(self) -> Option<&'static str> {
101        match self {
102            Self::LandXml10Tin | Self::LandXml10VersionMismatch => Some("LandXML-1.0"),
103            Self::LandXml11Tin | Self::LandXml11VersionMismatch => Some("LandXML-1.1"),
104            Self::LandXml12Tin | Self::LandXml12VersionMismatch => Some("LandXML-1.2"),
105            Self::NotLandXml
106            | Self::LandXml10Unsupported
107            | Self::LandXml11Unsupported
108            | Self::LandXml12Unsupported => None,
109        }
110    }
111
112    pub const fn has_compatibility_version_mismatch(self) -> bool {
113        matches!(
114            self,
115            Self::LandXml10VersionMismatch
116                | Self::LandXml11VersionMismatch
117                | Self::LandXml12VersionMismatch
118        )
119    }
120}
121
122/// Deterministic source identifier; it is not derived from any output mesh.
123#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
124#[serde(transparent)]
125pub struct LandXmlSourceId(pub String);
126
127#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
128pub struct LandXmlPoint {
129    /// Stable semantic identity, never a renderer mesh or vertex index.
130    pub source_id: LandXmlSourceId,
131    pub id: String,
132    pub northing: f64,
133    pub easting: f64,
134    pub elevation: f64,
135}
136
137/// One canonical planar vertex used by generated constrained terrain.
138///
139/// The source records remain in [`LandXmlSurface::points`] (and their original
140/// overlay/source-data collections).  This mapping records which of those
141/// records collapsed to the one planar vertex used by generated faces without
142/// discarding a producer's duplicate identifiers.
143#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
144pub struct LandXmlCanonicalVertex {
145    pub id: String,
146    pub northing: f64,
147    pub easting: f64,
148    pub elevation: f64,
149    pub contributor_source_ids: Vec<LandXmlSourceId>,
150}
151
152/// Coordinates from `Surface/SourceData/DataPoints`, separate from face ids.
153#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
154pub struct LandXmlSourcePoint {
155    pub source_id: LandXmlSourceId,
156    pub ordinal: usize,
157    pub source_path: String,
158    pub coordinate_dimension: u8,
159    pub coordinates: Vec<f64>,
160}
161
162/// Source topology classification declared by a `Surface/Definition`.
163///
164/// It deliberately describes the source, rather than promising that an
165/// adapter can render it.  In particular, a GRID or volume surface is kept as
166/// source data and is never guessed into a TIN.
167#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
168#[serde(rename_all = "snake_case")]
169pub enum LandXmlSurfaceKind {
170    Tin,
171    Grid,
172    Volume,
173    Other,
174}
175
176/// Honest presentation state for one source surface.
177#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
178#[serde(rename_all = "snake_case")]
179pub enum LandXmlRenderState {
180    /// TIN source faces are available to a rendering adapter.
181    Rendered,
182    /// The source is retained but this crate has no topology adapter for it.
183    PreservedOnly,
184    /// The source declaration is known to be unsupported.
185    Unsupported,
186}
187
188/// Whether terrain faces came from the producer or the constrained adapter.
189#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
190#[serde(rename_all = "snake_case")]
191pub enum LandXmlTopologyOrigin {
192    AuthoredFaces,
193    ConstrainedTriangulation,
194    PreservedOnly,
195}
196
197/// Stable refusal category for optional constrained terrain adaptation.
198#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
199#[serde(rename_all = "snake_case")]
200pub enum LandXmlTerrainDiagnosticCode {
201    MissingOuterBoundary,
202    UnsupportedBoundarySemantics,
203    UnsupportedBreaklineSemantics,
204    MissingElevation,
205    ConflictingElevation,
206    IntersectingConstraints,
207    DegenerateConstraints,
208    WorkLimitExceeded,
209    Cancelled,
210}
211
212impl LandXmlTerrainDiagnosticCode {
213    pub const fn as_str(self) -> &'static str {
214        match self {
215            Self::MissingOuterBoundary => "LXMLT001",
216            Self::UnsupportedBoundarySemantics => "LXMLT002",
217            Self::UnsupportedBreaklineSemantics => "LXMLT003",
218            Self::MissingElevation => "LXMLT004",
219            Self::ConflictingElevation => "LXMLT005",
220            Self::IntersectingConstraints => "LXMLT006",
221            Self::DegenerateConstraints => "LXMLT007",
222            Self::WorkLimitExceeded => "LXMLT008",
223            Self::Cancelled => "LXMLT009",
224        }
225    }
226}
227
228/// Source-preserving diagnostic; a refusal never turns into guessed terrain.
229#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230pub struct LandXmlTerrainDiagnostic {
231    pub code: LandXmlTerrainDiagnosticCode,
232    pub message: String,
233}
234
235/// One preserved source line/ring (boundary, breakline, or contour).
236#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
237pub struct LandXmlPolyline {
238    pub source_id: LandXmlSourceId,
239    /// One-based ordinal within its containing overlay category.
240    pub ordinal: usize,
241    pub name: Option<String>,
242    /// Producer-declared subtype (`bndType`, `brkType`, or contour type).
243    pub kind: Option<String>,
244    pub source_path: String,
245    pub properties: LandXmlProperties,
246    /// Two- or three-dimensional coordinates in authored axis order.
247    pub coordinate_dimension: u8,
248    pub points: Vec<Vec<f64>>,
249    /// Stable, ordered ids for vertices authored as coordinate lists. LandXML
250    /// PntList3D has no P reference to preserve, so these are source vertices,
251    /// not guessed terrain point references.
252    pub point_source_ids: Vec<LandXmlSourceId>,
253}
254
255/// A non-LandXML namespace root retained for diagnostics and later adapters.
256#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
257pub struct LandXmlExtension {
258    pub namespace: String,
259    pub local_name: String,
260    pub path: String,
261}
262
263#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
264pub struct LandXmlSurface {
265    pub source_id: LandXmlSourceId,
266    /// One-based source order under `LandXML/Surfaces`.
267    pub ordinal: usize,
268    pub source_path: String,
269    pub properties: LandXmlProperties,
270    pub definition_properties: LandXmlProperties,
271    pub name: String,
272    pub kind: LandXmlSurfaceKind,
273    pub render_state: LandXmlRenderState,
274    pub topology_origin: LandXmlTopologyOrigin,
275    /// Set only when a faceless TIN was deliberately retained rather than
276    /// guessed into unconstrained terrain.
277    pub terrain_diagnostic: Option<LandXmlTerrainDiagnostic>,
278    pub points: Vec<LandXmlPoint>,
279    /// Canonical vertices used only when `topology_origin` is
280    /// `constrained_triangulation`; source point records are never replaced.
281    pub canonical_vertices: Vec<LandXmlCanonicalVertex>,
282    pub source_data_points: Vec<LandXmlSourcePoint>,
283    pub faces: Vec<[String; 3]>,
284    /// Mirrors `faces` by ordinal.  A face's identity remains stable when an
285    /// adapter splits, drops, or reorders meshes for precision.
286    pub face_source_ids: Vec<LandXmlSourceId>,
287    /// Visibility is source topology, aligned to `faces` and `face_source_ids`.
288    /// Hidden faces are preserved but never sent to the rendering adapter.
289    pub face_visibility: Vec<bool>,
290    pub hidden_face_count: usize,
291    pub boundaries: Vec<LandXmlPolyline>,
292    pub breaklines: Vec<LandXmlPolyline>,
293    pub contours: Vec<LandXmlPolyline>,
294}
295
296#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
297pub struct LandXmlUnits {
298    pub linear_unit: String,
299    pub elevation_unit: String,
300    pub linear_scale_to_meters: f64,
301    pub elevation_scale_to_meters: f64,
302    /// #5175: `true` when these units were never declared in the source and
303    /// were instead supplied by the caller as an explicit, audited
304    /// assumption (`LandXmlLimits::assumed_linear_unit`). `false` for every
305    /// unit record read from a source `<Units>` element. A consumer must be
306    /// able to tell the two apart without re-parsing; never collapse this
307    /// into a warning string or drop it on the way to a serialized surface.
308    pub assumed: bool,
309}
310
311/// Source-owned CRS declarations from LandXML 1.2's root CoordinateSystem.
312/// Kept as raw strings: only the viewer adapter may accept explicit EPSG IDs.
313///
314/// `#[non_exhaustive]` so a later CRS attribute is an additive change: the
315/// parser is the only constructor, and outside the crate this record is read,
316/// not built (Rust-only break #10, which added `epsg_code`).
317#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
318#[non_exhaustive]
319pub struct LandXmlCoordinateSystem {
320    pub horizontal_datum: Option<String>,
321    pub vertical_datum: Option<String>,
322    /// LandXML 1.2's own attribute for the CRS's EPSG code (`epsgCode`). It is
323    /// how real producers declare the CRS: Civil 3D 2021/2022 and 3D-Win 6.6.4
324    /// all write it, and none of them writes an EPSG id into `horizontalDatum`
325    /// (#5942 follow-up). Raw, like the other two: never resolved here.
326    #[serde(default)]
327    pub epsg_code: Option<String>,
328    /// LandXML 1.2's `CoordinateSystem@name`: the producer's own label for the
329    /// CRS (3D-Win writes `GK21` beside `epsgCode="3875"`; ifc-lite's IFC
330    /// terrain export writes `IfcProjectedCRS.Name` here). Raw, like the
331    /// others; which of these a consumer names the CRS by is its own policy
332    /// (#7604). Additive under `#[non_exhaustive]`.
333    #[serde(default)]
334    pub name: Option<String>,
335}
336
337#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
338pub struct LandXmlTinDocument {
339    pub format: String,
340    pub schema: String,
341    pub capabilities: LandXmlCapabilities,
342    pub version: String,
343    /// Missing only in a geometry-free source that has no unit declaration.
344    /// The document remains inspectable; consumers must refuse meter-based
345    /// operations until a later georeferencing adapter supplies units.
346    pub units: Option<LandXmlUnits>,
347    pub coordinate_system: Option<LandXmlCoordinateSystem>,
348    pub surfaces: Vec<LandXmlSurface>,
349    pub extensions: Vec<LandXmlExtension>,
350    pub warnings: Vec<String>,
351    /// Horizontal alignments are source records, separate from terrain TINs.
352    pub alignments: Vec<crate::LandXmlAlignment>,
353    /// Design and sampled profiles retain their distinct LandXML source kinds.
354    pub profiles: Vec<crate::LandXmlProfile>,
355    pub cross_sections: Vec<crate::LandXmlCrossSection>,
356    pub cross_section_surfaces: Vec<crate::LandXmlCrossSectionSurface>,
357    pub roadways: Vec<crate::LandXmlRoadway>,
358    pub capability_diagnostics: Vec<crate::LandXmlCapabilityDiagnostic>,
359    pub preserved_only_extensions: Vec<crate::LandXmlPreservedOnlyExtension>,
360    /// Pipe networks are retained in the same canonical source document as
361    /// terrain. Their absence means this LandXML source did not declare one.
362    pub pipe_networks: Option<crate::LandXmlPipeNetworkDocument>,
363}
364
365impl LandXmlTinDocument {
366    /// Split finalized terrain metadata into move-owned stream records.
367    pub(crate) fn into_stream_parts(self) -> crate::stream::metadata::TerrainStreamParts {
368        crate::stream::metadata::TerrainStreamParts::new(self)
369    }
370}
371
372#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
373pub struct LandXmlCapabilities {
374    pub renderable_tin: bool,
375    pub preserved_only_surfaces: usize,
376    pub unknown_extensions: usize,
377}