1use std::fmt::Write as FmtWrite;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum UsdSubdivScheme {
16 None,
18 CatmullClark,
20 Loop,
22 Bilinear,
24}
25
26impl UsdSubdivScheme {
27 fn as_token(self) -> &'static str {
29 match self {
30 Self::None => "none",
31 Self::CatmullClark => "catmullClark",
32 Self::Loop => "loop",
33 Self::Bilinear => "bilinear",
34 }
35 }
36}
37
38pub struct UsdMesh {
42 pub name: String,
44 pub positions: Vec<[f64; 3]>,
46 pub normals: Vec<[f64; 3]>,
48 pub uvs: Vec<[f64; 2]>,
50 pub face_vertex_counts: Vec<i32>,
52 pub face_vertex_indices: Vec<i32>,
54 pub subdivision_scheme: UsdSubdivScheme,
56}
57
58pub struct UsdMaterial {
60 pub name: String,
62 pub diffuse_color: [f64; 3],
64 pub metallic: f64,
66 pub roughness: f64,
68 pub opacity: f64,
70 pub normal_scale: f64,
72}
73
74pub struct UsdSkeleton {
76 pub joint_names: Vec<String>,
78 pub joint_paths: Vec<String>,
80 pub bind_transforms: Vec<[f64; 16]>,
82 pub rest_transforms: Vec<[f64; 16]>,
84}
85
86pub struct UsdSkinBinding {
88 pub joint_indices: Vec<Vec<i32>>,
90 pub joint_weights: Vec<Vec<f64>>,
92 pub skeleton_path: String,
94}
95
96pub struct UsdBlendShape {
98 pub name: String,
100 pub offsets: Vec<[f64; 3]>,
102 pub point_indices: Vec<i32>,
104}
105
106pub struct UsdaWriter {
114 output: String,
115 indent_level: usize,
116}
117
118impl Default for UsdaWriter {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl UsdaWriter {
125 pub fn new() -> Self {
129 Self {
130 output: String::with_capacity(4096),
131 indent_level: 0,
132 }
133 }
134
135 pub fn write_header(&mut self, up_axis: &str, meters_per_unit: f64) {
141 self.output.push_str("#usda 1.0\n(\n");
142 let _ = writeln!(self.output, " upAxis = \"{}\"", up_axis);
143 let _ = writeln!(
144 self.output,
145 " metersPerUnit = {:.6}",
146 meters_per_unit
147 );
148 self.output.push_str(")\n\n");
149 }
150
151 pub fn begin_def(&mut self, kind: &str, name: &str) {
155 self.write_indent();
156 let _ = writeln!(self.output, "def {} \"{}\" {{", kind, name);
157 self.indent_level += 1;
158 }
159
160 pub fn end_def(&mut self) {
162 if self.indent_level > 0 {
163 self.indent_level -= 1;
164 }
165 self.write_indent();
166 self.output.push_str("}\n");
167 }
168
169 pub fn write_mesh(&mut self, mesh: &UsdMesh) -> anyhow::Result<()> {
173 self.begin_def("Mesh", &mesh.name);
174
175 self.write_indent();
177 let _ = writeln!(
178 self.output,
179 "uniform token subdivisionScheme = \"{}\"",
180 mesh.subdivision_scheme.as_token()
181 );
182
183 self.write_indent();
185 self.output.push_str("point3f[] points = ");
186 self.write_f64x3_array(&mesh.positions);
187 self.output.push('\n');
188
189 if !mesh.normals.is_empty() {
191 self.write_indent();
192 self.output.push_str("normal3f[] normals = ");
193 self.write_f64x3_array(&mesh.normals);
194 self.output.push_str(" (\n");
195 self.write_indent();
196 self.output.push_str(" interpolation = \"faceVarying\"\n");
197 self.write_indent();
198 self.output.push_str(")\n");
199 }
200
201 if !mesh.uvs.is_empty() {
203 self.write_indent();
204 self.output.push_str("texCoord2f[] primvars:st = ");
205 self.write_f64x2_array(&mesh.uvs);
206 self.output.push_str(" (\n");
207 self.write_indent();
208 self.output.push_str(" interpolation = \"faceVarying\"\n");
209 self.write_indent();
210 self.output.push_str(")\n");
211 }
212
213 self.write_indent();
215 self.output.push_str("int[] faceVertexCounts = ");
216 self.write_i32_array(&mesh.face_vertex_counts);
217 self.output.push('\n');
218
219 self.write_indent();
221 self.output.push_str("int[] faceVertexIndices = ");
222 self.write_i32_array(&mesh.face_vertex_indices);
223 self.output.push('\n');
224
225 self.end_def();
226 Ok(())
227 }
228
229 pub fn write_material(&mut self, mat: &UsdMaterial) -> anyhow::Result<()> {
233 self.begin_def("Material", &mat.name);
234
235 self.write_indent();
237 let _ = writeln!(
238 self.output,
239 "token outputs:surface.connect = </{}/PBRShader.outputs:surface>",
240 mat.name
241 );
242
243 self.begin_def("Shader", "PBRShader");
245
246 self.write_indent();
247 self.output
248 .push_str("uniform token info:id = \"UsdPreviewSurface\"\n");
249
250 self.write_indent();
251 let _ = writeln!(
252 self.output,
253 "color3f inputs:diffuseColor = ({:.6}, {:.6}, {:.6})",
254 mat.diffuse_color[0], mat.diffuse_color[1], mat.diffuse_color[2]
255 );
256
257 self.write_indent();
258 let _ = writeln!(
259 self.output,
260 "float inputs:metallic = {:.6}",
261 mat.metallic
262 );
263
264 self.write_indent();
265 let _ = writeln!(
266 self.output,
267 "float inputs:roughness = {:.6}",
268 mat.roughness
269 );
270
271 self.write_indent();
272 let _ = writeln!(self.output, "float inputs:opacity = {:.6}", mat.opacity);
273
274 self.write_indent();
275 let _ = writeln!(
276 self.output,
277 "float inputs:normal = {:.6}",
278 mat.normal_scale
279 );
280
281 self.write_indent();
282 self.output
283 .push_str("token outputs:surface\n");
284
285 self.end_def(); self.end_def(); Ok(())
289 }
290
291 pub fn write_skeleton(&mut self, skel: &UsdSkeleton) -> anyhow::Result<()> {
295 self.begin_def("Skeleton", "Skeleton");
296
297 self.write_indent();
299 self.output.push_str("uniform token[] joints = ");
300 self.write_string_array(&skel.joint_paths);
301 self.output.push('\n');
302
303 self.write_indent();
305 self.output.push_str("uniform token[] jointNames = ");
306 self.write_string_array(&skel.joint_names);
307 self.output.push('\n');
308
309 self.write_indent();
311 self.output
312 .push_str("matrix4d[] bindTransforms = ");
313 self.write_matrix4d_array(&skel.bind_transforms);
314 self.output.push('\n');
315
316 self.write_indent();
318 self.output
319 .push_str("matrix4d[] restTransforms = ");
320 self.write_matrix4d_array(&skel.rest_transforms);
321 self.output.push('\n');
322
323 self.end_def();
324 Ok(())
325 }
326
327 pub fn write_skin_binding(
334 &mut self,
335 mesh_path: &str,
336 binding: &UsdSkinBinding,
337 ) -> anyhow::Result<()> {
338 self.begin_def("SkelBindingAPI", &format!("{}_SkelBinding", sanitise_name(mesh_path)));
339
340 self.write_indent();
342 let _ = writeln!(
343 self.output,
344 "uniform token primvars:skel:skeleton = \"{}\"",
345 binding.skeleton_path
346 );
347
348 let element_size = binding
352 .joint_indices
353 .iter()
354 .map(|v| v.len())
355 .max()
356 .unwrap_or(0);
357
358 if element_size > 0 {
359 self.write_indent();
361 self.output
362 .push_str("int[] primvars:skel:jointIndices = ");
363 self.write_flat_joint_indices(&binding.joint_indices, element_size);
364 self.output.push_str(" (\n");
365 self.write_indent();
366 let _ = writeln!(self.output, " elementSize = {}", element_size);
367 self.write_indent();
368 self.output.push_str(" interpolation = \"vertex\"\n");
369 self.write_indent();
370 self.output.push_str(")\n");
371
372 self.write_indent();
374 self.output
375 .push_str("float[] primvars:skel:jointWeights = ");
376 self.write_flat_joint_weights(&binding.joint_weights, element_size);
377 self.output.push_str(" (\n");
378 self.write_indent();
379 let _ = writeln!(self.output, " elementSize = {}", element_size);
380 self.write_indent();
381 self.output.push_str(" interpolation = \"vertex\"\n");
382 self.write_indent();
383 self.output.push_str(")\n");
384 }
385
386 self.end_def();
387 Ok(())
388 }
389
390 pub fn write_blend_shapes(
394 &mut self,
395 mesh_path: &str,
396 shapes: &[UsdBlendShape],
397 ) -> anyhow::Result<()> {
398 if shapes.is_empty() {
399 return Ok(());
400 }
401
402 self.begin_def(
403 "Scope",
404 &format!("{}_BlendShapes", sanitise_name(mesh_path)),
405 );
406
407 let target_names: Vec<String> = shapes
409 .iter()
410 .map(|s| format!("<./{}>", s.name))
411 .collect();
412
413 self.write_indent();
414 let _ = write!(self.output, "uniform token[] blendShapes = [");
415 for (i, shape) in shapes.iter().enumerate() {
416 if i > 0 {
417 self.output.push_str(", ");
418 }
419 let _ = write!(self.output, "\"{}\"", shape.name);
420 }
421 self.output.push_str("]\n");
422
423 self.write_indent();
424 let _ = write!(self.output, "uniform rel blendShapeTargets = [");
425 for (i, target) in target_names.iter().enumerate() {
426 if i > 0 {
427 self.output.push_str(", ");
428 }
429 self.output.push_str(target);
430 }
431 self.output.push_str("]\n");
432
433 for shape in shapes {
435 self.begin_def("BlendShape", &shape.name);
436
437 self.write_indent();
439 self.output.push_str("vector3f[] offsets = ");
440 self.write_f64x3_array(&shape.offsets);
441 self.output.push('\n');
442
443 self.write_indent();
445 self.output.push_str("int[] pointIndices = ");
446 self.write_i32_array(&shape.point_indices);
447 self.output.push('\n');
448
449 self.end_def();
450 }
451
452 self.end_def(); Ok(())
454 }
455
456 pub fn write_xform(&mut self, name: &str, matrix: &[f64; 16]) -> anyhow::Result<()> {
460 self.begin_def("Xform", name);
461
462 self.write_indent();
463 self.output
464 .push_str("matrix4d xformOp:transform = ");
465 self.write_matrix4d(matrix);
466 self.output.push('\n');
467
468 self.write_indent();
469 self.output
470 .push_str("uniform token[] xformOpOrder = [\"xformOp:transform\"]\n");
471
472 self.end_def();
473 Ok(())
474 }
475
476 pub fn finish(mut self) -> String {
482 while self.indent_level > 0 {
484 self.end_def();
485 }
486 self.output
487 }
488
489 fn write_indent(&mut self) {
492 for _ in 0..self.indent_level {
493 self.output.push_str(" ");
494 }
495 }
496
497 fn write_f64x3_array(&mut self, data: &[[f64; 3]]) {
498 self.output.push('[');
499 for (i, v) in data.iter().enumerate() {
500 if i > 0 {
501 self.output.push_str(", ");
502 }
503 let _ = write!(
504 self.output,
505 "({:.6}, {:.6}, {:.6})",
506 v[0], v[1], v[2]
507 );
508 }
509 self.output.push(']');
510 }
511
512 fn write_f64x2_array(&mut self, data: &[[f64; 2]]) {
513 self.output.push('[');
514 for (i, v) in data.iter().enumerate() {
515 if i > 0 {
516 self.output.push_str(", ");
517 }
518 let _ = write!(self.output, "({:.6}, {:.6})", v[0], v[1]);
519 }
520 self.output.push(']');
521 }
522
523 fn write_i32_array(&mut self, data: &[i32]) {
524 self.output.push('[');
525 for (i, v) in data.iter().enumerate() {
526 if i > 0 {
527 self.output.push_str(", ");
528 }
529 let _ = write!(self.output, "{}", v);
530 }
531 self.output.push(']');
532 }
533
534 fn write_string_array(&mut self, data: &[String]) {
535 self.output.push('[');
536 for (i, s) in data.iter().enumerate() {
537 if i > 0 {
538 self.output.push_str(", ");
539 }
540 let _ = write!(self.output, "\"{}\"", s);
541 }
542 self.output.push(']');
543 }
544
545 fn write_matrix4d(&mut self, m: &[f64; 16]) {
550 self.output.push_str("( ");
551 for row in 0..4 {
552 if row > 0 {
553 self.output.push_str(", ");
554 }
555 let base = row * 4;
556 let _ = write!(
557 self.output,
558 "({:.6}, {:.6}, {:.6}, {:.6})",
559 m[base],
560 m[base + 1],
561 m[base + 2],
562 m[base + 3]
563 );
564 }
565 self.output.push_str(" )");
566 }
567
568 fn write_matrix4d_array(&mut self, matrices: &[[f64; 16]]) {
569 self.output.push('[');
570 for (i, m) in matrices.iter().enumerate() {
571 if i > 0 {
572 self.output.push_str(", ");
573 }
574 self.write_matrix4d(m);
575 }
576 self.output.push(']');
577 }
578
579 fn write_flat_joint_indices(&mut self, data: &[Vec<i32>], element_size: usize) {
581 self.output.push('[');
582 let mut first = true;
583 for indices in data {
584 for j in 0..element_size {
585 if !first {
586 self.output.push_str(", ");
587 }
588 first = false;
589 let val = if j < indices.len() { indices[j] } else { 0 };
590 let _ = write!(self.output, "{}", val);
591 }
592 }
593 self.output.push(']');
594 }
595
596 fn write_flat_joint_weights(&mut self, data: &[Vec<f64>], element_size: usize) {
598 self.output.push('[');
599 let mut first = true;
600 for weights in data {
601 for j in 0..element_size {
602 if !first {
603 self.output.push_str(", ");
604 }
605 first = false;
606 let val = if j < weights.len() {
607 weights[j]
608 } else {
609 0.0
610 };
611 let _ = write!(self.output, "{:.6}", val);
612 }
613 }
614 self.output.push(']');
615 }
616}
617
618fn sanitise_name(path: &str) -> String {
622 path.chars()
623 .map(|c| match c {
624 '/' | ' ' | '.' => '_',
625 _ => c,
626 })
627 .collect()
628}
629
630pub struct BlendShapeTimeSamples {
640 pub shape_name: String,
642 pub time_weight_pairs: Vec<(f64, f32)>,
644}
645
646impl UsdaWriter {
647 pub fn write_blend_shape_animation(
666 &mut self,
667 mesh_path: &str,
668 samples: &[BlendShapeTimeSamples],
669 ) -> anyhow::Result<()> {
670 let mut shape_names: Vec<&str> = Vec::new();
672 for s in samples {
673 let name = s.shape_name.as_str();
674 if !shape_names.contains(&name) {
675 shape_names.push(name);
676 }
677 }
678
679 let mut time_codes: Vec<f64> = Vec::new();
681 for s in samples {
682 for &(t, _) in &s.time_weight_pairs {
683 if !time_codes.contains(&t) {
687 time_codes.push(t);
688 }
689 }
690 }
691 time_codes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
692
693 let n_shapes = shape_names.len();
698 let n_times = time_codes.len();
699
700 let mut weight_matrix: Vec<f32> = vec![0.0_f32; n_shapes * n_times];
702
703 for s in samples {
704 let shape_idx = shape_names
705 .iter()
706 .position(|&n| n == s.shape_name.as_str())
707 .ok_or_else(|| anyhow::anyhow!("shape '{}' not in name list", s.shape_name))?;
708
709 for &(t, w) in &s.time_weight_pairs {
710 let time_idx = time_codes
711 .iter()
712 .position(|&tc| tc == t)
713 .ok_or_else(|| anyhow::anyhow!("time code {} not in time list", t))?;
714 weight_matrix[shape_idx * n_times + time_idx] = w;
715 }
716 }
717
718 self.begin_def("SkelAnimation", "BodyAnim");
720
721 self.write_indent();
723 self.output
724 .push_str("uniform token purpose = \"default\"\n");
725
726 self.write_indent();
728 self.output.push_str("uniform token[] blendShapes = [");
729 for (i, name) in shape_names.iter().enumerate() {
730 if i > 0 {
731 self.output.push_str(", ");
732 }
733 let _ = write!(self.output, "\"{}\"", name);
734 }
735 self.output.push_str("]\n");
736
737 self.write_indent();
739 self.output
740 .push_str("float[] blendShapeWeights.timeSamples = {\n");
741
742 for (ti, &tc) in time_codes.iter().enumerate() {
743 self.write_indent();
744 let tc_str = if tc.fract() == 0.0 {
746 format!("{}", tc as i64)
747 } else {
748 format!("{}", tc)
749 };
750 let _ = write!(self.output, " {}: [", tc_str);
751 for si in 0..n_shapes {
752 if si > 0 {
753 self.output.push_str(", ");
754 }
755 let w = weight_matrix[si * n_times + ti];
756 let _ = write!(self.output, "{}", format_weight(w));
758 }
759 self.output.push_str("]\n");
760 }
761
762 self.write_indent();
763 self.output.push_str("}\n");
764
765 self.write_indent();
767 let _ = writeln!(self.output, "rel skelTargets = <{}>", mesh_path);
768
769 self.end_def(); Ok(())
771 }
772}
773
774fn format_weight(w: f32) -> String {
778 let s = format!("{:.6}", w);
780 let s = s.trim_end_matches('0');
781 if s.ends_with('.') {
783 format!("{}0", s)
784 } else {
785 s.to_string()
786 }
787}
788
789#[cfg(test)]
792mod tests {
793 use super::*;
794
795 fn identity_matrix() -> [f64; 16] {
796 [
797 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,
798 ]
799 }
800
801 fn sample_mesh() -> UsdMesh {
802 UsdMesh {
803 name: "Body".to_string(),
804 positions: vec![
805 [0.0, 0.0, 0.0],
806 [1.0, 0.0, 0.0],
807 [1.0, 1.0, 0.0],
808 [0.0, 1.0, 0.0],
809 ],
810 normals: vec![
811 [0.0, 0.0, 1.0],
812 [0.0, 0.0, 1.0],
813 [0.0, 0.0, 1.0],
814 [0.0, 0.0, 1.0],
815 [0.0, 0.0, 1.0],
816 [0.0, 0.0, 1.0],
817 ],
818 uvs: vec![
819 [0.0, 0.0],
820 [1.0, 0.0],
821 [1.0, 1.0],
822 [0.0, 0.0],
823 [1.0, 1.0],
824 [0.0, 1.0],
825 ],
826 face_vertex_counts: vec![3, 3],
827 face_vertex_indices: vec![0, 1, 2, 0, 2, 3],
828 subdivision_scheme: UsdSubdivScheme::None,
829 }
830 }
831
832 fn sample_material() -> UsdMaterial {
833 UsdMaterial {
834 name: "Skin".to_string(),
835 diffuse_color: [0.8, 0.6, 0.5],
836 metallic: 0.0,
837 roughness: 0.7,
838 opacity: 1.0,
839 normal_scale: 1.0,
840 }
841 }
842
843 fn sample_skeleton() -> UsdSkeleton {
844 UsdSkeleton {
845 joint_names: vec!["Hips".to_string(), "Spine".to_string()],
846 joint_paths: vec!["Hips".to_string(), "Hips/Spine".to_string()],
847 bind_transforms: vec![identity_matrix(), identity_matrix()],
848 rest_transforms: vec![identity_matrix(), identity_matrix()],
849 }
850 }
851
852 #[test]
855 fn test_header_contains_magic() {
856 let mut w = UsdaWriter::new();
857 w.write_header("Y", 1.0);
858 let out = w.finish();
859 assert!(out.starts_with("#usda 1.0"), "must start with #usda 1.0");
860 }
861
862 #[test]
863 fn test_header_up_axis_y() {
864 let mut w = UsdaWriter::new();
865 w.write_header("Y", 1.0);
866 let out = w.finish();
867 assert!(out.contains("upAxis = \"Y\""));
868 }
869
870 #[test]
871 fn test_header_up_axis_z() {
872 let mut w = UsdaWriter::new();
873 w.write_header("Z", 0.01);
874 let out = w.finish();
875 assert!(out.contains("upAxis = \"Z\""));
876 assert!(out.contains("metersPerUnit = 0.010000"));
877 }
878
879 #[test]
882 fn test_begin_end_def() {
883 let mut w = UsdaWriter::new();
884 w.begin_def("Xform", "Root");
885 w.end_def();
886 let out = w.finish();
887 assert!(out.contains("def Xform \"Root\" {"));
888 assert!(out.contains('}'));
889 }
890
891 #[test]
892 fn test_nested_def() {
893 let mut w = UsdaWriter::new();
894 w.begin_def("Xform", "Root");
895 w.begin_def("Xform", "Child");
896 w.end_def();
897 w.end_def();
898 let out = w.finish();
899 assert!(out.contains("def Xform \"Root\""));
900 assert!(out.contains(" def Xform \"Child\""));
901 }
902
903 #[test]
904 fn test_finish_closes_unclosed_defs() {
905 let mut w = UsdaWriter::new();
906 w.begin_def("Xform", "A");
907 w.begin_def("Xform", "B");
908 let out = w.finish();
910 let close_count = out.matches('}').count();
911 assert!(close_count >= 2, "finish must auto-close open defs");
912 }
913
914 #[test]
917 fn test_write_mesh_contains_points() {
918 let mut w = UsdaWriter::new();
919 w.write_header("Y", 1.0);
920 w.begin_def("Xform", "Root");
921 w.write_mesh(&sample_mesh()).expect("write_mesh");
922 w.end_def();
923 let out = w.finish();
924 assert!(out.contains("point3f[] points = "));
925 assert!(out.contains("(0.000000, 0.000000, 0.000000)"));
926 assert!(out.contains("(1.000000, 0.000000, 0.000000)"));
927 }
928
929 #[test]
930 fn test_write_mesh_contains_normals() {
931 let mut w = UsdaWriter::new();
932 w.write_mesh(&sample_mesh()).expect("write_mesh");
933 let out = w.finish();
934 assert!(out.contains("normal3f[] normals = "));
935 assert!(out.contains("interpolation = \"faceVarying\""));
936 }
937
938 #[test]
939 fn test_write_mesh_contains_uvs() {
940 let mut w = UsdaWriter::new();
941 w.write_mesh(&sample_mesh()).expect("write_mesh");
942 let out = w.finish();
943 assert!(out.contains("texCoord2f[] primvars:st = "));
944 }
945
946 #[test]
947 fn test_write_mesh_contains_face_data() {
948 let mut w = UsdaWriter::new();
949 w.write_mesh(&sample_mesh()).expect("write_mesh");
950 let out = w.finish();
951 assert!(out.contains("int[] faceVertexCounts = [3, 3]"));
952 assert!(out.contains("int[] faceVertexIndices = [0, 1, 2, 0, 2, 3]"));
953 }
954
955 #[test]
956 fn test_write_mesh_subdivision_scheme() {
957 let mut mesh = sample_mesh();
958 mesh.subdivision_scheme = UsdSubdivScheme::CatmullClark;
959 let mut w = UsdaWriter::new();
960 w.write_mesh(&mesh).expect("write_mesh");
961 let out = w.finish();
962 assert!(out.contains("subdivisionScheme = \"catmullClark\""));
963 }
964
965 #[test]
966 fn test_write_mesh_no_normals() {
967 let mut mesh = sample_mesh();
968 mesh.normals.clear();
969 let mut w = UsdaWriter::new();
970 w.write_mesh(&mesh).expect("write_mesh");
971 let out = w.finish();
972 assert!(
973 !out.contains("normal3f[]"),
974 "no normals section if empty"
975 );
976 }
977
978 #[test]
979 fn test_write_mesh_no_uvs() {
980 let mut mesh = sample_mesh();
981 mesh.uvs.clear();
982 let mut w = UsdaWriter::new();
983 w.write_mesh(&mesh).expect("write_mesh");
984 let out = w.finish();
985 assert!(
986 !out.contains("texCoord2f[]"),
987 "no UVs section if empty"
988 );
989 }
990
991 #[test]
994 fn test_write_material_basic() {
995 let mut w = UsdaWriter::new();
996 w.write_material(&sample_material()).expect("write_material");
997 let out = w.finish();
998 assert!(out.contains("def Material \"Skin\""));
999 assert!(out.contains("UsdPreviewSurface"));
1000 assert!(out.contains("diffuseColor"));
1001 assert!(out.contains("metallic"));
1002 assert!(out.contains("roughness"));
1003 assert!(out.contains("opacity"));
1004 }
1005
1006 #[test]
1007 fn test_write_material_diffuse_values() {
1008 let mut w = UsdaWriter::new();
1009 w.write_material(&sample_material()).expect("write_material");
1010 let out = w.finish();
1011 assert!(out.contains("0.800000"));
1012 assert!(out.contains("0.600000"));
1013 assert!(out.contains("0.500000"));
1014 }
1015
1016 #[test]
1017 fn test_write_material_surface_output() {
1018 let mut w = UsdaWriter::new();
1019 w.write_material(&sample_material()).expect("write_material");
1020 let out = w.finish();
1021 assert!(out.contains("outputs:surface"));
1022 }
1023
1024 #[test]
1027 fn test_write_skeleton_basic() {
1028 let mut w = UsdaWriter::new();
1029 w.write_skeleton(&sample_skeleton()).expect("write_skeleton");
1030 let out = w.finish();
1031 assert!(out.contains("def Skeleton \"Skeleton\""));
1032 assert!(out.contains("uniform token[] joints"));
1033 assert!(out.contains("\"Hips\""));
1034 assert!(out.contains("\"Hips/Spine\""));
1035 }
1036
1037 #[test]
1038 fn test_write_skeleton_transforms() {
1039 let mut w = UsdaWriter::new();
1040 w.write_skeleton(&sample_skeleton()).expect("write_skeleton");
1041 let out = w.finish();
1042 assert!(out.contains("matrix4d[] bindTransforms"));
1043 assert!(out.contains("matrix4d[] restTransforms"));
1044 assert!(out.contains("1.000000"));
1046 }
1047
1048 #[test]
1049 fn test_write_skeleton_joint_names() {
1050 let mut w = UsdaWriter::new();
1051 w.write_skeleton(&sample_skeleton()).expect("write_skeleton");
1052 let out = w.finish();
1053 assert!(out.contains("jointNames"));
1054 assert!(out.contains("\"Hips\""));
1055 assert!(out.contains("\"Spine\""));
1056 }
1057
1058 #[test]
1061 fn test_write_skin_binding_basic() {
1062 let binding = UsdSkinBinding {
1063 joint_indices: vec![vec![0, 1], vec![0], vec![1, 0]],
1064 joint_weights: vec![vec![0.7, 0.3], vec![1.0], vec![0.5, 0.5]],
1065 skeleton_path: "/Root/Skeleton".to_string(),
1066 };
1067 let mut w = UsdaWriter::new();
1068 w.write_skin_binding("/Root/Body", &binding)
1069 .expect("write_skin_binding");
1070 let out = w.finish();
1071 assert!(out.contains("primvars:skel:skeleton"));
1072 assert!(out.contains("/Root/Skeleton"));
1073 }
1074
1075 #[test]
1076 fn test_write_skin_binding_joint_indices_flattened() {
1077 let binding = UsdSkinBinding {
1078 joint_indices: vec![vec![0, 1], vec![0]],
1079 joint_weights: vec![vec![0.7, 0.3], vec![1.0]],
1080 skeleton_path: "/Root/Skeleton".to_string(),
1081 };
1082 let mut w = UsdaWriter::new();
1083 w.write_skin_binding("/Root/Body", &binding)
1084 .expect("write_skin_binding");
1085 let out = w.finish();
1086 assert!(out.contains("primvars:skel:jointIndices"));
1088 assert!(out.contains("elementSize = 2"));
1089 }
1090
1091 #[test]
1092 fn test_write_skin_binding_weights_flattened() {
1093 let binding = UsdSkinBinding {
1094 joint_indices: vec![vec![0, 1], vec![0]],
1095 joint_weights: vec![vec![0.7, 0.3], vec![1.0]],
1096 skeleton_path: "/Skel".to_string(),
1097 };
1098 let mut w = UsdaWriter::new();
1099 w.write_skin_binding("/Mesh", &binding)
1100 .expect("write_skin_binding");
1101 let out = w.finish();
1102 assert!(out.contains("primvars:skel:jointWeights"));
1103 assert!(out.contains("0.700000"));
1104 assert!(out.contains("0.300000"));
1105 assert!(out.contains("1.000000"));
1106 }
1107
1108 #[test]
1109 fn test_write_skin_binding_empty() {
1110 let binding = UsdSkinBinding {
1111 joint_indices: vec![],
1112 joint_weights: vec![],
1113 skeleton_path: "/Skel".to_string(),
1114 };
1115 let mut w = UsdaWriter::new();
1116 w.write_skin_binding("/Mesh", &binding)
1117 .expect("write_skin_binding");
1118 let out = w.finish();
1119 assert!(!out.contains("primvars:skel:jointIndices"));
1121 }
1122
1123 #[test]
1126 fn test_write_blend_shapes_basic() {
1127 let shapes = vec![
1128 UsdBlendShape {
1129 name: "Smile".to_string(),
1130 offsets: vec![[0.1, 0.2, 0.0], [0.05, 0.1, 0.0]],
1131 point_indices: vec![10, 11],
1132 },
1133 UsdBlendShape {
1134 name: "Frown".to_string(),
1135 offsets: vec![[-0.1, -0.2, 0.0]],
1136 point_indices: vec![10],
1137 },
1138 ];
1139 let mut w = UsdaWriter::new();
1140 w.write_blend_shapes("/Root/Body", &shapes)
1141 .expect("write_blend_shapes");
1142 let out = w.finish();
1143 assert!(out.contains("def BlendShape \"Smile\""));
1144 assert!(out.contains("def BlendShape \"Frown\""));
1145 assert!(out.contains("vector3f[] offsets"));
1146 assert!(out.contains("int[] pointIndices"));
1147 }
1148
1149 #[test]
1150 fn test_write_blend_shapes_names_array() {
1151 let shapes = vec![UsdBlendShape {
1152 name: "Open".to_string(),
1153 offsets: vec![[0.0, 0.1, 0.0]],
1154 point_indices: vec![5],
1155 }];
1156 let mut w = UsdaWriter::new();
1157 w.write_blend_shapes("/Mesh", &shapes)
1158 .expect("write_blend_shapes");
1159 let out = w.finish();
1160 assert!(out.contains("blendShapes = [\"Open\"]"));
1161 }
1162
1163 #[test]
1164 fn test_write_blend_shapes_targets_rel() {
1165 let shapes = vec![
1166 UsdBlendShape {
1167 name: "A".to_string(),
1168 offsets: vec![[1.0, 0.0, 0.0]],
1169 point_indices: vec![0],
1170 },
1171 UsdBlendShape {
1172 name: "B".to_string(),
1173 offsets: vec![[0.0, 1.0, 0.0]],
1174 point_indices: vec![1],
1175 },
1176 ];
1177 let mut w = UsdaWriter::new();
1178 w.write_blend_shapes("/M", &shapes)
1179 .expect("write_blend_shapes");
1180 let out = w.finish();
1181 assert!(out.contains("blendShapeTargets = [<./A>, <./B>]"));
1182 }
1183
1184 #[test]
1185 fn test_write_blend_shapes_empty() {
1186 let mut w = UsdaWriter::new();
1187 w.write_blend_shapes("/Mesh", &[])
1188 .expect("write_blend_shapes");
1189 let out = w.finish();
1190 assert!(
1191 !out.contains("BlendShape"),
1192 "empty shapes should produce no output"
1193 );
1194 }
1195
1196 #[test]
1199 fn test_write_xform_basic() {
1200 let mut w = UsdaWriter::new();
1201 w.write_xform("Root", &identity_matrix())
1202 .expect("write_xform");
1203 let out = w.finish();
1204 assert!(out.contains("def Xform \"Root\""));
1205 assert!(out.contains("matrix4d xformOp:transform"));
1206 assert!(out.contains("xformOpOrder"));
1207 }
1208
1209 #[test]
1210 fn test_write_xform_matrix_values() {
1211 let mat = [
1212 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 1.0, 2.0, 3.0, 1.0,
1213 ];
1214 let mut w = UsdaWriter::new();
1215 w.write_xform("Scaled", &mat).expect("write_xform");
1216 let out = w.finish();
1217 assert!(out.contains("2.000000"));
1218 assert!(out.contains("3.000000"));
1219 }
1220
1221 #[test]
1224 fn test_full_scene() {
1225 let mut w = UsdaWriter::new();
1226 w.write_header("Y", 1.0);
1227 w.begin_def("Xform", "Root");
1228 w.write_mesh(&sample_mesh()).expect("write_mesh");
1229 w.write_material(&sample_material()).expect("write_material");
1230 w.end_def();
1231 let out = w.finish();
1232
1233 assert!(out.starts_with("#usda 1.0"));
1234 assert!(out.contains("def Xform \"Root\""));
1235 assert!(out.contains("def Mesh \"Body\""));
1236 assert!(out.contains("def Material \"Skin\""));
1237 }
1238
1239 #[test]
1240 fn test_full_scene_with_skeleton_and_skin() {
1241 let mut w = UsdaWriter::new();
1242 w.write_header("Y", 1.0);
1243 w.begin_def("Xform", "Root");
1244 w.write_mesh(&sample_mesh()).expect("write_mesh");
1245 w.write_skeleton(&sample_skeleton()).expect("write_skeleton");
1246 let binding = UsdSkinBinding {
1247 joint_indices: vec![vec![0], vec![0, 1], vec![1], vec![0, 1]],
1248 joint_weights: vec![vec![1.0], vec![0.6, 0.4], vec![1.0], vec![0.5, 0.5]],
1249 skeleton_path: "/Root/Skeleton".to_string(),
1250 };
1251 w.write_skin_binding("/Root/Body", &binding)
1252 .expect("write_skin_binding");
1253 w.end_def();
1254 let out = w.finish();
1255
1256 assert!(out.contains("def Skeleton"));
1257 assert!(out.contains("primvars:skel:skeleton"));
1258 assert!(out.contains("primvars:skel:jointIndices"));
1259 assert!(out.contains("primvars:skel:jointWeights"));
1260 }
1261
1262 #[test]
1263 fn test_full_scene_with_blend_shapes() {
1264 let shapes = vec![
1265 UsdBlendShape {
1266 name: "Smile".to_string(),
1267 offsets: vec![[0.1, 0.2, 0.0]],
1268 point_indices: vec![0],
1269 },
1270 UsdBlendShape {
1271 name: "Blink".to_string(),
1272 offsets: vec![[0.0, -0.1, 0.0]],
1273 point_indices: vec![2],
1274 },
1275 ];
1276 let mut w = UsdaWriter::new();
1277 w.write_header("Y", 1.0);
1278 w.begin_def("Xform", "Root");
1279 w.write_mesh(&sample_mesh()).expect("write_mesh");
1280 w.write_blend_shapes("/Root/Body", &shapes)
1281 .expect("write_blend_shapes");
1282 w.end_def();
1283 let out = w.finish();
1284
1285 assert!(out.contains("def BlendShape \"Smile\""));
1286 assert!(out.contains("def BlendShape \"Blink\""));
1287 }
1288
1289 #[test]
1290 fn test_subdiv_scheme_tokens() {
1291 assert_eq!(UsdSubdivScheme::None.as_token(), "none");
1292 assert_eq!(UsdSubdivScheme::CatmullClark.as_token(), "catmullClark");
1293 assert_eq!(UsdSubdivScheme::Loop.as_token(), "loop");
1294 assert_eq!(UsdSubdivScheme::Bilinear.as_token(), "bilinear");
1295 }
1296
1297 #[test]
1298 fn test_sanitise_name() {
1299 assert_eq!(sanitise_name("/Root/Body"), "_Root_Body");
1300 assert_eq!(sanitise_name("hello world"), "hello_world");
1301 assert_eq!(sanitise_name("a.b.c"), "a_b_c");
1302 assert_eq!(sanitise_name("NoChange"), "NoChange");
1303 }
1304
1305 #[test]
1306 fn test_writer_default() {
1307 let w = UsdaWriter::default();
1308 let out = w.finish();
1309 assert!(out.is_empty(), "default writer should produce empty output");
1310 }
1311
1312 #[test]
1313 fn test_end_def_at_zero_indent() {
1314 let mut w = UsdaWriter::new();
1315 w.end_def();
1317 let out = w.finish();
1318 assert!(out.contains('}'));
1319 }
1320
1321 #[test]
1322 fn test_multiple_meshes() {
1323 let mut w = UsdaWriter::new();
1324 w.write_header("Y", 1.0);
1325 w.begin_def("Xform", "Root");
1326
1327 let mut mesh1 = sample_mesh();
1328 mesh1.name = "Head".to_string();
1329 w.write_mesh(&mesh1).expect("write_mesh head");
1330
1331 let mut mesh2 = sample_mesh();
1332 mesh2.name = "Hand".to_string();
1333 w.write_mesh(&mesh2).expect("write_mesh hand");
1334
1335 w.end_def();
1336 let out = w.finish();
1337
1338 assert!(out.contains("def Mesh \"Head\""));
1339 assert!(out.contains("def Mesh \"Hand\""));
1340 }
1341
1342 #[test]
1343 fn test_xform_with_translation() {
1344 let mut mat = identity_matrix();
1345 mat[12] = 5.0;
1346 mat[13] = 10.0;
1347 mat[14] = -3.0;
1348 let mut w = UsdaWriter::new();
1349 w.write_xform("Offset", &mat).expect("write_xform");
1350 let out = w.finish();
1351 assert!(out.contains("5.000000"));
1352 assert!(out.contains("10.000000"));
1353 assert!(out.contains("-3.000000"));
1354 }
1355
1356 #[test]
1357 fn test_write_to_file() {
1358 let mut w = UsdaWriter::new();
1359 w.write_header("Y", 1.0);
1360 w.begin_def("Xform", "Root");
1361 w.write_mesh(&sample_mesh()).expect("write_mesh");
1362 w.end_def();
1363 let out = w.finish();
1364
1365 let path = std::env::temp_dir().join("test_usda_export_writer.usda");
1366 std::fs::write(&path, &out).expect("write file");
1367
1368 let read_back = std::fs::read_to_string(&path).expect("read file");
1369 assert_eq!(out, read_back);
1370 assert!(read_back.starts_with("#usda 1.0"));
1371
1372 let _ = std::fs::remove_file(&path);
1373 }
1374
1375 #[test]
1379 fn test_blend_shape_animation_single_frame() {
1380 let samples = vec![BlendShapeTimeSamples {
1381 shape_name: "smile".to_string(),
1382 time_weight_pairs: vec![(0.0, 1.0)],
1383 }];
1384 let mut w = UsdaWriter::new();
1385 w.write_blend_shape_animation("/Root/Body", &samples)
1386 .expect("write_blend_shape_animation");
1387 let out = w.finish();
1388 assert!(
1389 out.contains("timeSamples"),
1390 "output must contain 'timeSamples'"
1391 );
1392 assert!(
1393 out.contains("\"smile\""),
1394 "output must include shape name 'smile'"
1395 );
1396 assert!(
1397 out.contains("0: ["),
1398 "output must include time code 0"
1399 );
1400 }
1401
1402 #[test]
1404 fn test_blend_shape_animation_multi_frame_sorted() {
1405 let samples = vec![
1406 BlendShapeTimeSamples {
1407 shape_name: "smile".to_string(),
1408 time_weight_pairs: vec![(24.0, 1.0), (0.0, 0.0), (12.0, 0.5)],
1410 },
1411 BlendShapeTimeSamples {
1412 shape_name: "frown".to_string(),
1413 time_weight_pairs: vec![(0.0, 0.0), (12.0, 0.0), (24.0, 0.0)],
1414 },
1415 ];
1416 let mut w = UsdaWriter::new();
1417 w.write_blend_shape_animation("/Root/Body", &samples)
1418 .expect("write_blend_shape_animation");
1419 let out = w.finish();
1420
1421 assert!(out.contains("0: ["), "time 0 must be present");
1423 assert!(out.contains("12: ["), "time 12 must be present");
1424 assert!(out.contains("24: ["), "time 24 must be present");
1425
1426 assert!(out.contains("\"smile\""), "shape 'smile' must be in output");
1428 assert!(out.contains("\"frown\""), "shape 'frown' must be in output");
1429
1430 let pos0 = out.find("0: [").expect("pos of time 0");
1432 let pos12 = out.find("12: [").expect("pos of time 12");
1433 let pos24 = out.find("24: [").expect("pos of time 24");
1434 assert!(pos0 < pos12, "time 0 must appear before time 12");
1435 assert!(pos12 < pos24, "time 12 must appear before time 24");
1436 }
1437
1438 #[test]
1440 fn test_blend_shape_animation_contains_purpose_default() {
1441 let samples = vec![BlendShapeTimeSamples {
1442 shape_name: "blink".to_string(),
1443 time_weight_pairs: vec![(1.0, 0.5)],
1444 }];
1445 let mut w = UsdaWriter::new();
1446 w.write_blend_shape_animation("/Root/Face", &samples)
1447 .expect("write_blend_shape_animation");
1448 let out = w.finish();
1449 assert!(
1450 out.contains("uniform token purpose = \"default\""),
1451 "output must contain purpose = \"default\""
1452 );
1453 assert!(
1455 out.contains("rel skelTargets = </Root/Face>"),
1456 "output must contain rel skelTargets = </Root/Face>"
1457 );
1458 }
1459}