Skip to main content

fbx_dom/
document.rs

1//! Parsed FBX file as header metadata, an element arena for `Objects` rows, and connection graphs.
2//!
3//! ## Connections (ASCII `Connections` section)
4//!
5//! - **`OO`** — Object → object. Stored in [`Document::object_connections`]: `source_id → [dest_id, …]`.
6//!   Typical: `Model` → `Geometry`, `Model` → `Material`.
7//! - **`OP`** — Object → object property. [`Document::object_property_connections`]:
8//!   `source_id → [{ dest, property }]`. Example: texture linked to material’s `DiffuseColor`.
9//! - **`PP`** — Property → property. [`Document::property_connections`] maps a **source**
10//!   `(object_id, property_name)` (packed as [`ObjectPropertyConnection`] with `dest` = source id)
11//!   to one or more destination [`ObjectPropertyConnection`] rows.
12//!
13//! [`DocumentLoader`] is implemented by ASCII element lists and by the binary tree adapter so both
14//! ingress paths share the same in-memory shape.
15
16use fbxcel::tree::any::AnyTree;
17use fbxscii::{ElementAmphitheatre, ElementParseError, Parser, ParserError};
18use std::{
19    collections::HashMap,
20    fmt::{Display, Formatter, Result as FmtResult},
21    io::{BufRead, Read, Seek},
22    result::Result,
23};
24
25use crate::{Object, global::GlobalSettings, object::Objects};
26
27#[derive(Debug, PartialEq)]
28pub enum DocumentParseError {
29    ParserError(ParserError),
30    BinaryParserError(String),
31    UnsupportedVersion(u32, Option<String>),
32    RequiredElementNotFound(String),
33    ElementParseError(ElementParseError),
34    PropertyParseError(PropertyParseError),
35}
36
37#[derive(Debug, Default)]
38pub struct ImportSettings {
39    pub strict: bool,
40}
41
42#[derive(Debug, PartialEq, Clone)]
43pub enum Property {
44    String(String),
45    Bool(bool),
46    Int(i32),
47    Float(f32),
48    ULongLong(u64),
49    ILongLong(i64),
50    Vec3([f32; 3]),
51    Vec4([f32; 4]),
52}
53
54#[derive(Debug)]
55pub struct PropertyDetails {
56    pub name: String,
57    pub property: Property,
58}
59
60#[derive(Debug, PartialEq)]
61pub enum PropertyParseError {
62    InvalidTokenLength(usize, Option<String>),
63    MissingPropertyType(String),
64    TokenParseError(String, String),
65}
66
67impl Display for PropertyParseError {
68    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
69        match self {
70            PropertyParseError::InvalidTokenLength(len, property_type) => write!(
71                f,
72                "Invalid token length: {} for property type: {}",
73                len,
74                property_type.as_ref().unwrap_or(&String::new())
75            ),
76            PropertyParseError::MissingPropertyType(property_type) => {
77                write!(f, "Missing property type: {}", property_type)
78            }
79            PropertyParseError::TokenParseError(property_type, token) => write!(
80                f,
81                "Token parse does not match property type: {} for token: {}",
82                property_type, token
83            ),
84        }
85    }
86}
87impl std::error::Error for PropertyParseError {}
88
89pub type Template = HashMap<String, Property>;
90
91#[derive(Debug, PartialEq, Clone)]
92pub struct LazyObject {
93    pub name: String,
94    pub type_name: String,
95    pub class_name: String,
96    /// Index of this object’s root element in the document’s object element arena.
97    ///
98    /// Populated by both ASCII and binary loaders. [`crate::Object::element`] resolves this index
99    /// while a [`Document`] is still live.
100    pub element_index: usize,
101}
102
103/// Endpoint for `OP` / `PP` rows: **destination** object id and property name on that object.
104///
105/// For **`PP`** map **keys**, Assimp/FBX use the source side: [`ObjectPropertyConnection::dest`] is
106/// the **source** object id and `property` the **source** property name; values are the actual
107/// destination links. See [`crate::Object::pp_targets`].
108#[derive(Debug, PartialEq, Clone, Hash, Eq)]
109pub struct ObjectPropertyConnection {
110    pub dest: u64,
111    pub property: String,
112}
113
114#[derive(Default, Debug, Clone)]
115pub struct Document {
116    /// The version of the FBX file
117    pub(crate) fbx_version: u32,
118    /// The creating program of the FBX file
119    pub(crate) creator: String,
120    /// The creation date of the FBX file
121    pub(crate) creation_date: [u32; 7],
122    /// The templates of the FBX file
123    pub(crate) templates: HashMap<String, Template>,
124    /// Maps `ObjectType` (e.g. `Geometry`) to the first full template key
125    /// (`ObjectType.PropertyTemplate`, e.g. `Geometry.FbxMesh`) in file order.
126    pub(crate) default_template_by_object_type: HashMap<String, String>,
127    /// The global settings of the FBX file
128    pub(crate) global_settings: Template,
129    /// The element amphitheatre containing object information
130    pub(crate) object_element_amphitheatre: ElementAmphitheatre,
131    /// The objects of the FBX file
132    pub(crate) objects: HashMap<u64, LazyObject>,
133    /// The connections between objects
134    pub(crate) object_connections: HashMap<u64, Vec<u64>>,
135    /// The connections between object properties
136    pub(crate) object_property_connections: HashMap<u64, Vec<ObjectPropertyConnection>>,
137    /// The connections between properties
138    pub(crate) property_connections:
139        HashMap<ObjectPropertyConnection, Vec<ObjectPropertyConnection>>,
140    /// For each source object id in a `PP` connection, the source-side property names on that object.
141    pub(crate) object_to_source_properties: HashMap<u64, Vec<String>>,
142}
143
144impl Document {
145    pub fn version(&self) -> u32 {
146        self.fbx_version
147    }
148
149    pub fn creator(&self) -> &str {
150        &self.creator
151    }
152
153    pub fn creation_date(&self) -> &[u32; 7] {
154        &self.creation_date
155    }
156
157    pub fn global_settings(&self) -> GlobalSettings<'_> {
158        GlobalSettings::new(self, &self.global_settings)
159    }
160
161    pub fn objects(&self) -> Objects<'_> {
162        Objects {
163            iter: self.objects.iter(),
164            document: self,
165        }
166    }
167
168    pub fn object_by_index(&self, index: u64) -> Option<Object<'_>> {
169        let object = self.object_for_index(index)?;
170        self.template_for_object(object)
171            .map(|template| Object::new(self, template, object, index))
172    }
173
174    pub(crate) fn object_for_index(&self, index: u64) -> Option<&LazyObject> {
175        self.objects.get(&index)
176    }
177
178    pub(crate) fn template_for_object(&self, object: &LazyObject) -> Option<&Template> {
179        self.templates.get(&object.type_name).or_else(|| {
180            self.default_template_by_object_type
181                .get(&object.type_name)
182                .and_then(|full_key| self.templates.get(full_key))
183        })
184    }
185
186    pub fn from_parser<R>(
187        parser: Parser<R>,
188        settings: ImportSettings,
189    ) -> Result<Self, DocumentParseError>
190    where
191        R: BufRead,
192    {
193        let elements = parser.load().map_err(DocumentParseError::ParserError)?;
194        let mut document = Self::default();
195        elements.load_into_document(&mut document, settings)?;
196        Ok(document)
197    }
198
199    /// Loads a binary FBX document through `fbxcel`'s tree API.
200    ///
201    /// This is the chosen primary binary ingress for now:
202    /// - parse bytes into `AnyTree`
203    /// - adapt tree nodes into the existing `Document` fields
204    /// - drop the `AnyTree` after materialization so we do not persist
205    ///   both a full tree and a long-lived binary DOM side-by-side.
206    pub fn from_binary_reader<R>(
207        reader: R,
208        settings: ImportSettings,
209    ) -> Result<Self, DocumentParseError>
210    where
211        R: Read + Seek,
212    {
213        let any_tree = AnyTree::from_seekable_reader(reader)
214            .map_err(|error| DocumentParseError::BinaryParserError(error.to_string()))?;
215        let mut document = Self::default();
216        any_tree.load_into_document(&mut document, settings)?;
217        Ok(document)
218    }
219}
220
221pub trait DocumentLoader {
222    fn load_into_document(
223        self,
224        document: &mut Document,
225        settings: ImportSettings,
226    ) -> Result<(), DocumentParseError>;
227}