draco-oxide 0.1.0-alpha.9

draco-oxide is a rust rewrite of Google's draco mesh compression library.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! KHR_draco_mesh_compression extension handling.

use serde_json::{json, Map, Value};
use std::collections::HashMap;

/// Name of the glTF Draco mesh compression extension.
pub const EXTENSION_NAME: &str = "KHR_draco_mesh_compression";

/// Mapping from glTF attribute names (e.g. "POSITION") to attribute ids in
/// the Draco stream, as written into the extension's `attributes` object.
#[derive(Debug, Clone, Default)]
pub struct DracoAttributeIds {
    /// Attribute name to Draco attribute id.
    pub ids: HashMap<String, u32>,
}

impl DracoAttributeIds {
    /// Creates an empty mapping.
    pub fn new() -> Self {
        Self {
            ids: HashMap::new(),
        }
    }

    /// Maps the glTF attribute `name` to the Draco attribute `id`.
    pub fn insert(&mut self, name: &str, id: u32) {
        self.ids.insert(name.to_string(), id);
    }
}

/// Whether the primitive carries the Draco compression extension.
pub fn is_draco_compressed(primitive: &Value) -> bool {
    primitive
        .get("extensions")
        .and_then(|e| e.get(EXTENSION_NAME))
        .is_some()
}

/// Whether the primitive's mode is TRIANGLES (the glTF default).
pub fn is_triangle_primitive(primitive: &Value) -> bool {
    primitive.get("mode").and_then(|m| m.as_u64()).unwrap_or(4) == 4
}

/// The glTF name of a primitive mode value.
pub fn primitive_mode_name(mode: u64) -> &'static str {
    match mode {
        0 => "POINTS",
        1 => "LINES",
        2 => "LINE_LOOP",
        3 => "LINE_STRIP",
        4 => "TRIANGLES",
        5 => "TRIANGLE_STRIP",
        6 => "TRIANGLE_FAN",
        _ => "UNKNOWN",
    }
}

/// Adds the Draco compression extension to a primitive, pointing it at the
/// given buffer view and attribute id mapping, and clears the buffer
/// references of the accessors the primitive uses.
pub fn add_draco_extension(
    json: &mut Value,
    mesh_idx: usize,
    primitive_idx: usize,
    draco_buffer_view_idx: usize,
    attribute_ids: &DracoAttributeIds,
    indices_accessor_idx: Option<u64>,
) {
    let mut extension = Map::new();
    extension.insert("bufferView".to_string(), json!(draco_buffer_view_idx));

    let mut attributes = Map::new();
    for (name, id) in &attribute_ids.ids {
        attributes.insert(name.clone(), json!(id));
    }
    extension.insert("attributes".to_string(), Value::Object(attributes));

    let primitive = &mut json["meshes"][mesh_idx]["primitives"][primitive_idx];

    if primitive.get("extensions").is_none() {
        primitive["extensions"] = json!({});
    }
    primitive["extensions"][EXTENSION_NAME] = Value::Object(extension);

    if let Some(attrs) = primitive.get("attributes").cloned() {
        if let Some(attrs_obj) = attrs.as_object() {
            for (_, accessor_idx) in attrs_obj {
                if let Some(idx) = accessor_idx.as_u64() {
                    clear_accessor_buffer_refs(json, idx as usize);
                }
            }
        }
    }

    if let Some(idx) = indices_accessor_idx {
        clear_accessor_buffer_refs(json, idx as usize);
    }
}

fn clear_accessor_buffer_refs(json: &mut Value, accessor_idx: usize) {
    if let Some(accessor) = json
        .get_mut("accessors")
        .and_then(|a| a.get_mut(accessor_idx))
        .and_then(|a| a.as_object_mut())
    {
        accessor.remove("bufferView");
        accessor.remove("byteOffset");
    }
}

/// Adds the Draco extension to the glTF `extensionsUsed` and
/// `extensionsRequired` arrays if it is not already listed.
pub fn ensure_extension_declared(json: &mut Value) {
    if json.get("extensionsUsed").is_none() {
        json["extensionsUsed"] = json!([]);
    }
    if let Some(arr) = json["extensionsUsed"].as_array_mut() {
        if !arr.iter().any(|v| v.as_str() == Some(EXTENSION_NAME)) {
            arr.push(json!(EXTENSION_NAME));
        }
    }

    if json.get("extensionsRequired").is_none() {
        json["extensionsRequired"] = json!([]);
    }
    if let Some(arr) = json["extensionsRequired"].as_array_mut() {
        if !arr.iter().any(|v| v.as_str() == Some(EXTENSION_NAME)) {
            arr.push(json!(EXTENSION_NAME));
        }
    }
}

/// Appends a buffer view to the glTF JSON and returns its index.
pub fn add_buffer_view(
    json: &mut Value,
    buffer_idx: usize,
    byte_offset: usize,
    byte_length: usize,
) -> usize {
    if json.get("bufferViews").is_none() {
        json["bufferViews"] = json!([]);
    }

    let buffer_view =
        json!({ "buffer": buffer_idx, "byteOffset": byte_offset, "byteLength": byte_length });
    let arr = json["bufferViews"].as_array_mut().unwrap();
    let idx = arr.len();
    arr.push(buffer_view);
    idx
}

/// Sets the byte length of the buffer at `buffer_idx`.
pub fn update_buffer_length(json: &mut Value, buffer_idx: usize, byte_length: usize) {
    if let Some(buffer) = json
        .get_mut("buffers")
        .and_then(|b| b.get_mut(buffer_idx))
        .and_then(|b| b.as_object_mut())
    {
        buffer.insert("byteLength".to_string(), json!(byte_length));
    }
}

/// Sets or removes the URI of the buffer at `buffer_idx`.
pub fn set_buffer_uri(json: &mut Value, buffer_idx: usize, uri: Option<&str>) {
    if let Some(buffer) = json
        .get_mut("buffers")
        .and_then(|b| b.get_mut(buffer_idx))
        .and_then(|b| b.as_object_mut())
    {
        match uri {
            Some(u) => {
                buffer.insert("uri".to_string(), json!(u));
            }
            None => {
                buffer.remove("uri");
            }
        }
    }
}

/// Sets the byte offset of the buffer view at `buffer_view_idx`.
pub fn update_buffer_view_offset(json: &mut Value, buffer_view_idx: usize, new_offset: usize) {
    if let Some(bv) = json
        .get_mut("bufferViews")
        .and_then(|b| b.get_mut(buffer_view_idx))
        .and_then(|b| b.as_object_mut())
    {
        bv.insert("byteOffset".to_string(), json!(new_offset));
    }
}

/// Clear bufferView and byteOffset for all accessors that reference any of the given bufferViews.
/// This handles orphan accessors that aren't used by any primitive but still reference geometry bufferViews.
pub fn clear_accessors_referencing_views(
    json: &mut Value,
    views_to_clear: &std::collections::HashSet<usize>,
) {
    if let Some(accessors) = json.get_mut("accessors").and_then(|a| a.as_array_mut()) {
        for accessor in accessors.iter_mut() {
            if let Some(bv_idx) = accessor.get("bufferView").and_then(|v| v.as_u64()) {
                if views_to_clear.contains(&(bv_idx as usize)) {
                    if let Some(obj) = accessor.as_object_mut() {
                        obj.remove("bufferView");
                        obj.remove("byteOffset");
                    }
                }
            }
        }
    }
}

/// Remove geometry bufferViews and remap all bufferView references.
/// Returns a mapping from old indices to new indices.
pub fn remove_buffer_views(
    json: &mut Value,
    views_to_remove: &std::collections::HashSet<usize>,
) -> std::collections::HashMap<usize, usize> {
    let mut old_to_new: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();

    if let Some(buffer_views) = json.get_mut("bufferViews").and_then(|b| b.as_array_mut()) {
        // Build the new array and mapping
        let mut new_views = Vec::new();
        for (old_idx, bv) in buffer_views.iter().enumerate() {
            if !views_to_remove.contains(&old_idx) {
                old_to_new.insert(old_idx, new_views.len());
                new_views.push(bv.clone());
            }
        }
        *buffer_views = new_views;
    }

    // Remap bufferView references in images
    if let Some(images) = json.get_mut("images").and_then(|i| i.as_array_mut()) {
        for image in images.iter_mut() {
            if let Some(bv_idx) = image.get("bufferView").and_then(|v| v.as_u64()) {
                if let Some(&new_idx) = old_to_new.get(&(bv_idx as usize)) {
                    image
                        .as_object_mut()
                        .unwrap()
                        .insert("bufferView".to_string(), json!(new_idx));
                }
            }
        }
    }

    // Remap bufferView references in EXT_structural_metadata property tables
    if let Some(ext) = json
        .get_mut("extensions")
        .and_then(|e| e.get_mut("EXT_structural_metadata"))
    {
        if let Some(tables) = ext.get_mut("propertyTables").and_then(|t| t.as_array_mut()) {
            for table in tables.iter_mut() {
                if let Some(properties) =
                    table.get_mut("properties").and_then(|p| p.as_object_mut())
                {
                    for (_, prop) in properties.iter_mut() {
                        // Remap "values" bufferView reference
                        if let Some(bv_idx) = prop.get("values").and_then(|v| v.as_u64()) {
                            if let Some(&new_idx) = old_to_new.get(&(bv_idx as usize)) {
                                prop.as_object_mut()
                                    .unwrap()
                                    .insert("values".to_string(), json!(new_idx));
                            }
                        }
                        // Remap "stringOffsets" bufferView reference
                        if let Some(bv_idx) = prop.get("stringOffsets").and_then(|v| v.as_u64()) {
                            if let Some(&new_idx) = old_to_new.get(&(bv_idx as usize)) {
                                prop.as_object_mut()
                                    .unwrap()
                                    .insert("stringOffsets".to_string(), json!(new_idx));
                            }
                        }
                        // Remap "arrayOffsets" bufferView reference (for variable-length arrays)
                        if let Some(bv_idx) = prop.get("arrayOffsets").and_then(|v| v.as_u64()) {
                            if let Some(&new_idx) = old_to_new.get(&(bv_idx as usize)) {
                                prop.as_object_mut()
                                    .unwrap()
                                    .insert("arrayOffsets".to_string(), json!(new_idx));
                            }
                        }
                    }
                }
            }
        }
    }

    old_to_new
}

/// glTF componentType value for unsigned 16-bit integers.
pub const COMPONENT_TYPE_UNSIGNED_SHORT: u64 = 5123;
/// glTF componentType value for 32-bit floats.
pub const COMPONENT_TYPE_FLOAT: u64 = 5126;

/// Update an accessor's componentType, e.g. for feature ID attributes whose
/// encoded width differs from the source accessor's declaration.
pub fn update_accessor_component_type(json: &mut Value, accessor_idx: u64, component_type: u64) {
    if let Some(accessor) = json
        .get_mut("accessors")
        .and_then(|a| a.get_mut(accessor_idx as usize))
        .and_then(|a| a.as_object_mut())
    {
        accessor.insert("componentType".to_string(), json!(component_type));
    }
}

/// Duplicate an accessor with a new count.
/// Returns the index of the new accessor.
pub fn duplicate_accessor(json: &mut Value, original_idx: usize, new_count: usize) -> usize {
    let accessors = json
        .get_mut("accessors")
        .and_then(|a| a.as_array_mut())
        .expect("accessors array should exist");

    // Clone the original accessor
    let mut new_accessor = accessors[original_idx].clone();

    // Update the count
    if let Some(obj) = new_accessor.as_object_mut() {
        obj.insert("count".to_string(), json!(new_count));
        // Also clear bufferView and byteOffset since this will be Draco-compressed
        obj.remove("bufferView");
        obj.remove("byteOffset");
    }

    // Add the new accessor and return its index
    let new_idx = accessors.len();
    accessors.push(new_accessor);
    new_idx
}

/// Update accessor count.
pub fn update_accessor_count(json: &mut Value, accessor_idx: usize, new_count: usize) {
    if let Some(accessor) = json
        .get_mut("accessors")
        .and_then(|a| a.get_mut(accessor_idx))
        .and_then(|a| a.as_object_mut())
    {
        accessor.insert("count".to_string(), json!(new_count));
    }
}

/// Update a primitive's attribute accessor index.
pub fn update_primitive_attribute(
    json: &mut Value,
    mesh_idx: usize,
    prim_idx: usize,
    attr_name: &str,
    new_accessor_idx: usize,
) {
    if let Some(primitive) = json
        .get_mut("meshes")
        .and_then(|m| m.get_mut(mesh_idx))
        .and_then(|m| m.get_mut("primitives"))
        .and_then(|p| p.get_mut(prim_idx))
    {
        if let Some(attrs) = primitive
            .get_mut("attributes")
            .and_then(|a| a.as_object_mut())
        {
            attrs.insert(attr_name.to_string(), json!(new_accessor_idx));
        }
    }
}

/// Update a primitive's indices accessor index.
pub fn update_primitive_indices(
    json: &mut Value,
    mesh_idx: usize,
    prim_idx: usize,
    new_accessor_idx: usize,
) {
    if let Some(primitive) = json
        .get_mut("meshes")
        .and_then(|m| m.get_mut(mesh_idx))
        .and_then(|m| m.get_mut("primitives"))
        .and_then(|p| p.get_mut(prim_idx))
        .and_then(|p| p.as_object_mut())
    {
        primitive.insert("indices".to_string(), json!(new_accessor_idx));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_draco_compressed() {
        let compressed =
            json!({ "extensions": { "KHR_draco_mesh_compression": { "bufferView": 0 } } });
        let uncompressed = json!({ "attributes": { "POSITION": 0 } });
        assert!(is_draco_compressed(&compressed));
        assert!(!is_draco_compressed(&uncompressed));
    }

    #[test]
    fn test_is_triangle_primitive() {
        assert!(is_triangle_primitive(&json!({})));
        assert!(is_triangle_primitive(&json!({"mode": 4})));
        assert!(!is_triangle_primitive(&json!({"mode": 0})));
    }
}