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