1use std::collections::HashMap;
15use std::path::Path;
16
17use crate::components::{SkeletonJoint, SkinnedVertexData, VertexData};
18use crate::gfx::skeleton::JointPose;
19use crate::gfx::transform::euler_yxz_from_quat;
20use crate::import::gltf_source::GltfDoc;
21
22use crate::import::NEUTRAL_COLOR;
23
24pub(crate) struct ImportedSkinnedMesh {
26 pub vertices: Vec<SkinnedVertexData>,
27 pub indices: Vec<u16>,
28 pub skeleton: Vec<SkeletonJoint>,
29 pub(crate) morph_target_names: Vec<String>,
31 pub(crate) morph_deltas: Vec<crate::components::MorphDelta>,
33}
34
35pub(crate) fn import_skinned_from_doc(
39 doc: &GltfDoc,
40 source: &str,
41 skin_index: u32,
42) -> Result<ImportedSkinnedMesh, String> {
43 let SkinnedNode { mesh, skin, .. } = skinned_node(doc, source, skin_index)?;
44
45 let skeleton = import_skeleton(&skin)?;
46 let (vertices, indices, morph_deltas) = import_geometry(&mesh, doc, &skeleton.remap)?;
47 let target_count = if vertices.is_empty() {
48 0
49 } else {
50 morph_deltas.len() / vertices.len()
51 };
52 let morph_target_names = morph_target_names(&mesh, target_count);
53
54 Ok(ImportedSkinnedMesh {
55 vertices,
56 indices,
57 skeleton: skeleton.joints,
58 morph_target_names,
59 morph_deltas,
60 })
61}
62
63fn morph_target_names(mesh: &gltf::Mesh<'_>, target_count: usize) -> Vec<String> {
66 let from_extras: Vec<String> = mesh
67 .extras()
68 .as_deref()
69 .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw.get()).ok())
70 .and_then(|v| {
71 v.get("targetNames").map(|names| {
72 names
73 .as_array()
74 .map(|a| {
75 a.iter()
76 .map(|n| n.as_str().unwrap_or("").to_string())
77 .collect()
78 })
79 .unwrap_or_default()
80 })
81 })
82 .unwrap_or_default();
83 (0..target_count)
84 .map(|i| {
85 from_extras
86 .get(i)
87 .filter(|n| !n.is_empty())
88 .cloned()
89 .unwrap_or_else(|| format!("target_{i}"))
90 })
91 .collect()
92}
93
94pub fn parse_glb(source: &str, assets_dir: Option<&Path>) -> Result<GltfDoc, String> {
100 GltfDoc::parse_file(&resolve_source(source, assets_dir))
101}
102
103pub(crate) fn import_static_glb_primitive_from_doc(
115 doc: &GltfDoc,
116 source: &str,
117 primitive_index: u32,
118) -> Result<(Vec<VertexData>, Vec<u16>), String> {
119 let (vertices, indices_u32) = read_primitive_geometry(doc, source, primitive_index)?;
120 let mut indices: Vec<u16> = Vec::with_capacity(indices_u32.len());
121 for v in indices_u32 {
122 if v > u16::MAX as u32 {
123 return Err(format!(
124 "'{}': primitive {} exceeds the {}-vertex u16 index limit; \
125 import via `cn add` to auto-split it",
126 source,
127 primitive_index,
128 u16::MAX
129 ));
130 }
131 indices.push(v as u16);
132 }
133 Ok((vertices, indices))
134}
135
136pub(crate) fn read_primitive_geometry(
141 doc: &GltfDoc,
142 source: &str,
143 primitive_index: u32,
144) -> Result<(Vec<VertexData>, Vec<u32>), String> {
145 let primitive = doc
146 .doc
147 .document
148 .meshes()
149 .flat_map(|m| m.primitives())
150 .nth(primitive_index as usize)
151 .ok_or_else(|| {
152 format!(
153 "'{}': primitive_index {} is out of range",
154 source, primitive_index
155 )
156 })?;
157
158 if primitive.mode() != gltf::mesh::Mode::Triangles {
159 return Err(format!(
160 "'{}': primitive {} uses topology {:?}; only TRIANGLES is supported",
161 source,
162 primitive_index,
163 primitive.mode()
164 ));
165 }
166
167 let reader = primitive.reader(|b| doc.buffer_bytes(b));
168
169 let positions: Vec<[f32; 3]> = reader
170 .read_positions()
171 .ok_or_else(|| {
172 format!(
173 "'{}': primitive {} has no POSITION data (missing buffer data)",
174 source, primitive_index
175 )
176 })?
177 .collect();
178 let uvs: Vec<[f32; 2]> = reader
179 .read_tex_coords(0)
180 .map(|t| t.into_f32().collect())
181 .unwrap_or_default();
182 let colors: Vec<[f32; 3]> = reader
183 .read_colors(0)
184 .map(|c| c.into_rgb_f32().collect())
185 .unwrap_or_default();
186
187 let mut vertices: Vec<VertexData> = Vec::with_capacity(positions.len());
188 for (i, &pos) in positions.iter().enumerate() {
189 vertices.push(VertexData {
190 pos,
191 color: colors.get(i).copied().unwrap_or(NEUTRAL_COLOR),
192 uv: uvs.get(i).copied().unwrap_or([0.0, 0.0]),
193 });
194 }
195
196 let indices: Vec<u32> = match reader.read_indices() {
197 Some(idx) => idx.into_u32().collect(),
198 None => (0..positions.len() as u32).collect(),
199 };
200
201 if vertices.is_empty() {
202 return Err(format!(
203 "'{}': primitive {} has no vertices",
204 source, primitive_index
205 ));
206 }
207 if let Some(&bad) = indices.iter().find(|&&i| i as usize >= vertices.len()) {
211 return Err(format!(
212 "'{}': primitive {} index {} out of range ({} vertices)",
213 source,
214 primitive_index,
215 bad,
216 vertices.len()
217 ));
218 }
219 Ok((vertices, indices))
220}
221
222pub(crate) fn count_u16_chunks(indices: &[u32]) -> usize {
233 let limit: usize = u16::MAX as usize + 1;
234 let mut chunks = 0usize;
235 let mut cur_len = 0usize;
236 let mut seen: std::collections::HashSet<u32> = std::collections::HashSet::new();
237 for tri in indices.chunks_exact(3) {
238 let new_in_tri = tri.iter().filter(|&&v| !seen.contains(&v)).count();
239 if cur_len != 0 && cur_len + new_in_tri > limit {
240 chunks += 1;
241 cur_len = 0;
242 seen.clear();
243 }
244 for &v in tri {
245 if seen.insert(v) {
246 cur_len += 1;
247 }
248 }
249 }
250 if cur_len != 0 {
251 chunks += 1;
252 }
253 chunks
254}
255
256pub(crate) fn split_into_u16_chunks(
257 vertices: &[VertexData],
258 indices: &[u32],
259) -> Vec<(Vec<VertexData>, Vec<u16>)> {
260 let limit: usize = u16::MAX as usize + 1;
261 let mut chunks: Vec<(Vec<VertexData>, Vec<u16>)> = Vec::new();
262 let mut cur_verts: Vec<VertexData> = Vec::new();
263 let mut cur_indices: Vec<u16> = Vec::new();
264 let mut remap: std::collections::HashMap<u32, u16> = std::collections::HashMap::new();
265
266 for tri in indices.chunks_exact(3) {
267 let new_in_tri = tri.iter().filter(|&&v| !remap.contains_key(&v)).count();
268 if !cur_verts.is_empty() && cur_verts.len() + new_in_tri > limit {
269 chunks.push((
270 std::mem::take(&mut cur_verts),
271 std::mem::take(&mut cur_indices),
272 ));
273 remap.clear();
274 }
275 for &v in tri {
276 let local = *remap.entry(v).or_insert_with(|| {
277 let idx = cur_verts.len() as u16;
278 cur_verts.push(vertices[v as usize].clone());
279 idx
280 });
281 cur_indices.push(local);
282 }
283 }
284 if !cur_verts.is_empty() {
285 chunks.push((cur_verts, cur_indices));
286 }
287 chunks
288}
289
290pub(crate) fn resolve_source(source: &str, assets_dir: Option<&Path>) -> String {
296 crate::source::resolve_source_path(source, assets_dir)
297}
298
299pub(crate) struct ImportedSkeleton {
303 pub joints: Vec<SkeletonJoint>,
304 pub remap: Vec<usize>,
306 pub(crate) node_to_joint: HashMap<usize, usize>,
310}
311
312pub(crate) fn import_skeleton(skin: &gltf::Skin<'_>) -> Result<ImportedSkeleton, String> {
316 let joint_nodes: Vec<gltf::Node<'_>> = skin.joints().collect();
317 let n = joint_nodes.len();
318 if n == 0 {
319 return Err("glTF skin has no joints".to_string());
320 }
321
322 let node_to_joint: HashMap<usize, usize> = joint_nodes
325 .iter()
326 .enumerate()
327 .map(|(sj, node)| (node.index(), sj))
328 .collect();
329
330 let mut parents: Vec<Option<usize>> = vec![None; n];
333 for (sj, node) in joint_nodes.iter().enumerate() {
334 for child in node.children() {
335 if let Some(&cj) = node_to_joint.get(&child.index())
336 && cj != sj
337 {
338 parents[cj] = Some(sj);
339 }
340 }
341 }
342
343 let (order, remap) = topological_order(&parents);
344
345 let joints = order
346 .iter()
347 .map(|&sj| {
348 let node = &joint_nodes[sj];
349 let (translation, rotation, scale) = node.transform().decomposed();
350 SkeletonJoint {
351 name: node.name().unwrap_or("").to_string(),
352 parent: parents[sj].map_or(-1, |p| remap[p] as i32),
353 translation,
354 rotation_deg: euler_yxz_from_quat(rotation),
355 scale,
356 }
357 })
358 .collect();
359
360 Ok(ImportedSkeleton {
361 joints,
362 remap,
363 node_to_joint,
364 })
365}
366
367fn topological_order(parents: &[Option<usize>]) -> (Vec<usize>, Vec<usize>) {
373 let n = parents.len();
374 let mut order: Vec<usize> = Vec::with_capacity(n);
375 let mut emitted = vec![false; n];
376
377 loop {
378 let progress = order.len();
379 for (s, parent) in parents.iter().enumerate() {
380 if emitted[s] {
381 continue;
382 }
383 let ready = match *parent {
384 None => true,
385 Some(p) => p >= n || emitted[p],
386 };
387 if ready {
388 emitted[s] = true;
389 order.push(s);
390 }
391 }
392 if order.len() == progress {
393 break;
394 }
395 }
396 for (s, done) in emitted.iter().enumerate() {
398 if !done {
399 order.push(s);
400 }
401 }
402
403 let mut remap = vec![0usize; n];
404 for (new_idx, &s) in order.iter().enumerate() {
405 remap[s] = new_idx;
406 }
407 (order, remap)
408}
409
410type SkinnedGeometry = (
413 Vec<SkinnedVertexData>,
414 Vec<u16>,
415 Vec<crate::components::MorphDelta>,
416);
417
418type PrimTargetDeltas = (Vec<[f32; 3]>, Vec<[f32; 3]>);
420
421fn import_geometry(
422 mesh: &gltf::Mesh<'_>,
423 doc: &GltfDoc,
424 remap: &[usize],
425) -> Result<SkinnedGeometry, String> {
426 use crate::components::MorphDelta;
427
428 let mut vertices: Vec<SkinnedVertexData> = Vec::new();
429 let mut indices: Vec<u16> = Vec::new();
430 let mut targets: Vec<Vec<MorphDelta>> = Vec::new();
433 let mut tangent_deltas_seen = false;
434
435 for primitive in mesh.primitives() {
436 let reader = primitive.reader(|b| doc.buffer_bytes(b));
437
438 let joints: Vec<[u16; 4]> = match reader.read_joints(0) {
441 Some(j) => j.into_u16().collect(),
442 None => continue,
443 };
444 let positions: Vec<[f32; 3]> = reader
445 .read_positions()
446 .ok_or_else(|| {
447 "skinned primitive has no POSITION data (missing buffer data)".to_string()
448 })?
449 .collect();
450 let weights: Vec<[f32; 4]> = reader
451 .read_weights(0)
452 .ok_or_else(|| "skinned primitive missing WEIGHTS_0".to_string())?
453 .into_f32()
454 .collect();
455 let uvs: Vec<[f32; 2]> = reader
456 .read_tex_coords(0)
457 .map(|t| t.into_f32().collect())
458 .unwrap_or_default();
459 let colors: Vec<[f32; 3]> = reader
460 .read_colors(0)
461 .map(|c| c.into_rgb_f32().collect())
462 .unwrap_or_default();
463
464 let base = vertices.len() as u32;
465
466 let prim_targets: Vec<PrimTargetDeltas> = reader
467 .read_morph_targets()
468 .map(|(dp, dn, dt)| {
469 tangent_deltas_seen |= dt.is_some();
470 (
471 dp.map(|it| it.collect()).unwrap_or_default(),
472 dn.map(|it| it.collect()).unwrap_or_default(),
473 )
474 })
475 .collect();
476 for t in 0..targets.len().max(prim_targets.len()) {
477 if targets.len() <= t {
478 targets.push(vec![MorphDelta::default(); base as usize]);
479 }
480 let dst = &mut targets[t];
481 match prim_targets.get(t) {
482 Some((dp, dn)) => {
483 for i in 0..positions.len() {
484 dst.push(MorphDelta {
485 position: dp.get(i).copied().unwrap_or_default(),
486 normal: dn.get(i).copied().unwrap_or_default(),
487 });
488 }
489 }
490 None => {
491 dst.extend(std::iter::repeat_with(MorphDelta::default).take(positions.len()));
492 }
493 }
494 }
495
496 for (i, &pos) in positions.iter().enumerate() {
497 let raw = joints.get(i).copied().unwrap_or([0; 4]);
498 let bound = |j: u16| -> u32 {
499 remap.get(j as usize).map_or(0, |&r| r as u32)
502 };
503 vertices.push(SkinnedVertexData {
504 pos,
505 color: colors.get(i).copied().unwrap_or([1.0, 1.0, 1.0]),
506 uv: uvs.get(i).copied().unwrap_or([0.0, 0.0]),
507 joints: [bound(raw[0]), bound(raw[1]), bound(raw[2]), bound(raw[3])],
508 weights: weights.get(i).copied().unwrap_or([1.0, 0.0, 0.0, 0.0]),
509 });
510 }
511
512 let push_index = |indices: &mut Vec<u16>, v: u32| -> Result<(), String> {
513 let abs = base + v;
514 if abs > u16::MAX as u32 {
515 return Err(format!(
516 "imported skinned mesh exceeds the {}-vertex u16 index limit",
517 u16::MAX
518 ));
519 }
520 indices.push(abs as u16);
521 Ok(())
522 };
523 match reader.read_indices() {
524 Some(idx) => {
525 for v in idx.into_u32() {
526 push_index(&mut indices, v)?;
527 }
528 }
529 None => {
530 for v in 0..positions.len() as u32 {
532 push_index(&mut indices, v)?;
533 }
534 }
535 }
536 }
537
538 if vertices.is_empty() {
539 return Err("glTF mesh has no skinned primitives (no JOINTS_0)".to_string());
540 }
541 if tangent_deltas_seen {
542 tracing::info!("glTF morph targets carry tangent deltas; they are not imported");
543 }
544 let total = vertices.len();
545 let mut morph_deltas = Vec::with_capacity(targets.len() * total);
546 for mut t in targets {
547 t.resize(total, crate::components::MorphDelta::default());
548 morph_deltas.extend(t);
549 }
550 Ok((vertices, indices, morph_deltas))
551}
552
553#[derive(Debug, Clone, Copy)]
557pub struct ImportedKeyframe {
558 pub time: f32,
560 pub pose: JointPose,
562}
563
564#[derive(Debug, Clone)]
566pub struct ImportedAnimationTrack {
567 pub joint: usize,
569 pub keys: Vec<ImportedKeyframe>,
571}
572
573#[derive(Debug, Clone)]
575pub struct ImportedMorphKey {
576 pub time: f32,
578 pub weights: Vec<f32>,
580}
581
582#[derive(Debug, Clone)]
584pub struct ImportedAnimation {
585 pub name: String,
587 pub duration: f32,
589 pub tracks: Vec<ImportedAnimationTrack>,
592 pub morph_track: Vec<ImportedMorphKey>,
595}
596
597pub(crate) fn import_glb_animations_from_doc(
602 doc: &GltfDoc,
603 source: &str,
604 skin_index: u32,
605) -> Result<Vec<ImportedAnimation>, String> {
606 let node = skinned_node(doc, source, skin_index)?;
609 let (mesh_node, skin) = (node.index, node.skin);
610 let skeleton = import_skeleton(&skin)?;
611 Ok(doc
612 .doc
613 .document
614 .animations()
615 .map(|anim| import_animation(&anim, &skeleton, doc, mesh_node))
616 .collect())
617}
618
619pub fn import_glb_animation_from_doc(
626 doc: &GltfDoc,
627 source: &str,
628 skin_index: u32,
629 animation_index: u32,
630 animation_name: &str,
631) -> Result<ImportedAnimation, String> {
632 let mut anims = import_glb_animations_from_doc(doc, source, skin_index)?;
633 let idx = if !animation_name.is_empty() {
634 anims
635 .iter()
636 .position(|a| a.name == animation_name)
637 .ok_or_else(|| {
638 format!(
639 "'{}': no animation named '{}' (file has {} clip{})",
640 source,
641 animation_name,
642 anims.len(),
643 if anims.len() == 1 { "" } else { "s" }
644 )
645 })?
646 } else {
647 let i = animation_index as usize;
648 if i >= anims.len() {
649 return Err(format!(
650 "'{}': animation_index {} out of range (file has {} animation{})",
651 source,
652 animation_index,
653 anims.len(),
654 if anims.len() == 1 { "" } else { "s" }
655 ));
656 }
657 i
658 };
659 Ok(anims.swap_remove(idx))
660}
661
662pub(crate) struct SkinnedNode<'a> {
672 pub index: usize,
673 pub mesh: gltf::Mesh<'a>,
674 pub skin: gltf::Skin<'a>,
675}
676
677pub(crate) fn skinned_node<'a>(
678 doc: &'a GltfDoc,
679 source: &str,
680 skin_index: u32,
681) -> Result<SkinnedNode<'a>, String> {
682 let skinned: Vec<SkinnedNode<'a>> = doc
683 .doc
684 .document
685 .nodes()
686 .filter_map(|n| {
687 Some(SkinnedNode {
688 index: n.index(),
689 mesh: n.mesh()?,
690 skin: n.skin()?,
691 })
692 })
693 .collect();
694 if skinned.is_empty() {
695 return Err(format!("'{}': no node with both a mesh and a skin", source));
696 }
697 let count = skinned.len();
698 skinned.into_iter().nth(skin_index as usize).ok_or_else(|| {
699 format!(
700 "'{}': skin_index {} out of range (file has {} skinned mesh{})",
701 source,
702 skin_index,
703 count,
704 if count == 1 { "" } else { "es" }
705 )
706 })
707}
708
709fn import_animation(
713 anim: &gltf::Animation<'_>,
714 skeleton: &ImportedSkeleton,
715 doc: &GltfDoc,
716 mesh_node: usize,
717) -> ImportedAnimation {
718 let bind_pose = |j: usize| -> JointPose {
722 let def = &skeleton.joints[j];
723 JointPose {
724 translation: def.translation,
725 rotation_deg: def.rotation_deg,
726 scale: def.scale,
727 }
728 };
729
730 let mut tracks: HashMap<usize, Vec<(f32, JointPose)>> = HashMap::new();
732 let mut morph_track: Vec<ImportedMorphKey> = Vec::new();
733 let mut max_time: f32 = 0.0;
734
735 for channel in anim.channels() {
736 let target_node = channel.target().node().index();
737
738 if target_node == mesh_node
740 && channel.target().property() == gltf::animation::Property::MorphTargetWeights
741 {
742 let reader = channel.reader(|b| doc.buffer_bytes(b));
743 let times: Vec<f32> = match reader.read_inputs() {
744 Some(t) => t.collect(),
745 None => continue,
746 };
747 let Some(gltf::animation::util::ReadOutputs::MorphTargetWeights(w)) =
748 reader.read_outputs()
749 else {
750 continue;
751 };
752 let flat: Vec<f32> = w.into_f32().collect();
753 if times.is_empty() || !flat.len().is_multiple_of(times.len()) {
754 continue;
755 }
756 for &t in × {
757 max_time = max_time.max(t);
758 }
759 let stride = flat.len() / times.len();
762 let (stride, offset) = match channel.sampler().interpolation() {
763 gltf::animation::Interpolation::CubicSpline if stride.is_multiple_of(3) => {
764 (stride / 3, stride / 3)
765 }
766 _ => (stride, 0),
767 };
768 morph_track = times
769 .iter()
770 .enumerate()
771 .map(|(i, &time)| {
772 let start = i * (stride + 2 * offset) + offset;
773 ImportedMorphKey {
774 time,
775 weights: flat[start..start + stride].to_vec(),
776 }
777 })
778 .collect();
779 continue;
780 }
781
782 let Some(&skin_joint) = skeleton.node_to_joint.get(&target_node) else {
783 continue;
785 };
786 let joint_idx = skeleton
787 .remap
788 .get(skin_joint)
789 .copied()
790 .unwrap_or(skin_joint);
791 let reader = channel.reader(|b| doc.buffer_bytes(b));
792 let times: Vec<f32> = match reader.read_inputs() {
793 Some(t) => t.collect(),
794 None => continue,
795 };
796 for &t in × {
797 if t > max_time {
798 max_time = t;
799 }
800 }
801
802 let interpolation = channel.sampler().interpolation();
803 let entry = tracks.entry(joint_idx).or_default();
804 let bind = bind_pose(joint_idx);
805 let upsert = |entry: &mut Vec<(f32, JointPose)>, time: f32| -> usize {
806 if let Some(pos) = entry.iter().position(|(t, _)| (*t - time).abs() < 1e-6) {
808 pos
809 } else {
810 entry.push((time, bind));
811 entry.len() - 1
812 }
813 };
814
815 match reader.read_outputs() {
816 Some(gltf::animation::util::ReadOutputs::Translations(it)) => {
817 let values: Vec<[f32; 3]> = it.collect();
818 let samples = sampled(×, &values, interpolation);
819 for (time, t) in samples {
820 let i = upsert(entry, time);
821 entry[i].1.translation = t;
822 }
823 }
824 Some(gltf::animation::util::ReadOutputs::Rotations(rot)) => {
825 let values: Vec<[f32; 4]> = rot.into_f32().collect();
826 let samples = sampled(×, &values, interpolation);
827 for (time, q) in samples {
828 let i = upsert(entry, time);
829 entry[i].1.rotation_deg = euler_yxz_from_quat(q);
830 }
831 }
832 Some(gltf::animation::util::ReadOutputs::Scales(it)) => {
833 let values: Vec<[f32; 3]> = it.collect();
834 let samples = sampled(×, &values, interpolation);
835 for (time, s) in samples {
836 let i = upsert(entry, time);
837 entry[i].1.scale = s;
838 }
839 }
840 Some(gltf::animation::util::ReadOutputs::MorphTargetWeights(_)) | None => continue,
843 }
844 }
845
846 let mut sorted_tracks: Vec<ImportedAnimationTrack> = tracks
849 .into_iter()
850 .map(|(joint, mut keys)| {
851 keys.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
852 ImportedAnimationTrack {
853 joint,
854 keys: keys
855 .into_iter()
856 .map(|(time, pose)| ImportedKeyframe { time, pose })
857 .collect(),
858 }
859 })
860 .collect();
861 sorted_tracks.sort_by_key(|t| t.joint);
862
863 morph_track.sort_by(|a, b| {
864 a.time
865 .partial_cmp(&b.time)
866 .unwrap_or(std::cmp::Ordering::Equal)
867 });
868 ImportedAnimation {
869 name: anim.name().unwrap_or("").to_string(),
870 duration: max_time.max(1e-3),
871 tracks: sorted_tracks,
872 morph_track,
873 }
874}
875
876fn sampled<T: Copy>(
891 times: &[f32],
892 values: &[T],
893 interp: gltf::animation::Interpolation,
894) -> Vec<(f32, T)> {
895 use gltf::animation::Interpolation;
896 match interp {
897 Interpolation::Linear | Interpolation::Step => {
898 if values.len() != times.len() {
899 return Vec::new();
900 }
901 times.iter().copied().zip(values.iter().copied()).collect()
902 }
903 Interpolation::CubicSpline => {
904 if values.len() != times.len() * 3 {
905 return Vec::new();
906 }
907 times
908 .iter()
909 .copied()
910 .enumerate()
911 .map(|(i, t)| (t, values[i * 3 + 1]))
912 .collect()
913 }
914 }
915}
916
917#[cfg(test)]
921pub(crate) mod test_fixtures {
922 pub(crate) fn f32s(vals: &[f32]) -> Vec<u8> {
924 concinnity_testing::fixtures::glb::f32_bytes(vals)
925 }
926
927 pub(crate) fn u16s(vals: &[u16]) -> Vec<u8> {
928 concinnity_testing::fixtures::glb::u16_bytes(vals)
929 }
930
931 pub(crate) fn make_glb(json: &serde_json::Value, bin: Option<&[u8]>) -> Vec<u8> {
933 concinnity_testing::fixtures::glb::container(&json.to_string(), bin)
934 }
935
936 pub(crate) fn static_triangle_bin() -> Vec<u8> {
938 let mut bin = f32s(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
939 bin.extend(u16s(&[0, 1, 2]));
940 bin
941 }
942
943 pub(crate) fn static_triangle_json() -> serde_json::Value {
944 serde_json::json!({
945 "asset": {"version": "2.0"},
946 "buffers": [{"byteLength": 42}],
947 "bufferViews": [
948 {"buffer": 0, "byteOffset": 0, "byteLength": 36},
949 {"buffer": 0, "byteOffset": 36, "byteLength": 6}
950 ],
951 "accessors": [
952 {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3",
953 "min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 0.0]},
954 {"bufferView": 1, "componentType": 5123, "count": 3, "type": "SCALAR"}
955 ],
956 "meshes": [{"primitives": [{"attributes": {"POSITION": 0}, "indices": 1}]}],
957 "nodes": [{"mesh": 0}],
958 "scenes": [{"nodes": [0]}],
959 "scene": 0
960 })
961 }
962
963 pub(crate) fn static_triangle_glb() -> Vec<u8> {
964 make_glb(&static_triangle_json(), Some(&static_triangle_bin()))
965 }
966
967 pub(crate) fn skinned_bin() -> Vec<u8> {
973 let mut bin = f32s(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]); bin.extend(u16s(&[0, 1, 2])); bin.extend([0u8; 2]); bin.extend([0u8; 12]); bin.extend(f32s(&[
978 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
979 ])); bin.extend(f32s(&[0.0, 1.0])); bin.extend(f32s(&[0.0, 0.0, 0.0, 0.0, 2.0, 0.0])); bin
983 }
984
985 pub(crate) fn skinned_json(
986 with_joints: bool,
987 with_weights: bool,
988 with_anim: bool,
989 ) -> serde_json::Value {
990 let mut attributes = serde_json::json!({"POSITION": 0});
991 if with_joints {
992 attributes["JOINTS_0"] = 2.into();
993 }
994 if with_weights {
995 attributes["WEIGHTS_0"] = 3.into();
996 }
997 let mut root = serde_json::json!({
998 "asset": {"version": "2.0"},
999 "buffers": [{"byteLength": 136}],
1000 "bufferViews": [
1001 {"buffer": 0, "byteOffset": 0, "byteLength": 36},
1002 {"buffer": 0, "byteOffset": 36, "byteLength": 6},
1003 {"buffer": 0, "byteOffset": 44, "byteLength": 12},
1004 {"buffer": 0, "byteOffset": 56, "byteLength": 48},
1005 {"buffer": 0, "byteOffset": 104, "byteLength": 8},
1006 {"buffer": 0, "byteOffset": 112, "byteLength": 24}
1007 ],
1008 "accessors": [
1009 {"bufferView": 0, "componentType": 5126, "count": 3, "type": "VEC3",
1010 "min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 0.0]},
1011 {"bufferView": 1, "componentType": 5123, "count": 3, "type": "SCALAR"},
1012 {"bufferView": 2, "componentType": 5121, "count": 3, "type": "VEC4"},
1013 {"bufferView": 3, "componentType": 5126, "count": 3, "type": "VEC4"},
1014 {"bufferView": 4, "componentType": 5126, "count": 2, "type": "SCALAR",
1015 "min": [0.0], "max": [1.0]},
1016 {"bufferView": 5, "componentType": 5126, "count": 2, "type": "VEC3"}
1017 ],
1018 "meshes": [{"primitives": [{"attributes": attributes, "indices": 1}]}],
1019 "skins": [{"joints": [2, 1]}],
1020 "nodes": [
1021 {"mesh": 0, "skin": 0},
1022 {"name": "root", "children": [2], "translation": [0.0, 1.0, 0.0]},
1023 {"name": "tip", "translation": [0.0, 0.5, 0.0]}
1024 ],
1025 "scenes": [{"nodes": [0, 1]}],
1026 "scene": 0
1027 });
1028 if with_anim {
1029 root["animations"] = serde_json::json!([{
1032 "name": "wave",
1033 "channels": [
1034 {"sampler": 0, "target": {"node": 2, "path": "translation"}},
1035 {"sampler": 1, "target": {"node": 0, "path": "translation"}}
1036 ],
1037 "samplers": [
1038 {"input": 4, "output": 5, "interpolation": "LINEAR"},
1039 {"input": 4, "output": 5, "interpolation": "LINEAR"}
1040 ]
1041 }]);
1042 }
1043 root
1044 }
1045
1046 pub(crate) fn skinned_glb() -> Vec<u8> {
1047 make_glb(&skinned_json(true, true, true), Some(&skinned_bin()))
1048 }
1049
1050 pub(crate) fn two_skin_bin() -> Vec<u8> {
1053 let mut bin = skinned_bin();
1054 bin.extend(f32s(&[5.0, 0.0, 0.0, 6.0, 0.0, 0.0, 5.0, 1.0, 0.0])); bin
1056 }
1057
1058 pub(crate) fn two_skin_json() -> serde_json::Value {
1062 let mut root = skinned_json(true, true, true);
1063 root["buffers"] = serde_json::json!([{"byteLength": 172}]);
1064 root["bufferViews"]
1065 .as_array_mut()
1066 .expect("bufferViews")
1067 .push(serde_json::json!({"buffer": 0, "byteOffset": 136, "byteLength": 36}));
1068 root["accessors"]
1069 .as_array_mut()
1070 .expect("accessors")
1071 .push(serde_json::json!({
1072 "bufferView": 6, "componentType": 5126, "count": 3, "type": "VEC3",
1073 "min": [5.0, 0.0, 0.0], "max": [6.0, 1.0, 0.0]
1074 }));
1075 root["meshes"] = serde_json::json!([
1076 {"primitives": [{
1077 "attributes": {"POSITION": 0, "JOINTS_0": 2, "WEIGHTS_0": 3},
1078 "indices": 1, "material": 0
1079 }]},
1080 {"primitives": [{
1081 "attributes": {"POSITION": 6, "JOINTS_0": 2, "WEIGHTS_0": 3},
1082 "indices": 1, "material": 1
1083 }]}
1084 ]);
1085 root["materials"] = serde_json::json!([
1086 {"pbrMetallicRoughness": {"metallicFactor": 0.0, "roughnessFactor": 1.0}},
1087 {"pbrMetallicRoughness": {"metallicFactor": 0.0, "roughnessFactor": 0.5}}
1088 ]);
1089 root["nodes"] = serde_json::json!([
1090 {"mesh": 0, "skin": 0, "name": "body"},
1091 {"name": "root", "children": [2], "translation": [0.0, 1.0, 0.0]},
1092 {"name": "tip", "translation": [0.0, 0.5, 0.0]},
1093 {"mesh": 1, "skin": 0, "name": "hair"}
1094 ]);
1095 root["scenes"] = serde_json::json!([{"nodes": [0, 1, 3]}]);
1096 root
1097 }
1098
1099 pub(crate) fn two_skin_glb() -> Vec<u8> {
1100 make_glb(&two_skin_json(), Some(&two_skin_bin()))
1101 }
1102
1103 pub(crate) fn parse(bytes: &[u8]) -> crate::import::gltf_source::GltfDoc {
1104 crate::import::gltf_source::GltfDoc::from_slice(bytes, None, "fixture")
1105 .expect("fixture must parse")
1106 }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111 use super::test_fixtures::*;
1112 use super::*;
1113
1114 #[test]
1115 fn topological_order_keeps_an_already_sorted_chain() {
1116 let parents = [None, Some(0), Some(1)];
1117 let (order, remap) = topological_order(&parents);
1118 assert_eq!(order, vec![0, 1, 2]);
1119 assert_eq!(remap, vec![0, 1, 2]);
1120 }
1121
1122 #[test]
1123 fn topological_order_sorts_children_after_parents() {
1124 let parents = [Some(1), Some(2), None];
1127 let (order, remap) = topological_order(&parents);
1128 assert_eq!(order, vec![2, 1, 0]);
1129 assert_eq!(remap, vec![2, 1, 0]);
1130 for (sj, p) in parents.iter().enumerate() {
1132 if let Some(&p) = p.as_ref() {
1133 assert!(remap[p] < remap[sj], "parent {p} not before child {sj}");
1134 }
1135 }
1136 }
1137
1138 #[test]
1139 fn topological_order_handles_a_forest_with_multiple_roots() {
1140 let parents = [None, Some(0), None, Some(2)];
1142 let (order, remap) = topological_order(&parents);
1143 assert_eq!(order.len(), 4);
1144 for (sj, p) in parents.iter().enumerate() {
1145 if let Some(&p) = p.as_ref() {
1146 assert!(remap[p] < remap[sj]);
1147 }
1148 }
1149 }
1150
1151 #[test]
1152 fn topological_order_does_not_drop_joints_in_a_cycle() {
1153 let parents = [Some(1), Some(0)];
1156 let (order, _) = topological_order(&parents);
1157 assert_eq!(order.len(), 2);
1158 let mut seen = order.clone();
1159 seen.sort();
1160 assert_eq!(seen, vec![0, 1]);
1161 }
1162
1163 #[test]
1164 fn topological_order_treats_an_out_of_range_parent_as_a_root() {
1165 let parents = [Some(9), Some(0)];
1166 let (order, remap) = topological_order(&parents);
1167 assert_eq!(order.len(), 2);
1168 assert!(remap[0] < remap[1]);
1169 }
1170
1171 use gltf::animation::Interpolation;
1172
1173 #[test]
1174 fn sampled_linear_pairs_times_with_values() {
1175 let times = [0.0_f32, 0.5, 1.0];
1176 let values = [[1.0_f32, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1177 let out = sampled(×, &values, Interpolation::Linear);
1178 assert_eq!(out.len(), 3);
1179 assert_eq!(out[1], (0.5, [0.0, 1.0, 0.0]));
1180 }
1181
1182 #[test]
1183 fn sampled_step_is_pass_through() {
1184 let times = [0.0_f32, 1.0];
1185 let values = [[1.0_f32; 3], [2.0_f32; 3]];
1186 let out = sampled(×, &values, Interpolation::Step);
1187 assert_eq!(out.len(), 2);
1188 }
1189
1190 #[test]
1191 fn sampled_cubicspline_takes_middle_value_of_each_triplet() {
1192 let times = [0.0_f32, 0.5];
1194 let values = [
1196 [11.0_f32; 3],
1197 [12.0; 3],
1198 [13.0; 3],
1199 [21.0; 3],
1200 [22.0; 3],
1201 [23.0; 3],
1202 ];
1203 let out = sampled(×, &values, Interpolation::CubicSpline);
1204 assert_eq!(out, vec![(0.0, [12.0; 3]), (0.5, [22.0; 3])]);
1205 }
1206
1207 #[test]
1208 fn sampled_with_mismatched_lengths_returns_empty() {
1209 let times = [0.0_f32, 1.0];
1210 let values: Vec<[f32; 3]> = vec![[0.0; 3]];
1211 assert!(sampled(×, &values, Interpolation::Linear).is_empty());
1212 assert!(sampled(×, &values, Interpolation::CubicSpline).is_empty());
1213 }
1214
1215 #[test]
1218 fn read_primitive_geometry_reads_an_indexed_triangle() {
1219 let doc = parse(&static_triangle_glb());
1220 let (vertices, indices) = read_primitive_geometry(&doc, "t.glb", 0).expect("geometry");
1221 assert_eq!(vertices.len(), 3);
1222 assert_eq!(indices, vec![0, 1, 2]);
1223 assert_eq!(vertices[1].pos, [1.0, 0.0, 0.0]);
1224 assert_eq!(vertices[0].uv, [0.0, 0.0]);
1226 assert_eq!(vertices[0].color, NEUTRAL_COLOR);
1227 }
1228
1229 #[test]
1230 fn read_primitive_geometry_without_indices_draws_sequentially() {
1231 let mut json = static_triangle_json();
1232 json["meshes"][0]["primitives"][0]
1233 .as_object_mut()
1234 .unwrap()
1235 .remove("indices");
1236 let doc = parse(&make_glb(&json, Some(&static_triangle_bin())));
1237 let (vertices, indices) = read_primitive_geometry(&doc, "t.glb", 0).expect("geometry");
1238 assert_eq!(vertices.len(), 3);
1239 assert_eq!(indices, vec![0, 1, 2]);
1240 }
1241
1242 #[test]
1243 fn read_primitive_geometry_rejects_out_of_range_primitive_index() {
1244 let doc = parse(&static_triangle_glb());
1245 let err = read_primitive_geometry(&doc, "t.glb", 3).unwrap_err();
1246 assert!(err.contains("out of range"), "got: {err}");
1247 }
1248
1249 #[test]
1250 fn read_primitive_geometry_rejects_non_triangle_topology() {
1251 let mut json = static_triangle_json();
1252 json["meshes"][0]["primitives"][0]["mode"] = 0.into();
1254 let doc = parse(&make_glb(&json, Some(&static_triangle_bin())));
1255 let err = read_primitive_geometry(&doc, "t.glb", 0).unwrap_err();
1256 assert!(err.contains("only TRIANGLES is supported"), "got: {err}");
1257 }
1258
1259 #[test]
1260 fn read_primitive_geometry_rejects_missing_binary_chunk() {
1261 let doc = parse(&make_glb(&static_triangle_json(), None));
1263 let err = read_primitive_geometry(&doc, "t.glb", 0).unwrap_err();
1264 assert!(err.contains("no POSITION data"), "got: {err}");
1265 }
1266
1267 #[test]
1268 fn read_primitive_geometry_rejects_indices_past_the_vertex_array() {
1269 let mut bin = f32s(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
1270 bin.extend(u16s(&[0, 1, 9]));
1271 let doc = parse(&make_glb(&static_triangle_json(), Some(&bin)));
1272 let err = read_primitive_geometry(&doc, "t.glb", 0).unwrap_err();
1273 assert!(err.contains("index 9 out of range"), "got: {err}");
1274 }
1275
1276 #[test]
1277 fn import_static_glb_primitive_narrows_indices_to_u16() {
1278 let doc = parse(&static_triangle_glb());
1279 let (vertices, indices) =
1280 import_static_glb_primitive_from_doc(&doc, "t.glb", 0).expect("import");
1281 assert_eq!(vertices.len(), 3);
1282 assert_eq!(indices, vec![0u16, 1, 2]);
1283 }
1284
1285 fn oversized_nonindexed_glb() -> Vec<u8> {
1289 let count = u16::MAX as usize + 3; let bin = vec![0u8; count * 12];
1291 let json = serde_json::json!({
1292 "asset": {"version": "2.0"},
1293 "buffers": [{"byteLength": bin.len()}],
1294 "bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": bin.len()}],
1295 "accessors": [
1296 {"bufferView": 0, "componentType": 5126, "count": count, "type": "VEC3",
1297 "min": [0.0, 0.0, 0.0], "max": [0.0, 0.0, 0.0]}
1298 ],
1299 "meshes": [{"primitives": [{"attributes": {"POSITION": 0}}]}],
1300 "nodes": [{"mesh": 0}],
1301 "scenes": [{"nodes": [0]}],
1302 "scene": 0
1303 });
1304 make_glb(&json, Some(&bin))
1305 }
1306
1307 #[test]
1308 fn import_static_glb_primitive_rejects_oversized_primitives() {
1309 let doc = parse(&oversized_nonindexed_glb());
1310 let err = import_static_glb_primitive_from_doc(&doc, "t.glb", 0).unwrap_err();
1311 assert!(err.contains("u16 index limit"), "got: {err}");
1312 }
1313
1314 #[test]
1317 fn import_skinned_reorders_joints_and_remaps_vertex_bindings() {
1318 let doc = parse(&skinned_glb());
1319 let imported = import_skinned_from_doc(&doc, "s.glb", 0).expect("skinned import");
1320
1321 assert_eq!(imported.skeleton.len(), 2);
1324 assert_eq!(imported.skeleton[0].name, "root");
1325 assert_eq!(imported.skeleton[0].parent, -1);
1326 assert_eq!(imported.skeleton[0].translation, [0.0, 1.0, 0.0]);
1327 assert_eq!(imported.skeleton[1].name, "tip");
1328 assert_eq!(imported.skeleton[1].parent, 0);
1329 assert_eq!(imported.skeleton[1].translation, [0.0, 0.5, 0.0]);
1330
1331 assert_eq!(imported.vertices.len(), 3);
1333 assert_eq!(imported.indices, vec![0, 1, 2]);
1334 assert_eq!(imported.vertices[0].joints, [1, 1, 1, 1]);
1335 assert_eq!(imported.vertices[0].weights, [1.0, 0.0, 0.0, 0.0]);
1336 }
1337
1338 #[test]
1339 fn skin_index_selects_each_skinned_node_in_declaration_order() {
1340 let doc = parse(&two_skin_glb());
1341 let body = import_skinned_from_doc(&doc, "s.glb", 0).expect("body import");
1342 let hair = import_skinned_from_doc(&doc, "s.glb", 1).expect("hair import");
1343
1344 assert_eq!(body.skeleton.len(), 2);
1347 assert_eq!(hair.skeleton.len(), body.skeleton.len());
1348 assert_eq!(body.vertices[0].pos, [0.0, 0.0, 0.0]);
1349 assert_eq!(hair.vertices[0].pos, [5.0, 0.0, 0.0]);
1350 }
1351
1352 #[test]
1353 fn skin_index_past_the_last_skinned_node_errors() {
1354 let doc = parse(&two_skin_glb());
1355 let err = import_skinned_from_doc(&doc, "s.glb", 2)
1356 .err()
1357 .expect("only two skinned nodes");
1358 assert!(
1359 err.contains("skin_index 2 out of range") && err.contains("2 skinned meshes"),
1360 "got: {err}"
1361 );
1362 }
1363
1364 #[test]
1365 fn animations_resolve_against_the_selected_skin() {
1366 let doc = parse(&two_skin_glb());
1370 for skin_index in 0..2 {
1371 let anims = import_glb_animations_from_doc(&doc, "s.glb", skin_index)
1372 .expect("animations for every skin");
1373 assert_eq!(anims.len(), 1);
1374 assert_eq!(anims[0].name, "wave");
1375 }
1376 let err = import_glb_animations_from_doc(&doc, "s.glb", 5).expect_err("out of range");
1377 assert!(err.contains("skin_index 5 out of range"), "got: {err}");
1378 }
1379
1380 #[test]
1381 fn import_skinned_rejects_a_file_with_no_skinned_node() {
1382 let doc = parse(&static_triangle_glb());
1383 let err = import_skinned_from_doc(&doc, "t.glb", 0)
1384 .err()
1385 .expect("expected error");
1386 assert!(
1387 err.contains("no node with both a mesh and a skin"),
1388 "got: {err}"
1389 );
1390 }
1391
1392 #[test]
1393 fn import_skinned_rejects_missing_weights() {
1394 let doc = parse(&make_glb(
1395 &skinned_json(true, false, false),
1396 Some(&skinned_bin()),
1397 ));
1398 let err = import_skinned_from_doc(&doc, "s.glb", 0)
1399 .err()
1400 .expect("expected error");
1401 assert!(err.contains("missing WEIGHTS_0"), "got: {err}");
1402 }
1403
1404 #[test]
1405 fn import_skinned_rejects_a_mesh_with_only_static_primitives() {
1406 let doc = parse(&make_glb(
1408 &skinned_json(false, false, false),
1409 Some(&skinned_bin()),
1410 ));
1411 let err = import_skinned_from_doc(&doc, "s.glb", 0)
1412 .err()
1413 .expect("expected error");
1414 assert!(err.contains("no skinned primitives"), "got: {err}");
1415 }
1416
1417 #[test]
1420 fn import_animations_extracts_joint_tracks_and_drops_non_joint_channels() {
1421 let doc = parse(&skinned_glb());
1422 let anims = import_glb_animations_from_doc(&doc, "s.glb", 0).expect("animations");
1423 assert_eq!(anims.len(), 1);
1424 let anim = &anims[0];
1425 assert_eq!(anim.name, "wave");
1426 assert!((anim.duration - 1.0).abs() < 1e-6);
1427
1428 assert_eq!(anim.tracks.len(), 1);
1431 let track = &anim.tracks[0];
1432 assert_eq!(track.joint, 1);
1433 assert_eq!(track.keys.len(), 2);
1434 assert_eq!(track.keys[0].time, 0.0);
1435 assert_eq!(track.keys[0].pose.translation, [0.0, 0.0, 0.0]);
1436 assert_eq!(track.keys[1].time, 1.0);
1437 assert_eq!(track.keys[1].pose.translation, [0.0, 2.0, 0.0]);
1438 assert_eq!(track.keys[1].pose.scale, [1.0, 1.0, 1.0]);
1440 }
1441
1442 #[test]
1443 fn import_animation_from_doc_selects_by_name() {
1444 let doc = parse(&skinned_glb());
1445 let anim = import_glb_animation_from_doc(&doc, "s.glb", 0, 7, "wave").expect("clip");
1446 assert_eq!(anim.name, "wave");
1447 }
1448
1449 #[test]
1450 fn import_animation_from_doc_rejects_an_unknown_name() {
1451 let doc = parse(&skinned_glb());
1452 let err = import_glb_animation_from_doc(&doc, "s.glb", 0, 0, "sprint").unwrap_err();
1453 assert!(err.contains("no animation named 'sprint'"), "got: {err}");
1454 assert!(err.contains("1 clip"), "got: {err}");
1455 }
1456
1457 #[test]
1458 fn import_animation_from_doc_falls_back_to_index_when_name_is_empty() {
1459 let doc = parse(&skinned_glb());
1460 let anim = import_glb_animation_from_doc(&doc, "s.glb", 0, 0, "").expect("clip");
1461 assert_eq!(anim.name, "wave");
1462 }
1463
1464 #[test]
1465 fn import_animation_from_doc_rejects_an_out_of_range_index() {
1466 let doc = parse(&skinned_glb());
1467 let err = import_glb_animation_from_doc(&doc, "s.glb", 0, 1, "").unwrap_err();
1468 assert!(err.contains("animation_index 1 out of range"), "got: {err}");
1469 }
1470
1471 #[test]
1472 fn import_animation_from_doc_pluralizes_a_multi_clip_count() {
1473 let doc = parse(&animated_glb());
1474 let err = import_glb_animation_from_doc(&doc, "a.glb", 0, 99, "").unwrap_err();
1475 assert!(err.contains("file has 7 animations"), "got: {err}");
1476 let err = import_glb_animation_from_doc(&doc, "a.glb", 0, 0, "sprint").unwrap_err();
1477 assert!(err.contains("file has 7 clips"), "got: {err}");
1478 }
1479
1480 #[test]
1481 fn importing_animations_requires_a_skinned_node_with_joints() {
1482 let doc = parse(&static_triangle_glb());
1484 let err = import_glb_animations_from_doc(&doc, "t.glb", 0).unwrap_err();
1485 assert!(
1486 err.contains("no node with both a mesh and a skin"),
1487 "got: {err}"
1488 );
1489 let err = import_glb_animation_from_doc(&doc, "t.glb", 0, 0, "").unwrap_err();
1491 assert!(
1492 err.contains("no node with both a mesh and a skin"),
1493 "got: {err}"
1494 );
1495 }
1496
1497 const UNBACKED_BASE: usize = 1 << 20;
1500
1501 struct Fixture {
1505 bin: Vec<u8>,
1506 views: Vec<serde_json::Value>,
1507 accessors: Vec<serde_json::Value>,
1508 unbacked_len: usize,
1509 }
1510
1511 impl Fixture {
1512 fn new() -> Self {
1513 Self {
1514 bin: Vec::new(),
1515 views: Vec::new(),
1516 accessors: Vec::new(),
1517 unbacked_len: 0,
1518 }
1519 }
1520
1521 fn accessor(&mut self, bytes: &[u8], component_type: u32, count: usize, ty: &str) -> usize {
1522 while !self.bin.len().is_multiple_of(4) {
1523 self.bin.push(0);
1524 }
1525 let byte_offset = self.bin.len();
1526 self.bin.extend_from_slice(bytes);
1527 self.views.push(serde_json::json!({
1528 "buffer": 0,
1529 "byteOffset": byte_offset,
1530 "byteLength": bytes.len(),
1531 }));
1532 self.accessors.push(serde_json::json!({
1533 "bufferView": self.views.len() - 1,
1534 "componentType": component_type,
1535 "count": count,
1536 "type": ty,
1537 }));
1538 self.accessors.len() - 1
1539 }
1540
1541 fn unbacked(&mut self, count: usize, ty: &str) -> usize {
1545 let components = if ty == "SCALAR" { 1 } else { 3 };
1546 let byte_length = count * components * 4;
1547 let byte_offset = UNBACKED_BASE + self.unbacked_len;
1548 self.unbacked_len += byte_length;
1549 self.views.push(serde_json::json!({
1550 "buffer": 0,
1551 "byteOffset": byte_offset,
1552 "byteLength": byte_length,
1553 }));
1554 self.accessors.push(serde_json::json!({
1555 "bufferView": self.views.len() - 1,
1556 "componentType": 5126,
1557 "count": count,
1558 "type": ty,
1559 }));
1560 self.accessors.len() - 1
1561 }
1562
1563 fn vec2(&mut self, values: &[[f32; 2]]) -> usize {
1564 let flat: Vec<f32> = values.iter().flatten().copied().collect();
1565 self.accessor(&f32s(&flat), 5126, values.len(), "VEC2")
1566 }
1567
1568 fn vec3(&mut self, values: &[[f32; 3]]) -> usize {
1569 let flat: Vec<f32> = values.iter().flatten().copied().collect();
1570 self.accessor(&f32s(&flat), 5126, values.len(), "VEC3")
1571 }
1572
1573 fn set_bounds(&mut self, index: usize, min: [f32; 3], max: [f32; 3]) {
1575 self.accessors[index]["min"] = serde_json::json!(min);
1576 self.accessors[index]["max"] = serde_json::json!(max);
1577 }
1578
1579 fn positions(&mut self, values: &[[f32; 3]]) -> usize {
1580 let index = self.vec3(values);
1581 let mut min = [f32::MAX; 3];
1582 let mut max = [f32::MIN; 3];
1583 for v in values {
1584 for i in 0..3 {
1585 min[i] = min[i].min(v[i]);
1586 max[i] = max[i].max(v[i]);
1587 }
1588 }
1589 self.set_bounds(index, min, max);
1590 index
1591 }
1592
1593 fn vec4(&mut self, values: &[[f32; 4]]) -> usize {
1594 let flat: Vec<f32> = values.iter().flatten().copied().collect();
1595 self.accessor(&f32s(&flat), 5126, values.len(), "VEC4")
1596 }
1597
1598 fn scalars(&mut self, values: &[f32]) -> usize {
1599 self.accessor(&f32s(values), 5126, values.len(), "SCALAR")
1600 }
1601
1602 fn build(self, mut root: serde_json::Value) -> Vec<u8> {
1603 let declared = if self.unbacked_len > 0 {
1604 UNBACKED_BASE + self.unbacked_len
1605 } else {
1606 self.bin.len()
1607 };
1608 root["buffers"] = serde_json::json!([{"byteLength": declared}]);
1609 root["bufferViews"] = serde_json::Value::Array(self.views);
1610 root["accessors"] = serde_json::Value::Array(self.accessors);
1611 make_glb(&root, Some(&self.bin))
1612 }
1613 }
1614
1615 fn skinned_nodes() -> serde_json::Value {
1618 serde_json::json!([
1619 {"mesh": 0, "skin": 0},
1620 {"name": "root", "children": [2], "translation": [0.0, 1.0, 0.0]},
1621 {"name": "tip", "translation": [0.0, 0.5, 0.0]}
1622 ])
1623 }
1624
1625 fn morph_glb(extras: serde_json::Value) -> Vec<u8> {
1630 let mut f = Fixture::new();
1631 let pos = f.positions(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1632 let idx = f.accessor(&u16s(&[0, 1, 2]), 5123, 3, "SCALAR");
1633 let joints = f.accessor(&[0u8; 12], 5121, 3, "VEC4");
1634 let weights = f.vec4(&[[1.0, 0.0, 0.0, 0.0]; 3]);
1635 let dp0 = f.vec3(&[[1.0, 0.0, 0.0]; 3]);
1636 let dn0 = f.vec3(&[[0.0, 1.0, 0.0]; 3]);
1637 let dt0 = f.vec3(&[[0.0, 0.0, 1.0]; 3]);
1638 let dp1 = f.vec3(&[[0.0, 2.0, 0.0]; 3]);
1639 let dp2 = f.vec3(&[[3.0, 0.0, 0.0]; 3]);
1640 let uv = f.vec2(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
1641 let color = f.vec3(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1642 let attributes =
1643 serde_json::json!({"POSITION": pos, "JOINTS_0": joints, "WEIGHTS_0": weights});
1644 let textured = serde_json::json!({
1646 "POSITION": pos,
1647 "JOINTS_0": joints,
1648 "WEIGHTS_0": weights,
1649 "TEXCOORD_0": uv,
1650 "COLOR_0": color,
1651 });
1652 f.build(serde_json::json!({
1653 "asset": {"version": "2.0"},
1654 "scene": 0,
1655 "scenes": [{"nodes": [0, 1]}],
1656 "nodes": skinned_nodes(),
1657 "skins": [{"joints": [2, 1]}],
1658 "meshes": [{
1659 "extras": extras,
1660 "primitives": [
1661 {
1662 "attributes": attributes,
1663 "indices": idx,
1664 "targets": [
1665 {"POSITION": dp0, "NORMAL": dn0, "TANGENT": dt0},
1666 {"POSITION": dp1}
1667 ]
1668 },
1669 {
1670 "attributes": textured,
1671 "targets": [{"POSITION": dp2}]
1672 }
1673 ]
1674 }]
1675 }))
1676 }
1677
1678 #[test]
1679 fn import_skinned_concatenates_primitives_and_pads_absent_morph_targets() {
1680 let doc = parse(&morph_glb(serde_json::json!({"targetNames": ["bulge", 7]})));
1681 let mesh = import_skinned_from_doc(&doc, "m.glb", 0).expect("skinned import");
1682
1683 assert_eq!(mesh.vertices.len(), 6);
1686 assert_eq!(mesh.indices, vec![0, 1, 2, 3, 4, 5]);
1687
1688 assert_eq!(mesh.morph_target_names, vec!["bulge", "target_1"]);
1690
1691 assert_eq!(mesh.vertices[0].uv, [0.0, 0.0]);
1694 assert_eq!(mesh.vertices[0].color, [1.0, 1.0, 1.0]);
1695 assert_eq!(mesh.vertices[4].uv, [1.0, 0.0]);
1696 assert_eq!(mesh.vertices[4].color, [0.0, 1.0, 0.0]);
1697
1698 assert_eq!(mesh.morph_deltas.len(), 12);
1700 assert_eq!(mesh.morph_deltas[0].position, [1.0, 0.0, 0.0]);
1701 assert_eq!(mesh.morph_deltas[0].normal, [0.0, 1.0, 0.0]);
1702 assert_eq!(mesh.morph_deltas[3].position, [3.0, 0.0, 0.0]);
1704 assert_eq!(mesh.morph_deltas[6].position, [0.0, 2.0, 0.0]);
1706 assert_eq!(mesh.morph_deltas[9].position, [0.0, 0.0, 0.0]);
1707 assert_eq!(mesh.morph_deltas[9].normal, [0.0, 0.0, 0.0]);
1708 }
1709
1710 #[test]
1711 fn morph_target_names_fall_back_when_extras_are_unusable() {
1712 for extras in [
1713 serde_json::json!({"targetNames": "not an array"}),
1714 serde_json::json!({"unrelated": 1}),
1715 ] {
1716 let doc = parse(&morph_glb(extras.clone()));
1717 let mesh = import_skinned_from_doc(&doc, "m.glb", 0).expect("skinned import");
1718 assert_eq!(
1719 mesh.morph_target_names,
1720 vec!["target_0", "target_1"],
1721 "extras {extras}"
1722 );
1723 }
1724 }
1725
1726 #[test]
1727 fn read_primitive_geometry_reads_texcoords_and_vertex_colors() {
1728 let mut f = Fixture::new();
1729 let pos = f.positions(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1730 let uv = f.vec2(&[[0.0, 0.0], [1.0, 0.0], [0.25, 0.5]]);
1731 let color = f.vec3(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
1732 let glb = f.build(serde_json::json!({
1733 "asset": {"version": "2.0"},
1734 "scene": 0,
1735 "scenes": [{"nodes": [0]}],
1736 "nodes": [{"mesh": 0}],
1737 "meshes": [{"primitives": [{
1738 "attributes": {"POSITION": pos, "TEXCOORD_0": uv, "COLOR_0": color}
1739 }]}]
1740 }));
1741 let doc = parse(&glb);
1742 let (vertices, indices) = read_primitive_geometry(&doc, "t.glb", 0).expect("geometry");
1743
1744 assert_eq!(indices, vec![0, 1, 2]);
1745 assert_eq!(vertices[2].uv, [0.25, 0.5]);
1746 assert_eq!(vertices[0].color, [1.0, 0.0, 0.0]);
1748 assert_eq!(vertices[2].color, [0.0, 0.0, 1.0]);
1749 }
1750
1751 #[test]
1752 fn import_skinned_rejects_a_primitive_whose_positions_have_no_data() {
1753 let mut f = Fixture::new();
1754 let joints = f.accessor(&[0u8; 12], 5121, 3, "VEC4");
1755 let weights = f.vec4(&[[1.0, 0.0, 0.0, 0.0]; 3]);
1756 let pos = f.unbacked(3, "VEC3");
1759 f.set_bounds(pos, [0.0; 3], [1.0, 1.0, 0.0]);
1760 let glb = f.build(serde_json::json!({
1761 "asset": {"version": "2.0"},
1762 "scene": 0,
1763 "scenes": [{"nodes": [0, 1]}],
1764 "nodes": skinned_nodes(),
1765 "skins": [{"joints": [2, 1]}],
1766 "meshes": [{"primitives": [
1767 {"attributes": {"POSITION": pos, "JOINTS_0": joints, "WEIGHTS_0": weights}}
1768 ]}]
1769 }));
1770 let doc = parse(&glb);
1771 let err = import_skinned_from_doc(&doc, "m.glb", 0)
1772 .err()
1773 .expect("expected error");
1774 assert!(err.contains("no POSITION data"), "got: {err}");
1775 }
1776
1777 #[test]
1778 fn import_skinned_rejects_indices_past_the_u16_limit() {
1779 let mut f = Fixture::new();
1780 let pos = f.positions(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1781 let idx = f.accessor(
1782 &[0u32, 1, 70_000]
1783 .iter()
1784 .flat_map(|v| v.to_le_bytes())
1785 .collect::<Vec<u8>>(),
1786 5125,
1787 3,
1788 "SCALAR",
1789 );
1790 let joints = f.accessor(&[0u8; 12], 5121, 3, "VEC4");
1791 let weights = f.vec4(&[[1.0, 0.0, 0.0, 0.0]; 3]);
1792 let glb = f.build(serde_json::json!({
1793 "asset": {"version": "2.0"},
1794 "scene": 0,
1795 "scenes": [{"nodes": [0, 1]}],
1796 "nodes": skinned_nodes(),
1797 "skins": [{"joints": [2, 1]}],
1798 "meshes": [{"primitives": [{
1799 "attributes": {"POSITION": pos, "JOINTS_0": joints, "WEIGHTS_0": weights},
1800 "indices": idx
1801 }]}]
1802 }));
1803 let doc = parse(&glb);
1804 let err = import_skinned_from_doc(&doc, "m.glb", 0)
1805 .err()
1806 .expect("expected error");
1807 assert_eq!(
1808 err,
1809 "imported skinned mesh exceeds the 65535-vertex u16 index limit"
1810 );
1811 }
1812
1813 fn jointless_glb() -> Vec<u8> {
1815 let mut f = Fixture::new();
1816 let pos = f.positions(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1817 let joints = f.accessor(&[0u8; 12], 5121, 3, "VEC4");
1818 let weights = f.vec4(&[[1.0, 0.0, 0.0, 0.0]; 3]);
1819 f.build(serde_json::json!({
1820 "asset": {"version": "2.0"},
1821 "scene": 0,
1822 "scenes": [{"nodes": [0]}],
1823 "nodes": [{"mesh": 0, "skin": 0}],
1824 "skins": [{"joints": []}],
1825 "meshes": [{"primitives": [{
1826 "attributes": {"POSITION": pos, "JOINTS_0": joints, "WEIGHTS_0": weights}
1827 }]}]
1828 }))
1829 }
1830
1831 #[test]
1832 fn import_skeleton_rejects_a_skin_with_no_joints() {
1833 let doc = parse(&jointless_glb());
1834 let err = import_skinned_from_doc(&doc, "m.glb", 0)
1835 .err()
1836 .expect("expected error");
1837 assert_eq!(err, "glTF skin has no joints");
1838 let err = import_glb_animations_from_doc(&doc, "m.glb", 0).unwrap_err();
1840 assert_eq!(err, "glTF skin has no joints");
1841 }
1842
1843 fn animated_glb() -> Vec<u8> {
1847 let mut f = Fixture::new();
1848 let pos = f.positions(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1849 let joints = f.accessor(&[0u8; 12], 5121, 3, "VEC4");
1850 let weights = f.vec4(&[[1.0, 0.0, 0.0, 0.0]; 3]);
1851 let times = f.scalars(&[0.0, 1.0]);
1852 let translations = f.vec3(&[[0.0, 0.0, 0.0], [0.0, 2.0, 0.0]]);
1853 let scales = f.vec3(&[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]);
1854 let half = std::f32::consts::FRAC_1_SQRT_2;
1856 let rotations = f.vec4(&[[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, half, half]]);
1857 let tangent = [1.0, 0.0, 0.0, 0.0];
1860 let rotations_cubic = f.vec4(&[
1861 tangent,
1862 [0.0, 0.0, 0.0, 1.0],
1863 tangent,
1864 tangent,
1865 [0.0, 0.0, half, half],
1866 tangent,
1867 ]);
1868 let rotations_ragged = f.vec4(&[[0.0, 0.0, 0.0, 1.0]; 3]);
1869 let w_linear = f.scalars(&[0.0, 0.25, 1.0, 0.5]);
1870 let w_cubic = f.scalars(&[9.0, 9.0, 0.1, 0.2, 9.0, 9.0, 9.0, 9.0, 0.3, 0.4, 9.0, 9.0]);
1871 let w_ragged = f.scalars(&[0.0, 0.5, 1.0]);
1872 let unbacked_times = f.unbacked(2, "SCALAR");
1873 let unbacked_weights = f.unbacked(4, "SCALAR");
1874
1875 f.build(serde_json::json!({
1876 "asset": {"version": "2.0"},
1877 "scene": 0,
1878 "scenes": [{"nodes": [0, 1]}],
1879 "nodes": skinned_nodes(),
1880 "skins": [{"joints": [2, 1]}],
1881 "meshes": [{"primitives": [{
1882 "attributes": {"POSITION": pos, "JOINTS_0": joints, "WEIGHTS_0": weights}
1883 }]}],
1884 "animations": [
1885 {
1886 "name": "pose",
1887 "samplers": [
1888 {"input": times, "output": translations, "interpolation": "LINEAR"},
1889 {"input": times, "output": rotations, "interpolation": "LINEAR"},
1890 {"input": times, "output": scales, "interpolation": "LINEAR"},
1891 {"input": times, "output": times, "interpolation": "LINEAR"},
1892 {"input": unbacked_times, "output": translations}
1893 ],
1894 "channels": [
1895 {"sampler": 0, "target": {"node": 2, "path": "translation"}},
1896 {"sampler": 1, "target": {"node": 2, "path": "rotation"}},
1897 {"sampler": 2, "target": {"node": 2, "path": "scale"}},
1898 {"sampler": 3, "target": {"node": 2, "path": "weights"}},
1899 {"sampler": 4, "target": {"node": 2, "path": "translation"}}
1900 ]
1901 },
1902 {
1903 "name": "pose_cubic",
1904 "samplers": [
1905 {"input": times, "output": rotations_cubic, "interpolation": "CUBICSPLINE"}
1906 ],
1907 "channels": [{"sampler": 0, "target": {"node": 2, "path": "rotation"}}]
1908 },
1909 {
1910 "name": "pose_ragged",
1911 "samplers": [
1912 {"input": times, "output": rotations_ragged, "interpolation": "LINEAR"}
1913 ],
1914 "channels": [{"sampler": 0, "target": {"node": 2, "path": "rotation"}}]
1915 },
1916 {
1917 "name": "morph_linear",
1918 "samplers": [{"input": times, "output": w_linear, "interpolation": "LINEAR"}],
1919 "channels": [{"sampler": 0, "target": {"node": 0, "path": "weights"}}]
1920 },
1921 {
1922 "name": "morph_cubic",
1923 "samplers": [
1924 {"input": times, "output": w_cubic, "interpolation": "CUBICSPLINE"}
1925 ],
1926 "channels": [{"sampler": 0, "target": {"node": 0, "path": "weights"}}]
1927 },
1928 {
1929 "name": "morph_ragged",
1930 "samplers": [{"input": times, "output": w_ragged, "interpolation": "LINEAR"}],
1931 "channels": [{"sampler": 0, "target": {"node": 0, "path": "weights"}}]
1932 },
1933 {
1934 "name": "morph_unreadable",
1935 "samplers": [
1936 {"input": unbacked_times, "output": w_linear},
1937 {"input": times, "output": unbacked_weights}
1938 ],
1939 "channels": [
1940 {"sampler": 0, "target": {"node": 0, "path": "weights"}},
1941 {"sampler": 1, "target": {"node": 0, "path": "weights"}}
1942 ]
1943 }
1944 ]
1945 }))
1946 }
1947
1948 fn clip<'a>(anims: &'a [ImportedAnimation], name: &str) -> &'a ImportedAnimation {
1949 anims
1950 .iter()
1951 .find(|a| a.name == name)
1952 .unwrap_or_else(|| panic!("no clip named '{name}'"))
1953 }
1954
1955 #[test]
1956 fn import_animation_merges_translation_rotation_and_scale_at_shared_times() {
1957 let doc = parse(&animated_glb());
1958 let anims = import_glb_animations_from_doc(&doc, "a.glb", 0).expect("animations");
1959 let pose = clip(&anims, "pose");
1960
1961 assert_eq!(pose.tracks.len(), 1);
1964 let track = &pose.tracks[0];
1965 assert_eq!(track.joint, 1);
1966 assert_eq!(track.keys.len(), 2);
1967
1968 assert_eq!(track.keys[0].time, 0.0);
1969 assert_eq!(track.keys[0].pose.translation, [0.0, 0.0, 0.0]);
1970 assert_eq!(track.keys[0].pose.scale, [1.0, 1.0, 1.0]);
1971 assert_eq!(track.keys[0].pose.rotation_deg, [0.0, 0.0, 0.0]);
1972
1973 assert_eq!(track.keys[1].time, 1.0);
1974 assert_eq!(track.keys[1].pose.translation, [0.0, 2.0, 0.0]);
1975 assert_eq!(track.keys[1].pose.scale, [2.0, 2.0, 2.0]);
1976 let roll = track.keys[1].pose.rotation_deg[2];
1978 assert!((roll - 90.0).abs() < 1e-3, "roll was {roll}");
1979
1980 assert!(pose.morph_track.is_empty());
1983 assert!((pose.duration - 1.0).abs() < 1e-6);
1984 }
1985
1986 #[test]
1987 fn import_animation_takes_the_value_of_each_cubicspline_rotation_triplet() {
1988 let doc = parse(&animated_glb());
1989 let anims = import_glb_animations_from_doc(&doc, "a.glb", 0).expect("animations");
1990 let track = &clip(&anims, "pose_cubic").tracks[0];
1991
1992 assert_eq!(track.keys.len(), 2);
1994 assert_eq!(track.keys[0].pose.rotation_deg, [0.0, 0.0, 0.0]);
1995 let roll = track.keys[1].pose.rotation_deg[2];
1996 assert!((roll - 90.0).abs() < 1e-3, "roll was {roll}");
1997 }
1998
1999 #[test]
2000 fn import_animation_drops_samples_when_the_output_count_disagrees() {
2001 let doc = parse(&animated_glb());
2002 let anims = import_glb_animations_from_doc(&doc, "a.glb", 0).expect("animations");
2003 let track = &clip(&anims, "pose_ragged").tracks[0];
2004 assert_eq!(track.joint, 1);
2005 assert!(track.keys.is_empty());
2006 }
2007
2008 #[test]
2009 fn import_animation_reads_linear_morph_weight_keys() {
2010 let doc = parse(&animated_glb());
2011 let anims = import_glb_animations_from_doc(&doc, "a.glb", 0).expect("animations");
2012 let morph = clip(&anims, "morph_linear");
2013
2014 assert!(morph.tracks.is_empty());
2015 assert_eq!(morph.morph_track.len(), 2);
2016 assert_eq!(morph.morph_track[0].time, 0.0);
2017 assert_eq!(morph.morph_track[0].weights, vec![0.0, 0.25]);
2018 assert_eq!(morph.morph_track[1].time, 1.0);
2019 assert_eq!(morph.morph_track[1].weights, vec![1.0, 0.5]);
2020 }
2021
2022 #[test]
2023 fn import_animation_takes_the_value_of_each_cubicspline_morph_triplet() {
2024 let doc = parse(&animated_glb());
2025 let anims = import_glb_animations_from_doc(&doc, "a.glb", 0).expect("animations");
2026 let morph = clip(&anims, "morph_cubic");
2027
2028 assert_eq!(morph.morph_track.len(), 2);
2031 assert_eq!(morph.morph_track[0].weights, vec![0.1, 0.2]);
2032 assert_eq!(morph.morph_track[1].weights, vec![0.3, 0.4]);
2033 }
2034
2035 #[test]
2036 fn import_animation_drops_unusable_morph_channels() {
2037 let doc = parse(&animated_glb());
2038 let anims = import_glb_animations_from_doc(&doc, "a.glb", 0).expect("animations");
2039 assert!(clip(&anims, "morph_ragged").morph_track.is_empty());
2043 assert!(clip(&anims, "morph_unreadable").morph_track.is_empty());
2044 }
2045
2046 #[test]
2049 fn parse_glb_reads_a_file_from_disk() {
2050 let dir = tempfile::tempdir().expect("tempdir");
2051 let path = dir.path().join("tri.glb");
2052 std::fs::write(&path, static_triangle_glb()).expect("write glb");
2053 let doc = parse_glb(path.to_str().unwrap(), None).expect("parse");
2054 assert_eq!(doc.doc.document.meshes().count(), 1);
2055 }
2056
2057 #[test]
2058 fn parse_glb_reports_a_missing_file() {
2059 let dir = tempfile::tempdir().expect("tempdir");
2060 let path = dir.path().join("missing.glb");
2061 let err = parse_glb(path.to_str().unwrap(), None).unwrap_err();
2062 assert!(err.contains("failed to read"), "got: {err}");
2063 }
2064
2065 #[test]
2066 fn parse_glb_reports_invalid_content() {
2067 let dir = tempfile::tempdir().expect("tempdir");
2068 let path = dir.path().join("junk.glb");
2069 std::fs::write(&path, b"not a glb at all").expect("write junk");
2070 let err = parse_glb(path.to_str().unwrap(), None).unwrap_err();
2071 assert!(err.contains("not a valid glTF/GLB file"), "got: {err}");
2072 }
2073
2074 #[test]
2075 fn resolve_source_keeps_paths_with_a_directory_component() {
2076 let dir = tempfile::tempdir().expect("tempdir");
2077 let assets = Some(dir.path());
2078 assert_eq!(resolve_source("sub/f.glb", assets), "sub/f.glb");
2079 assert_eq!(resolve_source("./f.glb", assets), "./f.glb");
2080 assert_eq!(resolve_source("/abs/f.glb", assets), "/abs/f.glb");
2081 }
2082
2083 #[test]
2084 fn resolve_source_anchors_a_bare_filename_under_the_assets_dir() {
2085 let dir = tempfile::tempdir().expect("tempdir");
2088 assert_eq!(
2089 resolve_source("cn_test_no_such_model.glb", Some(dir.path())),
2090 dir.path()
2091 .join("cn_test_no_such_model.glb")
2092 .to_string_lossy()
2093 );
2094 }
2095
2096 #[test]
2099 fn resolve_source_without_an_assets_dir_returns_the_bare_name() {
2100 assert_eq!(
2101 resolve_source("cn_test_no_such_model.glb", None),
2102 "cn_test_no_such_model.glb"
2103 );
2104 }
2105
2106 fn vert(i: u32) -> VertexData {
2109 VertexData {
2110 pos: [i as f32, 0.0, 0.0],
2111 color: [0.0; 3],
2112 uv: [0.0; 2],
2113 }
2114 }
2115
2116 #[test]
2117 fn split_small_mesh_yields_one_chunk_with_first_use_order() {
2118 let vertices: Vec<VertexData> = (0..4).map(vert).collect();
2119 let indices = [2u32, 1, 0, 0, 2, 3];
2121 let chunks = split_into_u16_chunks(&vertices, &indices);
2122 assert_eq!(chunks.len(), 1);
2123 let (cv, ci) = &chunks[0];
2124 assert_eq!(cv.len(), 4);
2125 assert_eq!(ci, &vec![0u16, 1, 2, 2, 0, 3]);
2127 assert_eq!(cv[ci[0] as usize].pos, vertices[2].pos);
2129 assert_eq!(cv[ci[5] as usize].pos, vertices[3].pos);
2130 }
2131
2132 #[test]
2136 fn count_u16_chunks_agrees_with_the_split_it_mirrors() {
2137 let cases: Vec<Vec<u32>> = vec![
2138 vec![],
2139 vec![0, 1],
2140 vec![2, 1, 0, 0, 2, 3],
2141 vec![5, 5, 5],
2143 (0..u16::MAX as u32 + 3).collect(),
2144 (0..(u16::MAX as u32 + 1) * 2 + 3).collect(),
2145 ];
2146 for indices in cases {
2147 let max = indices.iter().copied().max().map_or(0, |m| m + 1);
2148 let vertices: Vec<VertexData> = (0..max).map(vert).collect();
2149 assert_eq!(
2150 count_u16_chunks(&indices),
2151 split_into_u16_chunks(&vertices, &indices).len(),
2152 "index stream of len {}",
2153 indices.len()
2154 );
2155 }
2156 }
2157
2158 #[test]
2159 fn split_with_no_triangles_yields_no_chunks() {
2160 let vertices: Vec<VertexData> = (0..3).map(vert).collect();
2161 assert!(split_into_u16_chunks(&vertices, &[]).is_empty());
2162 assert!(split_into_u16_chunks(&vertices, &[0, 1]).is_empty());
2164 }
2165
2166 #[test]
2167 fn split_flushes_a_chunk_when_the_u16_limit_would_overflow() {
2168 let count = u16::MAX as u32 + 3;
2170 let vertices: Vec<VertexData> = (0..count).map(vert).collect();
2171 let indices: Vec<u32> = (0..count).collect();
2172 let chunks = split_into_u16_chunks(&vertices, &indices);
2173 assert_eq!(chunks.len(), 2);
2174 assert_eq!(chunks[0].0.len(), 65535);
2175 assert_eq!(chunks[1].0.len(), 3);
2176 let mut triangles = 0;
2179 for (cv, ci) in &chunks {
2180 assert!(ci.iter().all(|&i| (i as usize) < cv.len()));
2181 triangles += ci.len() / 3;
2182 }
2183 assert_eq!(triangles, count as usize / 3);
2184 let (cv, ci) = &chunks[1];
2186 assert_eq!(cv[ci[0] as usize].pos, vert(count - 3).pos);
2187 assert_eq!(cv[ci[2] as usize].pos, vert(count - 1).pos);
2188 }
2189}