Skip to main content

oxihuman_export/
vrm_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! VRM 1.0 export — glTF 2.0 + VRMC extensions for humanoid avatars.
5//!
6//! Produces a valid GLB binary (`.vrm`) with the `VRMC_vrm` extension
7//! containing humanoid bone mapping, avatar metadata, and optional
8//! blend shape (expression) data.
9
10use serde_json::json;
11
12// ── GLB constants ────────────────────────────────────────────────────────────
13
14const GLB_MAGIC: u32 = 0x46546C67; // "glTF"
15const GLB_VERSION: u32 = 2;
16const CHUNK_JSON: u32 = 0x4E4F534A; // "JSON"
17const CHUNK_BIN: u32 = 0x004E4942; // "BIN\0"
18
19// ── VRM bone names ───────────────────────────────────────────────────────────
20
21/// All humanoid bone names defined in the VRM 1.0 specification.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum VrmBoneName {
24    Hips,
25    Spine,
26    Chest,
27    UpperChest,
28    Neck,
29    Head,
30    LeftUpperArm,
31    LeftLowerArm,
32    LeftHand,
33    RightUpperArm,
34    RightLowerArm,
35    RightHand,
36    LeftUpperLeg,
37    LeftLowerLeg,
38    LeftFoot,
39    RightUpperLeg,
40    RightLowerLeg,
41    RightFoot,
42    LeftThumbProximal,
43    LeftThumbIntermediate,
44    LeftThumbDistal,
45    LeftIndexProximal,
46    LeftIndexIntermediate,
47    LeftIndexDistal,
48    LeftMiddleProximal,
49    LeftMiddleIntermediate,
50    LeftMiddleDistal,
51    LeftRingProximal,
52    LeftRingIntermediate,
53    LeftRingDistal,
54    LeftLittleProximal,
55    LeftLittleIntermediate,
56    LeftLittleDistal,
57    RightThumbProximal,
58    RightThumbIntermediate,
59    RightThumbDistal,
60    RightIndexProximal,
61    RightIndexIntermediate,
62    RightIndexDistal,
63    RightMiddleProximal,
64    RightMiddleIntermediate,
65    RightMiddleDistal,
66    RightRingProximal,
67    RightRingIntermediate,
68    RightRingDistal,
69    RightLittleProximal,
70    RightLittleIntermediate,
71    RightLittleDistal,
72    LeftEye,
73    RightEye,
74    Jaw,
75    LeftShoulder,
76    RightShoulder,
77    LeftToes,
78    RightToes,
79}
80
81impl VrmBoneName {
82    /// Returns the VRM 1.0 spec camelCase string for this bone.
83    pub fn as_str(&self) -> &'static str {
84        match self {
85            Self::Hips => "hips",
86            Self::Spine => "spine",
87            Self::Chest => "chest",
88            Self::UpperChest => "upperChest",
89            Self::Neck => "neck",
90            Self::Head => "head",
91            Self::LeftUpperArm => "leftUpperArm",
92            Self::LeftLowerArm => "leftLowerArm",
93            Self::LeftHand => "leftHand",
94            Self::RightUpperArm => "rightUpperArm",
95            Self::RightLowerArm => "rightLowerArm",
96            Self::RightHand => "rightHand",
97            Self::LeftUpperLeg => "leftUpperLeg",
98            Self::LeftLowerLeg => "leftLowerLeg",
99            Self::LeftFoot => "leftFoot",
100            Self::RightUpperLeg => "rightUpperLeg",
101            Self::RightLowerLeg => "rightLowerLeg",
102            Self::RightFoot => "rightFoot",
103            Self::LeftThumbProximal => "leftThumbMetacarpal",
104            Self::LeftThumbIntermediate => "leftThumbProximal",
105            Self::LeftThumbDistal => "leftThumbDistal",
106            Self::LeftIndexProximal => "leftIndexProximal",
107            Self::LeftIndexIntermediate => "leftIndexIntermediate",
108            Self::LeftIndexDistal => "leftIndexDistal",
109            Self::LeftMiddleProximal => "leftMiddleProximal",
110            Self::LeftMiddleIntermediate => "leftMiddleIntermediate",
111            Self::LeftMiddleDistal => "leftMiddleDistal",
112            Self::LeftRingProximal => "leftRingProximal",
113            Self::LeftRingIntermediate => "leftRingIntermediate",
114            Self::LeftRingDistal => "leftRingDistal",
115            Self::LeftLittleProximal => "leftLittleProximal",
116            Self::LeftLittleIntermediate => "leftLittleIntermediate",
117            Self::LeftLittleDistal => "leftLittleDistal",
118            Self::RightThumbProximal => "rightThumbMetacarpal",
119            Self::RightThumbIntermediate => "rightThumbProximal",
120            Self::RightThumbDistal => "rightThumbDistal",
121            Self::RightIndexProximal => "rightIndexProximal",
122            Self::RightIndexIntermediate => "rightIndexIntermediate",
123            Self::RightIndexDistal => "rightIndexDistal",
124            Self::RightMiddleProximal => "rightMiddleProximal",
125            Self::RightMiddleIntermediate => "rightMiddleIntermediate",
126            Self::RightMiddleDistal => "rightMiddleDistal",
127            Self::RightRingProximal => "rightRingProximal",
128            Self::RightRingIntermediate => "rightRingIntermediate",
129            Self::RightRingDistal => "rightRingDistal",
130            Self::RightLittleProximal => "rightLittleProximal",
131            Self::RightLittleIntermediate => "rightLittleIntermediate",
132            Self::RightLittleDistal => "rightLittleDistal",
133            Self::LeftEye => "leftEye",
134            Self::RightEye => "rightEye",
135            Self::Jaw => "jaw",
136            Self::LeftShoulder => "leftShoulder",
137            Self::RightShoulder => "rightShoulder",
138            Self::LeftToes => "leftToes",
139            Self::RightToes => "rightToes",
140        }
141    }
142
143    /// Returns `true` if this bone is required by the VRM 1.0 specification.
144    pub fn is_required(&self) -> bool {
145        matches!(
146            self,
147            Self::Hips
148                | Self::Spine
149                | Self::Chest
150                | Self::Neck
151                | Self::Head
152                | Self::LeftUpperArm
153                | Self::LeftLowerArm
154                | Self::LeftHand
155                | Self::RightUpperArm
156                | Self::RightLowerArm
157                | Self::RightHand
158                | Self::LeftUpperLeg
159                | Self::LeftLowerLeg
160                | Self::LeftFoot
161                | Self::RightUpperLeg
162                | Self::RightLowerLeg
163                | Self::RightFoot
164        )
165    }
166
167    /// Returns the complete list of all required VRM bone names.
168    pub fn all_required() -> &'static [VrmBoneName] {
169        &[
170            Self::Hips,
171            Self::Spine,
172            Self::Chest,
173            Self::Neck,
174            Self::Head,
175            Self::LeftUpperArm,
176            Self::LeftLowerArm,
177            Self::LeftHand,
178            Self::RightUpperArm,
179            Self::RightLowerArm,
180            Self::RightHand,
181            Self::LeftUpperLeg,
182            Self::LeftLowerLeg,
183            Self::LeftFoot,
184            Self::RightUpperLeg,
185            Self::RightLowerLeg,
186            Self::RightFoot,
187        ]
188    }
189}
190
191// ── VRM humanoid ─────────────────────────────────────────────────────────────
192
193/// A single bone mapping: bone name to glTF node index.
194#[derive(Debug, Clone)]
195pub struct VrmHumanBone {
196    pub name: VrmBoneName,
197    pub node_index: usize,
198}
199
200/// VRM humanoid bone mapping — maps VRM bone identifiers to glTF node indices.
201#[derive(Debug, Clone)]
202pub struct VrmHumanoid {
203    pub bones: Vec<VrmHumanBone>,
204}
205
206impl VrmHumanoid {
207    /// Validates that all required bones are present and node indices are unique.
208    pub fn validate(&self) -> anyhow::Result<()> {
209        for req in VrmBoneName::all_required() {
210            if !self.bones.iter().any(|b| b.name == *req) {
211                anyhow::bail!(
212                    "required VRM bone '{}' is missing from humanoid mapping",
213                    req.as_str()
214                );
215            }
216        }
217        let mut seen_indices = std::collections::HashSet::new();
218        for bone in &self.bones {
219            if !seen_indices.insert(bone.node_index) {
220                anyhow::bail!(
221                    "duplicate node index {} for bone '{}'",
222                    bone.node_index,
223                    bone.name.as_str()
224                );
225            }
226        }
227        Ok(())
228    }
229
230    /// Builds the `humanBones` JSON object for VRMC_vrm.
231    fn to_json(&self) -> serde_json::Value {
232        let mut bones_obj = serde_json::Map::new();
233        for bone in &self.bones {
234            bones_obj.insert(
235                bone.name.as_str().to_string(),
236                json!({ "node": bone.node_index }),
237            );
238        }
239        json!({ "humanBones": bones_obj })
240    }
241}
242
243// ── VRM meta ─────────────────────────────────────────────────────────────────
244
245/// Commercial usage permission level.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum VrmCommercialUsage {
248    PersonalNonProfit,
249    PersonalProfit,
250    Corporation,
251}
252
253impl VrmCommercialUsage {
254    fn as_str(&self) -> &'static str {
255        match self {
256            Self::PersonalNonProfit => "personalNonProfit",
257            Self::PersonalProfit => "personalProfit",
258            Self::Corporation => "corporation",
259        }
260    }
261}
262
263/// Credit notation requirement.
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum VrmCreditNotation {
266    Required,
267    Unnecessary,
268}
269
270impl VrmCreditNotation {
271    fn as_str(&self) -> &'static str {
272        match self {
273            Self::Required => "required",
274            Self::Unnecessary => "unnecessary",
275        }
276    }
277}
278
279/// Model modification permission.
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub enum VrmModification {
282    Prohibited,
283    AllowModification,
284    AllowModificationRedistribution,
285}
286
287impl VrmModification {
288    fn as_str(&self) -> &'static str {
289        match self {
290            Self::Prohibited => "prohibited",
291            Self::AllowModification => "allowModification",
292            Self::AllowModificationRedistribution => "allowModificationRedistribution",
293        }
294    }
295}
296
297/// VRM 1.0 avatar metadata (the `meta` block inside `VRMC_vrm`).
298#[derive(Debug, Clone)]
299pub struct VrmMeta {
300    pub name: String,
301    pub version: String,
302    pub authors: Vec<String>,
303    pub license_url: String,
304    pub allow_antisocial_actions: bool,
305    pub allow_political_or_religious_usage: bool,
306    pub allow_excessively_violent_usage: bool,
307    pub allow_excessively_sexual_usage: bool,
308    pub commercial_usage: VrmCommercialUsage,
309    pub credit_notation: VrmCreditNotation,
310    pub modification: VrmModification,
311}
312
313impl VrmMeta {
314    /// Creates a default meta with CC-BY-4.0 license and permissive settings.
315    pub fn default_cc_by(name: &str) -> Self {
316        Self {
317            name: name.to_string(),
318            version: "1.0".to_string(),
319            authors: vec!["OxiHuman".to_string()],
320            license_url: "https://creativecommons.org/licenses/by/4.0/".to_string(),
321            allow_antisocial_actions: false,
322            allow_political_or_religious_usage: false,
323            allow_excessively_violent_usage: false,
324            allow_excessively_sexual_usage: false,
325            commercial_usage: VrmCommercialUsage::PersonalProfit,
326            credit_notation: VrmCreditNotation::Required,
327            modification: VrmModification::AllowModificationRedistribution,
328        }
329    }
330
331    /// Validates that meta fields are non-empty.
332    pub fn validate(&self) -> anyhow::Result<()> {
333        if self.name.trim().is_empty() {
334            anyhow::bail!("VRM meta name must not be empty");
335        }
336        if self.authors.is_empty() {
337            anyhow::bail!("VRM meta must have at least one author");
338        }
339        if self.license_url.trim().is_empty() {
340            anyhow::bail!("VRM meta license_url must not be empty");
341        }
342        Ok(())
343    }
344
345    /// Builds the `meta` JSON object for VRMC_vrm.
346    fn to_json(&self) -> serde_json::Value {
347        json!({
348            "name": self.name,
349            "version": self.version,
350            "authors": self.authors,
351            "licenseUrl": self.license_url,
352            "allowAntisocialActions": self.allow_antisocial_actions,
353            "allowPoliticalOrReligiousUsage": self.allow_political_or_religious_usage,
354            "allowExcessivelyViolentUsage": self.allow_excessively_violent_usage,
355            "allowExcessivelySexualUsage": self.allow_excessively_sexual_usage,
356            "commercialUsage": self.commercial_usage.as_str(),
357            "creditNotation": self.credit_notation.as_str(),
358            "modification": self.modification.as_str(),
359        })
360    }
361}
362
363// ── VRM exporter ─────────────────────────────────────────────────────────────
364
365/// VRM 1.0 exporter — builds a GLB binary with VRMC_vrm extensions.
366pub struct VrmExporter {
367    gltf_json: serde_json::Value,
368    binary_buffer: Vec<u8>,
369    has_mesh: bool,
370    has_skeleton: bool,
371    has_humanoid: bool,
372    has_meta: bool,
373    vertex_count: usize,
374    index_count: usize,
375    node_count: usize,
376}
377
378impl VrmExporter {
379    /// Creates a new VRM exporter with an empty glTF document.
380    pub fn new() -> Self {
381        let gltf_json = json!({
382            "asset": {
383                "version": "2.0",
384                "generator": "OxiHuman VRM Exporter 0.1.0"
385            },
386            "scene": 0,
387            "scenes": [{ "nodes": [] }],
388            "nodes": [],
389            "meshes": [],
390            "accessors": [],
391            "bufferViews": [],
392            "buffers": [{ "byteLength": 0 }],
393            "extensionsUsed": ["VRMC_vrm"],
394            "extensions": {
395                "VRMC_vrm": {
396                    "specVersion": "1.0"
397                }
398            }
399        });
400        Self {
401            gltf_json,
402            binary_buffer: Vec::new(),
403            has_mesh: false,
404            has_skeleton: false,
405            has_humanoid: false,
406            has_meta: false,
407            vertex_count: 0,
408            index_count: 0,
409            node_count: 0,
410        }
411    }
412
413    /// Sets the mesh geometry data.
414    pub fn set_mesh(
415        &mut self,
416        positions: &[[f64; 3]],
417        normals: &[[f64; 3]],
418        uvs: &[[f64; 2]],
419        triangles: &[[usize; 3]],
420    ) -> anyhow::Result<()> {
421        let n_verts = positions.len();
422        if normals.len() != n_verts {
423            anyhow::bail!(
424                "normals count ({}) must match positions count ({})",
425                normals.len(),
426                n_verts
427            );
428        }
429        if uvs.len() != n_verts {
430            anyhow::bail!(
431                "uvs count ({}) must match positions count ({})",
432                uvs.len(),
433                n_verts
434            );
435        }
436        if n_verts == 0 {
437            anyhow::bail!("mesh must have at least one vertex");
438        }
439        if triangles.is_empty() {
440            anyhow::bail!("mesh must have at least one triangle");
441        }
442
443        for (ti, tri) in triangles.iter().enumerate() {
444            for &idx in tri {
445                if idx >= n_verts {
446                    anyhow::bail!(
447                        "triangle {} has index {} which exceeds vertex count {}",
448                        ti,
449                        idx,
450                        n_verts
451                    );
452                }
453            }
454        }
455
456        self.binary_buffer.clear();
457
458        // Write positions as f32
459        let pos_offset = self.binary_buffer.len();
460        let mut pos_min = [f64::MAX; 3];
461        let mut pos_max = [f64::MIN; 3];
462        for pos in positions {
463            for axis in 0..3 {
464                if pos[axis] < pos_min[axis] {
465                    pos_min[axis] = pos[axis];
466                }
467                if pos[axis] > pos_max[axis] {
468                    pos_max[axis] = pos[axis];
469                }
470                let val = pos[axis] as f32;
471                self.binary_buffer.extend_from_slice(&val.to_le_bytes());
472            }
473        }
474        let pos_byte_len = self.binary_buffer.len() - pos_offset;
475
476        let norm_offset = self.binary_buffer.len();
477        for norm in normals {
478            for &component in norm.iter().take(3) {
479                let val = component as f32;
480                self.binary_buffer.extend_from_slice(&val.to_le_bytes());
481            }
482        }
483        let norm_byte_len = self.binary_buffer.len() - norm_offset;
484
485        let uv_offset = self.binary_buffer.len();
486        for uv in uvs {
487            for &component in uv.iter().take(2) {
488                let val = component as f32;
489                self.binary_buffer.extend_from_slice(&val.to_le_bytes());
490            }
491        }
492        let uv_byte_len = self.binary_buffer.len() - uv_offset;
493
494        let idx_offset = self.binary_buffer.len();
495        let n_indices = triangles.len() * 3;
496        for tri in triangles {
497            for &idx in tri {
498                let val = idx as u32;
499                self.binary_buffer.extend_from_slice(&val.to_le_bytes());
500            }
501        }
502        let idx_byte_len = self.binary_buffer.len() - idx_offset;
503
504        self.gltf_json["bufferViews"] = json!([
505            { "buffer": 0, "byteOffset": pos_offset,  "byteLength": pos_byte_len,  "target": 34962 },
506            { "buffer": 0, "byteOffset": norm_offset, "byteLength": norm_byte_len, "target": 34962 },
507            { "buffer": 0, "byteOffset": uv_offset,   "byteLength": uv_byte_len,   "target": 34962 },
508            { "buffer": 0, "byteOffset": idx_offset,  "byteLength": idx_byte_len,  "target": 34963 }
509        ]);
510
511        self.gltf_json["accessors"] = json!([
512            {
513                "bufferView": 0, "componentType": 5126, "count": n_verts, "type": "VEC3",
514                "min": [pos_min[0] as f32, pos_min[1] as f32, pos_min[2] as f32],
515                "max": [pos_max[0] as f32, pos_max[1] as f32, pos_max[2] as f32]
516            },
517            { "bufferView": 1, "componentType": 5126, "count": n_verts, "type": "VEC3" },
518            { "bufferView": 2, "componentType": 5126, "count": n_verts, "type": "VEC2" },
519            { "bufferView": 3, "componentType": 5125, "count": n_indices, "type": "SCALAR" }
520        ]);
521
522        self.gltf_json["meshes"] = json!([{
523            "name": "VRM_Mesh",
524            "primitives": [{
525                "attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 },
526                "indices": 3,
527                "mode": 4
528            }]
529        }]);
530
531        self.vertex_count = n_verts;
532        self.index_count = n_indices;
533        self.has_mesh = true;
534        Ok(())
535    }
536
537    /// Sets the skeleton (bone hierarchy) for the avatar.
538    pub fn set_skeleton(
539        &mut self,
540        bone_names: &[String],
541        bone_parents: &[Option<usize>],
542        bind_poses: &[[f64; 16]],
543    ) -> anyhow::Result<()> {
544        let n_bones = bone_names.len();
545        if bone_parents.len() != n_bones {
546            anyhow::bail!(
547                "bone_parents length ({}) must match bone_names length ({})",
548                bone_parents.len(),
549                n_bones
550            );
551        }
552        if bind_poses.len() != n_bones {
553            anyhow::bail!(
554                "bind_poses length ({}) must match bone_names length ({})",
555                bind_poses.len(),
556                n_bones
557            );
558        }
559        if n_bones == 0 {
560            anyhow::bail!("skeleton must have at least one bone");
561        }
562
563        for (i, parent) in bone_parents.iter().enumerate() {
564            if let Some(p) = parent {
565                if *p >= n_bones {
566                    anyhow::bail!(
567                        "bone {} has parent index {} which exceeds bone count {}",
568                        i,
569                        p,
570                        n_bones
571                    );
572                }
573                if *p == i {
574                    anyhow::bail!("bone {} cannot be its own parent", i);
575                }
576            }
577        }
578
579        let mut children: Vec<Vec<usize>> = vec![Vec::new(); n_bones];
580        let mut roots: Vec<usize> = Vec::new();
581        for (i, parent) in bone_parents.iter().enumerate() {
582            match parent {
583                Some(p) => children[*p].push(i),
584                None => roots.push(i),
585            }
586        }
587
588        if roots.is_empty() {
589            anyhow::bail!("skeleton must have at least one root bone (no parent)");
590        }
591
592        let mut nodes = serde_json::Value::Array(Vec::new());
593        let nodes_arr = nodes
594            .as_array_mut()
595            .ok_or_else(|| anyhow::anyhow!("internal: failed to create nodes array"))?;
596
597        for i in 0..n_bones {
598            let (translation, rotation, scale) = decompose_matrix(&bind_poses[i]);
599            let mut node = json!({
600                "name": bone_names[i],
601                "translation": [translation[0] as f32, translation[1] as f32, translation[2] as f32],
602                "rotation": [rotation[0] as f32, rotation[1] as f32, rotation[2] as f32, rotation[3] as f32],
603                "scale": [scale[0] as f32, scale[1] as f32, scale[2] as f32]
604            });
605            if !children[i].is_empty() {
606                node["children"] = json!(children[i]);
607            }
608            nodes_arr.push(node);
609        }
610
611        let mesh_node_idx = n_bones;
612        nodes_arr.push(json!({ "name": "VRM_MeshNode", "mesh": 0, "skin": 0 }));
613
614        let mut scene_nodes: Vec<usize> = roots.clone();
615        scene_nodes.push(mesh_node_idx);
616
617        // Write inverse bind matrices to binary buffer
618        let ibm_offset = self.binary_buffer.len();
619        for bind_pose in bind_poses {
620            let inv = invert_matrix_4x4(bind_pose);
621            for val in &inv {
622                let f = *val as f32;
623                self.binary_buffer.extend_from_slice(&f.to_le_bytes());
624            }
625        }
626        let ibm_byte_len = self.binary_buffer.len() - ibm_offset;
627
628        let bv_idx = self.gltf_json["bufferViews"]
629            .as_array()
630            .map(|a| a.len())
631            .unwrap_or(0);
632        let acc_idx = self.gltf_json["accessors"]
633            .as_array()
634            .map(|a| a.len())
635            .unwrap_or(0);
636
637        if let Some(bvs) = self.gltf_json["bufferViews"].as_array_mut() {
638            bvs.push(json!({
639                "buffer": 0, "byteOffset": ibm_offset, "byteLength": ibm_byte_len
640            }));
641        }
642        if let Some(accs) = self.gltf_json["accessors"].as_array_mut() {
643            accs.push(json!({
644                "bufferView": bv_idx, "componentType": 5126, "count": n_bones, "type": "MAT4"
645            }));
646        }
647
648        let all_joints: Vec<usize> = (0..n_bones).collect();
649        let skeleton_root = roots.first().copied().unwrap_or(0);
650
651        self.gltf_json["skins"] = json!([{
652            "joints": all_joints,
653            "skeleton": skeleton_root,
654            "inverseBindMatrices": acc_idx
655        }]);
656
657        self.gltf_json["nodes"] = nodes;
658        self.gltf_json["scenes"] = json!([{ "nodes": scene_nodes }]);
659        self.node_count = n_bones + 1;
660        self.has_skeleton = true;
661        Ok(())
662    }
663
664    /// Sets the VRM humanoid bone mapping.
665    pub fn set_humanoid(&mut self, humanoid: &VrmHumanoid) -> anyhow::Result<()> {
666        humanoid.validate()?;
667        if self.has_skeleton {
668            for bone in &humanoid.bones {
669                if bone.node_index >= self.node_count {
670                    anyhow::bail!(
671                        "humanoid bone '{}' references node index {} but only {} nodes exist",
672                        bone.name.as_str(),
673                        bone.node_index,
674                        self.node_count
675                    );
676                }
677            }
678        }
679        self.gltf_json["extensions"]["VRMC_vrm"]["humanoid"] = humanoid.to_json();
680        self.has_humanoid = true;
681        Ok(())
682    }
683
684    /// Sets the VRM avatar metadata.
685    pub fn set_meta(&mut self, meta: &VrmMeta) -> anyhow::Result<()> {
686        meta.validate()?;
687        self.gltf_json["extensions"]["VRMC_vrm"]["meta"] = meta.to_json();
688        self.has_meta = true;
689        Ok(())
690    }
691
692    /// Sets blend shape (morph target) data for the mesh.
693    pub fn set_blend_shapes(&mut self, shapes: &[(String, Vec<[f64; 3]>)]) -> anyhow::Result<()> {
694        if !self.has_mesh {
695            anyhow::bail!("set_mesh must be called before set_blend_shapes");
696        }
697        if shapes.is_empty() {
698            return Ok(());
699        }
700
701        for (name, deltas) in shapes {
702            if deltas.len() != self.vertex_count {
703                anyhow::bail!(
704                    "blend shape '{}' has {} deltas but mesh has {} vertices",
705                    name,
706                    deltas.len(),
707                    self.vertex_count
708                );
709            }
710        }
711
712        let mut morph_targets: Vec<serde_json::Value> = Vec::new();
713
714        for (_, deltas) in shapes {
715            let delta_offset = self.binary_buffer.len();
716            for delta in deltas {
717                for &component in delta.iter().take(3) {
718                    let val = component as f32;
719                    self.binary_buffer.extend_from_slice(&val.to_le_bytes());
720                }
721            }
722            let delta_byte_len = self.binary_buffer.len() - delta_offset;
723
724            let bv_idx = self.gltf_json["bufferViews"]
725                .as_array()
726                .map(|a| a.len())
727                .unwrap_or(0);
728            if let Some(bvs) = self.gltf_json["bufferViews"].as_array_mut() {
729                bvs.push(json!({
730                    "buffer": 0, "byteOffset": delta_offset, "byteLength": delta_byte_len
731                }));
732            }
733
734            let acc_idx = self.gltf_json["accessors"]
735                .as_array()
736                .map(|a| a.len())
737                .unwrap_or(0);
738            if let Some(accs) = self.gltf_json["accessors"].as_array_mut() {
739                accs.push(json!({
740                    "bufferView": bv_idx, "componentType": 5126,
741                    "count": self.vertex_count, "type": "VEC3"
742                }));
743            }
744
745            morph_targets.push(json!({ "POSITION": acc_idx }));
746        }
747
748        if let Some(meshes) = self.gltf_json["meshes"].as_array_mut() {
749            if let Some(mesh) = meshes.first_mut() {
750                if let Some(prims) = mesh["primitives"].as_array_mut() {
751                    if let Some(prim) = prims.first_mut() {
752                        prim["targets"] = json!(morph_targets);
753                    }
754                }
755                let target_names: Vec<&str> = shapes.iter().map(|(n, _)| n.as_str()).collect();
756                mesh["extras"] = json!({ "targetNames": target_names });
757            }
758        }
759
760        // Build VRMC_vrm expressions
761        let mut expressions = serde_json::Map::new();
762        let mut preset_map = serde_json::Map::new();
763        let mut custom_map = serde_json::Map::new();
764
765        for (i, (name, _)) in shapes.iter().enumerate() {
766            let expression_entry = json!({
767                "morphTargetBinds": [{ "node": 0, "index": i, "weight": 1.0 }]
768            });
769            match map_expression_preset(name) {
770                Some(preset_name) => {
771                    preset_map.insert(preset_name.to_string(), expression_entry);
772                }
773                None => {
774                    custom_map.insert(name.clone(), expression_entry);
775                }
776            }
777        }
778
779        expressions.insert("preset".to_string(), serde_json::Value::Object(preset_map));
780        if !custom_map.is_empty() {
781            expressions.insert("custom".to_string(), serde_json::Value::Object(custom_map));
782        }
783
784        self.gltf_json["extensions"]["VRMC_vrm"]["expressions"] =
785            serde_json::Value::Object(expressions);
786        Ok(())
787    }
788
789    /// Exports the VRM as a GLB binary (`.vrm` file contents).
790    pub fn export(&self) -> anyhow::Result<Vec<u8>> {
791        if !self.has_mesh {
792            anyhow::bail!("cannot export VRM: no mesh data set (call set_mesh first)");
793        }
794        if !self.has_humanoid {
795            anyhow::bail!("cannot export VRM: no humanoid mapping set (call set_humanoid first)");
796        }
797        if !self.has_meta {
798            anyhow::bail!("cannot export VRM: no meta set (call set_meta first)");
799        }
800
801        let mut gltf = self.gltf_json.clone();
802
803        let mut bin_data = self.binary_buffer.clone();
804        while !bin_data.len().is_multiple_of(4) {
805            bin_data.push(0x00);
806        }
807
808        gltf["buffers"] = json!([{ "byteLength": bin_data.len() }]);
809
810        let mut json_bytes = serde_json::to_vec(&gltf)?;
811        while json_bytes.len() % 4 != 0 {
812            json_bytes.push(b' ');
813        }
814
815        let json_chunk_len = json_bytes.len() as u32;
816        let bin_chunk_len = bin_data.len() as u32;
817        let total_len: u32 = 12 + 8 + json_chunk_len + 8 + bin_chunk_len;
818
819        let mut output: Vec<u8> = Vec::with_capacity(total_len as usize);
820
821        // GLB header
822        output.extend_from_slice(&GLB_MAGIC.to_le_bytes());
823        output.extend_from_slice(&GLB_VERSION.to_le_bytes());
824        output.extend_from_slice(&total_len.to_le_bytes());
825
826        // JSON chunk
827        output.extend_from_slice(&json_chunk_len.to_le_bytes());
828        output.extend_from_slice(&CHUNK_JSON.to_le_bytes());
829        output.extend_from_slice(&json_bytes);
830
831        // BIN chunk
832        output.extend_from_slice(&bin_chunk_len.to_le_bytes());
833        output.extend_from_slice(&CHUNK_BIN.to_le_bytes());
834        output.extend_from_slice(&bin_data);
835
836        Ok(output)
837    }
838}
839
840impl Default for VrmExporter {
841    fn default() -> Self {
842        Self::new()
843    }
844}
845
846// ── Expression preset mapping ────────────────────────────────────────────────
847
848fn map_expression_preset(name: &str) -> Option<&'static str> {
849    let lower = name.to_ascii_lowercase();
850    match lower.as_str() {
851        "happy" | "joy" | "smile" => Some("happy"),
852        "angry" | "anger" => Some("angry"),
853        "sad" | "sorrow" => Some("sad"),
854        "relaxed" | "calm" => Some("relaxed"),
855        "surprised" | "surprise" => Some("surprised"),
856        "aa" | "a" => Some("aa"),
857        "ih" | "i" => Some("ih"),
858        "ou" | "u" => Some("ou"),
859        "ee" | "e" => Some("ee"),
860        "oh" | "o" => Some("oh"),
861        "blink" => Some("blink"),
862        "blinkleft" | "blink_left" | "blink_l" => Some("blinkLeft"),
863        "blinkright" | "blink_right" | "blink_r" => Some("blinkRight"),
864        "lookup" | "look_up" => Some("lookUp"),
865        "lookdown" | "look_down" => Some("lookDown"),
866        "lookleft" | "look_left" => Some("lookLeft"),
867        "lookright" | "look_right" => Some("lookRight"),
868        "neutral" => Some("neutral"),
869        _ => None,
870    }
871}
872
873// ── Matrix math helpers ──────────────────────────────────────────────────────
874
875fn decompose_matrix(m: &[f64; 16]) -> ([f64; 3], [f64; 4], [f64; 3]) {
876    let translation = [m[12], m[13], m[14]];
877
878    let col0 = [m[0], m[1], m[2]];
879    let col1 = [m[4], m[5], m[6]];
880    let col2 = [m[8], m[9], m[10]];
881
882    let sx = vec3_length(&col0);
883    let sy = vec3_length(&col1);
884    let sz = vec3_length(&col2);
885    let scale = [sx, sy, sz];
886
887    let safe_sx = if sx.abs() < 1e-12 { 1.0 } else { sx };
888    let safe_sy = if sy.abs() < 1e-12 { 1.0 } else { sy };
889    let safe_sz = if sz.abs() < 1e-12 { 1.0 } else { sz };
890
891    let r00 = col0[0] / safe_sx;
892    let r10 = col0[1] / safe_sx;
893    let r20 = col0[2] / safe_sx;
894    let r01 = col1[0] / safe_sy;
895    let r11 = col1[1] / safe_sy;
896    let r21 = col1[2] / safe_sy;
897    let r02 = col2[0] / safe_sz;
898    let r12 = col2[1] / safe_sz;
899    let r22 = col2[2] / safe_sz;
900
901    let rotation = rotation_matrix_to_quat(r00, r01, r02, r10, r11, r12, r20, r21, r22);
902    (translation, rotation, scale)
903}
904
905#[allow(clippy::too_many_arguments)]
906fn rotation_matrix_to_quat(
907    r00: f64,
908    r01: f64,
909    r02: f64,
910    r10: f64,
911    r11: f64,
912    r12: f64,
913    r20: f64,
914    r21: f64,
915    r22: f64,
916) -> [f64; 4] {
917    let trace = r00 + r11 + r22;
918    let (x, y, z, w) = if trace > 0.0 {
919        let s = 0.5 / (trace + 1.0).sqrt();
920        ((r21 - r12) * s, (r02 - r20) * s, (r10 - r01) * s, 0.25 / s)
921    } else if r00 > r11 && r00 > r22 {
922        let s = 2.0 * (1.0 + r00 - r11 - r22).sqrt();
923        (0.25 * s, (r01 + r10) / s, (r02 + r20) / s, (r21 - r12) / s)
924    } else if r11 > r22 {
925        let s = 2.0 * (1.0 + r11 - r00 - r22).sqrt();
926        ((r01 + r10) / s, 0.25 * s, (r12 + r21) / s, (r02 - r20) / s)
927    } else {
928        let s = 2.0 * (1.0 + r22 - r00 - r11).sqrt();
929        ((r02 + r20) / s, (r12 + r21) / s, 0.25 * s, (r10 - r01) / s)
930    };
931
932    let len = (x * x + y * y + z * z + w * w).sqrt();
933    if len.abs() < 1e-12 {
934        return [0.0, 0.0, 0.0, 1.0];
935    }
936    [x / len, y / len, z / len, w / len]
937}
938
939fn vec3_length(v: &[f64; 3]) -> f64 {
940    (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
941}
942
943fn invert_matrix_4x4(m: &[f64; 16]) -> [f64; 16] {
944    let (a00, a01, a02, a03) = (m[0], m[1], m[2], m[3]);
945    let (a10, a11, a12, a13) = (m[4], m[5], m[6], m[7]);
946    let (a20, a21, a22, a23) = (m[8], m[9], m[10], m[11]);
947    let (a30, a31, a32, a33) = (m[12], m[13], m[14], m[15]);
948
949    let b00 = a00 * a11 - a01 * a10;
950    let b01 = a00 * a12 - a02 * a10;
951    let b02 = a00 * a13 - a03 * a10;
952    let b03 = a01 * a12 - a02 * a11;
953    let b04 = a01 * a13 - a03 * a11;
954    let b05 = a02 * a13 - a03 * a12;
955    let b06 = a20 * a31 - a21 * a30;
956    let b07 = a20 * a32 - a22 * a30;
957    let b08 = a20 * a33 - a23 * a30;
958    let b09 = a21 * a32 - a22 * a31;
959    let b10 = a21 * a33 - a23 * a31;
960    let b11 = a22 * a33 - a23 * a32;
961
962    let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
963
964    if det.abs() < 1e-14 {
965        return [
966            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
967        ];
968    }
969
970    let inv_det = 1.0 / det;
971    [
972        (a11 * b11 - a12 * b10 + a13 * b09) * inv_det,
973        (a02 * b10 - a01 * b11 - a03 * b09) * inv_det,
974        (a31 * b05 - a32 * b04 + a33 * b03) * inv_det,
975        (a22 * b04 - a21 * b05 - a23 * b03) * inv_det,
976        (a12 * b08 - a10 * b11 - a13 * b07) * inv_det,
977        (a00 * b11 - a02 * b08 + a03 * b07) * inv_det,
978        (a32 * b02 - a30 * b05 - a33 * b01) * inv_det,
979        (a20 * b05 - a22 * b02 + a23 * b01) * inv_det,
980        (a10 * b10 - a11 * b08 + a13 * b06) * inv_det,
981        (a01 * b08 - a00 * b10 - a03 * b06) * inv_det,
982        (a30 * b04 - a31 * b02 + a33 * b00) * inv_det,
983        (a21 * b02 - a20 * b04 - a23 * b00) * inv_det,
984        (a11 * b07 - a10 * b09 - a12 * b06) * inv_det,
985        (a00 * b09 - a01 * b07 + a02 * b06) * inv_det,
986        (a31 * b01 - a30 * b03 - a32 * b00) * inv_det,
987        (a20 * b03 - a21 * b01 + a22 * b00) * inv_det,
988    ]
989}
990
991// ── Tests ────────────────────────────────────────────────────────────────────
992
993#[cfg(test)]
994mod tests {
995    use super::*;
996
997    fn minimal_humanoid() -> VrmHumanoid {
998        let required = VrmBoneName::all_required();
999        VrmHumanoid {
1000            bones: required
1001                .iter()
1002                .enumerate()
1003                .map(|(i, &name)| VrmHumanBone {
1004                    name,
1005                    node_index: i,
1006                })
1007                .collect(),
1008        }
1009    }
1010
1011    fn identity_matrix() -> [f64; 16] {
1012        [
1013            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1014        ]
1015    }
1016
1017    fn minimal_exporter() -> VrmExporter {
1018        let mut exporter = VrmExporter::new();
1019        exporter
1020            .set_mesh(
1021                &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1022                &[[0.0, 0.0, 1.0]; 3],
1023                &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
1024                &[[0, 1, 2]],
1025            )
1026            .expect("set_mesh failed");
1027
1028        let humanoid = minimal_humanoid();
1029        let n_bones = humanoid.bones.len();
1030        let bone_names: Vec<String> = humanoid
1031            .bones
1032            .iter()
1033            .map(|b| b.name.as_str().to_string())
1034            .collect();
1035        let mut bone_parents: Vec<Option<usize>> = vec![Some(0); n_bones];
1036        bone_parents[0] = None;
1037        let bind_poses: Vec<[f64; 16]> = vec![identity_matrix(); n_bones];
1038
1039        exporter
1040            .set_skeleton(&bone_names, &bone_parents, &bind_poses)
1041            .expect("set_skeleton failed");
1042        exporter
1043            .set_humanoid(&humanoid)
1044            .expect("set_humanoid failed");
1045        exporter
1046            .set_meta(&VrmMeta::default_cc_by("TestAvatar"))
1047            .expect("set_meta failed");
1048        exporter
1049    }
1050
1051    #[test]
1052    fn bone_name_as_str_hips() {
1053        assert_eq!(VrmBoneName::Hips.as_str(), "hips");
1054    }
1055
1056    #[test]
1057    fn bone_name_as_str_head() {
1058        assert_eq!(VrmBoneName::Head.as_str(), "head");
1059    }
1060
1061    #[test]
1062    fn bone_name_required_hips() {
1063        assert!(VrmBoneName::Hips.is_required());
1064    }
1065
1066    #[test]
1067    fn bone_name_optional_jaw() {
1068        assert!(!VrmBoneName::Jaw.is_required());
1069    }
1070
1071    #[test]
1072    fn all_required_count() {
1073        assert_eq!(VrmBoneName::all_required().len(), 17);
1074    }
1075
1076    #[test]
1077    fn humanoid_validate_ok() {
1078        assert!(minimal_humanoid().validate().is_ok());
1079    }
1080
1081    #[test]
1082    fn humanoid_validate_missing_bone() {
1083        let h = VrmHumanoid {
1084            bones: vec![VrmHumanBone {
1085                name: VrmBoneName::Hips,
1086                node_index: 0,
1087            }],
1088        };
1089        assert!(h.validate().is_err());
1090    }
1091
1092    #[test]
1093    fn humanoid_validate_duplicate_node() {
1094        let bones: Vec<VrmHumanBone> = VrmBoneName::all_required()
1095            .iter()
1096            .map(|&name| VrmHumanBone {
1097                name,
1098                node_index: 0,
1099            })
1100            .collect();
1101        assert!(VrmHumanoid { bones }.validate().is_err());
1102    }
1103
1104    #[test]
1105    fn meta_validate_ok() {
1106        assert!(VrmMeta::default_cc_by("Test").validate().is_ok());
1107    }
1108
1109    #[test]
1110    fn meta_validate_empty_name() {
1111        let mut meta = VrmMeta::default_cc_by("Test");
1112        meta.name = "  ".to_string();
1113        assert!(meta.validate().is_err());
1114    }
1115
1116    #[test]
1117    fn meta_validate_no_authors() {
1118        let mut meta = VrmMeta::default_cc_by("Test");
1119        meta.authors.clear();
1120        assert!(meta.validate().is_err());
1121    }
1122
1123    #[test]
1124    fn new_exporter_defaults() {
1125        let exp = VrmExporter::new();
1126        assert!(!exp.has_mesh);
1127        assert!(!exp.has_skeleton);
1128        assert!(!exp.has_humanoid);
1129        assert!(!exp.has_meta);
1130    }
1131
1132    #[test]
1133    fn set_mesh_basic() {
1134        let mut exp = VrmExporter::new();
1135        let result = exp.set_mesh(
1136            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1137            &[[0.0, 0.0, 1.0]; 3],
1138            &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
1139            &[[0, 1, 2]],
1140        );
1141        assert!(result.is_ok());
1142        assert!(exp.has_mesh);
1143    }
1144
1145    #[test]
1146    fn set_mesh_mismatched_normals() {
1147        let mut exp = VrmExporter::new();
1148        let result = exp.set_mesh(
1149            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
1150            &[[0.0, 0.0, 1.0]],
1151            &[[0.0, 0.0], [1.0, 0.0]],
1152            &[[0, 1, 0]],
1153        );
1154        assert!(result.is_err());
1155    }
1156
1157    #[test]
1158    fn set_mesh_invalid_index() {
1159        let mut exp = VrmExporter::new();
1160        let result = exp.set_mesh(
1161            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1162            &[[0.0, 0.0, 1.0]; 3],
1163            &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
1164            &[[0, 1, 99]],
1165        );
1166        assert!(result.is_err());
1167    }
1168
1169    #[test]
1170    fn export_without_mesh_fails() {
1171        assert!(VrmExporter::new().export().is_err());
1172    }
1173
1174    #[test]
1175    fn export_minimal_vrm_produces_valid_glb() {
1176        let bytes = minimal_exporter().export().expect("export failed");
1177        assert!(bytes.len() >= 12);
1178        let magic = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1179        assert_eq!(magic, GLB_MAGIC);
1180        let version = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1181        assert_eq!(version, 2);
1182        let total_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
1183        assert_eq!(total_len as usize, bytes.len());
1184    }
1185
1186    #[test]
1187    fn export_contains_vrmc_vrm_extension() {
1188        let bytes = minimal_exporter().export().expect("export failed");
1189        let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1190        let json_str = std::str::from_utf8(&bytes[20..20 + json_len])
1191            .expect("invalid utf8")
1192            .trim_end_matches(' ');
1193        let parsed: serde_json::Value = serde_json::from_str(json_str).expect("invalid JSON");
1194        assert!(parsed["extensions"]["VRMC_vrm"].is_object());
1195        assert_eq!(
1196            parsed["extensions"]["VRMC_vrm"]["specVersion"]
1197                .as_str()
1198                .unwrap_or(""),
1199            "1.0"
1200        );
1201    }
1202
1203    #[test]
1204    fn export_contains_humanoid_bones() {
1205        let bytes = minimal_exporter().export().expect("export failed");
1206        let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1207        let json_str = std::str::from_utf8(&bytes[20..20 + json_len])
1208            .expect("invalid utf8")
1209            .trim_end_matches(' ');
1210        let parsed: serde_json::Value = serde_json::from_str(json_str).expect("invalid JSON");
1211        let humanoid = &parsed["extensions"]["VRMC_vrm"]["humanoid"];
1212        assert!(humanoid["humanBones"]["hips"].is_object());
1213        assert!(humanoid["humanBones"]["head"].is_object());
1214    }
1215
1216    #[test]
1217    fn export_contains_meta() {
1218        let bytes = minimal_exporter().export().expect("export failed");
1219        let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1220        let json_str = std::str::from_utf8(&bytes[20..20 + json_len])
1221            .expect("invalid utf8")
1222            .trim_end_matches(' ');
1223        let parsed: serde_json::Value = serde_json::from_str(json_str).expect("invalid JSON");
1224        let meta = &parsed["extensions"]["VRMC_vrm"]["meta"];
1225        assert_eq!(meta["name"].as_str().unwrap_or(""), "TestAvatar");
1226    }
1227
1228    #[test]
1229    fn export_has_extensions_used() {
1230        let bytes = minimal_exporter().export().expect("export failed");
1231        let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1232        let json_str = std::str::from_utf8(&bytes[20..20 + json_len])
1233            .expect("invalid utf8")
1234            .trim_end_matches(' ');
1235        let parsed: serde_json::Value = serde_json::from_str(json_str).expect("invalid JSON");
1236        let ext_used = parsed["extensionsUsed"]
1237            .as_array()
1238            .expect("extensionsUsed missing");
1239        assert!(ext_used.iter().any(|v| v.as_str() == Some("VRMC_vrm")));
1240    }
1241
1242    #[test]
1243    fn export_has_skin_with_joints() {
1244        let bytes = minimal_exporter().export().expect("export failed");
1245        let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1246        let json_str = std::str::from_utf8(&bytes[20..20 + json_len])
1247            .expect("invalid utf8")
1248            .trim_end_matches(' ');
1249        let parsed: serde_json::Value = serde_json::from_str(json_str).expect("invalid JSON");
1250        let skins = parsed["skins"].as_array().expect("skins missing");
1251        assert!(!skins.is_empty());
1252        assert!(!skins[0]["joints"]
1253            .as_array()
1254            .expect("joints missing")
1255            .is_empty());
1256    }
1257
1258    #[test]
1259    fn export_with_blend_shapes() {
1260        let mut exp = minimal_exporter();
1261        let shapes = vec![
1262            ("happy".to_string(), vec![[0.0, 0.01, 0.0]; 3]),
1263            ("angry".to_string(), vec![[0.0, -0.01, 0.0]; 3]),
1264            ("custom_face".to_string(), vec![[0.01, 0.0, 0.0]; 3]),
1265        ];
1266        exp.set_blend_shapes(&shapes)
1267            .expect("set_blend_shapes failed");
1268        let bytes = exp.export().expect("export failed");
1269        let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1270        let json_str = std::str::from_utf8(&bytes[20..20 + json_len])
1271            .expect("invalid utf8")
1272            .trim_end_matches(' ');
1273        let parsed: serde_json::Value = serde_json::from_str(json_str).expect("invalid JSON");
1274
1275        let targets = &parsed["meshes"][0]["primitives"][0]["targets"];
1276        assert_eq!(targets.as_array().map(|a| a.len()).unwrap_or(0), 3);
1277
1278        let expressions = &parsed["extensions"]["VRMC_vrm"]["expressions"];
1279        assert!(expressions["preset"]["happy"].is_object());
1280        assert!(expressions["preset"]["angry"].is_object());
1281        assert!(expressions["custom"]["custom_face"].is_object());
1282    }
1283
1284    #[test]
1285    fn blend_shapes_before_mesh_fails() {
1286        let mut exp = VrmExporter::new();
1287        assert!(exp
1288            .set_blend_shapes(&[("test".to_string(), vec![[0.0; 3]])])
1289            .is_err());
1290    }
1291
1292    #[test]
1293    fn write_vrm_to_file() {
1294        let bytes = minimal_exporter().export().expect("export failed");
1295        let path = std::env::temp_dir().join("test_oxihuman_vrm_export.vrm");
1296        std::fs::write(&path, &bytes).expect("write failed");
1297        assert!(path.exists());
1298        assert_eq!(
1299            std::fs::read(&path).expect("read failed").len(),
1300            bytes.len()
1301        );
1302        std::fs::remove_file(&path).ok();
1303    }
1304
1305    #[test]
1306    fn decompose_identity_matrix_test() {
1307        let (t, r, s) = decompose_matrix(&identity_matrix());
1308        assert!((t[0]).abs() < 1e-6);
1309        assert!((r[3] - 1.0).abs() < 1e-6);
1310        assert!((s[0] - 1.0).abs() < 1e-6);
1311    }
1312
1313    #[test]
1314    fn invert_identity_returns_identity() {
1315        let inv = invert_matrix_4x4(&identity_matrix());
1316        for (i, &val) in inv.iter().enumerate() {
1317            let expected = if i % 5 == 0 { 1.0 } else { 0.0 };
1318            assert!((val - expected).abs() < 1e-10);
1319        }
1320    }
1321
1322    #[test]
1323    fn expression_preset_mapping_test() {
1324        assert_eq!(map_expression_preset("happy"), Some("happy"));
1325        assert_eq!(map_expression_preset("Happy"), Some("happy"));
1326        assert_eq!(map_expression_preset("custom_thing"), None);
1327    }
1328
1329    #[test]
1330    fn default_exporter_impl() {
1331        assert!(!VrmExporter::default().has_mesh);
1332    }
1333
1334    #[test]
1335    fn set_skeleton_self_parent() {
1336        let mut exp = VrmExporter::new();
1337        assert!(exp
1338            .set_skeleton(&["Root".to_string()], &[Some(0)], &[identity_matrix()])
1339            .is_err());
1340    }
1341
1342    #[test]
1343    fn commercial_usage_str_values() {
1344        assert_eq!(
1345            VrmCommercialUsage::PersonalNonProfit.as_str(),
1346            "personalNonProfit"
1347        );
1348        assert_eq!(VrmCommercialUsage::Corporation.as_str(), "corporation");
1349    }
1350
1351    #[test]
1352    fn modification_str_values() {
1353        assert_eq!(VrmModification::Prohibited.as_str(), "prohibited");
1354        assert_eq!(
1355            VrmModification::AllowModificationRedistribution.as_str(),
1356            "allowModificationRedistribution"
1357        );
1358    }
1359}