Skip to main content

fbx_dom/
loader.rs

1//! ASCII FBX → [`crate::Document`]: parse [`fbxscii`] tree, read header/definitions/objects/connections,
2//! and populate [`crate::document::Document`].
3//!
4//! Object rows in `Objects` are keyed by id; each row’s subtree is extracted into
5//! [`Document::object_element_amphitheatre`]. `C:` connection rows fill the `OO` / `OP` / `PP`
6//! maps. Version bounds apply when [`ImportSettings::strict`] is relevant (see header handling).
7
8use std::convert::TryFrom;
9
10use fbxscii::{ElementAmphitheatre, ElementHandle};
11
12use crate::document::{
13    Document, DocumentLoader, DocumentParseError, ImportSettings, LazyObject,
14    ObjectPropertyConnection, Property, PropertyDetails, PropertyParseError, Template,
15};
16
17/// Minimum `FBXVersion` supported by the ASCII loader (below this → [`DocumentParseError::UnsupportedVersion`]).
18pub const LOWEST_SUPPORTED_VERSION: u32 = 7100;
19/// `FBXVersion` above this is rejected when [`ImportSettings::strict`] is true.
20pub const UPPER_SUPPORTED_VERSION: u32 = 7400;
21
22/// Fills [`Document::fbx_version`], creator string, and creation timestamp from `FBXHeaderExtension`.
23///
24/// Enforces [`LOWEST_SUPPORTED_VERSION`] always, and [`UPPER_SUPPORTED_VERSION`] when `settings.strict`.
25fn read_header(
26    amphitheatre: &ElementAmphitheatre,
27    document: &mut Document,
28    settings: ImportSettings,
29) -> Result<(), DocumentParseError> {
30    let header_extension = amphitheatre.get_handle_by_key("FBXHeaderExtension");
31    if header_extension.is_none() {
32        return Err(DocumentParseError::RequiredElementNotFound(
33            "FBXHeaderExtension".to_string(),
34        ));
35    }
36    let header_extension = header_extension.unwrap();
37
38    // Handle FBXVersion element
39    let version_element = header_extension.first_child_by_key("FBXVersion").ok_or(
40        DocumentParseError::RequiredElementNotFound("FBXVersion".to_string()),
41    )?;
42    document.fbx_version = version_element
43        .try_into()
44        .map_err(DocumentParseError::ElementParseError)?;
45
46    // Check if the version is supported
47    if document.fbx_version < LOWEST_SUPPORTED_VERSION {
48        return Err(DocumentParseError::UnsupportedVersion(
49            document.fbx_version,
50            None,
51        ));
52    }
53    if document.fbx_version > UPPER_SUPPORTED_VERSION && settings.strict {
54        return Err(DocumentParseError::UnsupportedVersion(
55            document.fbx_version,
56            Some("Turn off strict mode to import this version.".to_string()),
57        ));
58    }
59
60    // Handle Creator element
61    document.creator = header_extension
62        .first_child_by_key("Creator")
63        .ok_or(DocumentParseError::RequiredElementNotFound(
64            "Creator".to_string(),
65        ))?
66        .try_into()
67        .map_err(DocumentParseError::ElementParseError)?;
68
69    // Handle CreationTimeStamp element
70    let creation_date_element = header_extension
71        .first_child_by_key("CreationTimeStamp")
72        .ok_or(DocumentParseError::RequiredElementNotFound(
73            "CreationTimeStamp".to_string(),
74        ))?;
75    let year = creation_date_element
76        .first_child_by_key("Year")
77        .ok_or(DocumentParseError::RequiredElementNotFound(
78            "Year".to_string(),
79        ))?
80        .try_into()
81        .map_err(DocumentParseError::ElementParseError)?;
82    let month = creation_date_element
83        .first_child_by_key("Month")
84        .ok_or(DocumentParseError::RequiredElementNotFound(
85            "Month".to_string(),
86        ))?
87        .try_into()
88        .map_err(DocumentParseError::ElementParseError)?;
89    let day = creation_date_element
90        .first_child_by_key("Day")
91        .ok_or(DocumentParseError::RequiredElementNotFound(
92            "Day".to_string(),
93        ))?
94        .try_into()
95        .map_err(DocumentParseError::ElementParseError)?;
96    let hour = creation_date_element
97        .first_child_by_key("Hour")
98        .ok_or(DocumentParseError::RequiredElementNotFound(
99            "Hour".to_string(),
100        ))?
101        .try_into()
102        .map_err(DocumentParseError::ElementParseError)?;
103    let minute = creation_date_element
104        .first_child_by_key("Minute")
105        .ok_or(DocumentParseError::RequiredElementNotFound(
106            "Minute".to_string(),
107        ))?
108        .try_into()
109        .map_err(DocumentParseError::ElementParseError)?;
110    let second = creation_date_element
111        .first_child_by_key("Second")
112        .ok_or(DocumentParseError::RequiredElementNotFound(
113            "Second".to_string(),
114        ))?
115        .try_into()
116        .map_err(DocumentParseError::ElementParseError)?;
117    let millisecond = creation_date_element
118        .first_child_by_key("Millisecond")
119        .ok_or(DocumentParseError::RequiredElementNotFound(
120            "Millisecond".to_string(),
121        ))?
122        .try_into()
123        .map_err(DocumentParseError::ElementParseError)?;
124    document.creation_date = [year, month, day, hour, minute, second, millisecond];
125    Ok(())
126}
127
128/// Loads `Definitions` → per-object-type default templates and full template maps from each
129/// `PropertyTemplate`’s `Properties70` children.
130///
131/// Rows that fail [`PropertyDetails::try_from`] are skipped after a `log::debug!` (e.g. unsupported `"object"` types in shipped files).
132fn read_definitions(
133    amphitheatre: &ElementAmphitheatre,
134    document: &mut Document,
135) -> Result<(), DocumentParseError> {
136    let definition_handle_opt = amphitheatre.get_handle_by_key("Definitions");
137    if definition_handle_opt.is_none() {
138        return Ok(());
139    }
140    let definition_handle = definition_handle_opt.unwrap();
141    let object_type_handles = definition_handle.children_by_key("ObjectType");
142    for object_type_handle in object_type_handles {
143        if !object_type_handle.has_children() {
144            continue;
145        }
146
147        let object_tokens = object_type_handle.tokens();
148        if object_tokens.is_empty() {
149            continue;
150        }
151        let object_name = &object_tokens[0];
152        let property_template_handles = object_type_handle.children_by_key("PropertyTemplate");
153        for property_template_handle in property_template_handles {
154            if !property_template_handle.has_children() {
155                continue;
156            }
157            let property_tokens = property_template_handle.tokens();
158            if property_tokens.is_empty() {
159                continue;
160            }
161            let property_name = &property_tokens[0];
162            let property_table_handle_opt =
163                property_template_handle.first_child_by_key("Properties70");
164            if let Some(property_table_handle) = property_table_handle_opt {
165                let template_name = format!("{}.{}", object_name, property_name);
166                document
167                    .default_template_by_object_type
168                    .entry(object_name.to_string())
169                    .or_insert_with(|| template_name.clone());
170                let template = document
171                    .templates
172                    .entry(template_name.clone())
173                    .or_insert_with(Template::default);
174                for property_detail in property_table_handle.children() {
175                    match PropertyDetails::try_from(property_detail) {
176                        Ok(property_details) => {
177                            template.insert(property_details.name, property_details.property);
178                        }
179                        Err(e) => {
180                            log::debug!(
181                                target: "fbx_dom",
182                                "skipped Properties70 default-template entry (template={}, error={})",
183                                template_name,
184                                e
185                            );
186                        }
187                    }
188                }
189            }
190        }
191    }
192
193    Ok(())
194}
195
196/// Parses `GlobalSettings.Properties70` into [`Document::global_settings`] (all entries required to parse).
197fn read_global_settings(
198    amphitheatre: &ElementAmphitheatre,
199    document: &mut Document,
200) -> Result<(), DocumentParseError> {
201    let global_settings_handle_opt = amphitheatre.get_handle_by_key("GlobalSettings");
202    if global_settings_handle_opt.is_none() {
203        return Ok(());
204    }
205    let global_settings_handle = global_settings_handle_opt.unwrap();
206    let property_table_handle_opt = global_settings_handle.first_child_by_key("Properties70");
207    if let Some(property_table_handle) = property_table_handle_opt {
208        for property_detail in property_table_handle.children() {
209            let property_details: PropertyDetails = property_detail
210                .try_into()
211                .map_err(DocumentParseError::PropertyParseError)?;
212            document
213                .global_settings
214                .insert(property_details.name, property_details.property);
215        }
216    }
217    Ok(())
218}
219
220/// Reads `Connections` children: each row’s tokens are `[kind, …]` where `kind` is `OO`, `OP`, or `PP`.
221///
222/// - **`OO`** — `src, dest` → [`Document::object_connections`].
223/// - **`OP`** — `src, dest, property` → [`Document::object_property_connections`].
224/// - **`PP`** — `src, src_property, dest, dest_property` → [`Document::property_connections`] and
225///   [`Document::object_to_source_properties`].
226///
227/// Malformed rows (wrong arity or non-numeric ids) are skipped.
228fn read_connections(
229    amphitheatre: &ElementAmphitheatre,
230    document: &mut Document,
231) -> Result<(), DocumentParseError> {
232    let connections_handle_opt = amphitheatre.get_handle_by_key("Connections");
233    if connections_handle_opt.is_none() {
234        return Ok(());
235    }
236    let connections_handle = connections_handle_opt.unwrap();
237    for connection_handle in connections_handle.children() {
238        let connection_tokens = connection_handle.tokens();
239        if connection_tokens.len() < 2 {
240            continue;
241        }
242        let connection_type = &connection_tokens[0];
243        match connection_type.as_str() {
244            "OO" => {
245                if connection_tokens.len() != 3 {
246                    continue;
247                }
248                let Ok(src) = connection_tokens[1].parse::<u64>() else {
249                    continue;
250                };
251                let Ok(dest) = connection_tokens[2].parse::<u64>() else {
252                    continue;
253                };
254                document
255                    .object_connections
256                    .entry(src)
257                    .or_insert(Vec::new())
258                    .push(dest);
259            }
260            "OP" => {
261                if connection_tokens.len() != 4 {
262                    continue;
263                }
264                let Ok(src) = connection_tokens[1].parse::<u64>() else {
265                    continue;
266                };
267                let Ok(dest) = connection_tokens[2].parse::<u64>() else {
268                    continue;
269                };
270                let property = connection_tokens[3].to_string();
271                document
272                    .object_property_connections
273                    .entry(src)
274                    .or_insert(Vec::new())
275                    .push(ObjectPropertyConnection { dest, property });
276            }
277            "PP" => {
278                if connection_tokens.len() != 5 {
279                    continue;
280                }
281                let Ok(src) = connection_tokens[1].parse::<u64>() else {
282                    continue;
283                };
284                let src_property = connection_tokens[2].to_string();
285                let Ok(dest) = connection_tokens[3].parse::<u64>() else {
286                    continue;
287                };
288                let dest_property = connection_tokens[4].to_string();
289                document
290                    .object_to_source_properties
291                    .entry(src)
292                    .or_insert(Vec::new())
293                    .push(src_property.clone());
294                document
295                    .property_connections
296                    .entry(ObjectPropertyConnection {
297                        dest: src,
298                        property: src_property,
299                    })
300                    .or_insert(Vec::new())
301                    .push(ObjectPropertyConnection {
302                        dest,
303                        property: dest_property,
304                    });
305            }
306            _ => continue,
307        }
308    }
309    Ok(())
310}
311
312/// Inserts each `Objects` child into [`Document::objects`] by numeric id; stores the element index for later subtree resolution.
313///
314/// Expects three tokens per row: `id`, `name`, `class_name`. The element **key** becomes [`LazyObject::type_name`].
315fn read_objects(
316    amphitheatre: &ElementAmphitheatre,
317    document: &mut Document,
318) -> Result<(), DocumentParseError> {
319    let objects_handle_opt = amphitheatre.get_handle_by_key("Objects");
320    if objects_handle_opt.is_none() {
321        return Ok(());
322    }
323    let objects_handle = objects_handle_opt.unwrap();
324    for object_handle in objects_handle.children() {
325        let object_tokens = object_handle.tokens();
326        if object_tokens.len() != 3 {
327            continue;
328        }
329        let object_index = object_tokens[0].parse::<u64>();
330        if object_index.is_err() {
331            continue;
332        }
333        let object_index = object_index.unwrap();
334
335        let object = LazyObject {
336            name: object_tokens[1].to_string(),
337            type_name: object_handle.key().to_string(),
338            class_name: object_tokens[2].to_string(),
339            element_index: object_handle.index(),
340        };
341        document.objects.insert(object_index, object);
342    }
343    Ok(())
344}
345
346/// Parses one ASCII `P:` row (`Properties70` child): tokens `[name, type, …, value…]` into [`PropertyDetails`].
347///
348/// Supported `type` strings mirror common FBX SDK property kinds; unknown types return [`PropertyParseError::MissingPropertyType`].
349impl<'a> TryFrom<ElementHandle<'a>> for PropertyDetails {
350    type Error = PropertyParseError;
351
352    fn try_from(handle: ElementHandle<'a>) -> Result<Self, PropertyParseError> {
353        let tokens = handle.tokens();
354        if tokens.len() < 2 {
355            return Err(PropertyParseError::InvalidTokenLength(tokens.len(), None));
356        }
357        let property_name = &tokens[0];
358        let property_type = &tokens[1];
359        let property = match property_type.as_str() {
360            "KString" => {
361                if tokens.len() != 5 {
362                    return Err(PropertyParseError::InvalidTokenLength(
363                        tokens.len(),
364                        Some(property_type.to_string()),
365                    ));
366                }
367                Property::String(tokens[4].to_string())
368            }
369            "bool" | "Bool" => {
370                if tokens.len() != 5 {
371                    return Err(PropertyParseError::InvalidTokenLength(
372                        tokens.len(),
373                        Some(property_type.to_string()),
374                    ));
375                }
376                let Ok(val) = tokens[4].parse::<i32>() else {
377                    return Err(PropertyParseError::TokenParseError(
378                        property_type.to_string(),
379                        tokens[4].to_string(),
380                    ));
381                };
382                Property::Bool(val != 0)
383            }
384            "int" | "Int" | "enum" | "Enum" | "Integer" => {
385                if tokens.len() != 5 {
386                    return Err(PropertyParseError::InvalidTokenLength(
387                        tokens.len(),
388                        Some(property_type.to_string()),
389                    ));
390                }
391                let Ok(val) = tokens[4].parse::<i32>() else {
392                    return Err(PropertyParseError::TokenParseError(
393                        property_type.to_string(),
394                        tokens[4].to_string(),
395                    ));
396                };
397                Property::Int(val)
398            }
399            "ULongLong" => {
400                if tokens.len() != 5 {
401                    return Err(PropertyParseError::InvalidTokenLength(
402                        tokens.len(),
403                        Some(property_type.to_string()),
404                    ));
405                }
406                let Ok(val) = tokens[4].parse::<u64>() else {
407                    return Err(PropertyParseError::TokenParseError(
408                        property_type.to_string(),
409                        tokens[4].to_string(),
410                    ));
411                };
412                Property::ULongLong(val)
413            }
414            "KTime" => {
415                if tokens.len() != 5 {
416                    return Err(PropertyParseError::InvalidTokenLength(
417                        tokens.len(),
418                        Some(property_type.to_string()),
419                    ));
420                }
421                let Ok(val) = tokens[4].parse::<i64>() else {
422                    return Err(PropertyParseError::TokenParseError(
423                        property_type.to_string(),
424                        tokens[4].to_string(),
425                    ));
426                };
427                Property::ILongLong(val)
428            }
429            "double" | "Number" | "float" | "Float" | "FieldOfView" | "UnitScaleFactor" => {
430                if tokens.len() != 5 {
431                    return Err(PropertyParseError::InvalidTokenLength(
432                        tokens.len(),
433                        Some(property_type.to_string()),
434                    ));
435                }
436                let Ok(val) = tokens[4].parse::<f32>() else {
437                    return Err(PropertyParseError::TokenParseError(
438                        property_type.to_string(),
439                        tokens[4].to_string(),
440                    ));
441                };
442                Property::Float(val)
443            }
444            "Vector3D" | "Vector" | "Color" | "ColorRGB" | "Lcl Translation" | "Lcl Rotation"
445            | "Lcl Scaling" => {
446                if tokens.len() != 7 {
447                    return Err(PropertyParseError::InvalidTokenLength(
448                        tokens.len(),
449                        Some(property_type.to_string()),
450                    ));
451                }
452                let Ok(x) = tokens[4].parse::<f32>() else {
453                    return Err(PropertyParseError::TokenParseError(
454                        property_type.to_string(),
455                        tokens[4].to_string(),
456                    ));
457                };
458                let Ok(y) = tokens[5].parse::<f32>() else {
459                    return Err(PropertyParseError::TokenParseError(
460                        property_type.to_string(),
461                        tokens[5].to_string(),
462                    ));
463                };
464                let Ok(z) = tokens[6].parse::<f32>() else {
465                    return Err(PropertyParseError::TokenParseError(
466                        property_type.to_string(),
467                        tokens[6].to_string(),
468                    ));
469                };
470                Property::Vec3([x, y, z])
471            }
472            "ColorAndAlpha" => {
473                if tokens.len() != 8 {
474                    return Err(PropertyParseError::InvalidTokenLength(
475                        tokens.len(),
476                        Some(property_type.to_string()),
477                    ));
478                }
479                let r = tokens[4].parse::<f32>().unwrap_or(0.0);
480                let g = tokens[5].parse::<f32>().unwrap_or(0.0);
481                let b = tokens[6].parse::<f32>().unwrap_or(0.0);
482                let a = tokens[7].parse::<f32>().unwrap_or(0.0);
483                Property::Vec4([r, g, b, a])
484            }
485            value => return Err(PropertyParseError::MissingPropertyType(value.to_string())),
486        };
487        Ok(PropertyDetails {
488            name: property_name.to_string(),
489            property,
490        })
491    }
492}
493
494impl DocumentLoader for ElementAmphitheatre {
495    /// ASCII ingress: header → definitions → global settings → objects → connections, then stores `self` as the object arena.
496    fn load_into_document(
497        self,
498        document: &mut Document,
499        settings: ImportSettings,
500    ) -> Result<(), DocumentParseError> {
501        read_header(&self, document, settings)?;
502        read_definitions(&self, document)?;
503        read_global_settings(&self, document)?;
504
505        read_objects(&self, document)?;
506        read_connections(&self, document)?;
507        document.object_element_amphitheatre = self;
508        Ok(())
509    }
510}
511
512#[cfg(test)]
513mod tests {
514
515    use std::convert::TryFrom;
516    use std::io::BufReader;
517
518    use fbxscii::{ElementAmphitheatre, ElementHandle, Parser, Tokenizer};
519
520    use crate::document::{
521        Document, DocumentParseError, ImportSettings, ObjectPropertyConnection, Property,
522        PropertyDetails, PropertyParseError,
523    };
524
525    /// Minimal ASCII FBX (7.2) with required header + `GlobalSettings.Properties70` body + tail.
526    fn minimal_ascii_fbx_7200(global_properties70_body: &str, tail: &str) -> String {
527        format!(
528            r#"; test
529FBXHeaderExtension:  {{
530	FBXHeaderVersion: 1003
531	FBXVersion: 7200
532	CreationTimeStamp:  {{
533		Version: 1000
534		Year: 2012
535		Month: 6
536		Day: 28
537		Hour: 16
538		Minute: 32
539		Second: 53
540		Millisecond: 433
541	}}
542	Creator: "test"
543}}
544GlobalSettings:  {{
545	Properties70:  {{
546{global_properties70_body}
547	}}
548}}
549{tail}"#
550        )
551    }
552
553    /// Parses `src` into an [`ElementAmphitheatre`] (tests only).
554    fn load_arena(src: &str) -> ElementAmphitheatre {
555        let tokenizer = Tokenizer::new(BufReader::new(src.as_bytes()));
556        let parser = Parser::new(tokenizer);
557        parser.load().expect("parse ASCII FBX")
558    }
559
560    /// First `P` element under `GlobalSettings` → `Properties70` (tests only).
561    fn first_p_child_properties70(arena: &ElementAmphitheatre) -> ElementHandle<'_> {
562        let gs = arena
563            .get_handle_by_key("GlobalSettings")
564            .expect("GlobalSettings");
565        let p70 = gs.first_child_by_key("Properties70").expect("Properties70");
566        p70.children().next().expect("at least one P row")
567    }
568
569    #[test]
570    fn test_header_parse() {
571        let test_document = r#"
572FBXHeaderExtension:  {
573	FBXHeaderVersion: 1003
574	FBXVersion: 7300
575	CreationTimeStamp:  {
576		Version: 1000
577		Year: 2012
578		Month: 6
579		Day: 28
580		Hour: 16
581		Minute: 32
582		Second: 53
583		Millisecond: 433
584	}
585	Creator: "FBX SDK/FBX Plugins version 2013.1"
586}"#;
587        let tokenizer = Tokenizer::new(BufReader::new(test_document.as_bytes()));
588        let parser = Parser::new(tokenizer);
589        let document = Document::from_parser(parser, ImportSettings::default()).unwrap();
590        assert_eq!(document.fbx_version, 7300);
591        assert_eq!(document.creator, "FBX SDK/FBX Plugins version 2013.1");
592        assert_eq!(document.creation_date, [2012, 6, 28, 16, 32, 53, 433]);
593    }
594
595    #[test]
596    fn test_empty_document_parse() {
597        let test_document = "";
598        let tokenizer = Tokenizer::new(BufReader::new(test_document.as_bytes()));
599        let parser = Parser::new(tokenizer);
600        let document = Document::from_parser(parser, ImportSettings::default());
601        assert!(document.is_err());
602        assert_eq!(
603            document.unwrap_err(),
604            DocumentParseError::RequiredElementNotFound("FBXHeaderExtension".to_string())
605        );
606    }
607
608    #[test]
609    fn connections_pp_ascii_populates_property_maps() {
610        let src = minimal_ascii_fbx_7200(
611            r#"		P: "UpAxis", "int", "", "",1
612"#,
613            r#"Definitions:  {
614	ObjectType: "Geometry" {
615		PropertyTemplate: "FbxMesh" {
616			Properties70:  {
617				P: "UnitScaleFactor", "double", "", "",1
618			}
619		}
620	}
621}
622Objects:  {
623	Geometry: 101, "A", "Mesh" {
624	}
625	Geometry: 202, "B", "Mesh" {
626	}
627}
628Connections:  {
629	C: "PP",101,"SrcProp",202,"DstProp"
630}
631"#,
632        );
633        let tokenizer = Tokenizer::new(BufReader::new(src.as_bytes()));
634        let parser = Parser::new(tokenizer);
635        let document = Document::from_parser(parser, ImportSettings::default()).unwrap();
636
637        let obj = document.object_by_index(101).expect("object 101");
638        assert_eq!(obj.pp_source_property_names(), &["SrcProp".to_string()]);
639        assert_eq!(
640            obj.pp_targets((101, "SrcProp")),
641            Some(
642                &[ObjectPropertyConnection {
643                    dest: 202,
644                    property: "DstProp".to_string(),
645                }][..],
646            )
647        );
648    }
649
650    #[test]
651    fn connections_pp_ascii_appends_multiple_destinations_for_same_source() {
652        let src = minimal_ascii_fbx_7200(
653            r#"		P: "UpAxis", "int", "", "",1
654"#,
655            r#"Definitions:  {
656	ObjectType: "Geometry" {
657		PropertyTemplate: "FbxMesh" {
658			Properties70:  {
659				P: "UnitScaleFactor", "double", "", "",1
660			}
661		}
662	}
663}
664Objects:  {
665	Geometry: 101, "A", "Mesh" {
666	}
667	Geometry: 202, "B", "Mesh" {
668	}
669	Geometry: 303, "C", "Mesh" {
670	}
671}
672Connections:  {
673	C: "PP",101,"SrcProp",202,"DstA"
674	C: "PP",101,"SrcProp",303,"DstB"
675}
676"#,
677        );
678        let tokenizer = Tokenizer::new(BufReader::new(src.as_bytes()));
679        let parser = Parser::new(tokenizer);
680        let document = Document::from_parser(parser, ImportSettings::default()).unwrap();
681
682        let obj = document.object_by_index(101).expect("object 101");
683        // One list entry per `PP` row (same name repeated when multiple rows share the source key).
684        assert_eq!(
685            obj.pp_source_property_names(),
686            &["SrcProp".to_string(), "SrcProp".to_string()]
687        );
688        let targets = obj.pp_targets((101, "SrcProp")).expect("PP targets");
689        assert_eq!(targets.len(), 2);
690        assert!(targets.contains(&ObjectPropertyConnection {
691            dest: 202,
692            property: "DstA".to_string(),
693        }));
694        assert!(targets.contains(&ObjectPropertyConnection {
695            dest: 303,
696            property: "DstB".to_string(),
697        }));
698    }
699
700    #[test]
701    fn property_details_try_from_int_kstring_and_vector3d() {
702        let src = minimal_ascii_fbx_7200(
703            r#"		P: "UpAxis", "int", "", "",2
704		P: "LayerName", "KString", "", "", "hello"
705		P: "Point", "Vector3D", "Vector", "",0,1,2
706"#,
707            "",
708        );
709        let arena = load_arena(&src);
710        let gs = arena.get_handle_by_key("GlobalSettings").unwrap();
711        let p70 = gs.first_child_by_key("Properties70").unwrap();
712        let mut it = p70.children();
713        let d0: PropertyDetails = PropertyDetails::try_from(it.next().unwrap()).unwrap();
714        assert_eq!(d0.name, "UpAxis");
715        assert_eq!(d0.property, Property::Int(2));
716        let d1: PropertyDetails = PropertyDetails::try_from(it.next().unwrap()).unwrap();
717        assert_eq!(d1.name, "LayerName");
718        assert_eq!(d1.property, Property::String("hello".to_string()));
719        let d2: PropertyDetails = PropertyDetails::try_from(it.next().unwrap()).unwrap();
720        assert_eq!(d2.name, "Point");
721        assert_eq!(d2.property, Property::Vec3([0.0, 1.0, 2.0]));
722    }
723
724    #[test]
725    fn property_details_try_from_missing_property_type_object() {
726        let src = minimal_ascii_fbx_7200(
727            r#"		P: "SourceObject", "object", "", ""
728"#,
729            "",
730        );
731        let arena = load_arena(&src);
732        let p = first_p_child_properties70(&arena);
733        let err = PropertyDetails::try_from(p).unwrap_err();
734        assert_eq!(
735            err,
736            PropertyParseError::MissingPropertyType("object".to_string())
737        );
738    }
739
740    #[test]
741    fn property_details_try_from_invalid_token_length() {
742        let src = minimal_ascii_fbx_7200(
743            r#"		P: "Short", "int"
744"#,
745            "",
746        );
747        let arena = load_arena(&src);
748        let p = first_p_child_properties70(&arena);
749        let err = PropertyDetails::try_from(p).unwrap_err();
750        assert_eq!(
751            err,
752            PropertyParseError::InvalidTokenLength(2, Some("int".to_string()))
753        );
754    }
755
756    #[test]
757    fn property_details_try_from_token_parse_error() {
758        let src = minimal_ascii_fbx_7200(
759            r#"		P: "BadInt", "int", "", "", "not_an_int"
760"#,
761            "",
762        );
763        let arena = load_arena(&src);
764        let p = first_p_child_properties70(&arena);
765        let err = PropertyDetails::try_from(p).unwrap_err();
766        assert_eq!(
767            err,
768            PropertyParseError::TokenParseError("int".to_string(), "not_an_int".to_string())
769        );
770    }
771}