1use serde_json::json;
11
12const GLB_MAGIC: u32 = 0x46546C67; const GLB_VERSION: u32 = 2;
16const CHUNK_JSON: u32 = 0x4E4F534A; const CHUNK_BIN: u32 = 0x004E4942; #[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 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 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 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#[derive(Debug, Clone)]
195pub struct VrmHumanBone {
196 pub name: VrmBoneName,
197 pub node_index: usize,
198}
199
200#[derive(Debug, Clone)]
202pub struct VrmHumanoid {
203 pub bones: Vec<VrmHumanBone>,
204}
205
206impl VrmHumanoid {
207 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 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#[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#[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#[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#[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 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 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 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
363pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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
846fn 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
873fn 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#[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}