Skip to main content

fbx_dom/objects/
extract.rs

1//! Lookup helpers for [`fbxscii::ElementAttribute`] maps on [`crate::OwnedObject::attributes`] and
2//! per-layer child maps in [`crate::objects::mesh_geometry`].
3//!
4//! [`AttrExtractorExt`] implements FBX-style “first token” accessors for scalar properties; large
5//! float/int arrays use the mesh layer’s parsers (comma-split tokens, optional `a:` child).
6
7use std::collections::HashMap;
8
9use fbxscii::ElementAttribute;
10
11use super::FbxTryFromReason;
12
13pub trait AttrExtractor {
14    fn extract(&self, name: &str) -> Option<&ElementAttribute>;
15    fn extract_case_insensitive(&self, name: &str) -> Option<&ElementAttribute>;
16}
17
18impl AttrExtractor for HashMap<String, ElementAttribute> {
19    fn extract(&self, name: &str) -> Option<&ElementAttribute> {
20        self.get(name)
21    }
22    fn extract_case_insensitive(&self, name: &str) -> Option<&ElementAttribute> {
23        self.get(name).or_else(|| {
24            self.iter()
25                .find(|(k, _)| k.eq_ignore_ascii_case(name))
26                .map(|(_, v)| v)
27        })
28    }
29}
30
31pub trait AttrExtractorExt {
32    fn require_token<'a>(&'a self, name: &'a str) -> Result<&'a str, FbxTryFromReason>;
33    fn require_token_case_insensitive(&self, name: &str) -> Result<&str, FbxTryFromReason>;
34    fn optional_token<'a>(&'a self, name: &'a str) -> Result<Option<&'a str>, FbxTryFromReason>;
35    fn optional_token_case_insensitive<'a>(
36        &'a self,
37        name: &'a str,
38    ) -> Result<Option<&'a str>, FbxTryFromReason>;
39    fn optional_tokens<'a>(
40        &'a self,
41        name: &'a str,
42    ) -> Result<Option<&'a [String]>, FbxTryFromReason>;
43    fn optional_tokens_case_insensitive<'a>(
44        &'a self,
45        name: &'a str,
46    ) -> Result<Option<&'a [String]>, FbxTryFromReason>;
47}
48
49impl<T: AttrExtractor> AttrExtractorExt for T {
50    fn require_token<'a>(&'a self, name: &'a str) -> Result<&'a str, FbxTryFromReason> {
51        let attr = self
52            .extract(name)
53            .ok_or_else(|| FbxTryFromReason::MissingAttribute {
54                name: name.to_string(),
55            })?;
56        let tok =
57            attr.get_tokens()
58                .first()
59                .ok_or_else(|| FbxTryFromReason::InvalidAttributeFormat {
60                    name: name.to_string(),
61                    detail: "missing value token".into(),
62                })?;
63        Ok(tok.as_str())
64    }
65    fn require_token_case_insensitive(&self, name: &str) -> Result<&str, FbxTryFromReason> {
66        let attr =
67            self.extract_case_insensitive(name)
68                .ok_or(FbxTryFromReason::MissingAttribute {
69                    name: name.to_string(),
70                })?;
71        let tok =
72            attr.get_tokens()
73                .first()
74                .ok_or_else(|| FbxTryFromReason::InvalidAttributeFormat {
75                    name: name.to_string(),
76                    detail: "missing value token".into(),
77                })?;
78        Ok(tok.as_str())
79    }
80    fn optional_token<'a>(&'a self, name: &'a str) -> Result<Option<&'a str>, FbxTryFromReason> {
81        let Some(attr) = self.extract(name) else {
82            return Ok(None);
83        };
84        Ok(attr.get_tokens().first().map(|s| s.as_str()))
85    }
86    fn optional_tokens<'a>(
87        &'a self,
88        name: &'a str,
89    ) -> Result<Option<&'a [String]>, FbxTryFromReason> {
90        let Some(attr) = self.extract(name) else {
91            return Ok(None);
92        };
93        Ok(Some(attr.get_tokens()))
94    }
95    fn optional_tokens_case_insensitive<'a>(
96        &'a self,
97        name: &'a str,
98    ) -> Result<Option<&'a [String]>, FbxTryFromReason> {
99        let Some(attr) = self.extract_case_insensitive(name) else {
100            return Ok(None);
101        };
102        Ok(Some(attr.get_tokens()))
103    }
104    fn optional_token_case_insensitive<'a>(
105        &'a self,
106        name: &'a str,
107    ) -> Result<Option<&'a str>, FbxTryFromReason> {
108        let Some(attr) = self.extract_case_insensitive(name) else {
109            return Ok(None);
110        };
111        Ok(attr.get_tokens().first().map(|s| s.as_str()))
112    }
113}
114
115// --- Parsed multi-token attributes (ModelUVTranslation, Cropping, …) ---------------------------
116
117fn parse_f32_token(attr_name: &str, tok: &str) -> Result<f32, FbxTryFromReason> {
118    tok.trim().parse().map_err(|e: std::num::ParseFloatError| {
119        FbxTryFromReason::InvalidAttributeFormat {
120            name: attr_name.to_string(),
121            detail: format!("invalid float token {tok:?}: {e}"),
122        }
123    })
124}
125
126fn parse_i32_token(attr_name: &str, tok: &str) -> Result<i32, FbxTryFromReason> {
127    tok.trim().parse().map_err(|e: std::num::ParseIntError| {
128        FbxTryFromReason::InvalidAttributeFormat {
129            name: attr_name.to_string(),
130            detail: format!("invalid int token {tok:?}: {e}"),
131        }
132    })
133}
134
135/// Optional parsing of typed token lists on top of [`AttrExtractor`].
136pub trait AttrExtractorParseExt {
137    fn optional_i32(&self, name: &str) -> Result<Option<i32>, FbxTryFromReason>;
138    /// First two value tokens as `f32` (e.g. `ModelUVTranslation`). `None` if the attribute is absent.
139    fn optional_two_f32(&self, name: &str) -> Result<Option<[f32; 2]>, FbxTryFromReason>;
140    fn optional_two_f32_case_insensitive(
141        &self,
142        name: &str,
143    ) -> Result<Option<[f32; 2]>, FbxTryFromReason>;
144    /// First four value tokens as `i32` (e.g. `Cropping`). `None` if the attribute is absent.
145    fn optional_four_i32(&self, name: &str) -> Result<Option<[i32; 4]>, FbxTryFromReason>;
146    fn optional_four_i32_case_insensitive(
147        &self,
148        name: &str,
149    ) -> Result<Option<[i32; 4]>, FbxTryFromReason>;
150}
151
152impl<T: AttrExtractor> AttrExtractorParseExt for T {
153    fn optional_i32(&self, name: &str) -> Result<Option<i32>, FbxTryFromReason> {
154        let Some(attr) = self.extract(name) else {
155            return Ok(None);
156        };
157        let t = attr.get_tokens();
158        if t.len() != 1 {
159            return Err(FbxTryFromReason::InvalidAttributeFormat {
160                name: name.to_string(),
161                detail: format!("expected 1 int token, got {}", t.len()),
162            });
163        }
164        Ok(Some(parse_i32_token(name, &t[0])?))
165    }
166    fn optional_two_f32(&self, name: &str) -> Result<Option<[f32; 2]>, FbxTryFromReason> {
167        let Some(attr) = self.extract(name) else {
168            return Ok(None);
169        };
170        let t = attr.get_tokens();
171        if t.len() < 2 {
172            return Err(FbxTryFromReason::InvalidAttributeFormat {
173                name: name.to_string(),
174                detail: format!(
175                    "expected at least 2 float tokens (e.g. ModelUVTranslation), got {}",
176                    t.len()
177                ),
178            });
179        }
180        Ok(Some([
181            parse_f32_token(name, &t[0])?,
182            parse_f32_token(name, &t[1])?,
183        ]))
184    }
185
186    fn optional_two_f32_case_insensitive(
187        &self,
188        name: &str,
189    ) -> Result<Option<[f32; 2]>, FbxTryFromReason> {
190        let Some(attr) = self.extract_case_insensitive(name) else {
191            return Ok(None);
192        };
193        let t = attr.get_tokens();
194        if t.len() < 2 {
195            return Err(FbxTryFromReason::InvalidAttributeFormat {
196                name: name.to_string(),
197                detail: format!(
198                    "expected at least 2 float tokens (e.g. ModelUVTranslation), got {}",
199                    t.len()
200                ),
201            });
202        }
203        Ok(Some([
204            parse_f32_token(name, &t[0])?,
205            parse_f32_token(name, &t[1])?,
206        ]))
207    }
208
209    fn optional_four_i32(&self, name: &str) -> Result<Option<[i32; 4]>, FbxTryFromReason> {
210        let Some(attr) = self.extract(name) else {
211            return Ok(None);
212        };
213        let t = attr.get_tokens();
214        if t.len() < 4 {
215            return Err(FbxTryFromReason::InvalidAttributeFormat {
216                name: name.to_string(),
217                detail: format!("expected 4 int tokens (Cropping), got {}", t.len()),
218            });
219        }
220        Ok(Some([
221            parse_i32_token(name, &t[0])?,
222            parse_i32_token(name, &t[1])?,
223            parse_i32_token(name, &t[2])?,
224            parse_i32_token(name, &t[3])?,
225        ]))
226    }
227
228    fn optional_four_i32_case_insensitive(
229        &self,
230        name: &str,
231    ) -> Result<Option<[i32; 4]>, FbxTryFromReason> {
232        let Some(attr) = self.extract_case_insensitive(name) else {
233            return Ok(None);
234        };
235        let t = attr.get_tokens();
236        if t.len() < 4 {
237            return Err(FbxTryFromReason::InvalidAttributeFormat {
238                name: name.to_string(),
239                detail: format!("expected 4 int tokens (Cropping), got {}", t.len()),
240            });
241        }
242        Ok(Some([
243            parse_i32_token(name, &t[0])?,
244            parse_i32_token(name, &t[1])?,
245            parse_i32_token(name, &t[2])?,
246            parse_i32_token(name, &t[3])?,
247        ]))
248    }
249}