1use avila_mesh::*;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::io::Write;
9
10pub type Result<T> = std::result::Result<T, GltfError>;
11
12#[derive(Debug, thiserror::Error)]
13pub enum GltfError {
14 #[error("Export error: {0}")]
15 ExportError(String),
16 #[error("Serialization error: {0}")]
17 SerializationError(#[from] serde_json::Error),
18 #[error("IO error: {0}")]
19 IoError(#[from] std::io::Error),
20}
21
22pub struct GltfExporter;
23
24#[derive(Debug, Clone)]
25pub struct ExportOptions {
26 pub asset_name: String,
27 pub include_normals: bool,
28 pub include_uvs: bool,
29}
30
31impl Default for ExportOptions {
32 fn default() -> Self {
33 Self {
34 asset_name: "Avila BIM".into(),
35 include_normals: true,
36 include_uvs: true,
37 }
38 }
39}
40
41impl GltfExporter {
42 pub fn new() -> Self {
43 Self
44 }
45
46 pub fn export_glb(&self, scene: &Scene, opts: &ExportOptions) -> Result<Vec<u8>> {
48 let (json, bin) = self.export_parts(scene, opts)?;
49
50 let mut glb = Vec::new();
51
52 glb.write_all(&0x46546C67u32.to_le_bytes())?; glb.write_all(&2u32.to_le_bytes())?; let json_bytes = json.as_bytes();
57 let json_padding = (4 - (json_bytes.len() % 4)) % 4;
58 let bin_padding = (4 - (bin.len() % 4)) % 4;
59
60 let total = 12 + 8 + json_bytes.len() + json_padding + 8 + bin.len() + bin_padding;
61 glb.write_all(&(total as u32).to_le_bytes())?;
62
63 glb.write_all(&((json_bytes.len() + json_padding) as u32).to_le_bytes())?;
65 glb.write_all(&0x4E4F534Au32.to_le_bytes())?; glb.write_all(json_bytes)?;
67 glb.write_all(&vec![0x20u8; json_padding])?;
68
69 if !bin.is_empty() {
71 glb.write_all(&((bin.len() + bin_padding) as u32).to_le_bytes())?;
72 glb.write_all(&0x004E4942u32.to_le_bytes())?; glb.write_all(&bin)?;
74 glb.write_all(&vec![0u8; bin_padding])?;
75 }
76
77 Ok(glb)
78 }
79
80 fn export_parts(&self, scene: &Scene, opts: &ExportOptions) -> Result<(String, Vec<u8>)> {
81 let mut gltf = GltfRoot {
82 asset: GltfAsset {
83 version: "2.0".into(),
84 generator: Some("avila-gltf".into()),
85 },
86 scene: Some(0),
87 scenes: vec![GltfScene {
88 nodes: (0..scene.meshes.len()).map(|i| i as u32).collect(),
89 }],
90 nodes: Vec::new(),
91 meshes: Vec::new(),
92 materials: Vec::new(),
93 buffers: Vec::new(),
94 buffer_views: Vec::new(),
95 accessors: Vec::new(),
96 };
97
98 let mut bin_data = Vec::new();
99 let mut material_map = HashMap::new();
100
101 for (mat_id, material) in &scene.materials {
103 let idx = gltf.materials.len() as u32;
104 material_map.insert(mat_id.clone(), idx);
105 gltf.materials.push(material_to_gltf(material));
106 }
107
108 for mesh in &scene.meshes {
110 let material_idx = mesh.material_id.as_ref()
111 .and_then(|id| material_map.get(id).copied());
112
113 let gltf_mesh = self.mesh_to_gltf(
114 mesh,
115 &mut bin_data,
116 &mut gltf.buffer_views,
117 &mut gltf.accessors,
118 material_idx,
119 opts
120 )?;
121
122 let mesh_idx = gltf.meshes.len() as u32;
123 gltf.meshes.push(gltf_mesh);
124
125 gltf.nodes.push(GltfNode {
126 mesh: Some(mesh_idx),
127 matrix: None,
128 });
129 }
130
131 if !bin_data.is_empty() {
132 gltf.buffers.push(GltfBuffer {
133 byte_length: bin_data.len() as u32,
134 uri: None,
135 });
136 }
137
138 let json = serde_json::to_string_pretty(&gltf)?;
139 Ok((json, bin_data))
140 }
141
142 fn mesh_to_gltf(
143 &self,
144 mesh: &Mesh,
145 bin_data: &mut Vec<u8>,
146 buffer_views: &mut Vec<GltfBufferView>,
147 accessors: &mut Vec<GltfAccessor>,
148 material_idx: Option<u32>,
149 opts: &ExportOptions,
150 ) -> Result<GltfMesh> {
151 let mut attributes = HashMap::new();
152
153 let buffers = mesh.to_buffers();
155
156 let pos_accessor = self.add_buffer(
158 bin_data,
159 buffer_views,
160 accessors,
161 &buffers.positions,
162 5126, "VEC3",
164 buffers.positions.len() / 3,
165 true,
166 )?;
167 attributes.insert("POSITION".into(), pos_accessor);
168
169 if opts.include_normals && !buffers.normals.is_empty() {
171 let norm_accessor = self.add_buffer(
172 bin_data,
173 buffer_views,
174 accessors,
175 &buffers.normals,
176 5126,
177 "VEC3",
178 buffers.normals.len() / 3,
179 false,
180 )?;
181 attributes.insert("NORMAL".into(), norm_accessor);
182 }
183
184 if opts.include_uvs && !buffers.uvs.is_empty() {
186 let uv_accessor = self.add_buffer(
187 bin_data,
188 buffer_views,
189 accessors,
190 &buffers.uvs,
191 5126,
192 "VEC2",
193 buffers.uvs.len() / 2,
194 false,
195 )?;
196 attributes.insert("TEXCOORD_0".into(), uv_accessor);
197 }
198
199 let indices_accessor = self.add_indices_buffer(
201 &buffers.indices,
202 bin_data,
203 buffer_views,
204 accessors,
205 )?;
206
207 Ok(GltfMesh {
208 primitives: vec![GltfPrimitive {
209 attributes,
210 indices: Some(indices_accessor),
211 material: material_idx,
212 mode: 4, }],
214 })
215 }
216
217 fn add_buffer(
218 &self,
219 bin_data: &mut Vec<u8>,
220 buffer_views: &mut Vec<GltfBufferView>,
221 accessors: &mut Vec<GltfAccessor>,
222 data: &[f32],
223 component_type: u32,
224 accessor_type: &str,
225 count: usize,
226 calc_bounds: bool,
227 ) -> Result<u32> {
228 let byte_offset = bin_data.len() as u32;
229
230 for &value in data {
231 bin_data.write_all(&value.to_le_bytes())?;
232 }
233
234 let byte_length = (bin_data.len() - byte_offset as usize) as u32;
235
236 let padding = (4 - (bin_data.len() % 4)) % 4;
238 bin_data.extend_from_slice(&vec![0u8; padding]);
239
240 let buffer_view_idx = buffer_views.len() as u32;
241 buffer_views.push(GltfBufferView {
242 buffer: 0,
243 byte_offset,
244 byte_length,
245 target: Some(34962), });
247
248 let (min, max) = if calc_bounds && accessor_type == "VEC3" {
249 calc_bounds_vec3(data)
250 } else {
251 (None, None)
252 };
253
254 let accessor_idx = accessors.len() as u32;
255 accessors.push(GltfAccessor {
256 buffer_view: Some(buffer_view_idx),
257 byte_offset: 0,
258 component_type,
259 count,
260 accessor_type: accessor_type.into(),
261 min,
262 max,
263 });
264
265 Ok(accessor_idx)
266 }
267
268 fn add_indices_buffer(
269 &self,
270 indices: &[u32],
271 bin_data: &mut Vec<u8>,
272 buffer_views: &mut Vec<GltfBufferView>,
273 accessors: &mut Vec<GltfAccessor>,
274 ) -> Result<u32> {
275 let byte_offset = bin_data.len() as u32;
276 let max_index = *indices.iter().max().unwrap_or(&0);
277 let use_u16 = max_index < 65536;
278
279 if use_u16 {
280 for &idx in indices {
281 bin_data.write_all(&(idx as u16).to_le_bytes())?;
282 }
283 } else {
284 for &idx in indices {
285 bin_data.write_all(&idx.to_le_bytes())?;
286 }
287 }
288
289 let byte_length = (bin_data.len() - byte_offset as usize) as u32;
290
291 let padding = (4 - (bin_data.len() % 4)) % 4;
293 bin_data.extend_from_slice(&vec![0u8; padding]);
294
295 let buffer_view_idx = buffer_views.len() as u32;
296 buffer_views.push(GltfBufferView {
297 buffer: 0,
298 byte_offset,
299 byte_length,
300 target: Some(34963), });
302
303 let accessor_idx = accessors.len() as u32;
304 accessors.push(GltfAccessor {
305 buffer_view: Some(buffer_view_idx),
306 byte_offset: 0,
307 component_type: if use_u16 { 5123 } else { 5125 },
308 count: indices.len(),
309 accessor_type: "SCALAR".into(),
310 min: None,
311 max: None,
312 });
313
314 Ok(accessor_idx)
315 }
316}
317
318impl Default for GltfExporter {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324fn calc_bounds_vec3(vertices: &[f32]) -> (Option<Vec<f32>>, Option<Vec<f32>>) {
325 if vertices.is_empty() {
326 return (None, None);
327 }
328
329 let mut min = [f32::INFINITY; 3];
330 let mut max = [f32::NEG_INFINITY; 3];
331
332 for chunk in vertices.chunks(3) {
333 for i in 0..3 {
334 min[i] = min[i].min(chunk[i]);
335 max[i] = max[i].max(chunk[i]);
336 }
337 }
338
339 (Some(min.to_vec()), Some(max.to_vec()))
340}
341
342fn material_to_gltf(mat: &PbrMaterial) -> GltfMaterial {
343 GltfMaterial {
344 name: Some(mat.name.clone()),
345 pbr_metallic_roughness: PbrMetallicRoughness {
346 base_color_factor: mat.base_color_factor,
347 metallic_factor: mat.metallic_factor,
348 roughness_factor: mat.roughness_factor,
349 },
350 double_sided: Some(mat.double_sided),
351 }
352}
353
354#[derive(Debug, Serialize, Deserialize)]
359struct GltfRoot {
360 asset: GltfAsset,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 scene: Option<u32>,
363 scenes: Vec<GltfScene>,
364 nodes: Vec<GltfNode>,
365 meshes: Vec<GltfMesh>,
366 #[serde(skip_serializing_if = "Vec::is_empty")]
367 materials: Vec<GltfMaterial>,
368 buffers: Vec<GltfBuffer>,
369 #[serde(rename = "bufferViews")]
370 buffer_views: Vec<GltfBufferView>,
371 accessors: Vec<GltfAccessor>,
372}
373
374#[derive(Debug, Serialize, Deserialize)]
375struct GltfAsset {
376 version: String,
377 #[serde(skip_serializing_if = "Option::is_none")]
378 generator: Option<String>,
379}
380
381#[derive(Debug, Serialize, Deserialize)]
382struct GltfScene {
383 nodes: Vec<u32>,
384}
385
386#[derive(Debug, Serialize, Deserialize)]
387struct GltfNode {
388 #[serde(skip_serializing_if = "Option::is_none")]
389 mesh: Option<u32>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 matrix: Option<[f32; 16]>,
392}
393
394#[derive(Debug, Serialize, Deserialize)]
395struct GltfMesh {
396 primitives: Vec<GltfPrimitive>,
397}
398
399#[derive(Debug, Serialize, Deserialize)]
400struct GltfPrimitive {
401 attributes: HashMap<String, u32>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 indices: Option<u32>,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 material: Option<u32>,
406 mode: u32,
407}
408
409#[derive(Debug, Serialize, Deserialize)]
410struct GltfMaterial {
411 #[serde(skip_serializing_if = "Option::is_none")]
412 name: Option<String>,
413 #[serde(rename = "pbrMetallicRoughness")]
414 pbr_metallic_roughness: PbrMetallicRoughness,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 double_sided: Option<bool>,
417}
418
419#[derive(Debug, Serialize, Deserialize)]
420struct PbrMetallicRoughness {
421 #[serde(rename = "baseColorFactor")]
422 base_color_factor: [f32; 4],
423 #[serde(rename = "metallicFactor")]
424 metallic_factor: f32,
425 #[serde(rename = "roughnessFactor")]
426 roughness_factor: f32,
427}
428
429#[derive(Debug, Serialize, Deserialize)]
430struct GltfBuffer {
431 #[serde(rename = "byteLength")]
432 byte_length: u32,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 uri: Option<String>,
435}
436
437#[derive(Debug, Serialize, Deserialize)]
438struct GltfBufferView {
439 buffer: u32,
440 #[serde(rename = "byteOffset")]
441 byte_offset: u32,
442 #[serde(rename = "byteLength")]
443 byte_length: u32,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 target: Option<u32>,
446}
447
448#[derive(Debug, Serialize, Deserialize)]
449struct GltfAccessor {
450 #[serde(rename = "bufferView")]
451 #[serde(skip_serializing_if = "Option::is_none")]
452 buffer_view: Option<u32>,
453 #[serde(rename = "byteOffset")]
454 byte_offset: u32,
455 #[serde(rename = "componentType")]
456 component_type: u32,
457 count: usize,
458 #[serde(rename = "type")]
459 accessor_type: String,
460 #[serde(skip_serializing_if = "Option::is_none")]
461 min: Option<Vec<f32>>,
462 #[serde(skip_serializing_if = "Option::is_none")]
463 max: Option<Vec<f32>>,
464}
465
466#[cfg(test)]
471mod tests {
472 use super::*;
473 use avila_mesh::primitives;
474
475 #[test]
476 fn test_export_cube() {
477 let mut scene = Scene::new();
478 let cube = primitives::cube(2.0);
479 scene.add_mesh(cube);
480
481 let exporter = GltfExporter::new();
482 let glb = exporter.export_glb(&scene, &ExportOptions::default()).unwrap();
483
484 assert_eq!(&glb[0..4], b"glTF");
485 assert!(glb.len() > 100);
486 }
487
488 #[test]
489 fn test_export_with_material() {
490 let mut scene = Scene::new();
491 let mut cube = primitives::cube(1.0);
492 cube.material_id = Some("concrete".into());
493 scene.add_mesh(cube);
494
495 let material = PbrMaterial::from_ifc_material("concrete", "Concreto");
496 scene.add_material(material);
497
498 let exporter = GltfExporter::new();
499 let glb = exporter.export_glb(&scene, &ExportOptions::default()).unwrap();
500
501 assert!(glb.len() > 100);
502 }
503}