1use crate::{
9 LoadError, build_document, capture_dependency_closure, extract_source_skeleton,
10 has_extension_object, project_extension_facts, project_resource_facts, resolve_buffers,
11 source_facts_builder, topology, validate_animations, validate_document, validate_glb_framing,
12};
13use animsmith_core::{Document, LoadedSource, SourceFactsViewV1, SourceSetCoverageStateV1};
14use serde::Serialize;
15use serde_json::{Map, Value};
16use std::collections::{BTreeMap, BTreeSet};
17use std::path::Path;
18
19const GLB_MAGIC: &[u8; 4] = b"glTF";
20const GLB_JSON_CHUNK: u32 = 0x4e4f_534a;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum GltfContainerKind {
26 Gltf,
28 Glb,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
34#[serde(rename_all = "snake_case")]
35pub enum GltfBufferSourceKind {
36 BinaryChunk,
38 DataUri,
40 External,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46pub struct GltfBufferCapability {
47 pub buffer_index: usize,
49 pub source_kind: GltfBufferSourceKind,
51 pub declared_byte_length: u64,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum GltfNodeRestKind {
59 Trs,
61 Matrix,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67pub struct GltfNodeCapability {
68 pub node_index: usize,
70 pub rest_kind: GltfNodeRestKind,
72 pub mesh_index: Option<usize>,
74 pub skin_index: Option<usize>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
80pub struct GltfAnimationChannelCapability {
81 pub animation_index: usize,
83 pub channel_index: usize,
85 pub target_node_index: usize,
87 pub target_path: String,
89 pub interpolation: String,
91 pub input_accessor_index: usize,
93 pub output_accessor_index: usize,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
99pub struct GltfAttributeCapability {
100 pub semantic: String,
102 pub accessor_index: usize,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
108pub struct GltfPrimitiveCapability {
109 pub mesh_index: usize,
111 pub primitive_index: usize,
113 pub mode: u64,
115 pub attributes: Vec<GltfAttributeCapability>,
117 pub morph_target_count: usize,
119 pub morph_position_accessors: Vec<usize>,
121 #[serde(skip)]
123 pub unsupported_morph_locations: Vec<String>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128pub struct GltfInstancingCapability {
129 pub node_index: usize,
131 pub attributes: Vec<GltfAttributeCapability>,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
137pub struct GltfAccessorCapability {
138 pub accessor_index: usize,
140 pub buffer_view_index: Option<usize>,
142 pub byte_offset: u64,
144 pub component_type: u64,
146 pub accessor_type: String,
148 pub count: u64,
150 pub normalized: bool,
152 pub sparse: bool,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct GltfBufferViewCapability {
159 pub buffer_view_index: usize,
161 pub buffer_index: usize,
163 pub byte_offset: u64,
165 pub byte_length: u64,
167 pub byte_stride: Option<u64>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
173pub struct GltfSkinCapability {
174 pub skin_index: usize,
176 pub joint_count: usize,
178 pub inverse_bind_accessor_index: Option<usize>,
180 pub inverse_bind_count: Option<u64>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
186pub struct GltfCapabilityManifest {
187 pub container: GltfContainerKind,
189 pub buffers: Vec<GltfBufferCapability>,
191 pub buffer_views: Vec<GltfBufferViewCapability>,
193 pub accessors: Vec<GltfAccessorCapability>,
195 pub nodes: Vec<GltfNodeCapability>,
197 pub animation_channels: Vec<GltfAnimationChannelCapability>,
199 pub primitives: Vec<GltfPrimitiveCapability>,
201 pub morph_weight_locations: Vec<String>,
203 pub instancing: Vec<GltfInstancingCapability>,
205 pub skins: Vec<GltfSkinCapability>,
207 pub camera_count: usize,
209 pub extensions: Vec<String>,
211 pub extension_locations: Vec<String>,
213 pub external_resource_locations: Vec<String>,
215 pub extras_locations: Vec<String>,
217 pub unknown_member_locations: Vec<String>,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
223#[non_exhaustive]
224#[serde(rename_all = "snake_case")]
225pub enum GltfCapabilityViolationKind {
226 ExternalResource,
228 MorphTarget,
230 MorphWeights,
232 Camera,
234 Light,
236 Instancing,
238 ExtensionDeclaration,
240 ExtensionPayload,
242 Extras,
244 UnknownJsonMember,
246 NonTrianglePrimitive,
248 UnsupportedVertexAttribute,
250 SecondarySkinInfluences,
252 MissingInverseBinds,
254 EmptyInverseBindAccessor,
256 InverseBindCountMismatch,
258 UnreadableInverseBinds,
260 UnsafeAccessorLayout,
262 ConflictingAccessorUse,
264 OverlappingAccessorRanges,
268 ConflictingNodeTransform,
271 NonAffineNodeMatrix,
274 AnimatedMatrixNode,
278 ImagePayloadOverlap,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
284pub struct GltfCapabilityViolation {
285 pub location: String,
287 pub kind: GltfCapabilityViolationKind,
289}
290
291#[derive(Debug)]
296pub struct GltfScaleSource {
297 loaded_source: LoadedSource,
298 #[cfg(test)]
299 document_override: Option<Document>,
300 manifest: GltfCapabilityManifest,
301 source_bytes: Vec<u8>,
302 raw_json: Value,
303 resolved_buffers: Vec<Vec<u8>>,
304 clip_track_projection_required: bool,
305}
306
307impl GltfScaleSource {
308 pub fn document(&self) -> &Document {
310 #[cfg(test)]
311 if let Some(document) = self.document_override.as_ref() {
312 return document;
313 }
314 self.loaded_source.document()
315 }
316
317 pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
319 self.loaded_source.source_facts()
320 }
321
322 pub fn manifest(&self) -> &GltfCapabilityManifest {
324 &self.manifest
325 }
326
327 pub fn source_bytes(&self) -> &[u8] {
329 &self.source_bytes
330 }
331
332 pub fn raw_json(&self) -> &Value {
334 &self.raw_json
335 }
336
337 pub fn resolved_buffers(&self) -> &[Vec<u8>] {
339 &self.resolved_buffers
340 }
341
342 pub const fn requires_clip_track_projection(&self) -> bool {
348 self.clip_track_projection_required
349 }
350}
351
352#[derive(Debug, thiserror::Error)]
354#[non_exhaustive]
355pub enum GltfScalePreflightError {
356 #[error(transparent)]
358 Load(#[from] LoadError),
359 #[error("glTF scale preflight rejected {count} unsupported source domain(s)")]
361 Unsupported {
362 manifest: Box<GltfCapabilityManifest>,
364 violations: Vec<GltfCapabilityViolation>,
366 count: usize,
368 },
369}
370
371pub fn preflight_scale_source(path: &Path) -> Result<GltfScaleSource, GltfScalePreflightError> {
379 let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
380 path: path.display().to_string(),
381 source,
382 })?;
383 preflight_scale_source_bytes(path, &bytes)
384}
385
386pub fn preflight_scale_source_bytes(
397 path: &Path,
398 bytes: &[u8],
399) -> Result<GltfScaleSource, GltfScalePreflightError> {
400 capture_scale_source(path, bytes, GatePolicy::Enforce)
401}
402
403pub fn preflight_clip_track_source(
425 path: &Path,
426) -> Result<GltfScaleSource, GltfScalePreflightError> {
427 let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
428 path: path.display().to_string(),
429 source,
430 })?;
431 preflight_clip_track_source_bytes(path, &bytes)
432}
433
434pub fn preflight_clip_track_source_bytes(
443 path: &Path,
444 bytes: &[u8],
445) -> Result<GltfScaleSource, GltfScalePreflightError> {
446 capture_source(path, bytes, CapturePolicy::ClipTracks)
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451enum GatePolicy {
452 Enforce,
455 #[cfg(test)]
468 Bypass,
469}
470
471fn capture_scale_source(
473 path: &Path,
474 bytes: &[u8],
475 policy: GatePolicy,
476) -> Result<GltfScaleSource, GltfScalePreflightError> {
477 capture_source(path, bytes, CapturePolicy::Scale(policy))
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482enum CapturePolicy {
483 Scale(GatePolicy),
485 ClipTracks,
487}
488
489fn capture_source(
492 path: &Path,
493 bytes: &[u8],
494 policy: CapturePolicy,
495) -> Result<GltfScaleSource, GltfScalePreflightError> {
496 validate_glb_framing(bytes)?;
497 let (container, json_bytes) = raw_json_bytes(bytes)?;
498 let raw_json: Value = serde_json::from_slice(json_bytes)
499 .map_err(|error| LoadError::Malformed(format!("invalid top-level JSON: {error}")))?;
500 if !raw_json.is_object() {
501 return Err(LoadError::Malformed("top-level glTF JSON is not an object".into()).into());
502 }
503 let gltf = gltf::Gltf::from_slice_without_validation(bytes).map_err(LoadError::Gltf)?;
504
505 let mut violations = Vec::new();
506 let manifest = inventory(&raw_json, container, &mut violations);
507 let accessor_uses = inspect_accessor_uses(&raw_json, &mut violations);
508 match validate_document(&gltf.document) {
509 Ok(()) => {}
510 Err(error) => return Err(LoadError::Gltf(error).into()),
511 }
512 validate_animations(&gltf.document)?;
513 let topology = topology(&gltf.document)?;
514
515 let can_resolve_buffers = !manifest
516 .buffers
517 .iter()
518 .any(|buffer| buffer.source_kind == GltfBufferSourceKind::External);
519 let resolved_buffers = if can_resolve_buffers {
520 resolve_buffers(&gltf, path.parent())?
521 } else {
522 Vec::new()
523 };
524 if can_resolve_buffers {
525 inspect_accessor_layouts(
526 &raw_json,
527 &resolved_buffers,
528 &accessor_uses,
529 &mut violations,
530 );
531 }
532 let (clip_track_projection_required, refuse) = match policy {
533 CapturePolicy::Scale(policy) => {
534 violations.sort();
535 violations.dedup();
536 (
537 false,
538 match policy {
539 GatePolicy::Enforce => !violations.is_empty(),
540 #[cfg(test)]
541 GatePolicy::Bypass => false,
542 },
543 )
544 }
545 CapturePolicy::ClipTracks => {
546 let animation_accessors = animation_accessor_indices(&raw_json);
547 let projection_required = violations
548 .iter()
549 .any(|violation| clip_track_projects_away(violation, &animation_accessors));
550 violations
551 .retain(|violation| !clip_track_projects_away(violation, &animation_accessors));
552 violations.sort();
553 violations.dedup();
554 (projection_required, !violations.is_empty())
555 }
556 };
557 if refuse {
558 let count = violations.len();
559 return Err(GltfScalePreflightError::Unsupported {
560 manifest: Box::new(manifest),
561 violations,
562 count,
563 });
564 }
565
566 if !clip_track_projection_required {
571 let loaded_source = crate::load_source_bytes(path, bytes)?;
572 if matches!(policy, CapturePolicy::ClipTracks)
573 && !clip_track_source_facts_complete(loaded_source.source_facts())
574 {
575 return Err(LoadError::Malformed(
576 "clip-track raw source facts coverage is incomplete".into(),
577 )
578 .into());
579 }
580 return Ok(captured_scale_source(
581 loaded_source,
582 bytes,
583 manifest,
584 raw_json,
585 resolved_buffers,
586 false,
587 ));
588 }
589
590 let mut facts = source_facts_builder(bytes).map_err(LoadError::from)?;
594 project_extension_facts(&gltf.document, &mut facts);
595 project_resource_facts(&gltf.document, &mut facts);
596 let has_unmodeled_extension_domain = has_extension_object(bytes)
597 || gltf.document.extensions_used().next().is_some()
598 || gltf.document.extensions_required().next().is_some();
599 let (dependency_closure, _) = capture_dependency_closure(
600 &facts,
601 None,
602 has_unmodeled_extension_domain,
603 &mut crate::read_external_file,
604 )?;
605 let source_skeleton = extract_source_skeleton(&gltf.document, &resolved_buffers, &topology);
606 let mut document = build_document(&gltf, &resolved_buffers, path, &topology, &mut facts)?;
607 document.assets.source_skeleton = source_skeleton;
608 let loaded_source = facts
609 .finish_with_dependency_closure(document, dependency_closure)
610 .map_err(LoadError::from)?;
611 if !clip_track_source_facts_complete(loaded_source.source_facts()) {
612 return Err(LoadError::Malformed(
613 "clip-track raw source facts coverage is incomplete".into(),
614 )
615 .into());
616 }
617
618 Ok(captured_scale_source(
619 loaded_source,
620 bytes,
621 manifest,
622 raw_json,
623 resolved_buffers,
624 clip_track_projection_required,
625 ))
626}
627
628fn captured_scale_source(
630 loaded_source: LoadedSource,
631 source_bytes: &[u8],
632 manifest: GltfCapabilityManifest,
633 raw_json: Value,
634 resolved_buffers: Vec<Vec<u8>>,
635 clip_track_projection_required: bool,
636) -> GltfScaleSource {
637 GltfScaleSource {
638 loaded_source,
639 #[cfg(test)]
640 document_override: None,
641 manifest,
642 source_bytes: source_bytes.to_vec(),
643 raw_json,
644 resolved_buffers,
645 clip_track_projection_required,
646 }
647}
648
649fn clip_track_projects_away(
652 violation: &GltfCapabilityViolation,
653 animation_accessors: &BTreeSet<usize>,
654) -> bool {
655 use GltfCapabilityViolationKind as Kind;
656 match violation.kind {
657 Kind::MorphTarget
658 | Kind::MorphWeights
659 | Kind::NonTrianglePrimitive
660 | Kind::UnsupportedVertexAttribute
661 | Kind::SecondarySkinInfluences
662 | Kind::MissingInverseBinds
663 | Kind::EmptyInverseBindAccessor
664 | Kind::InverseBindCountMismatch
665 | Kind::UnreadableInverseBinds => true,
666 Kind::UnsafeAccessorLayout
667 | Kind::ConflictingAccessorUse
668 | Kind::OverlappingAccessorRanges => accessor_index_at(&violation.location)
669 .is_some_and(|accessor| !animation_accessors.contains(&accessor)),
670 Kind::ImagePayloadOverlap => true,
675 Kind::ExternalResource
676 | Kind::Camera
677 | Kind::Light
678 | Kind::Instancing
679 | Kind::ExtensionDeclaration
680 | Kind::ExtensionPayload
681 | Kind::Extras
682 | Kind::UnknownJsonMember
683 | Kind::ConflictingNodeTransform
684 | Kind::NonAffineNodeMatrix
685 | Kind::AnimatedMatrixNode => false,
686 }
687}
688
689fn animation_accessor_indices(root: &Value) -> BTreeSet<usize> {
691 let Some(animations) = root.get("animations").and_then(Value::as_array) else {
692 return BTreeSet::new();
693 };
694 animations
695 .iter()
696 .flat_map(|animation| {
697 animation
698 .get("samplers")
699 .and_then(Value::as_array)
700 .into_iter()
701 .flatten()
702 })
703 .flat_map(|sampler| {
704 [
705 as_index(sampler.get("input")),
706 as_index(sampler.get("output")),
707 ]
708 })
709 .flatten()
710 .collect()
711}
712
713fn accessor_index_at(location: &str) -> Option<usize> {
715 location
716 .strip_prefix("/accessors/")?
717 .split('/')
718 .next()?
719 .parse()
720 .ok()
721}
722
723fn clip_track_source_facts_complete(source: SourceFactsViewV1<'_>) -> bool {
726 [
727 source.clips().coverage().state(),
728 source.constructs().coverage().state(),
729 source.resources().coverage().state(),
730 ]
731 .into_iter()
732 .all(|state| state == SourceSetCoverageStateV1::Complete)
733}
734
735#[cfg(test)]
746pub(crate) fn scale_source_past_the_gate(
747 path: &Path,
748 bytes: &[u8],
749) -> Result<GltfScaleSource, GltfScalePreflightError> {
750 capture_scale_source(path, bytes, GatePolicy::Bypass)
751}
752
753#[cfg(test)]
771pub(crate) fn scale_source_with_document(
772 mut source: GltfScaleSource,
773 document: Document,
774) -> GltfScaleSource {
775 source.document_override = Some(document);
776 source
777}
778
779pub(crate) fn raw_json_bytes(bytes: &[u8]) -> Result<(GltfContainerKind, &[u8]), LoadError> {
784 if !bytes.starts_with(GLB_MAGIC) {
785 return Ok((GltfContainerKind::Gltf, bytes));
786 }
787 let chunk_length = bytes
788 .get(12..16)
789 .and_then(|slice| slice.try_into().ok())
790 .map(u32::from_le_bytes)
791 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?
792 as usize;
793 let chunk_type = bytes
794 .get(16..20)
795 .and_then(|slice| slice.try_into().ok())
796 .map(u32::from_le_bytes)
797 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?;
798 if chunk_type != GLB_JSON_CHUNK {
799 return Err(LoadError::Buffer(
800 "GLB first chunk is not a JSON chunk".into(),
801 ));
802 }
803 let end = 20usize
804 .checked_add(chunk_length)
805 .ok_or_else(|| LoadError::Buffer("GLB JSON chunk range overflow".into()))?;
806 let json = bytes
807 .get(20..end)
808 .ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk length".into()))?;
809 Ok((GltfContainerKind::Glb, json))
810}
811
812fn violation(
813 violations: &mut Vec<GltfCapabilityViolation>,
814 kind: GltfCapabilityViolationKind,
815 location: impl Into<String>,
816) {
817 violations.push(GltfCapabilityViolation {
818 kind,
819 location: location.into(),
820 });
821}
822
823fn as_index(value: Option<&Value>) -> Option<usize> {
824 value?.as_u64()?.try_into().ok()
825}
826
827fn inventory(
828 root: &Value,
829 container: GltfContainerKind,
830 violations: &mut Vec<GltfCapabilityViolation>,
831) -> GltfCapabilityManifest {
832 let Some(object) = root.as_object() else {
833 return GltfCapabilityManifest {
834 container,
835 buffers: Vec::new(),
836 buffer_views: Vec::new(),
837 accessors: Vec::new(),
838 nodes: Vec::new(),
839 animation_channels: Vec::new(),
840 primitives: Vec::new(),
841 morph_weight_locations: Vec::new(),
842 instancing: Vec::new(),
843 skins: Vec::new(),
844 camera_count: 0,
845 extensions: Vec::new(),
846 extension_locations: Vec::new(),
847 external_resource_locations: Vec::new(),
848 extras_locations: Vec::new(),
849 unknown_member_locations: Vec::new(),
850 };
851 };
852 let mut manifest = GltfCapabilityManifest {
853 container,
854 buffers: Vec::new(),
855 buffer_views: Vec::new(),
856 accessors: Vec::new(),
857 nodes: Vec::new(),
858 animation_channels: Vec::new(),
859 primitives: Vec::new(),
860 morph_weight_locations: Vec::new(),
861 instancing: Vec::new(),
862 skins: Vec::new(),
863 camera_count: object
864 .get("cameras")
865 .and_then(Value::as_array)
866 .map_or(0, Vec::len),
867 extensions: Vec::new(),
868 extension_locations: Vec::new(),
869 external_resource_locations: Vec::new(),
870 extras_locations: Vec::new(),
871 unknown_member_locations: Vec::new(),
872 };
873
874 inspect_schema_members(root, "", &mut manifest, violations);
875 inventory_extensions(object, &mut manifest, violations);
876 inventory_buffers(object, container, &mut manifest, violations);
877 inventory_buffer_views_and_accessors(object, &mut manifest);
878 inventory_nodes(object, &mut manifest, violations);
879 inventory_animations(object, &mut manifest, violations);
880 inventory_meshes(object, &mut manifest, violations);
881 inventory_skins(object, &mut manifest, violations);
882
883 if manifest.camera_count > 0 {
884 violation(violations, GltfCapabilityViolationKind::Camera, "/cameras");
885 }
886 manifest.extensions.sort();
887 manifest.extensions.dedup();
888 manifest.extension_locations.sort();
889 manifest.extension_locations.dedup();
890 manifest.external_resource_locations.sort();
891 manifest.external_resource_locations.dedup();
892 manifest.extras_locations.sort();
893 manifest.extras_locations.dedup();
894 manifest.unknown_member_locations.sort();
895 manifest.unknown_member_locations.dedup();
896 manifest.morph_weight_locations.sort();
897 manifest.morph_weight_locations.dedup();
898 manifest
899}
900
901fn inventory_extensions(
902 root: &Map<String, Value>,
903 manifest: &mut GltfCapabilityManifest,
904 violations: &mut Vec<GltfCapabilityViolation>,
905) {
906 for key in ["extensionsUsed", "extensionsRequired"] {
907 let Some(values) = root.get(key).and_then(Value::as_array) else {
908 continue;
909 };
910 for (index, value) in values.iter().enumerate() {
911 let Some(name) = value.as_str() else { continue };
912 manifest.extensions.push(name.to_owned());
913 let kind = match name {
914 "KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
915 "EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
916 _ => GltfCapabilityViolationKind::ExtensionDeclaration,
917 };
918 violation(violations, kind, format!("/{key}/{index}"));
919 }
920 }
921}
922
923fn inventory_buffers(
924 root: &Map<String, Value>,
925 container: GltfContainerKind,
926 manifest: &mut GltfCapabilityManifest,
927 violations: &mut Vec<GltfCapabilityViolation>,
928) {
929 let Some(buffers) = root.get("buffers").and_then(Value::as_array) else {
930 return;
931 };
932 for (buffer_index, buffer) in buffers.iter().enumerate() {
933 let Some(buffer) = buffer.as_object() else {
934 continue;
935 };
936 let uri = buffer.get("uri").and_then(Value::as_str);
937 let source_kind = match uri {
938 Some(uri) if uri.starts_with("data:") => GltfBufferSourceKind::DataUri,
939 Some(_) => GltfBufferSourceKind::External,
940 None if container == GltfContainerKind::Glb => GltfBufferSourceKind::BinaryChunk,
941 None => GltfBufferSourceKind::External,
942 };
943 if source_kind == GltfBufferSourceKind::External {
944 manifest
945 .external_resource_locations
946 .push(format!("/buffers/{buffer_index}/uri"));
947 violation(
948 violations,
949 GltfCapabilityViolationKind::ExternalResource,
950 format!("/buffers/{buffer_index}/uri"),
951 );
952 }
953 manifest.buffers.push(GltfBufferCapability {
954 buffer_index,
955 source_kind,
956 declared_byte_length: buffer
957 .get("byteLength")
958 .and_then(Value::as_u64)
959 .unwrap_or(0),
960 });
961 }
962 if let Some(images) = root.get("images").and_then(Value::as_array) {
963 for (image_index, image) in images.iter().enumerate() {
964 if image
965 .get("uri")
966 .and_then(Value::as_str)
967 .is_some_and(|uri| !uri.starts_with("data:"))
968 {
969 manifest
970 .external_resource_locations
971 .push(format!("/images/{image_index}/uri"));
972 violation(
973 violations,
974 GltfCapabilityViolationKind::ExternalResource,
975 format!("/images/{image_index}/uri"),
976 );
977 }
978 }
979 }
980}
981
982fn inventory_buffer_views_and_accessors(
983 root: &Map<String, Value>,
984 manifest: &mut GltfCapabilityManifest,
985) {
986 if let Some(buffer_views) = root.get("bufferViews").and_then(Value::as_array) {
987 for (buffer_view_index, view) in buffer_views.iter().enumerate() {
988 let Some(view) = view.as_object() else {
989 continue;
990 };
991 manifest.buffer_views.push(GltfBufferViewCapability {
992 buffer_view_index,
993 buffer_index: as_index(view.get("buffer")).unwrap_or(usize::MAX),
994 byte_offset: view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0),
995 byte_length: view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
996 byte_stride: view.get("byteStride").and_then(Value::as_u64),
997 });
998 }
999 }
1000 if let Some(accessors) = root.get("accessors").and_then(Value::as_array) {
1001 for (accessor_index, accessor) in accessors.iter().enumerate() {
1002 let Some(accessor) = accessor.as_object() else {
1003 continue;
1004 };
1005 manifest.accessors.push(GltfAccessorCapability {
1006 accessor_index,
1007 buffer_view_index: as_index(accessor.get("bufferView")),
1008 byte_offset: accessor
1009 .get("byteOffset")
1010 .and_then(Value::as_u64)
1011 .unwrap_or(0),
1012 component_type: accessor
1013 .get("componentType")
1014 .and_then(Value::as_u64)
1015 .unwrap_or(0),
1016 accessor_type: accessor
1017 .get("type")
1018 .and_then(Value::as_str)
1019 .unwrap_or_default()
1020 .to_owned(),
1021 count: accessor.get("count").and_then(Value::as_u64).unwrap_or(0),
1022 normalized: accessor
1023 .get("normalized")
1024 .and_then(Value::as_bool)
1025 .unwrap_or(false),
1026 sparse: accessor.contains_key("sparse"),
1027 });
1028 }
1029 }
1030}
1031
1032pub(crate) const AFFINE_LAST_ROW: [(usize, f64); 4] = [(3, 0.0), (7, 0.0), (11, 0.0), (15, 1.0)];
1040
1041#[derive(Debug, Clone, Copy, PartialEq)]
1043pub(crate) enum NodeTransformFault {
1044 TrsBesideMatrix {
1046 node_index: usize,
1048 member: &'static str,
1050 },
1051 ProjectiveMatrixEntry {
1053 node_index: usize,
1055 component: usize,
1057 value: f64,
1059 expected: f64,
1061 },
1062 UnreadableMatrixEntry {
1065 node_index: usize,
1067 component: usize,
1069 },
1070}
1071
1072impl NodeTransformFault {
1073 pub(crate) fn location(self) -> String {
1075 match self {
1076 Self::TrsBesideMatrix { node_index, member } => format!("/nodes/{node_index}/{member}"),
1077 Self::ProjectiveMatrixEntry {
1078 node_index,
1079 component,
1080 ..
1081 }
1082 | Self::UnreadableMatrixEntry {
1083 node_index,
1084 component,
1085 } => format!("/nodes/{node_index}/matrix/{component}"),
1086 }
1087 }
1088
1089 fn kind(self) -> GltfCapabilityViolationKind {
1091 match self {
1092 Self::TrsBesideMatrix { .. } => GltfCapabilityViolationKind::ConflictingNodeTransform,
1093 Self::ProjectiveMatrixEntry { .. } | Self::UnreadableMatrixEntry { .. } => {
1096 GltfCapabilityViolationKind::NonAffineNodeMatrix
1097 }
1098 }
1099 }
1100}
1101
1102pub(crate) fn declared<'a>(object: &'a Value, member: &str) -> Option<&'a Value> {
1121 object.get(member).filter(|value| !value.is_null())
1122}
1123
1124pub(crate) fn node_transform_faults(nodes: &[Value]) -> Vec<NodeTransformFault> {
1147 let mut faults = Vec::new();
1148 for (node_index, node) in nodes.iter().enumerate() {
1149 let Some(matrix) = declared(node, "matrix") else {
1150 continue;
1151 };
1152 for member in ["translation", "rotation", "scale"] {
1153 if declared(node, member).is_some() {
1154 faults.push(NodeTransformFault::TrsBesideMatrix { node_index, member });
1155 }
1156 }
1157 let Some(values) = matrix.as_array().filter(|values| values.len() == 16) else {
1158 continue;
1159 };
1160 for (component, expected) in AFFINE_LAST_ROW {
1161 match values[component].as_f64() {
1162 None => faults.push(NodeTransformFault::UnreadableMatrixEntry {
1163 node_index,
1164 component,
1165 }),
1166 Some(value) if value != expected => {
1167 faults.push(NodeTransformFault::ProjectiveMatrixEntry {
1168 node_index,
1169 component,
1170 value,
1171 expected,
1172 });
1173 }
1174 Some(_) => {}
1175 }
1176 }
1177 }
1178 faults
1179}
1180
1181fn inventory_nodes(
1182 root: &Map<String, Value>,
1183 manifest: &mut GltfCapabilityManifest,
1184 violations: &mut Vec<GltfCapabilityViolation>,
1185) {
1186 let Some(nodes) = root.get("nodes").and_then(Value::as_array) else {
1187 return;
1188 };
1189 for fault in node_transform_faults(nodes) {
1190 violation(violations, fault.kind(), fault.location());
1191 }
1192 for (node_index, node) in nodes.iter().enumerate() {
1193 if !node.is_object() {
1194 continue;
1195 }
1196 if node.get("weights").is_some() {
1202 manifest
1203 .morph_weight_locations
1204 .push(format!("/nodes/{node_index}/weights"));
1205 }
1206 if node.get("camera").is_some() {
1207 violation(
1208 violations,
1209 GltfCapabilityViolationKind::Camera,
1210 format!("/nodes/{node_index}/camera"),
1211 );
1212 }
1213 if let Some(attributes) = node
1214 .get("extensions")
1215 .and_then(|extensions| extensions.get("EXT_mesh_gpu_instancing"))
1216 .and_then(|extension| extension.get("attributes"))
1217 .and_then(Value::as_object)
1218 {
1219 let mut attributes = attributes
1220 .iter()
1221 .map(|(semantic, accessor)| GltfAttributeCapability {
1222 semantic: semantic.clone(),
1223 accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
1224 })
1225 .collect::<Vec<_>>();
1226 attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
1227 manifest.instancing.push(GltfInstancingCapability {
1228 node_index,
1229 attributes,
1230 });
1231 }
1232 manifest.nodes.push(GltfNodeCapability {
1233 node_index,
1234 rest_kind: if declared(node, "matrix").is_some() {
1237 GltfNodeRestKind::Matrix
1238 } else {
1239 GltfNodeRestKind::Trs
1240 },
1241 mesh_index: as_index(node.get("mesh")),
1242 skin_index: as_index(node.get("skin")),
1243 });
1244 }
1245}
1246
1247fn inventory_animations(
1248 root: &Map<String, Value>,
1249 manifest: &mut GltfCapabilityManifest,
1250 violations: &mut Vec<GltfCapabilityViolation>,
1251) {
1252 let Some(animations) = root.get("animations").and_then(Value::as_array) else {
1253 return;
1254 };
1255 for (animation_index, animation) in animations.iter().enumerate() {
1256 let Some(animation) = animation.as_object() else {
1257 continue;
1258 };
1259 let samplers = animation
1260 .get("samplers")
1261 .and_then(Value::as_array)
1262 .map(Vec::as_slice)
1263 .unwrap_or_default();
1264 let channels = animation
1265 .get("channels")
1266 .and_then(Value::as_array)
1267 .map(Vec::as_slice)
1268 .unwrap_or_default();
1269 for (channel_index, channel) in channels.iter().enumerate() {
1270 let Some(channel) = channel.as_object() else {
1271 continue;
1272 };
1273 let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
1274 let Some(sampler) = samplers.get(sampler_index).and_then(Value::as_object) else {
1275 continue;
1276 };
1277 let Some(target) = channel.get("target").and_then(Value::as_object) else {
1278 continue;
1279 };
1280 let target_path = target
1281 .get("path")
1282 .and_then(Value::as_str)
1283 .unwrap_or_default()
1284 .to_owned();
1285 if target_path == "weights" {
1286 manifest.morph_weight_locations.push(format!(
1287 "/animations/{animation_index}/channels/{channel_index}/target/path"
1288 ));
1289 }
1290 let target_node_index = as_index(target.get("node")).unwrap_or(usize::MAX);
1291 if manifest
1292 .nodes
1293 .get(target_node_index)
1294 .is_some_and(|node| node.rest_kind == GltfNodeRestKind::Matrix)
1295 {
1296 violation(
1297 violations,
1298 GltfCapabilityViolationKind::AnimatedMatrixNode,
1299 format!("/animations/{animation_index}/channels/{channel_index}/target"),
1300 );
1301 }
1302 manifest
1303 .animation_channels
1304 .push(GltfAnimationChannelCapability {
1305 animation_index,
1306 channel_index,
1307 target_node_index,
1308 target_path,
1309 interpolation: sampler
1310 .get("interpolation")
1311 .and_then(Value::as_str)
1312 .unwrap_or("LINEAR")
1313 .to_owned(),
1314 input_accessor_index: as_index(sampler.get("input")).unwrap_or(usize::MAX),
1315 output_accessor_index: as_index(sampler.get("output")).unwrap_or(usize::MAX),
1316 });
1317 }
1318 }
1319}
1320
1321fn inventory_meshes(
1322 root: &Map<String, Value>,
1323 manifest: &mut GltfCapabilityManifest,
1324 violations: &mut Vec<GltfCapabilityViolation>,
1325) {
1326 let Some(meshes) = root.get("meshes").and_then(Value::as_array) else {
1327 return;
1328 };
1329 for (mesh_index, mesh) in meshes.iter().enumerate() {
1330 let Some(mesh) = mesh.as_object() else {
1331 continue;
1332 };
1333 if mesh.contains_key("weights") {
1334 manifest
1335 .morph_weight_locations
1336 .push(format!("/meshes/{mesh_index}/weights"));
1337 }
1338 let primitives = mesh
1339 .get("primitives")
1340 .and_then(Value::as_array)
1341 .map(Vec::as_slice)
1342 .unwrap_or_default();
1343 for (primitive_index, primitive) in primitives.iter().enumerate() {
1344 let Some(primitive) = primitive.as_object() else {
1345 continue;
1346 };
1347 let mode = primitive.get("mode").and_then(Value::as_u64).unwrap_or(4);
1348 if mode != 4 {
1349 violation(
1350 violations,
1351 GltfCapabilityViolationKind::NonTrianglePrimitive,
1352 format!("/meshes/{mesh_index}/primitives/{primitive_index}/mode"),
1353 );
1354 }
1355 let mut attributes = primitive
1356 .get("attributes")
1357 .and_then(Value::as_object)
1358 .map(|attributes| {
1359 attributes
1360 .iter()
1361 .map(|(semantic, accessor)| GltfAttributeCapability {
1362 semantic: semantic.clone(),
1363 accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
1364 })
1365 .collect::<Vec<_>>()
1366 })
1367 .unwrap_or_default();
1368 attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
1369 for attribute in &attributes {
1370 let semantic = &attribute.semantic;
1371 let semantic_pointer = json_pointer_token(semantic);
1372 let location = format!(
1373 "/meshes/{mesh_index}/primitives/{primitive_index}/attributes/{semantic_pointer}"
1374 );
1375 if is_secondary_influence(semantic) {
1376 violation(
1377 violations,
1378 GltfCapabilityViolationKind::SecondarySkinInfluences,
1379 location,
1380 );
1381 } else if !matches!(
1382 semantic.as_str(),
1383 "POSITION" | "NORMAL" | "TEXCOORD_0" | "JOINTS_0" | "WEIGHTS_0"
1384 ) {
1385 violation(
1386 violations,
1387 GltfCapabilityViolationKind::UnsupportedVertexAttribute,
1388 location,
1389 );
1390 }
1391 }
1392 let morph_target_count = primitive
1393 .get("targets")
1394 .and_then(Value::as_array)
1395 .map_or(0, Vec::len);
1396 let mut morph_position_accessors = Vec::new();
1397 let mut unsupported_morph_locations = Vec::new();
1398 for (target_index, target) in primitive
1399 .get("targets")
1400 .and_then(Value::as_array)
1401 .into_iter()
1402 .flatten()
1403 .enumerate()
1404 {
1405 let Some(target) = target.as_object() else {
1406 continue;
1407 };
1408 for (semantic, accessor) in target {
1409 let location = format!(
1410 "/meshes/{mesh_index}/primitives/{primitive_index}/targets/{target_index}/{}",
1411 json_pointer_token(semantic)
1412 );
1413 if semantic == "POSITION" {
1414 if let Some(accessor_index) = as_index(Some(accessor)) {
1415 morph_position_accessors.push(accessor_index);
1416 }
1417 } else {
1418 violation(
1419 violations,
1420 GltfCapabilityViolationKind::MorphTarget,
1421 location.clone(),
1422 );
1423 unsupported_morph_locations.push(location);
1424 }
1425 }
1426 }
1427 manifest.primitives.push(GltfPrimitiveCapability {
1428 mesh_index,
1429 primitive_index,
1430 mode,
1431 attributes,
1432 morph_target_count,
1433 morph_position_accessors,
1434 unsupported_morph_locations,
1435 });
1436 }
1437 }
1438}
1439
1440fn is_secondary_influence(semantic: &str) -> bool {
1441 semantic
1442 .strip_prefix("JOINTS_")
1443 .or_else(|| semantic.strip_prefix("WEIGHTS_"))
1444 .and_then(|index| index.parse::<u32>().ok())
1445 .is_some_and(|index| index >= 1)
1446}
1447
1448fn inventory_skins(
1449 root: &Map<String, Value>,
1450 manifest: &mut GltfCapabilityManifest,
1451 violations: &mut Vec<GltfCapabilityViolation>,
1452) {
1453 let accessors = root
1454 .get("accessors")
1455 .and_then(Value::as_array)
1456 .map(Vec::as_slice)
1457 .unwrap_or_default();
1458 let Some(skins) = root.get("skins").and_then(Value::as_array) else {
1459 return;
1460 };
1461 for (skin_index, skin) in skins.iter().enumerate() {
1462 let Some(skin) = skin.as_object() else {
1463 continue;
1464 };
1465 let joint_count = skin
1466 .get("joints")
1467 .and_then(Value::as_array)
1468 .map_or(0, Vec::len);
1469 let inverse_bind_accessor_index = as_index(skin.get("inverseBindMatrices"));
1470 let inverse_bind_count = inverse_bind_accessor_index
1471 .and_then(|index| accessors.get(index))
1472 .and_then(|accessor| accessor.get("count"))
1473 .and_then(Value::as_u64);
1474 let inverse_bind_readable = inverse_bind_accessor_index
1475 .and_then(|index| accessors.get(index))
1476 .and_then(Value::as_object)
1477 .is_some_and(|accessor| {
1478 accessor.get("bufferView").and_then(Value::as_u64).is_some()
1479 && accessor.get("componentType").and_then(Value::as_u64) == Some(5126)
1480 && accessor.get("type").and_then(Value::as_str) == Some("MAT4")
1481 && !accessor.contains_key("sparse")
1482 });
1483 match (inverse_bind_accessor_index, inverse_bind_count) {
1484 (None, _) => violation(
1485 violations,
1486 GltfCapabilityViolationKind::MissingInverseBinds,
1487 format!("/skins/{skin_index}/inverseBindMatrices"),
1488 ),
1489 (Some(_), Some(0)) => violation(
1490 violations,
1491 GltfCapabilityViolationKind::EmptyInverseBindAccessor,
1492 format!("/skins/{skin_index}/inverseBindMatrices"),
1493 ),
1494 (Some(_), Some(count)) if count != joint_count as u64 => violation(
1495 violations,
1496 GltfCapabilityViolationKind::InverseBindCountMismatch,
1497 format!("/skins/{skin_index}/inverseBindMatrices"),
1498 ),
1499 (Some(_), _) if !inverse_bind_readable => violation(
1500 violations,
1501 GltfCapabilityViolationKind::UnreadableInverseBinds,
1502 format!("/skins/{skin_index}/inverseBindMatrices"),
1503 ),
1504 _ => {}
1505 }
1506 manifest.skins.push(GltfSkinCapability {
1507 skin_index,
1508 joint_count,
1509 inverse_bind_accessor_index,
1510 inverse_bind_count,
1511 });
1512 }
1513}
1514
1515#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1516enum AccessorUse {
1517 ScaleBearing,
1518 Dimensionless,
1519}
1520
1521#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1523enum RangeOwner {
1524 Accessor(usize),
1526 SparseIndices(usize),
1528 SparseValues(usize),
1530 ImagePayload(usize),
1532}
1533
1534impl RangeOwner {
1535 fn location(self) -> String {
1537 match self {
1538 Self::Accessor(index) => format!("/accessors/{index}"),
1539 Self::SparseIndices(index) => {
1540 format!("/accessors/{index}/sparse/indices/bufferView")
1541 }
1542 Self::SparseValues(index) => {
1543 format!("/accessors/{index}/sparse/values/bufferView")
1544 }
1545 Self::ImagePayload(index) => format!("/images/{index}/bufferView"),
1546 }
1547 }
1548
1549 fn overlap_kind(self) -> GltfCapabilityViolationKind {
1552 match self {
1553 Self::Accessor(_) | Self::SparseIndices(_) | Self::SparseValues(_) => {
1554 GltfCapabilityViolationKind::OverlappingAccessorRanges
1555 }
1556 Self::ImagePayload(_) => GltfCapabilityViolationKind::ImagePayloadOverlap,
1557 }
1558 }
1559}
1560
1561type OwnedRange = (usize, usize, usize, RangeOwner, bool);
1563
1564fn inspect_accessor_layouts(
1565 root: &Value,
1566 buffers: &[Vec<u8>],
1567 uses: &BTreeMap<usize, BTreeSet<AccessorUse>>,
1568 violations: &mut Vec<GltfCapabilityViolation>,
1569) {
1570 let Some(root) = root.as_object() else { return };
1571 let mut ranges: Vec<OwnedRange> = Vec::new();
1572 let accessors = root
1573 .get("accessors")
1574 .and_then(Value::as_array)
1575 .map(Vec::as_slice)
1576 .unwrap_or_default();
1577 for accessor_index in 0..accessors.len() {
1578 let accessor_uses = uses.get(&accessor_index);
1579 let scale_bearing =
1580 accessor_uses.is_some_and(|uses| uses.contains(&AccessorUse::ScaleBearing));
1581 let accessor_ranges = if scale_bearing {
1582 dense_f32_accessor_range(root, buffers, accessor_index).map(|range| {
1583 vec![(
1584 range.0,
1585 range.1,
1586 range.2,
1587 RangeOwner::Accessor(accessor_index),
1588 true,
1589 )]
1590 })
1591 } else if accessor_uses.is_some() {
1592 accessor_range(root, buffers, accessor_index).map(|range| {
1593 vec![(
1594 range.buffer,
1595 range.start,
1596 range.end,
1597 RangeOwner::Accessor(accessor_index),
1598 false,
1599 )]
1600 })
1601 } else {
1602 preserved_accessor_ranges(root, buffers, accessor_index)
1603 };
1604 match accessor_ranges {
1605 Some(accessor_ranges) => ranges.extend(accessor_ranges),
1606 None => violation(
1607 violations,
1608 GltfCapabilityViolationKind::UnsafeAccessorLayout,
1609 format!("/accessors/{accessor_index}"),
1610 ),
1611 }
1612 }
1613 ranges.extend(image_payload_ranges(root));
1614 ranges.sort_unstable();
1615
1616 let mut overlapping = BTreeSet::new();
1617 let mut prior_scale: Option<(usize, usize, RangeOwner)> = None;
1618 for &(buffer, start, end, owner, scale_bearing) in &ranges {
1619 if let Some((left_buffer, left_end, left_owner)) = prior_scale
1620 && left_buffer == buffer
1621 && start < left_end
1622 {
1623 overlapping.insert(left_owner);
1624 overlapping.insert(owner);
1625 }
1626 if scale_bearing
1627 && prior_scale
1628 .is_none_or(|(left_buffer, left_end, _)| left_buffer != buffer || end > left_end)
1629 {
1630 prior_scale = Some((buffer, end, owner));
1631 }
1632 }
1633 let mut later_scale: Option<(usize, usize, RangeOwner)> = None;
1634 for &(buffer, start, end, owner, scale_bearing) in ranges.iter().rev() {
1635 if let Some((right_buffer, right_start, right_owner)) = later_scale
1636 && right_buffer == buffer
1637 && right_start < end
1638 {
1639 overlapping.insert(owner);
1640 overlapping.insert(right_owner);
1641 }
1642 if scale_bearing
1643 && later_scale.is_none_or(|(right_buffer, right_start, _)| {
1644 right_buffer != buffer || start < right_start
1645 })
1646 {
1647 later_scale = Some((buffer, start, owner));
1648 }
1649 }
1650 for owner in overlapping {
1651 violation(violations, owner.overlap_kind(), owner.location());
1652 }
1653}
1654
1655fn preserved_accessor_ranges(
1664 root: &Map<String, Value>,
1665 buffers: &[Vec<u8>],
1666 accessor_index: usize,
1667) -> Option<Vec<OwnedRange>> {
1668 let accessor = root
1669 .get("accessors")?
1670 .as_array()?
1671 .get(accessor_index)?
1672 .as_object()?;
1673 let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
1674 if count == 0 {
1675 return Some(Vec::new());
1676 }
1677 let Some(sparse) = accessor.get("sparse") else {
1678 let range = accessor_range(root, buffers, accessor_index)?;
1679 return Some(vec![(
1680 range.buffer,
1681 range.start,
1682 range.end,
1683 RangeOwner::Accessor(accessor_index),
1684 false,
1685 )]);
1686 };
1687
1688 let mut ranges = Vec::with_capacity(3);
1689 if accessor.get("bufferView").is_some() {
1690 let range = dense_accessor_range(root, buffers, accessor_index)?;
1691 ranges.push((
1692 range.buffer,
1693 range.start,
1694 range.end,
1695 RangeOwner::Accessor(accessor_index),
1696 false,
1697 ));
1698 }
1699
1700 let sparse = sparse.as_object()?;
1701 let sparse_count: usize = sparse.get("count")?.as_u64()?.try_into().ok()?;
1702 if sparse_count == 0 {
1703 return Some(ranges);
1704 }
1705 let indices = sparse.get("indices")?.as_object()?;
1706 let index_size = match indices.get("componentType")?.as_u64()? {
1707 5121 => 1,
1708 5123 => 2,
1709 5125 => 4,
1710 _ => return None,
1711 };
1712 let indices_range = packed_view_range(
1713 root,
1714 buffers,
1715 as_index(indices.get("bufferView"))?,
1716 indices
1717 .get("byteOffset")
1718 .and_then(Value::as_u64)
1719 .unwrap_or(0),
1720 sparse_count,
1721 index_size,
1722 index_size,
1723 )?;
1724 ranges.push((
1725 indices_range.0,
1726 indices_range.1,
1727 indices_range.2,
1728 RangeOwner::SparseIndices(accessor_index),
1729 false,
1730 ));
1731
1732 let values = sparse.get("values")?.as_object()?;
1733 let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
1734 let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
1735 let values_range = packed_view_range(
1736 root,
1737 buffers,
1738 as_index(values.get("bufferView"))?,
1739 values
1740 .get("byteOffset")
1741 .and_then(Value::as_u64)
1742 .unwrap_or(0),
1743 sparse_count,
1744 element_layout.stride,
1745 element_layout.terminal_size,
1746 )?;
1747 ranges.push((
1748 values_range.0,
1749 values_range.1,
1750 values_range.2,
1751 RangeOwner::SparseValues(accessor_index),
1752 false,
1753 ));
1754 Some(ranges)
1755}
1756
1757fn packed_view_range(
1759 root: &Map<String, Value>,
1760 buffers: &[Vec<u8>],
1761 view_index: usize,
1762 relative_offset: u64,
1763 count: usize,
1764 element_stride: usize,
1765 terminal_size: usize,
1766) -> Option<(usize, usize, usize)> {
1767 let view = root
1768 .get("bufferViews")?
1769 .as_array()?
1770 .get(view_index)?
1771 .as_object()?;
1772 let buffer_index = as_index(view.get("buffer"))?;
1773 let buffer = buffers.get(buffer_index)?;
1774 let view_offset: usize = view
1775 .get("byteOffset")
1776 .and_then(Value::as_u64)
1777 .unwrap_or(0)
1778 .try_into()
1779 .ok()?;
1780 let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
1781 if view_offset.checked_add(view_length)? > buffer.len() {
1782 return None;
1783 }
1784 let relative_offset: usize = relative_offset.try_into().ok()?;
1785 let relative_end = relative_offset
1786 .checked_add(count.checked_sub(1)?.checked_mul(element_stride)?)?
1787 .checked_add(terminal_size)?;
1788 if relative_end > view_length {
1789 return None;
1790 }
1791 let start = view_offset.checked_add(relative_offset)?;
1792 let end = view_offset.checked_add(relative_end)?;
1793 Some((buffer_index, start, end))
1794}
1795
1796fn image_payload_ranges(root: &Map<String, Value>) -> Vec<OwnedRange> {
1823 let Some(images) = root.get("images").and_then(Value::as_array) else {
1824 return Vec::new();
1825 };
1826 let buffer_views = root
1827 .get("bufferViews")
1828 .and_then(Value::as_array)
1829 .map(Vec::as_slice)
1830 .unwrap_or_default();
1831 let mut out = Vec::new();
1832 for (image_index, image) in images.iter().enumerate() {
1833 let Some(view_index) = as_index(image.get("bufferView")) else {
1834 continue;
1835 };
1836 let Some(view) = buffer_views.get(view_index).and_then(Value::as_object) else {
1839 continue;
1840 };
1841 let Some(buffer) = as_index(view.get("buffer")) else {
1842 continue;
1843 };
1844 let start = clamped_usize(view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0));
1845 let end = start.saturating_add(clamped_usize(
1846 view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
1847 ));
1848 if start < end {
1853 out.push((
1854 buffer,
1855 start,
1856 end,
1857 RangeOwner::ImagePayload(image_index),
1858 false,
1859 ));
1860 }
1861 }
1862 out
1863}
1864
1865fn clamped_usize(value: u64) -> usize {
1866 usize::try_from(value).unwrap_or(usize::MAX)
1867}
1868
1869fn inspect_accessor_uses(
1870 root: &Value,
1871 violations: &mut Vec<GltfCapabilityViolation>,
1872) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
1873 let Some(root) = root.as_object() else {
1874 return BTreeMap::new();
1875 };
1876 let uses = collect_accessor_uses(root);
1877 for (accessor_index, accessor_uses) in &uses {
1878 if accessor_uses.len() > 1 {
1879 violation(
1880 violations,
1881 GltfCapabilityViolationKind::ConflictingAccessorUse,
1882 format!("/accessors/{accessor_index}"),
1883 );
1884 }
1885 }
1886 uses
1887}
1888
1889fn collect_accessor_uses(root: &Map<String, Value>) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
1890 let mut uses: BTreeMap<usize, BTreeSet<AccessorUse>> = BTreeMap::new();
1891 let mut add = |index: Option<usize>, kind| {
1892 if let Some(index) = index {
1893 uses.entry(index).or_default().insert(kind);
1894 }
1895 };
1896 if let Some(meshes) = root.get("meshes").and_then(Value::as_array) {
1897 for mesh in meshes {
1898 let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
1899 continue;
1900 };
1901 for primitive in primitives {
1902 if let Some(attributes) = primitive.get("attributes").and_then(Value::as_object) {
1903 for (semantic, index) in attributes {
1904 add(
1905 as_index(Some(index)),
1906 if semantic == "POSITION" {
1907 AccessorUse::ScaleBearing
1908 } else {
1909 AccessorUse::Dimensionless
1910 },
1911 );
1912 }
1913 }
1914 add(
1915 as_index(primitive.get("indices")),
1916 AccessorUse::Dimensionless,
1917 );
1918 if let Some(targets) = primitive.get("targets").and_then(Value::as_array) {
1919 for target in targets {
1920 if let Some(target) = target.as_object() {
1921 for (semantic, index) in target {
1922 add(
1923 as_index(Some(index)),
1924 if semantic == "POSITION" {
1925 AccessorUse::ScaleBearing
1926 } else {
1927 AccessorUse::Dimensionless
1928 },
1929 );
1930 }
1931 }
1932 }
1933 }
1934 }
1935 }
1936 }
1937 if let Some(skins) = root.get("skins").and_then(Value::as_array) {
1938 for skin in skins {
1939 add(
1940 as_index(skin.get("inverseBindMatrices")),
1941 AccessorUse::ScaleBearing,
1942 );
1943 }
1944 }
1945 if let Some(animations) = root.get("animations").and_then(Value::as_array) {
1946 for animation in animations {
1947 let samplers = animation
1948 .get("samplers")
1949 .and_then(Value::as_array)
1950 .map(Vec::as_slice)
1951 .unwrap_or_default();
1952 let channels = animation
1953 .get("channels")
1954 .and_then(Value::as_array)
1955 .map(Vec::as_slice)
1956 .unwrap_or_default();
1957 let referenced: BTreeSet<usize> = channels
1958 .iter()
1959 .filter_map(|channel| as_index(channel.get("sampler")))
1960 .collect();
1961 for (sampler_index, sampler) in samplers.iter().enumerate() {
1962 add(as_index(sampler.get("input")), AccessorUse::Dimensionless);
1963 if !referenced.contains(&sampler_index) {
1964 add(as_index(sampler.get("output")), AccessorUse::Dimensionless);
1965 }
1966 }
1967 for channel in channels {
1968 let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
1969 let Some(sampler) = samplers.get(sampler_index) else {
1970 continue;
1971 };
1972 let path = channel
1973 .get("target")
1974 .and_then(|target| target.get("path"))
1975 .and_then(Value::as_str);
1976 add(
1977 as_index(sampler.get("output")),
1978 if path == Some("translation") {
1979 AccessorUse::ScaleBearing
1980 } else {
1981 AccessorUse::Dimensionless
1982 },
1983 );
1984 }
1985 }
1986 }
1987 uses
1988}
1989
1990pub(crate) fn dense_f32_accessor_range(
1998 root: &Map<String, Value>,
1999 buffers: &[Vec<u8>],
2000 accessor_index: usize,
2001) -> Option<(usize, usize, usize)> {
2002 let accessors = root.get("accessors")?.as_array()?;
2003 let accessor = accessors.get(accessor_index)?.as_object()?;
2004 if accessor.get("componentType")?.as_u64()? != 5126
2005 || accessor.get("normalized").and_then(Value::as_bool) == Some(true)
2006 || accessor.contains_key("sparse")
2007 {
2008 return None;
2009 }
2010 let range = accessor_range(root, buffers, accessor_index)?;
2011 if range.stride != range.element_stride || !range.start.is_multiple_of(4) {
2012 return None;
2013 }
2014 Some((range.buffer, range.start, range.end))
2015}
2016
2017pub(crate) fn resolved_accessor_range(
2026 root: &Map<String, Value>,
2027 buffers: &[Vec<u8>],
2028 accessor_index: usize,
2029) -> Option<(usize, usize, usize)> {
2030 accessor_range(root, buffers, accessor_index)
2031 .map(|range| (range.buffer, range.start, range.end))
2032}
2033
2034#[derive(Debug, Clone, Copy)]
2035struct AccessorRange {
2036 buffer: usize,
2037 start: usize,
2038 end: usize,
2039 stride: usize,
2040 element_stride: usize,
2041}
2042
2043fn accessor_range(
2044 root: &Map<String, Value>,
2045 buffers: &[Vec<u8>],
2046 accessor_index: usize,
2047) -> Option<AccessorRange> {
2048 let accessor = root
2049 .get("accessors")?
2050 .as_array()?
2051 .get(accessor_index)?
2052 .as_object()?;
2053 if accessor.contains_key("sparse") {
2054 return None;
2055 }
2056 dense_accessor_range(root, buffers, accessor_index)
2057}
2058
2059fn dense_accessor_range(
2062 root: &Map<String, Value>,
2063 buffers: &[Vec<u8>],
2064 accessor_index: usize,
2065) -> Option<AccessorRange> {
2066 let accessors = root.get("accessors")?.as_array()?;
2067 let buffer_views = root.get("bufferViews")?.as_array()?;
2068 let accessor = accessors.get(accessor_index)?.as_object()?;
2069 let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
2070 let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
2071 let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
2072 if count == 0 {
2073 return None;
2074 }
2075 let view_index = as_index(accessor.get("bufferView"))?;
2076 let view = buffer_views.get(view_index)?.as_object()?;
2077 let buffer_index = as_index(view.get("buffer"))?;
2078 let buffer = buffers.get(buffer_index)?;
2079 let view_offset: usize = view
2080 .get("byteOffset")
2081 .and_then(Value::as_u64)
2082 .unwrap_or(0)
2083 .try_into()
2084 .ok()?;
2085 let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
2086 if view_offset.checked_add(view_length)? > buffer.len() {
2087 return None;
2088 }
2089 let accessor_offset: usize = accessor
2090 .get("byteOffset")
2091 .and_then(Value::as_u64)
2092 .unwrap_or(0)
2093 .try_into()
2094 .ok()?;
2095 let stride: usize = view
2096 .get("byteStride")
2097 .and_then(Value::as_u64)
2098 .unwrap_or(element_layout.stride as u64)
2099 .try_into()
2100 .ok()?;
2101 if stride < element_layout.stride {
2102 return None;
2103 }
2104 let relative_end = accessor_offset
2105 .checked_add(count.checked_sub(1)?.checked_mul(stride)?)?
2106 .checked_add(element_layout.terminal_size)?;
2107 if relative_end > view_length {
2108 return None;
2109 }
2110 let start = view_offset.checked_add(accessor_offset)?;
2111 let end = view_offset.checked_add(relative_end)?;
2112 (end <= buffer.len()).then_some(AccessorRange {
2113 buffer: buffer_index,
2114 start,
2115 end,
2116 stride,
2117 element_stride: element_layout.stride,
2118 })
2119}
2120
2121fn component_size(component_type: u64) -> Option<usize> {
2122 match component_type {
2123 5120 | 5121 => Some(1),
2124 5122 | 5123 => Some(2),
2125 5125 | 5126 => Some(4),
2126 _ => None,
2127 }
2128}
2129
2130#[derive(Debug, Clone, Copy)]
2138struct AccessorElementLayout {
2139 stride: usize,
2140 terminal_size: usize,
2141}
2142
2143fn accessor_element_layout(
2144 accessor_type: &str,
2145 component_size: usize,
2146) -> Option<AccessorElementLayout> {
2147 let (columns, rows, matrix) = match accessor_type {
2148 "SCALAR" => (1usize, 1usize, false),
2149 "VEC2" => (1, 2, false),
2150 "VEC3" => (1, 3, false),
2151 "VEC4" => (1, 4, false),
2152 "MAT2" => (2, 2, true),
2153 "MAT3" => (3, 3, true),
2154 "MAT4" => (4, 4, true),
2155 _ => return None,
2156 };
2157 let column_size = rows.checked_mul(component_size)?;
2158 let stored_column_size = if matrix {
2159 column_size.checked_add(3)? & !3
2160 } else {
2161 column_size
2162 };
2163 let stride = columns.checked_mul(stored_column_size)?;
2164 let terminal_size = columns
2165 .checked_sub(1)?
2166 .checked_mul(stored_column_size)?
2167 .checked_add(column_size)?;
2168 Some(AccessorElementLayout {
2169 stride,
2170 terminal_size,
2171 })
2172}
2173
2174fn inspect_schema_members(
2175 value: &Value,
2176 pointer: &str,
2177 manifest: &mut GltfCapabilityManifest,
2178 violations: &mut Vec<GltfCapabilityViolation>,
2179) {
2180 match value {
2181 Value::Object(object) => {
2182 if object.get("extras").is_some_and(|value| !value.is_null()) {
2183 let location = format!("{pointer}/extras");
2184 manifest.extras_locations.push(location.clone());
2185 violation(violations, GltfCapabilityViolationKind::Extras, location);
2186 }
2187 if let Some(extensions) = object.get("extensions").and_then(Value::as_object) {
2188 for name in extensions.keys() {
2189 let location = json_pointer_child(&format!("{pointer}/extensions"), name);
2190 manifest.extensions.push(name.clone());
2191 manifest.extension_locations.push(location.clone());
2192 violation(
2193 violations,
2194 match name.as_str() {
2195 "KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
2196 "EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
2197 _ => GltfCapabilityViolationKind::ExtensionPayload,
2198 },
2199 location,
2200 );
2201 }
2202 }
2203 if let Some(allowed) = allowed_members(pointer) {
2204 for key in object.keys() {
2205 if !allowed.contains(&key.as_str()) {
2206 let location = json_pointer_child(pointer, key);
2207 manifest.unknown_member_locations.push(location.clone());
2208 violation(
2209 violations,
2210 GltfCapabilityViolationKind::UnknownJsonMember,
2211 location,
2212 );
2213 }
2214 }
2215 }
2216 for (key, child) in object {
2217 if key == "extras" || key == "extensions" {
2218 continue;
2219 }
2220 inspect_schema_members(
2221 child,
2222 &json_pointer_child(pointer, key),
2223 manifest,
2224 violations,
2225 );
2226 }
2227 }
2228 Value::Array(values) => {
2229 for (index, child) in values.iter().enumerate() {
2230 inspect_schema_members(child, &format!("{pointer}/{index}"), manifest, violations);
2231 }
2232 }
2233 _ => {}
2234 }
2235}
2236
2237fn json_pointer_child(pointer: &str, token: &str) -> String {
2238 format!("{pointer}/{}", json_pointer_token(token))
2239}
2240
2241fn json_pointer_token(token: &str) -> String {
2242 token.replace('~', "~0").replace('/', "~1")
2243}
2244
2245fn allowed_members(pointer: &str) -> Option<&'static [&'static str]> {
2246 const ROOT: &[&str] = &[
2247 "accessors",
2248 "animations",
2249 "asset",
2250 "buffers",
2251 "bufferViews",
2252 "cameras",
2253 "extensions",
2254 "extensionsRequired",
2255 "extensionsUsed",
2256 "extras",
2257 "images",
2258 "materials",
2259 "meshes",
2260 "nodes",
2261 "samplers",
2262 "scene",
2263 "scenes",
2264 "skins",
2265 "textures",
2266 ];
2267 const ASSET: &[&str] = &[
2268 "copyright",
2269 "extensions",
2270 "extras",
2271 "generator",
2272 "minVersion",
2273 "version",
2274 ];
2275 const ACCESSOR: &[&str] = &[
2276 "bufferView",
2277 "byteOffset",
2278 "componentType",
2279 "count",
2280 "extensions",
2281 "extras",
2282 "max",
2283 "min",
2284 "name",
2285 "normalized",
2286 "sparse",
2287 "type",
2288 ];
2289 const BUFFER: &[&str] = &["byteLength", "extensions", "extras", "name", "uri"];
2290 const VIEW: &[&str] = &[
2291 "buffer",
2292 "byteLength",
2293 "byteOffset",
2294 "byteStride",
2295 "extensions",
2296 "extras",
2297 "name",
2298 "target",
2299 ];
2300 const NODE: &[&str] = &[
2301 "camera",
2302 "children",
2303 "extensions",
2304 "extras",
2305 "matrix",
2306 "mesh",
2307 "name",
2308 "rotation",
2309 "scale",
2310 "skin",
2311 "translation",
2312 "weights",
2313 ];
2314 const MESH: &[&str] = &["extensions", "extras", "name", "primitives", "weights"];
2315 const PRIMITIVE: &[&str] = &[
2316 "attributes",
2317 "extensions",
2318 "extras",
2319 "indices",
2320 "material",
2321 "mode",
2322 "targets",
2323 ];
2324 const ANIMATION: &[&str] = &["channels", "extensions", "extras", "name", "samplers"];
2325 const CHANNEL: &[&str] = &["extensions", "extras", "sampler", "target"];
2326 const TARGET: &[&str] = &["extensions", "extras", "node", "path"];
2327 const ANIM_SAMPLER: &[&str] = &["extensions", "extras", "input", "interpolation", "output"];
2328 const SKIN: &[&str] = &[
2329 "extensions",
2330 "extras",
2331 "inverseBindMatrices",
2332 "joints",
2333 "name",
2334 "skeleton",
2335 ];
2336 const SCENE: &[&str] = &["extensions", "extras", "name", "nodes"];
2337 const IMAGE: &[&str] = &[
2338 "bufferView",
2339 "extensions",
2340 "extras",
2341 "mimeType",
2342 "name",
2343 "uri",
2344 ];
2345 const TEXTURE: &[&str] = &["extensions", "extras", "name", "sampler", "source"];
2346 const SAMPLER: &[&str] = &[
2347 "extensions",
2348 "extras",
2349 "magFilter",
2350 "minFilter",
2351 "name",
2352 "wrapS",
2353 "wrapT",
2354 ];
2355 const CAMERA: &[&str] = &[
2356 "extensions",
2357 "extras",
2358 "name",
2359 "orthographic",
2360 "perspective",
2361 "type",
2362 ];
2363 const MATERIAL: &[&str] = &[
2364 "alphaCutoff",
2365 "alphaMode",
2366 "doubleSided",
2367 "emissiveFactor",
2368 "emissiveTexture",
2369 "extensions",
2370 "extras",
2371 "name",
2372 "normalTexture",
2373 "occlusionTexture",
2374 "pbrMetallicRoughness",
2375 ];
2376 const PBR: &[&str] = &[
2377 "baseColorFactor",
2378 "baseColorTexture",
2379 "extensions",
2380 "extras",
2381 "metallicFactor",
2382 "metallicRoughnessTexture",
2383 "roughnessFactor",
2384 ];
2385 const TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "texCoord"];
2386 const NORMAL_TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "scale", "texCoord"];
2387 const OCCLUSION_TEXTURE_INFO: &[&str] =
2388 &["extensions", "extras", "index", "strength", "texCoord"];
2389 const PERSPECTIVE: &[&str] = &[
2390 "aspectRatio",
2391 "extensions",
2392 "extras",
2393 "yfov",
2394 "zfar",
2395 "znear",
2396 ];
2397 const ORTHOGRAPHIC: &[&str] = &["extensions", "extras", "xmag", "ymag", "zfar", "znear"];
2398 const SPARSE: &[&str] = &["count", "extensions", "extras", "indices", "values"];
2399 const SPARSE_INDICES: &[&str] = &[
2400 "bufferView",
2401 "byteOffset",
2402 "componentType",
2403 "extensions",
2404 "extras",
2405 ];
2406 const SPARSE_VALUES: &[&str] = &["bufferView", "byteOffset", "extensions", "extras"];
2407 if pointer.is_empty() {
2408 Some(ROOT)
2409 } else if pointer == "/asset" {
2410 Some(ASSET)
2411 } else if indexed_member(pointer, "/accessors/") {
2412 Some(ACCESSOR)
2413 } else if indexed_member(pointer, "/buffers/") {
2414 Some(BUFFER)
2415 } else if indexed_member(pointer, "/bufferViews/") {
2416 Some(VIEW)
2417 } else if indexed_member(pointer, "/nodes/") {
2418 Some(NODE)
2419 } else if indexed_member(pointer, "/meshes/") {
2420 Some(MESH)
2421 } else if indexed_nested_member(pointer, "/meshes/", "/primitives/") {
2422 Some(PRIMITIVE)
2423 } else if indexed_member(pointer, "/animations/") {
2424 Some(ANIMATION)
2425 } else if indexed_nested_member(pointer, "/animations/", "/channels/") {
2426 Some(CHANNEL)
2427 } else if pointer.contains("/animations/") && pointer.ends_with("/target") {
2428 Some(TARGET)
2429 } else if indexed_nested_member(pointer, "/animations/", "/samplers/") {
2430 Some(ANIM_SAMPLER)
2431 } else if indexed_member(pointer, "/skins/") {
2432 Some(SKIN)
2433 } else if indexed_member(pointer, "/scenes/") {
2434 Some(SCENE)
2435 } else if indexed_member(pointer, "/images/") {
2436 Some(IMAGE)
2437 } else if indexed_member(pointer, "/textures/") {
2438 Some(TEXTURE)
2439 } else if indexed_member(pointer, "/samplers/") {
2440 Some(SAMPLER)
2441 } else if indexed_member(pointer, "/cameras/") {
2442 Some(CAMERA)
2443 } else if indexed_member(pointer, "/materials/") {
2444 Some(MATERIAL)
2445 } else if pointer.contains("/materials/") && pointer.ends_with("/pbrMetallicRoughness") {
2446 Some(PBR)
2447 } else if pointer.contains("/materials/")
2448 && (pointer.ends_with("/baseColorTexture")
2449 || pointer.ends_with("/metallicRoughnessTexture")
2450 || pointer.ends_with("/emissiveTexture"))
2451 {
2452 Some(TEXTURE_INFO)
2453 } else if pointer.contains("/materials/") && pointer.ends_with("/normalTexture") {
2454 Some(NORMAL_TEXTURE_INFO)
2455 } else if pointer.contains("/materials/") && pointer.ends_with("/occlusionTexture") {
2456 Some(OCCLUSION_TEXTURE_INFO)
2457 } else if pointer.contains("/cameras/") && pointer.ends_with("/perspective") {
2458 Some(PERSPECTIVE)
2459 } else if pointer.contains("/cameras/") && pointer.ends_with("/orthographic") {
2460 Some(ORTHOGRAPHIC)
2461 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse") {
2462 Some(SPARSE)
2463 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/indices") {
2464 Some(SPARSE_INDICES)
2465 } else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/values") {
2466 Some(SPARSE_VALUES)
2467 } else {
2468 None
2469 }
2470}
2471
2472fn indexed_member(pointer: &str, prefix: &str) -> bool {
2473 pointer
2474 .strip_prefix(prefix)
2475 .is_some_and(|suffix| !suffix.is_empty() && !suffix.contains('/'))
2476}
2477
2478fn indexed_nested_member(pointer: &str, prefix: &str, nested: &str) -> bool {
2479 let Some(suffix) = pointer.strip_prefix(prefix) else {
2480 return false;
2481 };
2482 let Some((outer, inner)) = suffix.split_once(nested) else {
2483 return false;
2484 };
2485 !outer.is_empty() && !outer.contains('/') && !inner.is_empty() && !inner.contains('/')
2486}
2487
2488#[cfg(test)]
2489mod tests {
2490 use super::*;
2491 use serde_json::json;
2492
2493 #[test]
2494 fn accessor_use_inventory_claims_orphan_sampler_fields_without_self_conflicting_channels() {
2495 let root = json!({
2496 "animations": [{
2497 "samplers": [
2498 { "input": 1, "output": 2 },
2499 { "input": 3, "output": 4 }
2500 ],
2501 "channels": [{
2502 "sampler": 0,
2503 "target": { "node": 0, "path": "translation" }
2504 }]
2505 }]
2506 });
2507 let uses = collect_accessor_uses(root.as_object().expect("root"));
2508 assert_eq!(uses[&1], BTreeSet::from([AccessorUse::Dimensionless]));
2509 assert_eq!(uses[&2], BTreeSet::from([AccessorUse::ScaleBearing]));
2510 assert_eq!(uses[&3], BTreeSet::from([AccessorUse::Dimensionless]));
2511 assert_eq!(uses[&4], BTreeSet::from([AccessorUse::Dimensionless]));
2512 }
2513}