ifc_lite_processing/types/mesh.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//! Mesh data types for serialization.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10/// A decoded RGBA8 surface texture attached to a mesh (issue #961).
11/// Decoded entirely in Rust (`IfcBlobTexture` PNG / `IfcPixelTexture` raw); the
12/// browser only uploads `rgba` to a GPU texture — no image logic in TS.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MeshTextureData {
15 /// `width * height * 4` bytes, row-major, top-down, straight alpha.
16 pub rgba: Vec<u8>,
17 pub width: u32,
18 pub height: u32,
19 /// Sampler wrap from `IfcSurfaceTexture.RepeatS/RepeatT`.
20 pub repeat_s: bool,
21 pub repeat_t: bool,
22}
23
24/// Individual mesh data with geometry and metadata.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct MeshData {
27 /// Express ID of the IFC element.
28 pub express_id: u32,
29 /// IFC type name (e.g., "IfcWall").
30 pub ifc_type: String,
31 /// IFC GlobalId (Root attribute #0) when available.
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub global_id: Option<String>,
34 /// IFC Name (Root/Object attribute #2) when available.
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub name: Option<String>,
37 /// IFC presentation layer assignment name when available.
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub presentation_layer: Option<String>,
40 /// Vertex positions (x, y, z triplets).
41 pub positions: Vec<f32>,
42 /// Vertex normals (x, y, z triplets).
43 pub normals: Vec<f32>,
44 /// Triangle indices.
45 pub indices: Vec<u32>,
46 /// RGBA color [r, g, b, a] in 0-1 range.
47 pub color: [f32; 4],
48 /// Optional material/style name resolved from per-item IFC styling.
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub material_name: Option<String>,
51 /// Optional source geometry item id for submesh outputs.
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub geometry_item_id: Option<u32>,
54 /// Optional IFC property set values keyed by IFC property names.
55 /// Primarily attached for IfcSpace/IfcZone so downstream tools can build room attribute UIs.
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub properties: Option<BTreeMap<String, String>>,
58 /// Per-vertex texture coordinates (u, v pairs, 1:1 with `positions`),
59 /// present only for textured meshes (issue #961).
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub uvs: Option<Vec<f32>>,
62 /// Decoded surface texture, present only for textured meshes (#961).
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub texture: Option<MeshTextureData>,
65 /// Provenance of the geometry for the viewer's Model/Types switch (#957):
66 /// 0 = ordinary occurrence, 1 = orphan type-product RepresentationMap (no
67 /// occurrence instantiates it), 2 = instanced type-product map (the type
68 /// library shape; its occurrences already draw the real geometry).
69 /// Serde-default so existing JSON payloads and disk caches stay readable;
70 /// skipped when 0 so ordinary meshes serialize byte-identically.
71 #[serde(default, skip_serializing_if = "geometry_class_is_occurrence")]
72 pub geometry_class: u8,
73 /// Per-mesh local origin (world/RTC frame, f64). `positions` are stored
74 /// RELATIVE to this — the world position of a vertex is `origin + position` —
75 /// so building/georef-scale placement never collapses adjacent vertices to
76 /// bit-identical f32. The renderer applies it as a per-mesh translation
77 /// (camera-relative). `[0, 0, 0]` ⇒ positions are absolute (legacy/local).
78 /// Serde-default + skip-when-zero so existing payloads/caches stay readable
79 /// and local meshes serialize byte-identically.
80 #[serde(default, skip_serializing_if = "origin_is_zero")]
81 pub origin: [f64; 3],
82}
83
84fn geometry_class_is_occurrence(class: &u8) -> bool {
85 *class == 0
86}
87
88fn origin_is_zero(origin: &[f64; 3]) -> bool {
89 origin[0] == 0.0 && origin[1] == 0.0 && origin[2] == 0.0
90}
91
92impl MeshData {
93 /// Create a new MeshData from geometry components.
94 pub fn new(
95 express_id: u32,
96 ifc_type: String,
97 positions: Vec<f32>,
98 normals: Vec<f32>,
99 indices: Vec<u32>,
100 color: [f32; 4],
101 ) -> Self {
102 Self {
103 express_id,
104 ifc_type,
105 global_id: None,
106 name: None,
107 presentation_layer: None,
108 positions,
109 normals,
110 indices,
111 color,
112 material_name: None,
113 geometry_item_id: None,
114 properties: None,
115 uvs: None,
116 texture: None,
117 geometry_class: 0,
118 origin: [0.0; 3],
119 }
120 }
121
122 /// Tag the geometry's provenance for the Model/Types view switch (#957).
123 pub fn with_geometry_class(mut self, geometry_class: u8) -> Self {
124 self.geometry_class = geometry_class;
125 self
126 }
127
128 /// Set the per-mesh local origin (positions are relative to it).
129 pub fn with_origin(mut self, origin: [f64; 3]) -> Self {
130 self.origin = origin;
131 self
132 }
133
134 /// Attach per-vertex UVs + a decoded surface texture (issue #961).
135 /// `uvs` must be 1:1 with `positions` (2 floats per vertex).
136 pub fn with_texture(mut self, uvs: Vec<f32>, texture: MeshTextureData) -> Self {
137 self.uvs = Some(uvs);
138 self.texture = Some(texture);
139 self
140 }
141
142 /// Set element-level IFC metadata.
143 pub fn with_element_metadata(
144 mut self,
145 global_id: Option<String>,
146 name: Option<String>,
147 presentation_layer: Option<String>,
148 ) -> Self {
149 self.global_id = global_id;
150 self.name = name;
151 self.presentation_layer = presentation_layer;
152 self
153 }
154
155 /// Set material name and source geometry item id metadata.
156 pub fn with_style_metadata(
157 mut self,
158 material_name: Option<String>,
159 geometry_item_id: Option<u32>,
160 ) -> Self {
161 self.material_name = material_name;
162 self.geometry_item_id = geometry_item_id;
163 self
164 }
165
166 /// Attach optional IFC property set values.
167 pub fn with_properties(mut self, properties: Option<BTreeMap<String, String>>) -> Self {
168 self.properties = properties;
169 self
170 }
171
172 /// Get the number of vertices.
173 pub fn vertex_count(&self) -> usize {
174 self.positions.len() / 3
175 }
176
177 /// Get the number of triangles.
178 pub fn triangle_count(&self) -> usize {
179 self.indices.len() / 3
180 }
181
182 /// Check if the mesh is empty.
183 pub fn is_empty(&self) -> bool {
184 self.positions.is_empty() || self.indices.is_empty()
185 }
186}