Skip to main content

draco_gltf/
extensions.rs

1//! Extension contracts for the lossless document model.
2
3use std::sync::Arc;
4
5use crate::json::Value;
6use draco_core::Mesh;
7#[cfg(feature = "draco-decode")]
8use draco_core::{DecoderBuffer, MeshDecoder};
9
10use crate::{Document, Error, PrimitiveRef, Result};
11
12/// Extension name for the Khronos Draco mesh compression contract.
13pub const KHR_DRACO_MESH_COMPRESSION: &str = "KHR_draco_mesh_compression";
14
15/// Extension name for the meshoptimizer buffer view compression contract.
16///
17/// The import path decodes it eagerly into the fallback buffers, so the rest of
18/// the crate never sees a compressed buffer view.
19pub const EXT_MESHOPT_COMPRESSION: &str = "EXT_meshopt_compression";
20
21/// The name gltfpack wrote before the extension was ratified under the `EXT_`
22/// vendor prefix.
23///
24/// The extension object, the bitstream and the fallback-buffer convention are
25/// identical, so assets carrying the older name decode through exactly the same
26/// path. Refusing them means refusing a file over its spelling.
27pub const KHR_MESHOPT_COMPRESSION: &str = "KHR_meshopt_compression";
28
29/// Reads a `extensions` object's meshopt entry under either spelling.
30pub fn meshopt_extension(extensions: Option<&Value>) -> Option<(&'static str, &Value)> {
31    let extensions = extensions?;
32    for name in [EXT_MESHOPT_COMPRESSION, KHR_MESHOPT_COMPRESSION] {
33        if let Some(value) = extensions.get(name) {
34            return Some((name, value));
35        }
36    }
37    None
38}
39
40/// The mutable form of [`meshopt_extension`].
41pub fn meshopt_extension_mut(extensions: Option<&mut Value>) -> Option<(&'static str, &mut Value)> {
42    let extensions = extensions?;
43    let name = if extensions.get(EXT_MESHOPT_COMPRESSION).is_some() {
44        EXT_MESHOPT_COMPRESSION
45    } else if extensions.get(KHR_MESHOPT_COMPRESSION).is_some() {
46        KHR_MESHOPT_COMPRESSION
47    } else {
48        return None;
49    };
50    extensions.get_mut(name).map(|value| (name, value))
51}
52
53/// Extensions whose specifications name no accessor and no buffer view.
54///
55/// Every entry is an assertion about a published specification, not a guess
56/// from the extension's prefix: the JSON these define is factors, colors,
57/// names, enum values and indices into `materials`, `textures` or their own
58/// root arrays — never into `accessors` or `bufferViews`. A binary transform
59/// therefore cannot invalidate them, and nothing has to be remapped.
60///
61/// The list matters because the safety check in `Import` is whole-document: an
62/// unregistered extension anywhere refuses Draco compression for the entire
63/// file. Before this list existed that refused 21 of the 70 corpus assets over
64/// extensions that describe how a surface is lit.
65pub const BINARY_FREE_EXTENSIONS: &[&str] = &[
66    // The layered material model. None of these reach past `materials`.
67    "KHR_materials_unlit",
68    "KHR_materials_emissive_strength",
69    "KHR_materials_ior",
70    "KHR_materials_specular",
71    "KHR_materials_anisotropy",
72    "KHR_materials_transmission",
73    "KHR_materials_dispersion",
74    "KHR_materials_volume",
75    "KHR_materials_iridescence",
76    "KHR_materials_sheen",
77    "KHR_materials_clearcoat",
78    // Archived by Khronos, still present in assets, and equally binary-free.
79    "KHR_materials_pbrSpecularGlossiness",
80    // Rides on a texture binding: offset, scale, rotation and a texCoord set.
81    "KHR_texture_transform",
82    // Name an alternate `images[]` entry; the image itself is an ordinary one.
83    "EXT_texture_webp",
84    "EXT_texture_avif",
85    "KHR_texture_basisu",
86    // Scene-level, and both stay in their own index spaces: lights[] and
87    // variants[] are root arrays this crate never compacts.
88    "KHR_lights_punctual",
89    "KHR_materials_variants",
90    // A permission rather than a payload: it widens the component types an
91    // accessor may use, and names none of them.
92    "KHR_mesh_quantization",
93    // A Cesium vendor extension holding one origin offset, `center: [x, y, z]`.
94    "CESIUM_RTC",
95    // The one entry that looks like a counter-example and is not. Its
96    // `featureIds[].attribute: N` is a *name* — it selects `_FEATURE_ID_N` —
97    // and its remaining references are a texture and an index into the root
98    // metadata arrays. None of those is an accessor or a buffer view.
99    //
100    // What it does depend on is the encoder leaving the identifier attributes
101    // alone, since a quantized feature ID is a wrong one. Measured on BoxMeta:
102    // every vertex record survives compression with its values, its component
103    // types and its pairing intact, and the semantics keep their names.
104    "EXT_mesh_features",
105];
106
107/// Extension name for per-node GPU instancing.
108pub const EXT_MESH_GPU_INSTANCING: &str = "EXT_mesh_gpu_instancing";
109
110/// Extension name for the structural metadata contract.
111pub const EXT_STRUCTURAL_METADATA: &str = "EXT_structural_metadata";
112
113/// The three keys a property-table property may use to address a buffer view.
114const PROPERTY_TABLE_SLOTS: [&str; 3] = ["values", "arrayOffsets", "stringOffsets"];
115
116/// Every accessor reference `EXT_mesh_gpu_instancing` owns.
117///
118/// One per instanced node per semantic: `TRANSLATION`, `ROTATION` and `SCALE`
119/// each name an accessor of one element per instance. Every semantic is
120/// collected rather than those three by name, because an unrecognized one
121/// still holds an accessor index, and skipping it would leave a live reference
122/// pointing at whatever landed in that slot after compaction.
123///
124/// This and [`instancing_accessors_mut`] walk the same places and must keep
125/// doing so: a reference one of them keeps alive and the other does not
126/// rewrite ends up pointing at a slot that moved.
127fn instancing_accessors(root: &Value) -> impl Iterator<Item = &Value> {
128    root.get("nodes")
129        .and_then(Value::as_array)
130        .unwrap_or(&[])
131        .iter()
132        .filter_map(|node| {
133            node.get("extensions")?
134                .get(EXT_MESH_GPU_INSTANCING)?
135                .get("attributes")?
136                .as_object()
137        })
138        .flatten()
139        .map(|(_, value)| value)
140}
141
142/// The mutable form of [`instancing_accessors`].
143fn instancing_accessors_mut(root: &mut Value) -> impl Iterator<Item = &mut Value> {
144    root.get_mut("nodes")
145        .and_then(Value::as_array_mut)
146        .map(|nodes| nodes.iter_mut())
147        .into_iter()
148        .flatten()
149        .filter_map(|node| {
150            node.get_mut("extensions")?
151                .get_mut(EXT_MESH_GPU_INSTANCING)?
152                .get_mut("attributes")?
153                .as_object_mut()
154        })
155        .flatten()
156        .map(|(_, value)| value)
157}
158
159/// Every buffer-view reference `EXT_structural_metadata` owns.
160///
161/// Property tables hold their columns as raw buffer views rather than as
162/// accessors: a column of strings is a byte range plus a range of offsets into
163/// it, which no accessor can describe. Those are the only binary references
164/// the extension makes — property attributes name vertex attributes by string,
165/// and property textures name textures — so they are also the only thing a
166/// binary transform can invalidate.
167fn property_table_views(root: &Value) -> impl Iterator<Item = &Value> {
168    root.get("extensions")
169        .and_then(|extensions| extensions.get(EXT_STRUCTURAL_METADATA))
170        .and_then(|metadata| metadata.get("propertyTables"))
171        .and_then(Value::as_array)
172        .unwrap_or(&[])
173        .iter()
174        .filter_map(|table| table.get("properties")?.as_object())
175        .flatten()
176        .flat_map(|(_, property)| {
177            PROPERTY_TABLE_SLOTS
178                .iter()
179                .filter_map(|slot| property.get(slot))
180        })
181}
182
183/// The mutable form of [`property_table_views`].
184fn property_table_views_mut(root: &mut Value) -> impl Iterator<Item = &mut Value> {
185    root.get_mut("extensions")
186        .and_then(|extensions| extensions.get_mut(EXT_STRUCTURAL_METADATA))
187        .and_then(|metadata| metadata.get_mut("propertyTables"))
188        .and_then(Value::as_array_mut)
189        .map(|tables| tables.iter_mut())
190        .into_iter()
191        .flatten()
192        .filter_map(|table| table.get_mut("properties")?.as_object_mut())
193        .flatten()
194        .flat_map(|(_, property)| {
195            property
196                .as_object_mut()
197                .map(|entries| {
198                    entries
199                        .iter_mut()
200                        .filter(|(key, _)| PROPERTY_TABLE_SLOTS.contains(&key.as_str()))
201                        .map(|(_, value)| value)
202                })
203                .into_iter()
204                .flatten()
205        })
206}
207
208/// Marks one index as still in use, or reports that it never was valid.
209fn keep_reference(value: &Value, used: &mut [bool], kind: &str) -> Result<()> {
210    let index = value
211        .as_u64()
212        .and_then(|value| usize::try_from(value).ok())
213        .filter(|index| *index < used.len())
214        .ok_or_else(|| Error::Extension(format!("{kind} is invalid")))?;
215    used[index] = true;
216    Ok(())
217}
218
219/// Instance transforms, which are accessors like any vertex attribute.
220///
221/// They differ in that no primitive names them, so compaction sees them as
222/// unreferenced and would drop the instances rather than the metadata about
223/// them. Keeping them alive and rewriting their indices is the whole handler.
224#[derive(Clone, Copy, Debug, Default)]
225pub struct MeshGpuInstancingExtension;
226impl ExtensionHandler for MeshGpuInstancingExtension {
227    fn name(&self) -> &'static str {
228        EXT_MESH_GPU_INSTANCING
229    }
230    fn allows_binary_transform(&self) -> bool {
231        true
232    }
233    fn collect_binary_references(
234        &self,
235        document: &Document,
236        accessors: &mut [bool],
237        _buffer_views: &mut [bool],
238    ) -> Result<()> {
239        for value in instancing_accessors(document.as_value()) {
240            keep_reference(value, accessors, "EXT_mesh_gpu_instancing accessor")?;
241        }
242        Ok(())
243    }
244    fn remap_binary_references(
245        &self,
246        document: &mut Document,
247        accessors: &[Option<usize>],
248        _buffer_views: &[Option<usize>],
249    ) -> Result<()> {
250        for value in instancing_accessors_mut(document.as_value_mut()) {
251            remap_reference(value, accessors, "EXT_mesh_gpu_instancing accessor")?;
252        }
253        Ok(())
254    }
255}
256
257/// Property tables, whose columns are buffer views rather than accessors.
258#[derive(Clone, Copy, Debug, Default)]
259pub struct StructuralMetadataExtension;
260impl ExtensionHandler for StructuralMetadataExtension {
261    fn name(&self) -> &'static str {
262        EXT_STRUCTURAL_METADATA
263    }
264    fn allows_binary_transform(&self) -> bool {
265        true
266    }
267    fn collect_binary_references(
268        &self,
269        document: &Document,
270        _accessors: &mut [bool],
271        buffer_views: &mut [bool],
272    ) -> Result<()> {
273        for value in property_table_views(document.as_value()) {
274            keep_reference(value, buffer_views, "EXT_structural_metadata bufferView")?;
275        }
276        Ok(())
277    }
278    fn remap_binary_references(
279        &self,
280        document: &mut Document,
281        _accessors: &[Option<usize>],
282        buffer_views: &[Option<usize>],
283    ) -> Result<()> {
284        for value in property_table_views_mut(document.as_value_mut()) {
285            remap_reference(value, buffer_views, "EXT_structural_metadata bufferView")?;
286        }
287        Ok(())
288    }
289}
290
291/// An extension that owns no binary references.
292///
293/// Opting into binary transforms with the trait's own empty
294/// [`ExtensionHandler::collect_binary_references`] and
295/// [`ExtensionHandler::remap_binary_references`] is exactly the statement
296/// "this extension participates and owns nothing": there is nothing to keep
297/// alive and nothing to rewrite.
298#[derive(Clone, Copy, Debug)]
299pub struct BinaryFreeExtension(pub &'static str);
300impl ExtensionHandler for BinaryFreeExtension {
301    fn name(&self) -> &'static str {
302        self.0
303    }
304    fn allows_binary_transform(&self) -> bool {
305        true
306    }
307}
308
309/// Resolved binary resources indexed by glTF buffer index.
310#[derive(Clone, Debug, Default)]
311pub struct ResourceStore {
312    /// Resolved bytes indexed by glTF `buffers[]` position.
313    pub buffers: Vec<Vec<u8>>,
314}
315
316/// Narrow validation permissions granted by an extension.
317#[derive(Default)]
318pub struct ExtensionValidationContext {
319    accessors_without_buffer_view: Vec<usize>,
320}
321
322impl ExtensionValidationContext {
323    /// Allows a registered extension to omit a buffer view for one accessor.
324    pub fn allow_accessor_without_buffer_view(&mut self, index: usize) {
325        if !self.accessors_without_buffer_view.contains(&index) {
326            self.accessors_without_buffer_view.push(index);
327        }
328    }
329    /// Returns whether an accessor has received that narrow exemption.
330    pub fn allows_accessor_without_buffer_view(&self, index: usize) -> bool {
331        self.accessors_without_buffer_view.contains(&index)
332    }
333}
334
335/// A registered glTF extension with optional geometry decoding.
336pub trait ExtensionHandler: Send + Sync {
337    /// Returns the exact glTF extension name handled by this implementation.
338    fn name(&self) -> &'static str;
339    /// Performs extension-specific strict validation and records narrowly
340    /// scoped core-validation exemptions in `context`.
341    fn validate(
342        &self,
343        _document: &Document,
344        _context: &mut ExtensionValidationContext,
345    ) -> Result<()> {
346        Ok(())
347    }
348    /// Whether a transform may replace accessor and buffer-view binary data
349    /// while preserving this extension. Handlers must opt in explicitly after
350    /// validating their binary-reference semantics.
351    fn allows_binary_transform(&self) -> bool {
352        false
353    }
354    /// Marks every accessor and buffer-view reference owned by this extension.
355    ///
356    /// A handler that opts into binary transforms must implement this together
357    /// with [`Self::remap_binary_references`]. Unknown extension JSON is never
358    /// inspected or rewritten by the core document transformer.
359    fn collect_binary_references(
360        &self,
361        _document: &Document,
362        _accessors: &mut [bool],
363        _buffer_views: &mut [bool],
364    ) -> Result<()> {
365        Ok(())
366    }
367    /// Applies the maps produced by binary compaction to references owned by
368    /// this extension. This is called only for handlers that explicitly allow
369    /// binary transforms.
370    fn remap_binary_references(
371        &self,
372        _document: &mut Document,
373        _accessors: &[Option<usize>],
374        _buffer_views: &[Option<usize>],
375    ) -> Result<()> {
376        Ok(())
377    }
378    /// Decodes geometry for `primitive`, or returns `None` when this handler
379    /// does not own that primitive.
380    fn decode_primitive(
381        &self,
382        _document: &Document,
383        _resources: &ResourceStore,
384        _primitive: PrimitiveRef<'_>,
385    ) -> Option<Result<Mesh>> {
386        None
387    }
388}
389
390#[derive(Clone)]
391/// Registry of unique extension handlers used by document validation/transforms.
392pub struct ExtensionRegistry {
393    handlers: Vec<Arc<dyn ExtensionHandler>>,
394}
395impl ExtensionRegistry {
396    /// Creates the registry containing the built-in Draco handler.
397    pub fn new() -> Self {
398        Self::default()
399    }
400    /// Registers one extension handler. Extension names must be unique.
401    pub fn register<H: ExtensionHandler + 'static>(&mut self, handler: H) -> Result<()> {
402        if self
403            .handlers
404            .iter()
405            .any(|existing| existing.name() == handler.name())
406        {
407            return Err(Error::Extension(format!(
408                "extension handler {} is already registered",
409                handler.name()
410            )));
411        }
412        self.handlers.push(Arc::new(handler));
413        Ok(())
414    }
415    /// Returns whether a handler is registered for `name`.
416    pub fn contains(&self, name: &str) -> bool {
417        self.handlers.iter().any(|handler| handler.name() == name)
418    }
419    /// Returns whether `name` explicitly supports binary-reference transforms.
420    pub fn allows_binary_transform(&self, name: &str) -> bool {
421        self.handlers
422            .iter()
423            .any(|handler| handler.name() == name && handler.allows_binary_transform())
424    }
425    /// Validates every registered extension against `document`.
426    pub fn validate(&self, document: &Document) -> Result<ExtensionValidationContext> {
427        let mut context = ExtensionValidationContext::default();
428        for handler in &self.handlers {
429            handler.validate(document, &mut context)?;
430        }
431        Ok(context)
432    }
433    #[cfg(feature = "draco-encode")]
434    pub(crate) fn collect_binary_references(
435        &self,
436        document: &Document,
437        accessors: &mut [bool],
438        buffer_views: &mut [bool],
439    ) -> Result<()> {
440        for handler in &self.handlers {
441            if handler.allows_binary_transform() {
442                handler.collect_binary_references(document, accessors, buffer_views)?;
443            }
444        }
445        Ok(())
446    }
447    #[cfg(feature = "draco-encode")]
448    pub(crate) fn remap_binary_references(
449        &self,
450        document: &mut Document,
451        accessors: &[Option<usize>],
452        buffer_views: &[Option<usize>],
453    ) -> Result<()> {
454        for handler in &self.handlers {
455            if handler.allows_binary_transform() {
456                handler.remap_binary_references(document, accessors, buffer_views)?;
457            }
458        }
459        Ok(())
460    }
461    /// Dispatches geometry decoding to the handler that owns `primitive`.
462    pub fn decode_primitive(
463        &self,
464        document: &Document,
465        resources: &ResourceStore,
466        primitive: PrimitiveRef<'_>,
467    ) -> Result<Mesh> {
468        for handler in &self.handlers {
469            if let Some(result) = handler.decode_primitive(document, resources, primitive) {
470                return result;
471            }
472        }
473        Err(Error::Extension(
474            "primitive has no registered geometry extension decoder".into(),
475        ))
476    }
477}
478
479/// Default decoder for `KHR_draco_mesh_compression`.
480#[derive(Clone, Copy, Debug, Default)]
481pub struct DracoExtension;
482impl ExtensionHandler for DracoExtension {
483    fn name(&self) -> &'static str {
484        KHR_DRACO_MESH_COMPRESSION
485    }
486    fn allows_binary_transform(&self) -> bool {
487        true
488    }
489    fn collect_binary_references(
490        &self,
491        document: &Document,
492        _accessors: &mut [bool],
493        buffer_views: &mut [bool],
494    ) -> Result<()> {
495        if buffer_views.is_empty() {
496            return Ok(());
497        }
498        for mesh in document.meshes() {
499            for primitive in mesh
500                .value()
501                .get("primitives")
502                .and_then(Value::as_array)
503                .unwrap_or(&[])
504            {
505                let Some(extension) = primitive
506                    .get("extensions")
507                    .and_then(|value| value.get(KHR_DRACO_MESH_COMPRESSION))
508                else {
509                    continue;
510                };
511                let index = extension
512                    .get("bufferView")
513                    .and_then(Value::as_u64)
514                    .and_then(|value| usize::try_from(value).ok())
515                    .filter(|index| *index < buffer_views.len())
516                    .ok_or_else(|| Error::Extension("Draco bufferView is invalid".into()))?;
517                buffer_views[index] = true;
518            }
519        }
520        Ok(())
521    }
522    fn remap_binary_references(
523        &self,
524        document: &mut Document,
525        _accessors: &[Option<usize>],
526        buffer_views: &[Option<usize>],
527    ) -> Result<()> {
528        if buffer_views.is_empty() {
529            return Ok(());
530        }
531        let Some(meshes) = document
532            .as_value_mut()
533            .get_mut("meshes")
534            .and_then(Value::as_array_mut)
535        else {
536            return Ok(());
537        };
538        for mesh in meshes {
539            let Some(primitives) = mesh.get_mut("primitives").and_then(Value::as_array_mut) else {
540                continue;
541            };
542            for primitive in primitives {
543                let Some(value) = primitive
544                    .get_mut("extensions")
545                    .and_then(|value| value.get_mut(KHR_DRACO_MESH_COMPRESSION))
546                    .and_then(|value| value.get_mut("bufferView"))
547                else {
548                    continue;
549                };
550                remap_reference(value, buffer_views, "Draco bufferView")?;
551            }
552        }
553        Ok(())
554    }
555    fn validate(
556        &self,
557        document: &Document,
558        context: &mut ExtensionValidationContext,
559    ) -> Result<()> {
560        let accessors = document
561            .as_value()
562            .get("accessors")
563            .and_then(Value::as_array)
564            .unwrap_or(&[]);
565        for mesh in document.meshes() {
566            for primitive_index in mesh
567                .value()
568                .get("primitives")
569                .and_then(Value::as_array)
570                .into_iter()
571                .flatten()
572                .enumerate()
573            {
574                let primitive = primitive_index.1;
575                let Some(_parsed) = parse_draco_extension(
576                    primitive
577                        .get("extensions")
578                        .and_then(|extensions| extensions.get(KHR_DRACO_MESH_COMPRESSION)),
579                )?
580                else {
581                    continue;
582                };
583                for accessor in primitive
584                    .get("attributes")
585                    .and_then(Value::as_object)
586                    .into_iter()
587                    .flat_map(|attrs| attrs.iter().map(|(_, value)| value))
588                    .chain(primitive.get("indices"))
589                {
590                    if let Some(index) = accessor
591                        .as_u64()
592                        .and_then(|value| usize::try_from(value).ok())
593                    {
594                        if accessors.get(index).is_some_and(|value| {
595                            value.get("bufferView").is_none() && value.get("sparse").is_none()
596                        }) {
597                            context.allow_accessor_without_buffer_view(index);
598                        }
599                    }
600                }
601            }
602        }
603        Ok(())
604    }
605    #[cfg(feature = "draco-decode")]
606    fn decode_primitive(
607        &self,
608        document: &Document,
609        resources: &ResourceStore,
610        primitive: PrimitiveRef<'_>,
611    ) -> Option<Result<Mesh>> {
612        let extension = primitive.extension(self.name())?;
613        Some((|| {
614            let parsed = parse_draco_extension(Some(extension))?
615                .ok_or_else(|| Error::Extension("missing Draco extension".into()))?;
616            let view = document.as_value()["bufferViews"]
617                .as_array()
618                .and_then(|views| views.get(parsed.buffer_view))
619                .ok_or_else(|| Error::Extension("Draco bufferView out of range".into()))?;
620            let buffer = view
621                .get("buffer")
622                .and_then(Value::as_u64)
623                .and_then(|value| usize::try_from(value).ok())
624                .and_then(|index| resources.buffers.get(index))
625                .ok_or_else(|| Error::Extension("Draco buffer is not resolved".into()))?;
626            let start = view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0) as usize;
627            let length = view
628                .get("byteLength")
629                .and_then(Value::as_u64)
630                .and_then(|value| usize::try_from(value).ok())
631                .ok_or_else(|| Error::Extension("Draco bufferView length is invalid".into()))?;
632            let end = start
633                .checked_add(length)
634                .filter(|end| *end <= buffer.len())
635                .ok_or_else(|| Error::Extension("Draco bufferView out of bounds".into()))?;
636            let mut mesh = Mesh::new();
637            MeshDecoder::new()
638                .decode(&mut DecoderBuffer::new(&buffer[start..end]), &mut mesh)
639                .map_err(Error::Decode)?;
640            Ok(mesh)
641        })())
642    }
643}
644
645fn remap_reference(value: &mut Value, map: &[Option<usize>], kind: &str) -> Result<()> {
646    let old = value
647        .as_u64()
648        .and_then(|value| usize::try_from(value).ok())
649        .ok_or_else(|| Error::Extension(format!("{kind} is invalid")))?;
650    let new = map
651        .get(old)
652        .and_then(|value| *value)
653        .ok_or_else(|| Error::Extension(format!("{kind} was removed")))?;
654    *value = Value::from(new);
655    Ok(())
656}
657
658#[cfg_attr(not(feature = "draco-decode"), allow(dead_code))]
659#[derive(Clone, Debug)]
660pub(crate) struct DracoContract {
661    pub buffer_view: usize,
662    pub attributes: Vec<(String, u32)>,
663}
664
665pub(crate) fn parse_draco_extension(value: Option<&Value>) -> Result<Option<DracoContract>> {
666    let Some(value) = value else {
667        return Ok(None);
668    };
669    let buffer_view = value
670        .get("bufferView")
671        .and_then(Value::as_u64)
672        .and_then(|value| usize::try_from(value).ok())
673        .ok_or_else(|| Error::Extension("Draco bufferView is invalid".into()))?;
674    let attributes = value
675        .get("attributes")
676        .and_then(Value::as_object)
677        .ok_or_else(|| Error::Extension("Draco attributes is invalid".into()))?
678        .iter()
679        .map(|(name, value)| {
680            value
681                .as_u64()
682                .and_then(|value| u32::try_from(value).ok())
683                .map(|value| (name.clone(), value))
684                .ok_or_else(|| Error::Extension(format!("Draco attribute {name} is invalid")))
685        })
686        .collect::<Result<Vec<_>>>()?;
687    Ok(Some(DracoContract {
688        buffer_view,
689        attributes,
690    }))
691}
692
693impl Default for ExtensionRegistry {
694    fn default() -> Self {
695        let mut registry = Self {
696            handlers: Vec::new(),
697        };
698        registry
699            .register(DracoExtension)
700            .expect("built-in extension names are unique");
701        // Everything below exists to answer one question — may a binary
702        // transform touch this document — which a build that cannot write one
703        // never asks. Registering them there would put twenty handlers into a
704        // reader whose only use for the registry is decoding Draco geometry,
705        // and the WASM reader is measured against a size budget.
706        #[cfg(feature = "write")]
707        {
708            registry
709                .register(MeshGpuInstancingExtension)
710                .expect("built-in extension names are unique");
711            registry
712                .register(StructuralMetadataExtension)
713                .expect("built-in extension names are unique");
714            for name in BINARY_FREE_EXTENSIONS {
715                registry
716                    .register(BinaryFreeExtension(name))
717                    .expect("built-in extension names are unique");
718            }
719        }
720        registry
721    }
722}