Skip to main content

fbx_dom/
object.rs

1//! Borrowed view of one FBX `Objects` row: [`Object`] (needs [`Document`]) and eager [`OwnedObject`].
2//!
3//! [`Object`] resolves the row’s subtree in [`Document::object_element_amphitheatre`] and merges
4//! [`Template`] defaults where applicable. [`OwnedObject`] is the detached form used by
5//! [`crate::objects::ClassifiedFbxObject`].
6
7use fbxscii::ElementAttribute;
8
9use crate::document::{
10    Document, LazyObject, ObjectPropertyConnection, Property, PropertyDetails, Template,
11};
12use std::collections::HashMap;
13
14#[derive(Debug, PartialEq)]
15pub enum ObjectError {
16    MissingTemplate(String),
17}
18
19/// One FBX object id: metadata from [`LazyObject`], template from definitions, subtree from arena.
20#[derive(Debug)]
21pub struct Object<'a> {
22    document: &'a Document,
23    template: &'a Template,
24    object: &'a LazyObject,
25    /// Same as FBX `Objects` row id and keys in [`Document::object_connections`].
26    index: u64,
27}
28
29impl<'a> Object<'a> {
30    pub fn name(&self) -> &str {
31        &self.object.name
32    }
33
34    pub fn type_name(&self) -> &str {
35        &self.object.type_name
36    }
37
38    pub fn class_name(&self) -> &str {
39        &self.object.class_name
40    }
41
42    pub fn object_index(&self) -> u64 {
43        self.index
44    }
45
46    pub fn element(&self) -> Option<&'a fbxscii::Element> {
47        self.document
48            .object_element_amphitheatre
49            .get(self.object.element_index)
50    }
51
52    /// Indices of objects connected from this one via `OO` rows (`C: "OO", self, dest`).
53    pub fn connected_object_ids(&self) -> &[u64] {
54        self.document
55            .object_connections
56            .get(&self.index)
57            .map(|v| v.as_slice())
58            .unwrap_or(&[])
59    }
60
61    /// `OP` connections from this object: each entry is a destination object id and the
62    /// destination-side property name (`C: "OP", self, dest, property`).
63    pub fn object_property_targets(&self) -> &[ObjectPropertyConnection] {
64        self.document
65            .object_property_connections
66            .get(&self.index)
67            .map(|v| v.as_slice())
68            .unwrap_or(&[])
69    }
70
71    /// Property names on this object that appear as the source side of `PP` connections.
72    pub fn pp_source_property_names(&self) -> &[String] {
73        self.document
74            .object_to_source_properties
75            .get(&self.index)
76            .map(|v| v.as_slice())
77            .unwrap_or(&[])
78    }
79
80    /// `PP` targets for a source `(object_id, property_name)` key (`C: "PP", …`).
81    ///
82    /// The document’s `PP` map keys reuse [`ObjectPropertyConnection`]: for `PP` rows,
83    /// `ObjectPropertyConnection::dest` holds the **source** object id and `property` the source
84    /// property name.
85    pub fn pp_targets(&self, key: (u64, &str)) -> Option<&[ObjectPropertyConnection]> {
86        let (source_object, source_property) = key;
87        self.document
88            .property_connections
89            .get(&ObjectPropertyConnection {
90                dest: source_object,
91                property: source_property.to_string(),
92            })
93            .map(|v| v.as_slice())
94    }
95}
96
97impl<'a> Object<'a> {
98    pub fn new(
99        document: &'a Document,
100        template: &'a Template,
101        object: &'a LazyObject,
102        index: u64,
103    ) -> Self {
104        Self {
105            document,
106            template,
107            object,
108            index,
109        }
110    }
111
112    pub fn template(&self) -> &'a Template {
113        self.template
114    }
115
116    pub fn properties(&self) -> HashMap<String, Property> {
117        let object_index = self.object.element_index;
118        let object_handle = self
119            .document
120            .object_element_amphitheatre
121            .get_handle(object_index);
122        if object_handle.is_none() {
123            return HashMap::new();
124        }
125        let object_handle = object_handle.unwrap();
126        let property_table_handle_opt = object_handle.first_child_by_key("Properties70");
127        let mut properties = HashMap::new();
128        if let Some(property_table_handle) = property_table_handle_opt {
129            for property_detail in property_table_handle.children() {
130                let r: Result<PropertyDetails, _> = property_detail.try_into();
131                if let Ok(property_details) = r {
132                    properties.insert(property_details.name, property_details.property);
133                }
134            }
135        }
136        properties
137    }
138
139    pub fn attributes(&self) -> HashMap<String, ElementAttribute> {
140        let object_index = self.object.element_index;
141        let object_handle = self
142            .document
143            .object_element_amphitheatre
144            .get_handle(object_index);
145        if object_handle.is_none() {
146            return HashMap::new();
147        }
148        let object_handle = object_handle.unwrap();
149        let mut attributes = HashMap::new();
150        for attribute in object_handle.children() {
151            if attribute.key() == "Properties70" {
152                continue;
153            }
154            let subtree = self
155                .document
156                .object_element_amphitheatre
157                .extract_subtree(attribute.index());
158            if subtree.is_none() {
159                continue;
160            }
161            let subtree = subtree.unwrap();
162            attributes.insert(attribute.key().to_string(), subtree);
163        }
164        attributes
165    }
166}
167
168/// Detached FBX object: typed [`Property`] map from `Properties70`, non-property subtree as
169/// [`ElementAttribute`] map (mesh `Vertices`, layer elements, etc.), and outgoing connection edges.
170///
171/// Built from [`Object`] via [`From`]; used as input to [`crate::objects::ClassifiedFbxObject`].
172#[derive(Debug, PartialEq)]
173pub struct OwnedObject {
174    /// FBX object id from the `Objects` section (same as [`Object::object_index`]).
175    pub object_index: u64,
176    pub name: String,
177    pub type_name: String,
178    pub class_name: String,
179    pub properties: HashMap<String, Property>,
180    pub attributes: HashMap<String, ElementAttribute>,
181    /// `OO` destinations from this object (same as [`Object::connected_object_ids`]).
182    pub connected_object_ids: Vec<u64>,
183    /// `OP` targets from this object (same as [`Object::object_property_targets`]).
184    pub object_property_targets: Vec<ObjectPropertyConnection>,
185    /// For each source property name on this object, the first `PP` destination
186    /// ([`Object::pp_targets`]) when multiple targets exist for that property.
187    pub pp_property_targets: HashMap<String, ObjectPropertyConnection>,
188}
189
190impl<'a> From<Object<'a>> for OwnedObject {
191    fn from(object: Object<'a>) -> Self {
192        let idx = object.object_index();
193        let mut pp_property_targets = HashMap::new();
194        for source_property in object.pp_source_property_names() {
195            let Some(targets) = object.pp_targets((idx, source_property.as_str())) else {
196                continue;
197            };
198            if let Some(first) = targets.first() {
199                pp_property_targets.insert(source_property.clone(), first.clone());
200            }
201        }
202
203        Self {
204            object_index: idx,
205            name: object.name().to_string(),
206            type_name: object.type_name().to_string(),
207            class_name: object.class_name().to_string(),
208            properties: object.properties(),
209            attributes: object.attributes(),
210            connected_object_ids: object.connected_object_ids().to_vec(),
211            object_property_targets: object.object_property_targets().to_vec(),
212            pp_property_targets,
213        }
214    }
215}
216
217#[derive(Clone, Debug)]
218pub struct Objects<'a> {
219    /// Internal buffer iterator.
220    pub(crate) iter: std::collections::hash_map::Iter<'a, u64, LazyObject>,
221
222    /// The internal root document.
223    pub(crate) document: &'a Document,
224}
225
226impl ExactSizeIterator for Objects<'_> {}
227impl<'a> Iterator for Objects<'a> {
228    type Item = Result<Object<'a>, ObjectError>;
229    fn next(&mut self) -> Option<Self::Item> {
230        self.iter.next().map(|(index, object)| {
231            self.document
232                .template_for_object(object)
233                .map(|template| Object::new(self.document, template, object, *index))
234                .ok_or_else(|| ObjectError::MissingTemplate(object.type_name.clone()))
235        })
236    }
237    fn size_hint(&self) -> (usize, Option<usize>) {
238        self.iter.size_hint()
239    }
240    fn count(self) -> usize {
241        self.iter.count()
242    }
243    fn last(self) -> Option<Self::Item> {
244        let document = self.document;
245        self.iter.last().map(|(index, object)| {
246            document
247                .template_for_object(object)
248                .map(|template| Object::new(document, template, object, *index))
249                .ok_or_else(|| ObjectError::MissingTemplate(object.type_name.clone()))
250        })
251    }
252    fn nth(&mut self, n: usize) -> Option<Self::Item> {
253        self.iter.nth(n).map(|(index, object)| {
254            self.document
255                .template_for_object(object)
256                .map(|template| Object::new(self.document, template, object, *index))
257                .ok_or_else(|| ObjectError::MissingTemplate(object.type_name.clone()))
258        })
259    }
260}