Skip to main content

draco_gltf/
compression.rs

1use crate::{Error, Import, Result};
2use draco_core::{
3    draco_types::DataType,
4    encoder_buffer::EncoderBuffer,
5    encoder_options::EncoderOptions,
6    mesh_encoder::{EncodedMeshInfo, MeshEncoder},
7};
8
9/// How the exported primitive exposes its Draco payload.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum CompressionMode {
12    /// Preserve ordinary accessors as a non-Draco fallback. The extension is
13    /// listed in `extensionsUsed`, never `extensionsRequired`.
14    Fallback,
15    /// Require Draco and remove ordinary geometry payloads owned solely by
16    /// the transformed primitive.
17    #[default]
18    DracoOnly,
19}
20
21fn add_extension_name(root: &mut crate::JsonValue, field: &str) -> Result<()> {
22    if root.get(field).is_none() {
23        root[field] = crate::JsonValue::Array(Vec::new());
24    }
25    let list = root[field]
26        .as_array_mut()
27        .ok_or_else(|| Error::Validation(vec![format!("{field} is not an array")]))?;
28    if !list
29        .iter()
30        .any(|value| value.as_str() == Some(crate::KHR_DRACO_MESH_COMPRESSION))
31    {
32        list.push(crate::JsonValue::from(crate::KHR_DRACO_MESH_COMPRESSION));
33    }
34    Ok(())
35}
36
37fn detach_draco_only_accessors(
38    root: &mut crate::JsonValue,
39    mesh_index: usize,
40    primitive_index: usize,
41    mapping: &[(String, u32)],
42    layout: &DracoGeometryLayout,
43) -> Result<()> {
44    for (semantic, unique_id) in mapping {
45        let source = root["meshes"][mesh_index]["primitives"][primitive_index]["attributes"]
46            [semantic.as_str()]
47        .as_u64()
48        .and_then(|value| usize::try_from(value).ok())
49        .ok_or_else(|| Error::Extension(format!("Draco attribute {semantic} has no accessor")))?;
50        let accessor = clone_accessor(root, source)?;
51        root["meshes"][mesh_index]["primitives"][primitive_index]["attributes"]
52            [semantic.as_str()] = crate::JsonValue::from(accessor as u64);
53        let attribute = layout
54            .attributes
55            .iter()
56            .find(|attribute| attribute.unique_id == *unique_id)
57            .ok_or_else(|| {
58                Error::Extension(format!("encoded Draco attribute {unique_id} is missing"))
59            })?;
60        set_draco_accessor_layout(
61            root,
62            accessor,
63            layout.points,
64            attribute.components,
65            attribute.data_type,
66            attribute.position_bounds.as_ref(),
67        )?;
68    }
69
70    let source = root["meshes"][mesh_index]["primitives"][primitive_index]
71        .get("indices")
72        .and_then(crate::JsonValue::as_u64)
73        .and_then(|value| usize::try_from(value).ok());
74    let accessor = match source {
75        Some(source) => clone_accessor(root, source)?,
76        None => {
77            let accessors = root["accessors"]
78                .as_array_mut()
79                .ok_or_else(|| Error::Validation(vec!["accessors is not an array".into()]))?;
80            let index = accessors.len();
81            accessors.push(crate::JsonValue::Object(Vec::new()));
82            index
83        }
84    };
85    root["meshes"][mesh_index]["primitives"][primitive_index]["indices"] =
86        crate::JsonValue::from(accessor as u64);
87    set_draco_accessor_layout(root, accessor, layout.faces * 3, 1, DataType::Uint32, None)?;
88    Ok(())
89}
90
91#[derive(Clone)]
92struct DracoAttributeLayout {
93    unique_id: u32,
94    components: u8,
95    data_type: DataType,
96    position_bounds: Option<(Vec<f64>, Vec<f64>)>,
97}
98
99struct DracoGeometryLayout {
100    points: usize,
101    faces: usize,
102    attributes: Vec<DracoAttributeLayout>,
103}
104
105impl DracoGeometryLayout {
106    fn from_encoded_info(info: &EncodedMeshInfo, mapping: &[(String, u32)]) -> Result<Self> {
107        let attributes = mapping
108            .iter()
109            .map(|(semantic, unique_id)| {
110                let attribute = info
111                    .attributes
112                    .iter()
113                    .find(|attribute| attribute.unique_id == *unique_id)
114                    .ok_or_else(|| {
115                        Error::Extension(format!("encoded Draco attribute {unique_id} is missing"))
116                    })?;
117                let position_bounds = if semantic == "POSITION" {
118                    Some((
119                        attribute.position_min.clone().ok_or_else(|| {
120                            Error::Extension("encoded POSITION min bounds are missing".into())
121                        })?,
122                        attribute.position_max.clone().ok_or_else(|| {
123                            Error::Extension("encoded POSITION max bounds are missing".into())
124                        })?,
125                    ))
126                } else {
127                    None
128                };
129                Ok(DracoAttributeLayout {
130                    unique_id: *unique_id,
131                    components: attribute.num_components,
132                    data_type: attribute.data_type,
133                    position_bounds,
134                })
135            })
136            .collect::<Result<Vec<_>>>()?;
137        Ok(Self {
138            points: info.num_encoded_points,
139            faces: info.num_encoded_faces,
140            attributes,
141        })
142    }
143}
144
145fn clone_accessor(root: &mut crate::JsonValue, source: usize) -> Result<usize> {
146    let accessors = root["accessors"]
147        .as_array_mut()
148        .ok_or_else(|| Error::Validation(vec!["accessors is not an array".into()]))?;
149    let source = accessors
150        .get(source)
151        .cloned()
152        .ok_or_else(|| Error::Extension("Draco accessor out of range".into()))?;
153    let index = accessors.len();
154    accessors.push(source);
155    Ok(index)
156}
157
158fn set_draco_accessor_layout(
159    root: &mut crate::JsonValue,
160    index: usize,
161    count: usize,
162    components: u8,
163    data_type: DataType,
164    position_bounds: Option<&(Vec<f64>, Vec<f64>)>,
165) -> Result<()> {
166    let component_type = match data_type {
167        DataType::Int8 => 5120,
168        DataType::Uint8 => 5121,
169        DataType::Int16 => 5122,
170        DataType::Uint16 => 5123,
171        DataType::Uint32 => 5125,
172        DataType::Float32 => 5126,
173        _ => {
174            return Err(Error::Extension(format!(
175                "Draco attribute data type {data_type:?} cannot be represented by glTF 2.0"
176            )))
177        }
178    };
179    let accessor_type = match components {
180        1 => "SCALAR",
181        2 => "VEC2",
182        3 => "VEC3",
183        4 => "VEC4",
184        _ => {
185            return Err(Error::Extension(format!(
186                "Draco attribute component count {components} cannot be represented by glTF"
187            )))
188        }
189    };
190    let accessor = root["accessors"]
191        .as_array_mut()
192        .and_then(|values| values.get_mut(index))
193        .ok_or_else(|| Error::Extension("Draco accessor out of range".into()))?;
194    accessor["count"] = crate::JsonValue::from(count);
195    accessor["componentType"] = crate::JsonValue::from(component_type as u64);
196    accessor["type"] = crate::JsonValue::from(accessor_type);
197    if let Some(object) = accessor.as_object_mut() {
198        object.retain(|(name, _)| {
199            !matches!(
200                name.as_str(),
201                "bufferView" | "byteOffset" | "sparse" | "min" | "max"
202            )
203        });
204    }
205    if let Some((min, max)) = position_bounds {
206        accessor["min"] = crate::JsonValue::Array(
207            min.iter()
208                .map(|value| crate::JsonValue::Number(crate::writer::finite_float_lexeme(*value)))
209                .collect(),
210        );
211        accessor["max"] = crate::JsonValue::Array(
212            max.iter()
213                .map(|value| crate::JsonValue::Number(crate::writer::finite_float_lexeme(*value)))
214                .collect(),
215        );
216    }
217    Ok(())
218}
219
220fn compact_draco_only_resources(
221    document: &mut crate::Document,
222    buffers: &mut Vec<Vec<u8>>,
223    extensions: &crate::ExtensionRegistry,
224    max_output_bytes: Option<usize>,
225) -> Result<()> {
226    let used_accessors = collect_used_accessors(document.as_value(), extensions, document)?;
227    let accessor_map = {
228        let root = document.as_value_mut();
229        let accessor_map = prune_accessors(root, &used_accessors)?;
230        remap_accessor_references(root, &accessor_map)?;
231        accessor_map
232    };
233    let view_identity = (0..document.buffer_views().len())
234        .map(Some)
235        .collect::<Vec<_>>();
236    extensions.remap_binary_references(document, &accessor_map, &view_identity)?;
237
238    let used_views = collect_used_views(document.as_value(), extensions, document)?;
239    let old_views = document.as_value_mut()["bufferViews"]
240        .as_array_mut()
241        .ok_or_else(|| Error::Validation(vec!["bufferViews is not an array".into()]))
242        .map(std::mem::take)?;
243    if old_views.len() != used_views.len() {
244        return Err(Error::Validation(vec![
245            "bufferViews changed while compacting Draco resources".into(),
246        ]));
247    }
248    #[derive(Clone, Copy)]
249    struct Range {
250        buffer: usize,
251        start: usize,
252        end: usize,
253        output_offset: usize,
254    }
255
256    let mut ranges = Vec::new();
257    for (index, view) in old_views.iter().enumerate() {
258        if !used_views[index] {
259            continue;
260        }
261        let buffer_index = view
262            .get("buffer")
263            .and_then(crate::JsonValue::as_u64)
264            .and_then(|value| usize::try_from(value).ok())
265            .ok_or_else(|| Error::Validation(vec!["bufferView buffer is invalid".into()]))?;
266        let buffer = buffers
267            .get(buffer_index)
268            .ok_or_else(|| Error::Validation(vec!["bufferView buffer is invalid".into()]))?;
269        let start = view
270            .get("byteOffset")
271            .and_then(crate::JsonValue::as_u64)
272            .unwrap_or(0);
273        let start = usize::try_from(start)
274            .map_err(|_| Error::ResourceLimit("bufferView offset exceeds this platform".into()))?;
275        let length = view
276            .get("byteLength")
277            .and_then(crate::JsonValue::as_u64)
278            .and_then(|value| usize::try_from(value).ok())
279            .ok_or_else(|| Error::Validation(vec!["bufferView byteLength is invalid".into()]))?;
280        let end = start
281            .checked_add(length)
282            .filter(|end| *end <= buffer.len())
283            .ok_or_else(|| Error::Validation(vec!["bufferView range is invalid".into()]))?;
284        ranges.push((
285            index,
286            Range {
287                buffer: buffer_index,
288                start,
289                end,
290                output_offset: 0,
291            },
292        ));
293    }
294    ranges.sort_unstable_by_key(|(_, range)| (range.buffer, range.start, range.end));
295    let mut blocks = Vec::<Range>::new();
296    let mut block_for_view = vec![usize::MAX; old_views.len()];
297    for (view, range) in ranges {
298        let last_index = blocks.len().checked_sub(1);
299        let coalesces = last_index.is_some_and(|index| {
300            let last = blocks[index];
301            last.buffer == range.buffer && range.start <= last.end
302        });
303        if coalesces {
304            let index = last_index.expect("coalescing has a preceding range");
305            blocks[index].end = blocks[index].end.max(range.end);
306            block_for_view[view] = index;
307        } else {
308            block_for_view[view] = blocks.len();
309            blocks.push(range);
310        }
311    }
312    let mut compacted = Vec::new();
313    for block in &mut blocks {
314        let padding = (4 - compacted.len() % 4) % 4;
315        reserve_output(&mut compacted, padding, max_output_bytes)?;
316        compacted.resize(compacted.len() + padding, 0);
317        block.output_offset = compacted.len();
318        let length = block.end - block.start;
319        reserve_output(&mut compacted, length, max_output_bytes)?;
320        compacted.extend_from_slice(&buffers[block.buffer][block.start..block.end]);
321    }
322    let mut view_map = vec![None; old_views.len()];
323    let mut new_views = Vec::new();
324    for (index, mut view) in old_views.into_iter().enumerate() {
325        if !used_views[index] {
326            continue;
327        }
328        let start = view
329            .get("byteOffset")
330            .and_then(crate::JsonValue::as_u64)
331            .map(usize::try_from)
332            .transpose()
333            .map_err(|_| Error::ResourceLimit("bufferView offset exceeds this platform".into()))?
334            .unwrap_or(0);
335        let block = blocks
336            .get(block_for_view[index])
337            .ok_or_else(|| Error::Validation(vec!["bufferView range was not planned".into()]))?;
338        view["buffer"] = crate::JsonValue::from(0usize);
339        view["byteOffset"] = crate::JsonValue::from(block.output_offset + start - block.start);
340        view_map[index] = Some(new_views.len());
341        new_views.push(view);
342    }
343    {
344        let root = document.as_value_mut();
345        root["bufferViews"] = crate::JsonValue::Array(new_views);
346        remap_buffer_view_references(root, &view_map)?;
347    }
348    let accessor_identity = (0..document.accessors().len())
349        .map(Some)
350        .collect::<Vec<_>>();
351    extensions.remap_binary_references(document, &accessor_identity, &view_map)?;
352    document.as_value_mut()["buffers"] =
353        crate::JsonValue::Array(vec![crate::JsonValue::object([(
354            "byteLength",
355            crate::JsonValue::from(compacted.len()),
356        )])]);
357    *buffers = vec![compacted];
358    Ok(())
359}
360
361fn reserve_output(
362    output: &mut Vec<u8>,
363    additional: usize,
364    max_output_bytes: Option<usize>,
365) -> Result<()> {
366    let total = output
367        .len()
368        .checked_add(additional)
369        .ok_or_else(|| Error::ResourceLimit("compressed output size overflow".into()))?;
370    if max_output_bytes.is_some_and(|limit| total > limit) {
371        return Err(Error::ResourceLimit(format!(
372            "compressed output size {total} exceeds configured limit"
373        )));
374    }
375    output
376        .try_reserve(additional)
377        .map_err(|_| Error::ResourceLimit("unable to reserve compressed output".into()))?;
378    Ok(())
379}
380
381fn collect_used_accessors(
382    root: &crate::JsonValue,
383    extensions: &crate::ExtensionRegistry,
384    document: &crate::Document,
385) -> Result<Vec<bool>> {
386    let mut used = vec![
387        false;
388        root["accessors"]
389            .as_array()
390            .map_or(0, <[crate::JsonValue]>::len)
391    ];
392    visit_core_accessor_refs(root, &mut used)?;
393    let mut buffer_views = vec![
394        false;
395        root["bufferViews"]
396            .as_array()
397            .map_or(0, <[crate::JsonValue]>::len)
398    ];
399    extensions.collect_binary_references(document, &mut used, &mut buffer_views)?;
400    Ok(used)
401}
402
403fn prune_accessors(root: &mut crate::JsonValue, used: &[bool]) -> Result<Vec<Option<usize>>> {
404    let accessors = root["accessors"]
405        .as_array_mut()
406        .ok_or_else(|| Error::Validation(vec!["accessors is not an array".into()]))?;
407    if accessors.len() != used.len() {
408        return Err(Error::Validation(vec![
409            "accessor count changed while compacting".into(),
410        ]));
411    }
412    let old = std::mem::take(accessors);
413    let mut map = vec![None; old.len()];
414    for (index, accessor) in old.into_iter().enumerate() {
415        if used[index] {
416            map[index] = Some(accessors.len());
417            accessors.push(accessor);
418        }
419    }
420    Ok(map)
421}
422
423fn remap_accessor_references(root: &mut crate::JsonValue, map: &[Option<usize>]) -> Result<()> {
424    remap_core_accessor_refs(root, map)
425}
426
427fn collect_used_views(
428    root: &crate::JsonValue,
429    extensions: &crate::ExtensionRegistry,
430    document: &crate::Document,
431) -> Result<Vec<bool>> {
432    let mut used = vec![
433        false;
434        root["bufferViews"]
435            .as_array()
436            .map_or(0, <[crate::JsonValue]>::len)
437    ];
438    visit_core_buffer_view_refs(root, &mut used)?;
439    let mut accessors = vec![
440        false;
441        root["accessors"]
442            .as_array()
443            .map_or(0, <[crate::JsonValue]>::len)
444    ];
445    extensions.collect_binary_references(document, &mut accessors, &mut used)?;
446    Ok(used)
447}
448
449fn remap_buffer_view_references(root: &mut crate::JsonValue, map: &[Option<usize>]) -> Result<()> {
450    remap_core_buffer_view_refs(root, map)
451}
452
453fn visit_core_accessor_refs(root: &crate::JsonValue, used: &mut [bool]) -> Result<()> {
454    for mesh in root
455        .get("meshes")
456        .and_then(crate::JsonValue::as_array)
457        .unwrap_or(&[])
458    {
459        for primitive in mesh
460            .get("primitives")
461            .and_then(crate::JsonValue::as_array)
462            .unwrap_or(&[])
463        {
464            for (_, value) in primitive
465                .get("attributes")
466                .and_then(crate::JsonValue::as_object)
467                .unwrap_or(&[])
468            {
469                mark_used(value, used, "accessor")?;
470            }
471            if let Some(value) = primitive.get("indices") {
472                mark_used(value, used, "accessor")?;
473            }
474            for target in primitive
475                .get("targets")
476                .and_then(crate::JsonValue::as_array)
477                .unwrap_or(&[])
478            {
479                for (_, value) in target.as_object().unwrap_or(&[]) {
480                    mark_used(value, used, "accessor")?;
481                }
482            }
483        }
484    }
485    for skin in root
486        .get("skins")
487        .and_then(crate::JsonValue::as_array)
488        .unwrap_or(&[])
489    {
490        if let Some(value) = skin.get("inverseBindMatrices") {
491            mark_used(value, used, "accessor")?;
492        }
493    }
494    for animation in root
495        .get("animations")
496        .and_then(crate::JsonValue::as_array)
497        .unwrap_or(&[])
498    {
499        for sampler in animation
500            .get("samplers")
501            .and_then(crate::JsonValue::as_array)
502            .unwrap_or(&[])
503        {
504            mark_used(&sampler["input"], used, "accessor")?;
505            mark_used(&sampler["output"], used, "accessor")?;
506        }
507    }
508    Ok(())
509}
510
511fn visit_core_buffer_view_refs(root: &crate::JsonValue, used: &mut [bool]) -> Result<()> {
512    for accessor in root
513        .get("accessors")
514        .and_then(crate::JsonValue::as_array)
515        .unwrap_or(&[])
516    {
517        if let Some(value) = accessor.get("bufferView") {
518            mark_used(value, used, "bufferView")?;
519        }
520        if let Some(sparse) = accessor.get("sparse") {
521            mark_used(&sparse["indices"]["bufferView"], used, "bufferView")?;
522            mark_used(&sparse["values"]["bufferView"], used, "bufferView")?;
523        }
524    }
525    for name in ["images", "files"] {
526        for object in root
527            .get(name)
528            .and_then(crate::JsonValue::as_array)
529            .unwrap_or(&[])
530        {
531            if let Some(value) = object.get("bufferView") {
532                mark_used(value, used, "bufferView")?;
533            }
534        }
535    }
536    Ok(())
537}
538
539fn remap_core_accessor_refs(root: &mut crate::JsonValue, map: &[Option<usize>]) -> Result<()> {
540    if let Some(meshes) = root
541        .get_mut("meshes")
542        .and_then(crate::JsonValue::as_array_mut)
543    {
544        for mesh in meshes {
545            if let Some(primitives) = mesh
546                .get_mut("primitives")
547                .and_then(crate::JsonValue::as_array_mut)
548            {
549                for primitive in primitives {
550                    if let Some(values) = primitive
551                        .get_mut("attributes")
552                        .and_then(crate::JsonValue::as_object_mut)
553                    {
554                        for (_, value) in values {
555                            remap_index(value, map, "accessor")?;
556                        }
557                    }
558                    if let Some(value) = primitive.get_mut("indices") {
559                        remap_index(value, map, "accessor")?;
560                    }
561                    if let Some(targets) = primitive
562                        .get_mut("targets")
563                        .and_then(crate::JsonValue::as_array_mut)
564                    {
565                        for target in targets {
566                            if let Some(values) = target.as_object_mut() {
567                                for (_, value) in values {
568                                    remap_index(value, map, "accessor")?;
569                                }
570                            }
571                        }
572                    }
573                }
574            }
575        }
576    }
577    if let Some(skins) = root
578        .get_mut("skins")
579        .and_then(crate::JsonValue::as_array_mut)
580    {
581        for skin in skins {
582            if let Some(value) = skin.get_mut("inverseBindMatrices") {
583                remap_index(value, map, "accessor")?;
584            }
585        }
586    }
587    if let Some(animations) = root
588        .get_mut("animations")
589        .and_then(crate::JsonValue::as_array_mut)
590    {
591        for animation in animations {
592            if let Some(samplers) = animation
593                .get_mut("samplers")
594                .and_then(crate::JsonValue::as_array_mut)
595            {
596                for sampler in samplers {
597                    remap_index(&mut sampler["input"], map, "accessor")?;
598                    remap_index(&mut sampler["output"], map, "accessor")?;
599                }
600            }
601        }
602    }
603    Ok(())
604}
605
606fn remap_core_buffer_view_refs(root: &mut crate::JsonValue, map: &[Option<usize>]) -> Result<()> {
607    if let Some(accessors) = root
608        .get_mut("accessors")
609        .and_then(crate::JsonValue::as_array_mut)
610    {
611        for accessor in accessors {
612            if let Some(value) = accessor.get_mut("bufferView") {
613                remap_index(value, map, "bufferView")?;
614            }
615            if let Some(sparse) = accessor.get_mut("sparse") {
616                remap_index(&mut sparse["indices"]["bufferView"], map, "bufferView")?;
617                remap_index(&mut sparse["values"]["bufferView"], map, "bufferView")?;
618            }
619        }
620    }
621    for name in ["images", "files"] {
622        if let Some(values) = root.get_mut(name).and_then(crate::JsonValue::as_array_mut) {
623            for value in values {
624                if let Some(view) = value.get_mut("bufferView") {
625                    remap_index(view, map, "bufferView")?;
626                }
627            }
628        }
629    }
630    Ok(())
631}
632
633fn mark_used(value: &crate::JsonValue, used: &mut [bool], kind: &str) -> Result<()> {
634    let index = value
635        .as_u64()
636        .and_then(|value| usize::try_from(value).ok())
637        .filter(|index| *index < used.len())
638        .ok_or_else(|| Error::Validation(vec![format!("{kind} reference is invalid")]))?;
639    used[index] = true;
640    Ok(())
641}
642
643fn remap_index(value: &mut crate::JsonValue, map: &[Option<usize>], kind: &str) -> Result<()> {
644    let old = value
645        .as_u64()
646        .and_then(|value| usize::try_from(value).ok())
647        .ok_or_else(|| Error::Validation(vec![format!("{kind} reference is invalid")]))?;
648    let new = map
649        .get(old)
650        .and_then(|value| *value)
651        .ok_or_else(|| Error::Validation(vec![format!("{kind} reference was removed")]))?;
652    *value = crate::JsonValue::from(new);
653    Ok(())
654}
655
656#[derive(Clone, Copy, Debug)]
657/// Controls document-preserving Draco compression.
658///
659/// [`CompressionMode::DracoOnly`] is the default: it requires the Draco
660/// extension and removes raw geometry owned only by the compressed primitive.
661/// Use [`CompressionMode::Fallback`] when non-Draco readers must retain the
662/// original accessors.
663///
664/// ```
665/// # use draco_gltf::{CompressionMode, CompressionOptions};
666/// let options = CompressionOptions {
667///     mode: CompressionMode::Fallback,
668///     ..CompressionOptions::default()
669/// };
670/// assert_eq!(options.mode, CompressionMode::Fallback);
671/// ```
672pub struct CompressionOptions {
673    /// Draco encoder speed in the range accepted by `draco-core`.
674    pub encoding_speed: u8,
675    /// Draco decoder speed hint in the range accepted by `draco-core`.
676    pub decoding_speed: u8,
677    /// Whether output requires Draco or retains ordinary geometry as fallback.
678    pub mode: CompressionMode,
679    /// Maximum number of resolved binary bytes permitted after compression.
680    /// The limit includes retained fallback data and four-byte padding.
681    pub max_output_bytes: Option<usize>,
682}
683impl Default for CompressionOptions {
684    fn default() -> Self {
685        Self {
686            encoding_speed: 5,
687            decoding_speed: 5,
688            mode: CompressionMode::DracoOnly,
689            max_output_bytes: None,
690        }
691    }
692}
693#[derive(Clone, Debug, Default)]
694/// Measured result of one [`Import::compress_primitive`] operation.
695pub struct CompressionReport {
696    /// Export policy used for the transformed primitive.
697    pub mode: CompressionMode,
698    /// Number of primitives encoded by the operation.
699    pub compressed_primitives: usize,
700    /// Bytes in the newly encoded Draco payload.
701    pub encoded_bytes: usize,
702    /// Total resolved binary bytes before the transform.
703    pub source_bytes: usize,
704    /// Total resolved binary bytes after the transform.
705    pub output_bytes: usize,
706    /// Bytes removed from the resolved binary store; zero for a fallback that
707    /// retains all ordinary geometry.
708    pub reclaimed_bytes: usize,
709}
710
711impl Import {
712    /// Encodes an already decoded mesh to a raw Draco payload.
713    pub(crate) fn encode_draco_geometry(
714        &self,
715        mesh: draco_core::Mesh,
716        options: CompressionOptions,
717    ) -> Result<(Vec<u8>, EncodedMeshInfo)> {
718        let mut encoder = MeshEncoder::new();
719        encoder.set_mesh(mesh);
720        let mut settings = EncoderOptions::new();
721        settings.set_global_int("encoding_speed", options.encoding_speed as i32);
722        settings.set_global_int("decoding_speed", options.decoding_speed as i32);
723        let mut output = EncoderBuffer::new();
724        encoder
725            .encode(&settings, &mut output)
726            .map_err(|error| Error::Extension(error.to_string()))?;
727        let info = encoder
728            .encoded_mesh_info()
729            .cloned()
730            .ok_or_else(|| Error::Extension("Draco encoder did not return mesh info".into()))?;
731        Ok((output.data().to_vec(), info))
732    }
733
734    /// Compresses one ordinary triangle primitive atomically.
735    ///
736    /// The document and resolved resources are updated only after encoding,
737    /// validation, reference remapping, and output-limit checks all succeed.
738    /// In [`CompressionMode::DracoOnly`] the operation rejects unregistered
739    /// extensions whose binary references cannot be remapped safely.
740    pub fn compress_primitive(
741        &mut self,
742        mesh: crate::MeshIndex,
743        primitive: usize,
744        options: CompressionOptions,
745    ) -> Result<CompressionReport> {
746        let mut candidate = self.clone();
747        let report = candidate.compress_primitive_inner(mesh, primitive, options)?;
748        *self = candidate;
749        Ok(report)
750    }
751
752    fn compress_primitive_inner(
753        &mut self,
754        mesh: crate::MeshIndex,
755        primitive: usize,
756        options: CompressionOptions,
757    ) -> Result<CompressionReport> {
758        let source_bytes = self
759            .resources
760            .buffers
761            .iter()
762            .try_fold(0usize, |total, buffer| {
763                total
764                    .checked_add(buffer.len())
765                    .ok_or_else(|| Error::ResourceLimit("total source buffer size overflow".into()))
766            })?;
767        let reference = self
768            .document
769            .primitive(mesh, primitive)
770            .ok_or_else(|| Error::Extension("primitive out of range".into()))?;
771        self.ensure_transform_safe(reference)?;
772        if options.mode == CompressionMode::DracoOnly {
773            self.ensure_document_binary_transform_safe()?;
774        }
775        if reference.mode() != 4 {
776            return Err(Error::Extension(
777                "KHR_draco_mesh_compression encoding currently supports only TRIANGLES (mode 4)"
778                    .into(),
779            ));
780        }
781        let (geometry, mapping) = self.decode_geometry_primitive(reference)?;
782        let (bytes, encoded_info) = self.encode_draco_geometry(geometry, options)?;
783        let layout = DracoGeometryLayout::from_encoded_info(&encoded_info, &mapping)?;
784        let buffer = self.resources.buffers.len();
785        let view;
786        {
787            let root = self.document.as_value_mut();
788            let buffers = root["buffers"]
789                .as_array_mut()
790                .ok_or_else(|| Error::Extension("buffers is not an array".into()))?;
791            buffers.push(crate::JsonValue::object([(
792                "byteLength",
793                crate::JsonValue::from(bytes.len()),
794            )]));
795            let views = root["bufferViews"]
796                .as_array_mut()
797                .ok_or_else(|| Error::Extension("bufferViews is not an array".into()))?;
798            view = views.len();
799            views.push(crate::JsonValue::object([
800                ("buffer", crate::JsonValue::from(buffer)),
801                ("byteLength", crate::JsonValue::from(bytes.len())),
802            ]));
803            let attributes = crate::JsonValue::Object(
804                mapping
805                    .iter()
806                    .map(|(name, id)| (name.clone(), crate::JsonValue::from(*id as u64)))
807                    .collect(),
808            );
809            root["meshes"][mesh.0]["primitives"][primitive]["extensions"]
810                [crate::KHR_DRACO_MESH_COMPRESSION] = crate::JsonValue::object([
811                ("bufferView", crate::JsonValue::from(view)),
812                ("attributes", attributes),
813            ]);
814            add_extension_name(root, "extensionsUsed")?;
815            if options.mode == CompressionMode::DracoOnly {
816                add_extension_name(root, "extensionsRequired")?;
817            }
818            if options.mode == CompressionMode::DracoOnly {
819                detach_draco_only_accessors(root, mesh.0, primitive, &mapping, &layout)?;
820            }
821        }
822        self.resources.buffers.push(bytes.clone());
823        if options.mode == CompressionMode::DracoOnly {
824            compact_draco_only_resources(
825                &mut self.document,
826                &mut self.resources.buffers,
827                &self.extensions,
828                options.max_output_bytes,
829            )?;
830        }
831        let output_bytes = self
832            .resources
833            .buffers
834            .iter()
835            .try_fold(0usize, |total, buffer| {
836                total
837                    .checked_add(buffer.len())
838                    .ok_or_else(|| Error::ResourceLimit("total output buffer size overflow".into()))
839            })?;
840        if let Some(limit) = options.max_output_bytes {
841            if output_bytes > limit {
842                return Err(Error::ResourceLimit(format!(
843                    "compressed output size {output_bytes} exceeds limit {limit}"
844                )));
845            }
846        }
847        Ok(CompressionReport {
848            mode: options.mode,
849            compressed_primitives: 1,
850            encoded_bytes: bytes.len(),
851            source_bytes,
852            output_bytes,
853            reclaimed_bytes: source_bytes.saturating_sub(output_bytes),
854        })
855    }
856}