Skip to main content

fbx_dom/objects/
mesh_geometry.rs

1//! FBX `Geometry` / `Mesh` — Assimp [`MeshGeometry`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXMeshGeometry.h) / [`FBXMeshGeometry.cpp`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXMeshGeometry.cpp).
2//!
3//! ## Layout
4//!
5//! - **Control points** — `Vertices` / `PolygonVertexIndex` on the geometry root; ASCII stores large
6//!   arrays as `Name: *N { a: … }`; `parse_f32_array` / `parse_i32_array` read the optional **`a`**
7//!   child or fall back to tokens on the node (unit tests).
8//! - **Layers** — `LayerElementNormal`, `LayerElementUV`, … each hold `MappingInformationType`,
9//!   `ReferenceInformationType`, and channel arrays. `expand_mesh_polygon_vertices` duplicates
10//!   positions per corner and builds `mapping_*` tables for `resolve_flat_f32_channel`.
11
12use std::collections::HashMap;
13use std::convert::TryFrom;
14use std::num::ParseFloatError;
15use std::num::ParseIntError;
16
17use crate::OwnedObject;
18use fbxscii::ElementAttribute;
19
20use super::AttrExtractor;
21use super::AttrExtractorExt;
22use super::{FbxObjectTag, FbxTryFromReason, FbxTypeMismatch, fbx_object_tag};
23
24const MAX_UV_CHANNELS: usize = 8;
25const MAX_COLOR_SETS: usize = 8;
26
27const ATTR_MAPPING_INFORMATION_TYPE: &str = "MappingInformationType";
28const ATTR_REFERENCE_INFORMATION_TYPE: &str = "ReferenceInformationType";
29
30/// FBX SDK spelling (not "ByVertex").
31const MAPPING_BY_VERTICE: &str = "ByVertice";
32const MAPPING_BY_POLYGON_VERTEX: &str = "ByPolygonVertex";
33const MAPPING_BY_POLYGON: &str = "ByPolygon";
34const MAPPING_ALL_SAME: &str = "AllSame";
35
36const REFERENCE_DIRECT: &str = "Direct";
37const REFERENCE_INDEX_TO_DIRECT: &str = "IndexToDirect";
38
39const ATTR_VERTICES: &str = "Vertices";
40const ATTR_POLYGON_VERTEX_INDEX: &str = "PolygonVertexIndex";
41const ATTR_LAYER_ELEMENT_NORMAL: &str = "LayerElementNormal";
42const ATTR_LAYER_ELEMENT_TANGENT: &str = "LayerElementTangent";
43const ATTR_LAYER_ELEMENT_BINORMAL: &str = "LayerElementBinormal";
44const ATTR_LAYER_ELEMENT_UV: &str = "LayerElementUV";
45const ATTR_MATERIALS: &str = "Materials";
46
47const ACCESSOR_KEY: &str = "a";
48
49#[derive(Debug, PartialEq)]
50pub struct MeshGeometry {
51    pub object: OwnedObject,
52    pub vertices: Vec<[f32; 3]>,
53    pub face_vertex_counts: Vec<u32>,
54    pub normals: Vec<[f32; 3]>,
55    pub tangents: Vec<[f32; 3]>,
56    pub binormals: Vec<[f32; 3]>,
57    pub texture_coords: [Vec<[f32; 2]>; MAX_UV_CHANNELS],
58    pub texture_coord_names: [String; MAX_UV_CHANNELS],
59    pub vertex_colors: [Vec<[f32; 4]>; MAX_COLOR_SETS],
60    pub material_indices: Vec<i32>,
61}
62
63impl MeshGeometry {
64    pub fn inner(&self) -> &OwnedObject {
65        &self.object
66    }
67
68    pub fn into_inner(self) -> OwnedObject {
69        self.object
70    }
71}
72
73impl TryFrom<OwnedObject> for MeshGeometry {
74    type Error = FbxTypeMismatch;
75
76    fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
77        // Check if tagged as MeshGeometry
78        match fbx_object_tag(&o) {
79            FbxObjectTag::MeshGeometry => {}
80            _ => {
81                return Err(FbxTypeMismatch::wrong_object_kind(
82                    o,
83                    "MeshGeometry".to_string(),
84                ));
85            }
86        }
87
88        let attrs = &o.attributes;
89
90        // Extract Vertices (ASCII: `Vertices: *N { a: ... }`; tests may use a leaf with tokens only).
91        let verts_attr = match attrs.extract_case_insensitive(ATTR_VERTICES) {
92            Some(a) => a,
93            None => {
94                return Err(FbxTypeMismatch::new(
95                    o,
96                    FbxTryFromReason::MissingAttribute {
97                        name: ATTR_VERTICES.to_string(),
98                    },
99                ));
100            }
101        };
102        // Parse Vertices
103        let vertices_result = parse_f32_array(verts_attr);
104        let Ok(vertices) = vertices_result else {
105            return Err(FbxTypeMismatch::new(
106                o,
107                FbxTryFromReason::InvalidAttributeFormat {
108                    name: ATTR_VERTICES.to_string(),
109                    detail: format!("invalid float token: {}", vertices_result.unwrap_err()),
110                },
111            ));
112        };
113        let vertices = vertices
114            .chunks_exact(3)
115            .map(|c| [c[0], c[1], c[2]])
116            .collect::<Vec<[f32; 3]>>();
117
118        // Extract Face Indices
119        let poly_attr = match attrs.extract_case_insensitive(ATTR_POLYGON_VERTEX_INDEX) {
120            Some(a) => a,
121            None => {
122                return Err(FbxTypeMismatch::new(
123                    o,
124                    FbxTryFromReason::MissingAttribute {
125                        name: ATTR_POLYGON_VERTEX_INDEX.to_string(),
126                    },
127                ));
128            }
129        };
130        let temp_faces_result = parse_i32_array(poly_attr);
131        let Ok(temp_faces) = temp_faces_result else {
132            return Err(FbxTypeMismatch::new(
133                o,
134                FbxTryFromReason::InvalidAttributeFormat {
135                    name: ATTR_POLYGON_VERTEX_INDEX.to_string(),
136                    detail: format!("invalid int token: {}", temp_faces_result.unwrap_err()),
137                },
138            ));
139        };
140
141        let (vertices, face_vertex_counts, mapping_counts, mapping_offsets, mappings) =
142            match expand_mesh_polygon_vertices(&vertices, &temp_faces) {
143                Ok(v) => v,
144                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
145            };
146        let vertex_count = vertices.len();
147
148        let mut normals = Vec::new();
149        if let Some(el) = attrs.extract_case_insensitive(ATTR_LAYER_ELEMENT_NORMAL) {
150            let map = el.get_children_distinct();
151            let mapping_ty = match map
152                .require_token_case_insensitive(ATTR_MAPPING_INFORMATION_TYPE)
153                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
154            {
155                Ok(s) => s,
156                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
157            };
158            let reference_ty = match map
159                .require_token_case_insensitive(ATTR_REFERENCE_INFORMATION_TYPE)
160                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
161            {
162                Ok(s) => s,
163                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
164            };
165
166            let normals_flat = match resolve_flat_f32_channel(
167                &map,
168                ResolveFlatF32ChannelParams {
169                    data_name: "Normals",
170                    index_name: "NormalsIndex",
171                    vertex_count,
172                    components: 3,
173                    mapping_counts: &mapping_counts,
174                    mapping_offsets: &mapping_offsets,
175                    mappings: &mappings,
176                    mapping_ty: mapping_ty,
177                    reference_ty: reference_ty,
178                },
179            ) {
180                Ok(v) => v,
181                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
182            };
183            normals = normals_flat
184                .chunks_exact(3)
185                .map(|c| [c[0], c[1], c[2]])
186                .collect();
187        }
188
189        let mut tangents = Vec::new();
190        if let Some(el) = attrs.extract_case_insensitive(ATTR_LAYER_ELEMENT_TANGENT) {
191            let map = el.get_children_distinct();
192            let (data_name, index_name) = if map.extract_case_insensitive("Tangents").is_some() {
193                ("Tangents", "TangentsIndex")
194            } else {
195                ("Tangent", "TangentIndex")
196            };
197            let mapping_ty = match map
198                .require_token_case_insensitive(ATTR_MAPPING_INFORMATION_TYPE)
199                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
200            {
201                Ok(s) => s,
202                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
203            };
204            let reference_ty = match map
205                .require_token_case_insensitive(ATTR_REFERENCE_INFORMATION_TYPE)
206                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
207            {
208                Ok(s) => s,
209                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
210            };
211            let tangents_flat = match resolve_flat_f32_channel(
212                &map,
213                ResolveFlatF32ChannelParams {
214                    data_name,
215                    index_name,
216                    vertex_count,
217                    components: 3,
218                    mapping_counts: &mapping_counts,
219                    mapping_offsets: &mapping_offsets,
220                    mappings: &mappings,
221                    mapping_ty: mapping_ty,
222                    reference_ty: reference_ty,
223                },
224            ) {
225                Ok(v) => v,
226                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
227            };
228            tangents = tangents_flat
229                .chunks_exact(3)
230                .map(|c| [c[0], c[1], c[2]])
231                .collect();
232        }
233
234        let mut binormals = Vec::new();
235        if let Some(el) = attrs.extract_case_insensitive(ATTR_LAYER_ELEMENT_BINORMAL) {
236            let map = el.get_children_distinct();
237            let (data_name, index_name) = if map.extract_case_insensitive("Binormals").is_some() {
238                ("Binormals", "BinormalsIndex")
239            } else {
240                ("Binormal", "BinormalIndex")
241            };
242            let mapping_ty = match map
243                .require_token_case_insensitive(ATTR_MAPPING_INFORMATION_TYPE)
244                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
245            {
246                Ok(s) => s,
247                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
248            };
249            let reference_ty = match map
250                .require_token_case_insensitive(ATTR_REFERENCE_INFORMATION_TYPE)
251                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
252            {
253                Ok(s) => s,
254                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
255            };
256            let binormals_flat = match resolve_flat_f32_channel(
257                &map,
258                ResolveFlatF32ChannelParams {
259                    data_name,
260                    index_name,
261                    vertex_count,
262                    components: 3,
263                    mapping_counts: &mapping_counts,
264                    mapping_offsets: &mapping_offsets,
265                    mappings: &mappings,
266                    mapping_ty: mapping_ty,
267                    reference_ty: reference_ty,
268                },
269            ) {
270                Ok(v) => v,
271                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
272            };
273            binormals = binormals_flat
274                .chunks_exact(3)
275                .map(|c| [c[0], c[1], c[2]])
276                .collect();
277        }
278
279        let mut texture_coords: [Vec<[f32; 2]>; MAX_UV_CHANNELS] = Default::default();
280        let mut texture_coord_names: [String; MAX_UV_CHANNELS] = Default::default();
281
282        if let Some(el) = attrs.extract_case_insensitive(ATTR_LAYER_ELEMENT_UV) {
283            let map = el.get_children_distinct();
284            if let Ok(Some(name)) = map.optional_token_case_insensitive("Name") {
285                texture_coord_names[0] = name
286                    .trim()
287                    .trim_matches(|c| c == '"' || c == '\'')
288                    .to_string();
289            }
290            let mapping_ty = match map
291                .require_token_case_insensitive(ATTR_MAPPING_INFORMATION_TYPE)
292                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
293            {
294                Ok(s) => s,
295                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
296            };
297            let reference_ty = match map
298                .require_token_case_insensitive(ATTR_REFERENCE_INFORMATION_TYPE)
299                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
300            {
301                Ok(s) => s,
302                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
303            };
304            let uv_flat = match resolve_flat_f32_channel(
305                &map,
306                ResolveFlatF32ChannelParams {
307                    data_name: "UV",
308                    index_name: "UVIndex",
309                    vertex_count,
310                    components: 2,
311                    mapping_counts: &mapping_counts,
312                    mapping_offsets: &mapping_offsets,
313                    mappings: &mappings,
314                    mapping_ty: mapping_ty,
315                    reference_ty: reference_ty,
316                },
317            ) {
318                Ok(v) => v,
319                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
320            };
321            texture_coords[0] = uv_flat.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
322        }
323
324        let mut vertex_colors: [Vec<[f32; 4]>; MAX_COLOR_SETS] = Default::default();
325        if let Some(el) = attrs.extract_case_insensitive("LayerElementColor") {
326            let map = el.get_children_distinct();
327            let mapping_ty = match map
328                .require_token_case_insensitive(ATTR_MAPPING_INFORMATION_TYPE)
329                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
330            {
331                Ok(s) => s,
332                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
333            };
334            let reference_ty = match map
335                .require_token_case_insensitive(ATTR_REFERENCE_INFORMATION_TYPE)
336                .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
337            {
338                Ok(s) => s,
339                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
340            };
341            let colors_flat = match resolve_flat_f32_channel(
342                &map,
343                ResolveFlatF32ChannelParams {
344                    data_name: "Colors",
345                    index_name: "ColorIndex",
346                    vertex_count,
347                    components: 4,
348                    mapping_counts: &mapping_counts,
349                    mapping_offsets: &mapping_offsets,
350                    mappings: &mappings,
351                    mapping_ty: mapping_ty,
352                    reference_ty: reference_ty,
353                },
354            ) {
355                Ok(v) => v,
356                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
357            };
358            vertex_colors[0] = colors_flat
359                .chunks_exact(4)
360                .map(|c| [c[0], c[1], c[2], c[3]])
361                .collect();
362        }
363
364        let material_indices =
365            if let Some(el) = attrs.extract_case_insensitive("LayerElementMaterial") {
366                let map = el.get_children_distinct();
367                let mapping_ty = match map
368                    .require_token_case_insensitive(ATTR_MAPPING_INFORMATION_TYPE)
369                    .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
370                {
371                    Ok(s) => s,
372                    Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
373                };
374                let reference_ty = match map
375                    .require_token_case_insensitive(ATTR_REFERENCE_INFORMATION_TYPE)
376                    .map(|s| s.trim().trim_matches(|c| c == '"' || c == '\''))
377                {
378                    Ok(s) => s,
379                    Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
380                };
381                match read_vertex_data_materials(
382                    &map,
383                    &face_vertex_counts,
384                    vertex_count,
385                    &mapping_ty,
386                    &reference_ty,
387                ) {
388                    Ok(v) => v,
389                    Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
390                }
391            } else {
392                Vec::new()
393            };
394
395        Ok(MeshGeometry {
396            object: o,
397            vertices,
398            face_vertex_counts,
399            normals,
400            tangents,
401            binormals,
402            texture_coords,
403            texture_coord_names,
404            vertex_colors,
405            material_indices,
406        })
407    }
408}
409
410/// Comma-separated float list from `attr` tokens, after optional ASCII **`a:`** child (see `ACCESSOR_KEY`).
411fn parse_f32_array(attr: &ElementAttribute) -> Result<Vec<f32>, ParseFloatError> {
412    let children = attr.get_children_distinct();
413    let payload = children.get(ACCESSOR_KEY).unwrap_or(attr);
414    let tokens = payload.get_tokens();
415    tokens
416        .iter()
417        .flat_map(|t| t.split(','))
418        .map(|t| t.trim())
419        .filter(|t| !t.is_empty())
420        .map(|t| t.parse::<f32>())
421        .collect()
422}
423
424/// Comma-separated `i32` list; same `a:` drill-down as [`parse_f32_array`].
425fn parse_i32_array(attr: &ElementAttribute) -> Result<Vec<i32>, ParseIntError> {
426    let children = attr.get_children_distinct();
427    let payload = children.get(ACCESSOR_KEY).unwrap_or(attr);
428    let tokens = payload.get_tokens();
429    tokens
430        .iter()
431        .flat_map(|t| t.split(','))
432        .map(|t| t.trim())
433        .filter(|t| !t.is_empty())
434        .map(|t| t.parse::<i32>())
435        .collect()
436}
437
438/// Expand FBX mesh indices into a **per-polygon-vertex** (“corner”) linear layout and build tables to
439/// remap **per-corner** data from FBX’s indexed vertex pool.
440///
441/// Mirrors Assimp [`FBXMeshGeometry`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXMeshGeometry.cpp)
442/// (`MeshGeometry` ctor after `ParseVectorDataArray`).
443///
444/// ## FBX encoding (`PolygonVertexIndex`)
445///
446/// Indices reference rows in `Vertices` (the control-point / vertex pool). The **last** index of each
447/// polygon is **negative**; its absolute value is still `absi = (-index - 1)`, marking the end of that
448/// face. Non-final corners use non-negative indices. Example triangle `0, 1, -3` uses pool vertices
449/// 0, 1, and 2 (`-3` → abs index 2).
450///
451/// ## Why expand?
452///
453/// Layer data (normals, UVs, …) is often stored **ByPolygonVertex** or **ByVertice** with a different
454/// indexing than the raw pool. Assimp duplicates pool positions so each **corner** gets its own row in
455/// `expanded_vertices` (length = number of polygon corners = `temp_faces.len()` in well-formed files).
456/// We must also know, for each pool index `i`, which contiguous slice of “corner slots” belongs to
457/// that pool vertex so channels can be scattered into the expanded order.
458///
459/// ## Outputs
460///
461/// - **`expanded_vertices`**: `temp_verts[absi]` appended once per corner, in `temp_faces` order
462///   (ignoring sign on the last index of each face).
463/// - **`face_vertex_counts`**: number of corners per polygon (from running `count` reset on each
464///   negative index).
465/// - **`mapping_offsets[i]`**: start offset in corner space for pool vertex `i` (prefix sum of how
466///   many corners reference pool vertex `i`).
467/// - **`mapping_counts`**: after this function returns, again the number of corners referencing each
468///   pool vertex `i` (same as after pass 1; rebuilt during pass 3).
469/// - **`mappings`**: length = corner count. For each corner in `temp_faces` order (global corner
470///   index `cursor`), `mappings[slot] = cursor` where `slot` is the next free slot in the slice
471///   `[mapping_offsets[absi] ..)` reserved for pool vertex `absi`. So `mappings` ties pool-vertex
472///   corner slots to the global expanded corner index for channel gather/scatter in
473///   [`resolve_flat_f32_channel`].
474///
475/// ## Three passes (same as Assimp)
476///
477/// 1. **Walk `temp_faces` once**: append expanded positions; increment `mapping_counts[absi]` per
478///    reference; accumulate polygon sizes into `face_vertex_counts` when hitting a negative index.
479/// 2. **Prefix-sum `mapping_counts` → `mapping_offsets`**, then zero `mapping_counts` (slots will be
480///    filled in pass 3).
481/// 3. **Walk `temp_faces` again**: for each corner, assign `mappings[mapping_offsets[absi] +
482///    mapping_counts[absi]++] = global_corner_index` so each pool vertex’s corners occupy a stable,
483///    contiguous block in corner index space.
484fn expand_mesh_polygon_vertices(
485    temp_verts: &[[f32; 3]],
486    temp_faces: &[i32],
487) -> Result<(Vec<[f32; 3]>, Vec<u32>, Vec<u32>, Vec<u32>, Vec<u32>), FbxTryFromReason> {
488    let vertex_count = temp_verts.len();
489    let mut mapping_counts = vec![0u32; vertex_count];
490    let mut expanded_vertices = Vec::new();
491    let mut face_vertex_counts = Vec::new();
492    let mut count = 0u32;
493
494    // Pass 1 — see module-level docs on [`expand_mesh_polygon_vertices`].
495    for &index in temp_faces {
496        // Get absolute index
497        let absi = if index < 0 {
498            (-index - 1) as usize
499        } else {
500            index as usize
501        };
502        if absi >= vertex_count {
503            return Err(FbxTryFromReason::InvalidAttributeFormat {
504                name: ATTR_POLYGON_VERTEX_INDEX.to_string(),
505                detail: format!("index {absi} out of range (vertex count {vertex_count})"),
506            });
507        }
508        // Add vertex to expanded vertices
509        expanded_vertices.push(temp_verts[absi]);
510        count += 1;
511        // Update the running count of how many vertices are expanded.
512        mapping_counts[absi] = mapping_counts[absi].saturating_add(1);
513        if index < 0 {
514            // Count the index difference between this and the last negative index.
515            face_vertex_counts.push(count);
516            count = 0;
517        }
518    }
519
520    let polygon_vertex_count = expanded_vertices.len();
521    // Pass 2 — prefix sums into mapping_offsets; clear mapping_counts for pass 3 slot filling.
522    let mut mapping_offsets = vec![0u32; vertex_count];
523    let mut cursor = 0u32;
524    for i in 0..vertex_count {
525        mapping_offsets[i] = cursor;
526        cursor += mapping_counts[i];
527        mapping_counts[i] = 0;
528    }
529
530    // Pass 3 — for each corner in order, assign stable slot in per-pool-vertex ranges (see doc above).
531    let mut mappings = vec![0u32; polygon_vertex_count];
532    cursor = 0;
533    for &index in temp_faces {
534        let absi = if index < 0 {
535            (-index - 1) as usize
536        } else {
537            index as usize
538        };
539        let slot = mapping_offsets[absi] + mapping_counts[absi];
540        mapping_counts[absi] += 1;
541        mappings[slot as usize] = cursor;
542        cursor += 1;
543    }
544
545    Ok((
546        expanded_vertices,
547        face_vertex_counts,
548        mapping_counts,
549        mapping_offsets,
550        mappings,
551    ))
552}
553
554pub struct ResolveFlatF32ChannelParams<'a> {
555    data_name: &'a str,
556    index_name: &'a str,
557    vertex_count: usize,
558    components: usize,
559    mapping_counts: &'a [u32],
560    mapping_offsets: &'a [u32],
561    mappings: &'a [u32],
562    mapping_ty: &'a str,
563    reference_ty: &'a str,
564}
565
566/// Turn one mesh layer attribute (e.g. normals, UVs) into a flat `f32` slice **one scalar wide per
567/// expanded corner × `components`**, matching Assimp-style expanded mesh layout.
568///
569/// FBX combines **mapping** and **reference** (see `mapping_ty` / `reference_ty`):
570/// - **ByVertice** — one logical value per **pool** vertex; we scatter it to every expanded corner
571///   that references that pool vertex using `mapping_offsets` / `mapping_counts` / `mappings`.
572/// - **ByPolygonVertex** — one value per polygon corner; output order is already corner-linear.
573/// - **Direct** — floats live in the data array in mapping order.
574/// - **IndexToDirect** — a parallel `i32` index array picks **which element** (group of `components`
575///   floats) to copy from the data array; index `-1` means “no value” (zeros for that corner’s group).
576///
577/// All copies use `src..src+components` and `dst..dst+components` so multi-component channels stay aligned.
578fn resolve_flat_f32_channel(
579    source: &HashMap<String, ElementAttribute>,
580    params: ResolveFlatF32ChannelParams<'_>,
581) -> Result<Vec<f32>, FbxTryFromReason> {
582    let mut is_direct = params.reference_ty.eq_ignore_ascii_case(REFERENCE_DIRECT);
583    let mut is_index_to_direct = params
584        .reference_ty
585        .eq_ignore_ascii_case(REFERENCE_INDEX_TO_DIRECT);
586    let has_data = source.extract_case_insensitive(params.data_name).is_some();
587    let has_index = source.extract_case_insensitive(params.index_name).is_some();
588    // Some files declare IndexToDirect but omit the index channel; treat as Direct (Assimp-style).
589    if is_index_to_direct && !has_index {
590        is_direct = true;
591        is_index_to_direct = false;
592    }
593
594    if params.components == 0 {
595        return Ok(Vec::new());
596    }
597    // Linear layout: corner `k` occupies `vertex_out[k*components .. (k+1)*components)`.
598    let mut vertex_out = vec![0f32; params.vertex_count * params.components];
599
600    let by_vertice = params.mapping_ty.eq_ignore_ascii_case(MAPPING_BY_VERTICE);
601    let by_polygon_vertex = params
602        .mapping_ty
603        .eq_ignore_ascii_case(MAPPING_BY_POLYGON_VERTEX);
604
605    if by_vertice && is_direct {
606        // Case 1: ByVertice + Direct — `channel_data` is one vector per pool vertex; broadcast each
607        // to every expanded corner slot that references that pool vertex.
608        if !has_data {
609            return Ok(Vec::new());
610        }
611        let channel_attribute = source.extract_case_insensitive(params.data_name).ok_or(
612            FbxTryFromReason::InvalidAttributeFormat {
613                name: params.data_name.to_string(),
614                detail: "data channel not found".to_string(),
615            },
616        )?;
617        let channel_data_result = parse_f32_array(&channel_attribute);
618        let Ok(channel_data) = channel_data_result else {
619            return Err(FbxTryFromReason::InvalidAttributeFormat {
620                name: params.data_name.to_string(),
621                detail: format!("invalid float token: {}", channel_data_result.unwrap_err()),
622            });
623        };
624        if channel_data.len() != params.mapping_offsets.len() * params.components {
625            return Err(FbxTryFromReason::InvalidAttributeFormat {
626                name: params.data_name.to_string(),
627                detail: format!(
628                    "{} {}: expected {} floats, got {}",
629                    MAPPING_BY_VERTICE,
630                    REFERENCE_DIRECT,
631                    params.mapping_offsets.len() * params.components,
632                    channel_data.len()
633                ),
634            });
635        }
636        for i in 0..params.mapping_offsets.len() {
637            // Pool vertex `i` owns `channel_data[i*components ..)`; copy that block to each corner.
638            let src = i * params.components;
639            let istart = params.mapping_offsets[i] as usize;
640            let iend = istart + params.mapping_counts[i] as usize;
641            for j in istart..iend {
642                // `mappings[j]` is the global expanded-corner index for this use of pool vertex `i`.
643                let dst = params.mappings[j] as usize * params.components;
644                if src + params.components > channel_data.len()
645                    || dst + params.components > vertex_out.len()
646                {
647                    return Err(FbxTryFromReason::InvalidAttributeFormat {
648                        name: params.data_name.to_string(),
649                        detail: format!("length mismatch for {MAPPING_BY_VERTICE}"),
650                    });
651                }
652                vertex_out[dst..dst + params.components]
653                    .copy_from_slice(&channel_data[src..src + params.components]);
654            }
655        }
656    } else if by_vertice && is_index_to_direct {
657        // Case 2: ByVertice + IndexToDirect — same scatter as case 1, but each pool vertex picks a
658        // data element index first (`channel_index_data[i]`); that element is a `components`-wide slice.
659        if !has_data || !has_index {
660            return Ok(Vec::new());
661        }
662
663        let channel_attribute = source.extract_case_insensitive(params.data_name).ok_or(
664            FbxTryFromReason::InvalidAttributeFormat {
665                name: params.data_name.to_string(),
666                detail: "data channel not found".to_string(),
667            },
668        )?;
669        let channel_data_result = parse_f32_array(&channel_attribute);
670        let Ok(channel_data) = channel_data_result else {
671            return Err(FbxTryFromReason::InvalidAttributeFormat {
672                name: params.data_name.to_string(),
673                detail: format!("invalid float token: {}", channel_data_result.unwrap_err()),
674            });
675        };
676        let channel_index_attribute = source.extract_case_insensitive(params.index_name).ok_or(
677            FbxTryFromReason::InvalidAttributeFormat {
678                name: params.index_name.to_string(),
679                detail: "index channel not found".to_string(),
680            },
681        )?;
682        let channel_index_data_result = parse_i32_array(&channel_index_attribute);
683        let Ok(channel_index_data) = channel_index_data_result else {
684            return Err(FbxTryFromReason::InvalidAttributeFormat {
685                name: params.index_name.to_string(),
686                detail: format!(
687                    "invalid int token: {}",
688                    channel_index_data_result.unwrap_err()
689                ),
690            });
691        };
692        if channel_index_data.len() != params.mapping_offsets.len() {
693            return Err(FbxTryFromReason::InvalidAttributeFormat {
694                name: params.index_name.to_string(),
695                detail: format!("length mismatch for {MAPPING_BY_VERTICE}"),
696            });
697        }
698        for i in 0..params.mapping_offsets.len() {
699            let idx = channel_index_data[i] as usize;
700            let src = idx * params.components; // element `idx` in the direct table
701            let istart = params.mapping_offsets[i] as usize;
702            let iend = istart + params.mapping_counts[i] as usize;
703            for j in istart..iend {
704                let dst = params.mappings[j] as usize * params.components;
705                if src + params.components > channel_data.len()
706                    || dst + params.components > vertex_out.len()
707                {
708                    return Err(FbxTryFromReason::InvalidAttributeFormat {
709                        name: params.data_name.to_string(),
710                        detail: format!("length mismatch for {MAPPING_BY_VERTICE}"),
711                    });
712                }
713                vertex_out[dst..dst + params.components]
714                    .copy_from_slice(&channel_data[src..src + params.components]);
715            }
716        }
717    } else if by_polygon_vertex && is_direct {
718        // Case 3: ByPolygonVertex + Direct — floats are already in expanded-corner order; one memcpy.
719        if !has_data {
720            return Ok(Vec::new());
721        }
722        let channel_attribute = source.extract_case_insensitive(params.data_name).ok_or(
723            FbxTryFromReason::InvalidAttributeFormat {
724                name: params.data_name.to_string(),
725                detail: "data channel not found".to_string(),
726            },
727        )?;
728        let channel_data_result = parse_f32_array(&channel_attribute);
729        let Ok(channel_data) = channel_data_result else {
730            return Err(FbxTryFromReason::InvalidAttributeFormat {
731                name: params.data_name.to_string(),
732                detail: format!("invalid float token: {}", channel_data_result.unwrap_err()),
733            });
734        };
735        if channel_data.len() != params.vertex_count * params.components {
736            return Err(FbxTryFromReason::InvalidAttributeFormat {
737                name: params.data_name.to_string(),
738                detail: format!(
739                    "{} {}: expected {} floats, got {}",
740                    MAPPING_BY_POLYGON_VERTEX,
741                    REFERENCE_DIRECT,
742                    params.vertex_count * params.components,
743                    channel_data.len()
744                ),
745            });
746        }
747        vertex_out = channel_data;
748    } else if by_polygon_vertex && is_index_to_direct {
749        // Case 4: ByPolygonVertex + IndexToDirect — one index per corner; each index selects a
750        // `components`-wide slice in `channel_data`, laid out at `slot * components` in `vertex_out`.
751        if !has_data || !has_index {
752            return Ok(Vec::new());
753        }
754        // Get data and attributes for the data and index keys.
755        let channel_attribute = source.extract_case_insensitive(params.data_name).ok_or(
756            FbxTryFromReason::InvalidAttributeFormat {
757                name: params.data_name.to_string(),
758                detail: "data channel not found".to_string(),
759            },
760        )?;
761        let channel_data_result = parse_f32_array(&channel_attribute);
762        let Ok(channel_data) = channel_data_result else {
763            return Err(FbxTryFromReason::InvalidAttributeFormat {
764                name: params.data_name.to_string(),
765                detail: format!("invalid float token: {}", channel_data_result.unwrap_err()),
766            });
767        };
768        let channel_index_attribute = source.extract_case_insensitive(params.index_name).ok_or(
769            FbxTryFromReason::InvalidAttributeFormat {
770                name: params.index_name.to_string(),
771                detail: "index channel not found".to_string(),
772            },
773        )?;
774        let channel_index_data_result = parse_i32_array(&channel_index_attribute);
775        let Ok(mut channel_index_data) = channel_index_data_result else {
776            return Err(FbxTryFromReason::InvalidAttributeFormat {
777                name: params.index_name.to_string(),
778                detail: format!(
779                    "invalid int token: {}",
780                    channel_index_data_result.unwrap_err()
781                ),
782            });
783        };
784        if channel_index_data.len() > params.vertex_count {
785            channel_index_data.truncate(params.vertex_count);
786        }
787        // After optional truncation, require exactly one index per expanded corner.
788        if channel_index_data.len() != params.vertex_count {
789            return Err(FbxTryFromReason::InvalidAttributeFormat {
790                name: params.index_name.to_string(),
791                detail: format!(
792                    "{} {}: expected {} indices, got {}",
793                    MAPPING_BY_POLYGON_VERTEX,
794                    REFERENCE_INDEX_TO_DIRECT,
795                    params.vertex_count,
796                    channel_index_data.len()
797                ),
798            });
799        }
800        for (slot, &i) in channel_index_data.iter().enumerate() {
801            let dst = slot * params.components; // corner `slot` in expanded order
802            if dst + params.components > vertex_out.len() {
803                return Err(FbxTryFromReason::InvalidAttributeFormat {
804                    name: params.data_name.to_string(),
805                    detail: format!("length mismatch for {MAPPING_BY_POLYGON_VERTEX}"),
806                });
807            }
808            if i == -1 {
809                vertex_out[dst..dst + params.components].fill(0.0);
810                continue;
811            }
812            let src = i as usize * params.components; // direct-table element `i`
813            if src + params.components > channel_data.len() {
814                return Err(FbxTryFromReason::InvalidAttributeFormat {
815                    name: params.data_name.to_string(),
816                    detail: format!("length mismatch for {MAPPING_BY_POLYGON_VERTEX}"),
817                });
818            }
819            vertex_out[dst..dst + params.components]
820                .copy_from_slice(&channel_data[src..src + params.components]);
821        }
822    } else {
823        // Case 5: Unsupported mapping/reference pair — caller gets empty channel (Assimp skips similarly).
824        return Ok(Vec::new());
825    }
826    Ok(vertex_out)
827}
828
829/// Assimp `ReadVertexDataMaterials` (subset): `AllSame` and `ByPolygon` + `IndexToDirect`.
830fn read_vertex_data_materials(
831    source: &HashMap<String, ElementAttribute>,
832    face_vertex_counts: &[u32],
833    polygon_vertex_count: usize,
834    mapping_ty: &str,
835    reference_ty: &str,
836) -> Result<Vec<i32>, FbxTryFromReason> {
837    // Face count guard
838    let face_count = face_vertex_counts.len();
839    if face_count == 0 {
840        return Ok(Vec::new());
841    }
842
843    // Extract Materials
844    let Some(mat_el) = source.extract_case_insensitive("Materials") else {
845        return Ok(Vec::new());
846    };
847    let materials_out_result = parse_i32_array(mat_el);
848    let Ok(materials_out) = materials_out_result else {
849        return Err(FbxTryFromReason::InvalidAttributeFormat {
850            name: ATTR_MATERIALS.to_string(),
851            detail: format!("invalid int token: {}", materials_out_result.unwrap_err()),
852        });
853    };
854
855    if mapping_ty.eq_ignore_ascii_case(MAPPING_ALL_SAME) {
856        // Case 1: Map type is AllSame
857        // All materials are the same, so return a mapping of all polygons to the same material.
858        if materials_out.is_empty() {
859            return Ok(Vec::new());
860        }
861        let count_neg = materials_out.iter().filter(|&&n| n < 0).count();
862        if count_neg == materials_out.len() {
863            return Ok(Vec::new());
864        }
865        let v = materials_out[0];
866        Ok(vec![v; polygon_vertex_count])
867    } else if mapping_ty.eq_ignore_ascii_case(MAPPING_BY_POLYGON)
868        && reference_ty.eq_ignore_ascii_case(REFERENCE_INDEX_TO_DIRECT)
869    {
870        // Case 2: Map type is ByPolygon and reference type is IndexToDirect
871        // The materials are indexed by the face index, so we need to map the materials to the vertices.
872        if materials_out.len() != face_count {
873            return Err(FbxTryFromReason::InvalidAttributeFormat {
874                name: ATTR_MATERIALS.to_string(),
875                detail: format!(
876                    "{}: expected {} material indices, got {}",
877                    MAPPING_BY_POLYGON,
878                    face_count,
879                    materials_out.len()
880                ),
881            });
882        }
883        let count_neg = materials_out.iter().filter(|&&n| n < 0).count();
884        if count_neg == materials_out.len() {
885            return Ok(Vec::new());
886        }
887        let mut per_corner = Vec::with_capacity(polygon_vertex_count);
888        for (&m, &n) in materials_out.iter().zip(face_vertex_counts.iter()) {
889            for _ in 0..n {
890                per_corner.push(m);
891            }
892        }
893        if per_corner.len() != polygon_vertex_count {
894            return Err(FbxTryFromReason::InvalidAttributeFormat {
895                name: ATTR_MATERIALS.to_string(),
896                detail: format!(
897                    "expanded material indices length {} != polygon vertex count {}",
898                    per_corner.len(),
899                    polygon_vertex_count
900                ),
901            });
902        }
903        Ok(per_corner)
904    } else {
905        Ok(Vec::new())
906    }
907}
908
909#[cfg(test)]
910mod tests {
911    use std::collections::HashMap;
912    use std::convert::TryFrom;
913
914    use fbxscii::{
915        Element, ElementAmphitheatre, ElementAttribute, LeafAttribute, SubTreeAttribute,
916    };
917
918    use crate::OwnedObject;
919
920    use super::super::{
921        FbxTryFromReason, GEOMETRY_LINE_CLASS_NAME, GEOMETRY_MESH_CLASS_NAME, GEOMETRY_TYPE_NAME,
922    };
923    use super::MeshGeometry;
924
925    fn leaf(tokens: &[&str]) -> ElementAttribute {
926        ElementAttribute::Leaf(Box::new(LeafAttribute {
927            key: String::new(),
928            tokens: tokens.iter().map(|s| (*s).to_string()).collect(),
929        }))
930    }
931
932    fn append_layer_string_child(
933        arena: &mut ElementAmphitheatre,
934        root_idx: usize,
935        key: &str,
936        token: &str,
937    ) {
938        let mut el = Element::new(key.to_string());
939        el.tokens = vec![token.to_string()];
940        el.parent_index = Some(root_idx);
941        let idx = arena.insert(el);
942        arena.get_mut(root_idx).unwrap().children.push(idx);
943    }
944
945    /// Same nesting as ASCII FBX (e.g. `duck.fbx`): mapping + reference under the layer root, then data.
946    fn layer_element_normal(mapping: &str, reference: &str, normals_csv: &str) -> ElementAttribute {
947        let mut arena = ElementAmphitheatre::new();
948        let root_idx = arena.insert(Element::new("LayerElementNormal".into()));
949        append_layer_string_child(
950            &mut arena,
951            root_idx,
952            super::ATTR_MAPPING_INFORMATION_TYPE,
953            mapping,
954        );
955        append_layer_string_child(
956            &mut arena,
957            root_idx,
958            super::ATTR_REFERENCE_INFORMATION_TYPE,
959            reference,
960        );
961        let mut normals_el = Element::new("Normals".into());
962        normals_el.tokens = vec![normals_csv.to_string()];
963        normals_el.parent_index = Some(root_idx);
964        let normals_idx = arena.insert(normals_el);
965        arena.get_mut(root_idx).unwrap().children.push(normals_idx);
966
967        ElementAttribute::SubTree(Box::new(SubTreeAttribute {
968            amphitheatre: arena,
969            root_element_index: root_idx,
970        }))
971    }
972
973    fn layer_element_material(
974        mapping: &str,
975        reference: &str,
976        materials_csv: &str,
977    ) -> ElementAttribute {
978        let mut arena = ElementAmphitheatre::new();
979        let root_idx = arena.insert(Element::new("LayerElementMaterial".into()));
980        append_layer_string_child(
981            &mut arena,
982            root_idx,
983            super::ATTR_MAPPING_INFORMATION_TYPE,
984            mapping,
985        );
986        append_layer_string_child(
987            &mut arena,
988            root_idx,
989            super::ATTR_REFERENCE_INFORMATION_TYPE,
990            reference,
991        );
992        let mut mats_el = Element::new(super::ATTR_MATERIALS.into());
993        mats_el.tokens = vec![materials_csv.to_string()];
994        mats_el.parent_index = Some(root_idx);
995        let mats_idx = arena.insert(mats_el);
996        arena.get_mut(root_idx).unwrap().children.push(mats_idx);
997
998        ElementAttribute::SubTree(Box::new(SubTreeAttribute {
999            amphitheatre: arena,
1000            root_element_index: root_idx,
1001        }))
1002    }
1003
1004    fn owned_mesh(attrs: HashMap<String, ElementAttribute>) -> OwnedObject {
1005        OwnedObject {
1006            object_index: 11,
1007            name: "Geometry::TestMesh".into(),
1008            type_name: GEOMETRY_TYPE_NAME.into(),
1009            class_name: GEOMETRY_MESH_CLASS_NAME.into(),
1010            properties: HashMap::new(),
1011            attributes: attrs,
1012            connected_object_ids: vec![],
1013            object_property_targets: vec![],
1014            pp_property_targets: HashMap::new(),
1015        }
1016    }
1017
1018    fn minimal_base_attrs() -> HashMap<String, ElementAttribute> {
1019        let mut attrs = HashMap::new();
1020        attrs.insert("Vertices".into(), leaf(&["0,0,0,1,0,0,0,1,0"]));
1021        attrs.insert("PolygonVertexIndex".into(), leaf(&["0,1,-3"]));
1022        attrs
1023    }
1024
1025    #[test]
1026    fn minimal_triangle_expands_corners() {
1027        let attrs = minimal_base_attrs();
1028        let mesh = MeshGeometry::try_from(owned_mesh(attrs)).unwrap();
1029        assert_eq!(mesh.vertices.len(), 3);
1030        assert_eq!(mesh.face_vertex_counts, vec![3u32]);
1031        assert!(mesh.normals.is_empty());
1032        assert!(mesh.material_indices.is_empty());
1033    }
1034
1035    #[test]
1036    fn mapping_and_reference_trim_quotes_and_lowercase_keys() {
1037        let mut attrs = minimal_base_attrs();
1038        let mut arena = ElementAmphitheatre::new();
1039        let root_idx = arena.insert(Element::new("LayerElementNormal".into()));
1040        append_layer_string_child(
1041            &mut arena,
1042            root_idx,
1043            "mappinginformationtype",
1044            "  \"ByPolygonVertex\"  ",
1045        );
1046        append_layer_string_child(&mut arena, root_idx, "referenceinformationtype", "'Direct'");
1047        let mut normals_el = Element::new("Normals".into());
1048        normals_el.tokens = vec!["0,0,1,0,0,1,0,0,1".to_string()];
1049        normals_el.parent_index = Some(root_idx);
1050        let normals_idx = arena.insert(normals_el);
1051        arena.get_mut(root_idx).unwrap().children.push(normals_idx);
1052        attrs.insert(
1053            "LayerElementNormal".into(),
1054            ElementAttribute::SubTree(Box::new(SubTreeAttribute {
1055                amphitheatre: arena,
1056                root_element_index: root_idx,
1057            })),
1058        );
1059        let mesh = MeshGeometry::try_from(owned_mesh(attrs)).unwrap();
1060        assert_eq!(mesh.vertices.len(), 3);
1061        assert_eq!(mesh.normals.len(), 3);
1062    }
1063
1064    #[test]
1065    fn normals_by_polygon_vertex_direct() {
1066        let mut attrs = minimal_base_attrs();
1067        attrs.insert(
1068            "LayerElementNormal".into(),
1069            layer_element_normal("ByPolygonVertex", "Direct", "0,0,1,0,0,1,0,0,1"),
1070        );
1071        let mesh = MeshGeometry::try_from(owned_mesh(attrs)).unwrap();
1072        assert_eq!(
1073            mesh.normals,
1074            vec![[0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0],]
1075        );
1076    }
1077
1078    #[test]
1079    fn materials_all_same_replicates_first_index() {
1080        let mut attrs = minimal_base_attrs();
1081        attrs.insert(
1082            "LayerElementMaterial".into(),
1083            layer_element_material("AllSame", "IndexToDirect", "5"),
1084        );
1085        let mesh = MeshGeometry::try_from(owned_mesh(attrs)).unwrap();
1086        assert_eq!(mesh.material_indices, vec![5, 5, 5]);
1087    }
1088
1089    #[test]
1090    fn wrong_object_kind_line_geometry() {
1091        let o = OwnedObject {
1092            object_index: 1,
1093            name: "G".into(),
1094            type_name: GEOMETRY_TYPE_NAME.into(),
1095            class_name: GEOMETRY_LINE_CLASS_NAME.into(),
1096            properties: HashMap::new(),
1097            attributes: HashMap::new(),
1098            connected_object_ids: vec![],
1099            object_property_targets: vec![],
1100            pp_property_targets: HashMap::new(),
1101        };
1102        let err = MeshGeometry::try_from(o).unwrap_err();
1103        assert!(matches!(
1104            err.reason,
1105            FbxTryFromReason::WrongObjectKind { ref expected, .. } if expected == "MeshGeometry"
1106        ));
1107    }
1108
1109    #[test]
1110    fn missing_mapping_information_type() {
1111        let mut attrs = minimal_base_attrs();
1112        let mut arena = ElementAmphitheatre::new();
1113        let root_idx = arena.insert(Element::new("LayerElementNormal".into()));
1114        append_layer_string_child(
1115            &mut arena,
1116            root_idx,
1117            super::ATTR_REFERENCE_INFORMATION_TYPE,
1118            "Direct",
1119        );
1120        let mut normals_el = Element::new("Normals".into());
1121        normals_el.tokens = vec!["0,0,1,0,0,1,0,0,1".to_string()];
1122        normals_el.parent_index = Some(root_idx);
1123        let normals_idx = arena.insert(normals_el);
1124        arena.get_mut(root_idx).unwrap().children.push(normals_idx);
1125        attrs.insert(
1126            "LayerElementNormal".into(),
1127            ElementAttribute::SubTree(Box::new(SubTreeAttribute {
1128                amphitheatre: arena,
1129                root_element_index: root_idx,
1130            })),
1131        );
1132        let err = MeshGeometry::try_from(owned_mesh(attrs)).unwrap_err();
1133        assert!(matches!(
1134            err.reason,
1135            FbxTryFromReason::MissingAttribute { ref name } if name == "MappingInformationType"
1136        ));
1137    }
1138
1139    #[test]
1140    fn missing_reference_information_type() {
1141        let mut attrs = minimal_base_attrs();
1142        let mut arena = ElementAmphitheatre::new();
1143        let root_idx = arena.insert(Element::new("LayerElementNormal".into()));
1144        append_layer_string_child(
1145            &mut arena,
1146            root_idx,
1147            super::ATTR_MAPPING_INFORMATION_TYPE,
1148            "ByPolygonVertex",
1149        );
1150        let mut normals_el = Element::new("Normals".into());
1151        normals_el.tokens = vec!["0,0,1,0,0,1,0,0,1".to_string()];
1152        normals_el.parent_index = Some(root_idx);
1153        let normals_idx = arena.insert(normals_el);
1154        arena.get_mut(root_idx).unwrap().children.push(normals_idx);
1155        attrs.insert(
1156            "LayerElementNormal".into(),
1157            ElementAttribute::SubTree(Box::new(SubTreeAttribute {
1158                amphitheatre: arena,
1159                root_element_index: root_idx,
1160            })),
1161        );
1162        let err = MeshGeometry::try_from(owned_mesh(attrs)).unwrap_err();
1163        assert!(matches!(
1164            err.reason,
1165            FbxTryFromReason::MissingAttribute { ref name } if name == "ReferenceInformationType"
1166        ));
1167    }
1168}