Skip to main content

draco_io/
fbx_reader.rs

1//! FBX binary format reader for meshes.
2//!
3//! Supports reading:
4//! - Binary FBX format (versions 7.x)
5//! - Vertex positions
6//! - Polygon/face indices
7//!
8//! FBX layer elements such as normals, colors, UVs, materials, animation, and
9//! skinning are not mapped yet. They require explicit per-layer mapping support
10//! before they can be represented safely as Draco attributes.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use draco_io::fbx_reader::FbxReader;
16//! use draco_io::Reader;
17//!
18//! let mut reader = FbxReader::open("model.fbx")?;
19//! let meshes = reader.read_meshes()?;
20//! for mesh in meshes {
21//!     println!("Mesh has {} vertices", mesh.num_points());
22//! }
23//! # Ok::<(), std::io::Error>(())
24//! ```
25
26use std::fs::{self, File};
27use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom};
28use std::path::Path;
29
30use draco_core::draco_types::DataType;
31use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
32use draco_core::geometry_indices::{FaceIndex, PointIndex};
33use draco_core::mesh::Mesh;
34
35use crate::traits::ReadFromBytes;
36
37/// FBX file magic: "Kaydara FBX Binary  \0"
38const FBX_MAGIC: &[u8; 21] = b"Kaydara FBX Binary  \0";
39
40/// FBX reader for binary FBX files.
41pub struct FbxReader<R: Read + Seek = BufReader<File>> {
42    reader: R,
43    version: u32,
44}
45
46/// FBX reader backed by in-memory bytes.
47pub type FbxMemoryReader = FbxReader<Cursor<Vec<u8>>>;
48
49/// An FBX node with properties and children.
50#[derive(Debug, Clone)]
51pub struct FbxNode {
52    /// Node name, such as `Objects`, `Geometry`, `Model`, or `Connections`.
53    pub name: String,
54    /// Properties stored directly on this node.
55    pub properties: Vec<FbxProperty>,
56    /// Child nodes nested under this node.
57    pub children: Vec<FbxNode>,
58}
59
60/// FBX property value.
61#[derive(Debug, Clone)]
62pub enum FbxProperty {
63    /// Boolean property.
64    Bool(bool),
65    /// 16-bit signed integer property.
66    I16(i16),
67    /// 32-bit signed integer property.
68    I32(i32),
69    /// 64-bit signed integer property.
70    I64(i64),
71    /// 32-bit floating-point property.
72    F32(f32),
73    /// 64-bit floating-point property.
74    F64(f64),
75    /// UTF-8-ish string property decoded lossily from FBX bytes.
76    String(String),
77    /// Raw binary property.
78    Raw(Vec<u8>),
79    /// Boolean array property.
80    BoolArray(Vec<bool>),
81    /// 32-bit signed integer array property.
82    I32Array(Vec<i32>),
83    /// 64-bit signed integer array property.
84    I64Array(Vec<i64>),
85    /// 32-bit floating-point array property.
86    F32Array(Vec<f32>),
87    /// 64-bit floating-point array property.
88    F64Array(Vec<f64>),
89}
90
91impl FbxReader<BufReader<File>> {
92    /// Open an FBX file from a path.
93    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
94        let file = File::open(path)?;
95        let reader = BufReader::new(file);
96        Self::new(reader)
97    }
98}
99
100impl FbxReader<Cursor<Vec<u8>>> {
101    /// Create an FBX reader from in-memory bytes.
102    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> io::Result<Self> {
103        Self::new(Cursor::new(bytes.into()))
104    }
105
106    /// Read all meshes directly from in-memory bytes.
107    pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Vec<Mesh>> {
108        let mut reader = Self::from_bytes(bytes.to_vec())?;
109        reader.read_meshes()
110    }
111}
112
113// Implement the Reader trait for the concrete BufReader<File> specialization.
114impl crate::traits::Reader for FbxReader<BufReader<File>> {
115    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
116        FbxReader::open(path)
117    }
118
119    fn read_meshes(&mut self) -> io::Result<Vec<draco_core::mesh::Mesh>> {
120        // Call the inherent method which already reads all meshes.
121        // Use fully qualified syntax to avoid recursion.
122        FbxReader::read_meshes(self)
123    }
124}
125
126impl crate::traits::Reader for FbxReader<Cursor<Vec<u8>>> {
127    fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
128        Self::from_bytes(fs::read(path)?)
129    }
130
131    fn read_meshes(&mut self) -> io::Result<Vec<draco_core::mesh::Mesh>> {
132        FbxReader::read_meshes(self)
133    }
134}
135
136impl crate::scene::SceneReader for FbxReader<BufReader<File>> {
137    fn read_scene(&mut self) -> io::Result<crate::scene::Scene> {
138        let nodes = self.read_nodes()?;
139
140        // Build maps: id -> Model/Geometry nodes
141        use std::collections::HashMap;
142        let mut model_map: HashMap<i64, &FbxNode> = HashMap::new();
143        let mut geometry_map: HashMap<i64, &FbxNode> = HashMap::new();
144        let mut connections: Vec<(i64, i64)> = Vec::new(); // (child, parent)
145
146        for n in &nodes {
147            if n.name == "Objects" {
148                for child in &n.children {
149                    match child.name.as_str() {
150                        "Model" => {
151                            if let Some(FbxProperty::I64(id)) = child.properties.first() {
152                                model_map.insert(*id, child);
153                            }
154                        }
155                        "Geometry" => {
156                            if let Some(FbxProperty::I64(id)) = child.properties.first() {
157                                geometry_map.insert(*id, child);
158                            }
159                        }
160                        _ => {}
161                    }
162                }
163            } else if n.name == "Connections" {
164                for c in &n.children {
165                    // Expect properties: String("OO"), I64(child), I64(parent)
166                    if let (
167                        Some(FbxProperty::String(_kind)),
168                        Some(FbxProperty::I64(child)),
169                        Some(FbxProperty::I64(parent)),
170                    ) = (
171                        c.properties.first(),
172                        c.properties.get(1),
173                        c.properties.get(2),
174                    ) {
175                        connections.push((*child, *parent));
176                    }
177                }
178            }
179        }
180
181        // Build parent map for models
182        let mut model_children: HashMap<i64, Vec<i64>> = HashMap::new();
183        for (child, parent) in connections.iter() {
184            if model_map.contains_key(child) || model_map.contains_key(parent) {
185                model_children.entry(*parent).or_default().push(*child);
186            }
187        }
188
189        // Helper to parse transform from Model node's Properties70
190        fn parse_transform(node: &FbxNode) -> Option<crate::scene::Transform> {
191            let mut translation = None;
192            let mut rotation = None;
193            let mut scaling = None;
194
195            for child in &node.children {
196                if child.name == "Properties70" {
197                    for prop in &child.children {
198                        // property nodes often have first property as name string
199                        if let Some(crate::fbx_reader::FbxProperty::String(name)) =
200                            prop.properties.first()
201                        {
202                            if name.contains("Lcl Translation") {
203                                // find F64Array in properties
204                                for p in &prop.properties {
205                                    if let crate::fbx_reader::FbxProperty::F64Array(arr) = p {
206                                        if arr.len() >= 3 {
207                                            translation =
208                                                Some([arr[0] as f32, arr[1] as f32, arr[2] as f32]);
209                                        }
210                                    }
211                                }
212                            }
213                            if name.contains("Lcl Rotation") {
214                                for p in &prop.properties {
215                                    if let crate::fbx_reader::FbxProperty::F64Array(arr) = p {
216                                        if arr.len() >= 3 {
217                                            rotation =
218                                                Some([arr[0] as f32, arr[1] as f32, arr[2] as f32]);
219                                        }
220                                    }
221                                }
222                            }
223                            if name.contains("Lcl Scaling") {
224                                for p in &prop.properties {
225                                    if let crate::fbx_reader::FbxProperty::F64Array(arr) = p {
226                                        if arr.len() >= 3 {
227                                            scaling =
228                                                Some([arr[0] as f32, arr[1] as f32, arr[2] as f32]);
229                                        }
230                                    }
231                                }
232                            }
233                        }
234                    }
235                }
236            }
237
238            if translation.is_none() && rotation.is_none() && scaling.is_none() {
239                return None;
240            }
241
242            // Build simple 4x4 matrix from TRS (rotation in degrees XYZ)
243            let t = translation.unwrap_or([0.0, 0.0, 0.0]);
244            let r_deg = rotation.unwrap_or([0.0, 0.0, 0.0]);
245            let s = scaling.unwrap_or([1.0, 1.0, 1.0]);
246
247            let rx = r_deg[0].to_radians();
248            let ry = r_deg[1].to_radians();
249            let rz = r_deg[2].to_radians();
250
251            let (sx, cx) = rx.sin_cos();
252            let (sy, cy) = ry.sin_cos();
253            let (sz, cz) = rz.sin_cos();
254
255            // Rotation matrices around X, Y, Z (Rz * Ry * Rx)
256            let m00 = cz * cy;
257            let m01 = cz * sy * sx - sz * cx;
258            let m02 = cz * sy * cx + sz * sx;
259
260            let m10 = sz * cy;
261            let m11 = sz * sy * sx + cz * cx;
262            let m12 = sz * sy * cx - cz * sx;
263
264            let m20 = -sy;
265            let m21 = cy * sx;
266            let m22 = cy * cx;
267
268            let mat = [
269                [m00 * s[0], m01 * s[1], m02 * s[2], 0.0],
270                [m10 * s[0], m11 * s[1], m12 * s[2], 0.0],
271                [m20 * s[0], m21 * s[1], m22 * s[2], 0.0],
272                [t[0], t[1], t[2], 1.0],
273            ];
274
275            Some(crate::scene::Transform { matrix: mat })
276        }
277
278        // Build nodes recursively
279        fn build_model_node(
280            id: i64,
281            model_map: &std::collections::HashMap<i64, &FbxNode>,
282            model_children: &std::collections::HashMap<i64, Vec<i64>>,
283            model_mesh_instances: &std::collections::HashMap<i64, Vec<crate::scene::MeshInstance>>,
284        ) -> crate::scene::SceneNode {
285            let node_src = model_map.get(&id).unwrap();
286            let mut node = crate::scene::SceneNode::new(Some(node_src.name.clone()));
287            node.transform = parse_transform(node_src);
288            if let Some(mesh_instances) = model_mesh_instances.get(&id) {
289                node.mesh_instances.extend(mesh_instances.clone());
290            }
291
292            if let Some(children) = model_children.get(&id) {
293                for &cid in children {
294                    if model_map.contains_key(&cid) {
295                        node.children.push(build_model_node(
296                            cid,
297                            model_map,
298                            model_children,
299                            model_mesh_instances,
300                        ));
301                    }
302                }
303            }
304            node
305        }
306
307        // Map geometries to models and create mesh instances.
308        let mut model_mesh_instances: std::collections::HashMap<
309            i64,
310            Vec<crate::scene::MeshInstance>,
311        > = std::collections::HashMap::new();
312        for (geom_id, geom_node) in geometry_map.iter() {
313            if let Some(mesh) = self.geometry_to_mesh(geom_node)? {
314                // find connection mapping geometry -> model
315                for (child, parent) in connections.iter() {
316                    if *child == *geom_id && model_map.contains_key(parent) {
317                        let mesh_instance = crate::scene::MeshInstance {
318                            name: Some(geom_node.name.clone()),
319                            mesh: mesh.clone(),
320                            transform: None,
321                        };
322                        model_mesh_instances
323                            .entry(*parent)
324                            .or_default()
325                            .push(mesh_instance);
326                    }
327                }
328            }
329        }
330
331        // Build root nodes: any model with parent 0 (or with no parent present)
332        let mut root_nodes = Vec::new();
333        // find top-level model ids
334        let top_level: Vec<i64> = model_map
335            .keys()
336            .cloned()
337            .filter(|id| {
338                !connections
339                    .iter()
340                    .any(|(child, parent)| child == id && model_map.contains_key(parent))
341            })
342            .collect();
343
344        for id in top_level {
345            root_nodes.push(build_model_node(
346                id,
347                &model_map,
348                &model_children,
349                &model_mesh_instances,
350            ));
351        }
352
353        Ok(crate::scene::Scene {
354            name: None,
355            root_nodes,
356        })
357    }
358}
359
360impl ReadFromBytes for FbxReader<Cursor<Vec<u8>>> {
361    fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
362        Self::from_bytes(bytes.to_vec())
363    }
364}
365
366impl<R: Read + Seek> FbxReader<R> {
367    /// Create a new FBX reader from a reader.
368    pub fn new(mut reader: R) -> io::Result<Self> {
369        // Read and verify magic
370        let mut magic = [0u8; 21];
371        reader.read_exact(&mut magic)?;
372        if &magic != FBX_MAGIC {
373            return Err(io::Error::new(
374                io::ErrorKind::InvalidData,
375                "Not a valid binary FBX file",
376            ));
377        }
378
379        // Skip 2 unknown bytes
380        reader.seek(SeekFrom::Current(2))?;
381
382        // Read version
383        let mut version_bytes = [0u8; 4];
384        reader.read_exact(&mut version_bytes)?;
385        let version = u32::from_le_bytes(version_bytes);
386
387        Ok(Self { reader, version })
388    }
389
390    /// Get the FBX file version.
391    pub fn version(&self) -> u32 {
392        self.version
393    }
394
395    /// Check if this is FBX 7.5+ (uses 64-bit offsets).
396    fn is_64bit(&self) -> bool {
397        self.version >= 7500
398    }
399
400    /// Read a node record.
401    fn read_node(&mut self) -> io::Result<Option<FbxNode>> {
402        let (end_offset, num_properties, _property_list_len, name_len) = if self.is_64bit() {
403            let mut buf = [0u8; 25];
404            self.reader.read_exact(&mut buf)?;
405            let end_offset = u64::from_le_bytes([
406                buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
407            ]);
408            let num_properties = u64::from_le_bytes([
409                buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
410            ]);
411            let property_list_len = u64::from_le_bytes([
412                buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
413            ]);
414            let name_len = buf[24];
415            (
416                end_offset,
417                num_properties as u32,
418                property_list_len,
419                name_len,
420            )
421        } else {
422            let mut buf = [0u8; 13];
423            self.reader.read_exact(&mut buf)?;
424            let end_offset = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as u64;
425            let num_properties = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
426            let _property_list_len = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as u64;
427            let name_len = buf[12];
428            (end_offset, num_properties, _property_list_len, name_len)
429        };
430
431        // NULL record marks end of children
432        if end_offset == 0 {
433            return Ok(None);
434        }
435
436        // Read name
437        let mut name_bytes = vec![0u8; name_len as usize];
438        self.reader.read_exact(&mut name_bytes)?;
439        let name = String::from_utf8_lossy(&name_bytes).to_string();
440
441        // Read properties
442        let mut properties = Vec::with_capacity(num_properties as usize);
443        for _ in 0..num_properties {
444            properties.push(self.read_property()?);
445        }
446
447        // Read children
448        let mut children = Vec::new();
449        let current_pos = self.reader.stream_position()?;
450        if current_pos < end_offset {
451            while let Some(child) = self.read_node()? {
452                children.push(child);
453            }
454        }
455
456        // Seek to end offset to be safe
457        self.reader.seek(SeekFrom::Start(end_offset))?;
458
459        Ok(Some(FbxNode {
460            name,
461            properties,
462            children,
463        }))
464    }
465
466    /// Read a property.
467    fn read_property(&mut self) -> io::Result<FbxProperty> {
468        let mut type_code = [0u8; 1];
469        self.reader.read_exact(&mut type_code)?;
470
471        match type_code[0] {
472            b'C' => {
473                let mut v = [0u8; 1];
474                self.reader.read_exact(&mut v)?;
475                Ok(FbxProperty::Bool(v[0] != 0))
476            }
477            b'Y' => {
478                let mut v = [0u8; 2];
479                self.reader.read_exact(&mut v)?;
480                Ok(FbxProperty::I16(i16::from_le_bytes(v)))
481            }
482            b'I' => {
483                let mut v = [0u8; 4];
484                self.reader.read_exact(&mut v)?;
485                Ok(FbxProperty::I32(i32::from_le_bytes(v)))
486            }
487            b'L' => {
488                let mut v = [0u8; 8];
489                self.reader.read_exact(&mut v)?;
490                Ok(FbxProperty::I64(i64::from_le_bytes(v)))
491            }
492            b'F' => {
493                let mut v = [0u8; 4];
494                self.reader.read_exact(&mut v)?;
495                Ok(FbxProperty::F32(f32::from_le_bytes(v)))
496            }
497            b'D' => {
498                let mut v = [0u8; 8];
499                self.reader.read_exact(&mut v)?;
500                Ok(FbxProperty::F64(f64::from_le_bytes(v)))
501            }
502            b'S' | b'R' => {
503                let mut len_bytes = [0u8; 4];
504                self.reader.read_exact(&mut len_bytes)?;
505                let len = u32::from_le_bytes(len_bytes) as usize;
506                let mut data = vec![0u8; len];
507                self.reader.read_exact(&mut data)?;
508                if type_code[0] == b'S' {
509                    Ok(FbxProperty::String(
510                        String::from_utf8_lossy(&data).to_string(),
511                    ))
512                } else {
513                    Ok(FbxProperty::Raw(data))
514                }
515            }
516            b'b' => Ok(FbxProperty::BoolArray(self.read_array_bool()?)),
517            b'i' => Ok(FbxProperty::I32Array(self.read_array_i32()?)),
518            b'l' => Ok(FbxProperty::I64Array(self.read_array_i64()?)),
519            b'f' => Ok(FbxProperty::F32Array(self.read_array_f32()?)),
520            b'd' => Ok(FbxProperty::F64Array(self.read_array_f64()?)),
521            _ => Err(io::Error::new(
522                io::ErrorKind::InvalidData,
523                format!("Unknown property type: {}", type_code[0] as char),
524            )),
525        }
526    }
527
528    /// Read array header and return (length, encoding, compressed_length).
529    fn read_array_header(&mut self) -> io::Result<(u32, u32, u32)> {
530        let mut buf = [0u8; 12];
531        self.reader.read_exact(&mut buf)?;
532        let array_len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
533        let encoding = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
534        let compressed_len = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
535        Ok((array_len, encoding, compressed_len))
536    }
537
538    /// Read array data (handles compression).
539    fn read_array_data(
540        &mut self,
541        encoding: u32,
542        compressed_len: u32,
543        uncompressed_size: usize,
544    ) -> io::Result<Vec<u8>> {
545        if encoding == 0 {
546            let mut data = vec![0u8; uncompressed_size];
547            self.reader.read_exact(&mut data)?;
548            Ok(data)
549        } else if encoding == 1 {
550            // Deflate/zlib compressed
551            let mut compressed = vec![0u8; compressed_len as usize];
552            self.reader.read_exact(&mut compressed)?;
553
554            #[cfg(feature = "compression")]
555            {
556                use miniz_oxide::inflate::decompress_to_vec_zlib;
557                decompress_to_vec_zlib(&compressed).map_err(|e| {
558                    io::Error::new(
559                        io::ErrorKind::InvalidData,
560                        format!("Decompression error: {:?}", e),
561                    )
562                })
563            }
564
565            #[cfg(not(feature = "compression"))]
566            {
567                Err(io::Error::new(
568                    io::ErrorKind::Unsupported,
569                    "FBX array compression not supported (enable 'compression' feature)",
570                ))
571            }
572        } else {
573            Err(io::Error::new(
574                io::ErrorKind::InvalidData,
575                format!("Unknown array encoding: {}", encoding),
576            ))
577        }
578    }
579
580    fn read_array_bool(&mut self) -> io::Result<Vec<bool>> {
581        let (len, encoding, compressed_len) = self.read_array_header()?;
582        let data = self.read_array_data(encoding, compressed_len, len as usize)?;
583        Ok(data.into_iter().map(|b| b != 0).collect())
584    }
585
586    fn read_array_i32(&mut self) -> io::Result<Vec<i32>> {
587        let (len, encoding, compressed_len) = self.read_array_header()?;
588        let data = self.read_array_data(encoding, compressed_len, len as usize * 4)?;
589        Ok(data
590            .chunks_exact(4)
591            .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
592            .collect())
593    }
594
595    fn read_array_i64(&mut self) -> io::Result<Vec<i64>> {
596        let (len, encoding, compressed_len) = self.read_array_header()?;
597        let data = self.read_array_data(encoding, compressed_len, len as usize * 8)?;
598        Ok(data
599            .chunks_exact(8)
600            .map(|c| i64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
601            .collect())
602    }
603
604    fn read_array_f32(&mut self) -> io::Result<Vec<f32>> {
605        let (len, encoding, compressed_len) = self.read_array_header()?;
606        let data = self.read_array_data(encoding, compressed_len, len as usize * 4)?;
607        Ok(data
608            .chunks_exact(4)
609            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
610            .collect())
611    }
612
613    fn read_array_f64(&mut self) -> io::Result<Vec<f64>> {
614        let (len, encoding, compressed_len) = self.read_array_header()?;
615        let data = self.read_array_data(encoding, compressed_len, len as usize * 8)?;
616        Ok(data
617            .chunks_exact(8)
618            .map(|c| f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]))
619            .collect())
620    }
621
622    /// Read all top-level nodes.
623    pub fn read_nodes(&mut self) -> io::Result<Vec<FbxNode>> {
624        // Seek to start of nodes (after header)
625        self.reader.seek(SeekFrom::Start(27))?;
626
627        let mut nodes = Vec::new();
628        while let Some(node) = self.read_node()? {
629            nodes.push(node);
630        }
631        Ok(nodes)
632    }
633
634    /// Read meshes from the FBX file.
635    pub fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
636        let nodes = self.read_nodes()?;
637        let mut meshes = Vec::new();
638
639        // Find Objects node
640        for node in &nodes {
641            if node.name == "Objects" {
642                for child in &node.children {
643                    if child.name == "Geometry" {
644                        if let Some(mesh) = self.geometry_to_mesh(child)? {
645                            meshes.push(mesh);
646                        }
647                    }
648                }
649            }
650        }
651
652        Ok(meshes)
653    }
654
655    /// Convert a Geometry node to a Mesh.
656    fn geometry_to_mesh(&self, geometry: &FbxNode) -> io::Result<Option<Mesh>> {
657        let mut vertices: Option<Vec<f64>> = None;
658        let mut polygon_indices: Option<Vec<i32>> = None;
659
660        for child in &geometry.children {
661            match child.name.as_str() {
662                "Vertices" => {
663                    if let Some(FbxProperty::F64Array(arr)) = child.properties.first() {
664                        vertices = Some(arr.clone());
665                    }
666                }
667                "PolygonVertexIndex" => {
668                    if let Some(FbxProperty::I32Array(arr)) = child.properties.first() {
669                        polygon_indices = Some(arr.clone());
670                    }
671                }
672                _ => {}
673            }
674        }
675
676        let vertices = match vertices {
677            Some(v) => v,
678            None => return Ok(None),
679        };
680        let polygon_indices = match polygon_indices {
681            Some(p) => p,
682            None => return Ok(None),
683        };
684
685        // Build mesh
686        let mut mesh = Mesh::new();
687
688        // Add positions
689        let num_vertices = vertices.len() / 3;
690        let mut pos_att = PointAttribute::new();
691        pos_att.init(
692            GeometryAttributeType::Position,
693            3,
694            DataType::Float32,
695            false,
696            num_vertices,
697        );
698        let buffer = pos_att.buffer_mut();
699        for i in 0..num_vertices {
700            let x = vertices[i * 3] as f32;
701            let y = vertices[i * 3 + 1] as f32;
702            let z = vertices[i * 3 + 2] as f32;
703            let bytes: Vec<u8> = [x, y, z].iter().flat_map(|v| v.to_le_bytes()).collect();
704            buffer.write(i * 12, &bytes);
705        }
706        mesh.add_attribute(pos_att);
707
708        // Parse polygon indices (FBX uses negative index to mark end of polygon)
709        let mut faces: Vec<[u32; 3]> = Vec::new();
710        let mut current_polygon: Vec<i32> = Vec::new();
711
712        for &idx in &polygon_indices {
713            if idx < 0 {
714                // End of polygon (index is bitwise NOT of actual index)
715                let actual_idx = !idx;
716                current_polygon.push(actual_idx);
717
718                // Triangulate polygon (simple fan triangulation)
719                if current_polygon.len() >= 3 {
720                    let v0 = current_polygon[0] as u32;
721                    for i in 1..current_polygon.len() - 1 {
722                        let v1 = current_polygon[i] as u32;
723                        let v2 = current_polygon[i + 1] as u32;
724                        faces.push([v0, v1, v2]);
725                    }
726                }
727                current_polygon.clear();
728            } else {
729                current_polygon.push(idx);
730            }
731        }
732
733        // Set faces
734        mesh.set_num_faces(faces.len());
735        for (i, face) in faces.iter().enumerate() {
736            mesh.set_face(
737                FaceIndex(i as u32),
738                [
739                    PointIndex(face[0]),
740                    PointIndex(face[1]),
741                    PointIndex(face[2]),
742                ],
743            );
744        }
745
746        // Match C++ Draco behavior: deduplicate point IDs in face-traversal order.
747        // This ensures binary compatibility when encoding.
748        mesh.deduplicate_point_ids();
749
750        Ok(Some(mesh))
751    }
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757    use std::io::Cursor;
758
759    #[test]
760    fn test_fbx_magic() {
761        let mut data = Vec::new();
762        data.extend_from_slice(FBX_MAGIC);
763        data.extend_from_slice(&[0x1A, 0x00]); // Unknown bytes
764        data.extend_from_slice(&7300u32.to_le_bytes()); // Version 7.3
765                                                        // Add null record to end nodes
766        data.extend_from_slice(&[0u8; 13]);
767
768        let cursor = Cursor::new(data);
769        let reader = FbxReader::new(cursor).unwrap();
770        assert_eq!(reader.version(), 7300);
771    }
772
773    #[test]
774    fn test_invalid_magic() {
775        let data = b"Not an FBX file at all";
776        let cursor = Cursor::new(data.to_vec());
777        assert!(FbxReader::new(cursor).is_err());
778    }
779}