avila_gltf/
lib.rs

1//! # avila-gltf
2//!
3//! Exporter glTF 2.0 / GLB **100% Rust nativo - DO ZERO**.
4
5use 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    /// Exporta cena completa para GLB (binĂ¡rio glTF 2.0)
47    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        // Header (12 bytes)
53        glb.write_all(&0x46546C67u32.to_le_bytes())?; // magic "glTF"
54        glb.write_all(&2u32.to_le_bytes())?; // version 2
55
56        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        // JSON chunk
64        glb.write_all(&((json_bytes.len() + json_padding) as u32).to_le_bytes())?;
65        glb.write_all(&0x4E4F534Au32.to_le_bytes())?; // "JSON"
66        glb.write_all(json_bytes)?;
67        glb.write_all(&vec![0x20u8; json_padding])?;
68
69        // BIN chunk
70        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())?; // "BIN"
73            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        // Materiais
102        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        // Meshes
109        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        // Converter para buffers
154        let buffers = mesh.to_buffers();
155
156        // POSITION
157        let pos_accessor = self.add_buffer(
158            bin_data,
159            buffer_views,
160            accessors,
161            &buffers.positions,
162            5126, // FLOAT
163            "VEC3",
164            buffers.positions.len() / 3,
165            true,
166        )?;
167        attributes.insert("POSITION".into(), pos_accessor);
168
169        // NORMAL
170        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        // TEXCOORD_0
185        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        // INDICES
200        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, // TRIANGLES
213            }],
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        // Padding
237        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), // ARRAY_BUFFER
246        });
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        // Padding
292        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), // ELEMENT_ARRAY_BUFFER
301        });
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// ============================================================================
355// ESTRUTURAS glTF 2.0
356// ============================================================================
357
358#[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// ============================================================================
467// TESTES
468// ============================================================================
469
470#[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}