1use crate::encode::Config as DracoConfig;
7use draco_oxide_core::attribute::{AttributeDomain, AttributeType};
8use draco_oxide_core::mesh::builder::MeshBuilder;
9use draco_oxide_core::types::ConfigType;
10use draco_oxide_core::types::NdVector;
11use serde_json::Value;
12use std::collections::{HashMap, HashSet};
13use std::path::Path;
14
15use super::buffer_builder::BufferBuilder;
16use super::draco_extension::{self, DracoAttributeIds};
17use super::geometry_extractor::{
18 self, read_accessor_as_scalar_f32, read_accessor_as_u32, read_accessor_as_vec2,
19 read_accessor_as_vec3, read_accessor_as_vec4,
20};
21use super::glb;
22
23#[derive(Debug, thiserror::Error)]
24pub enum Error {
25 #[error("GLB parse error: {0}")]
26 GlbParse(#[from] glb::Error),
27 #[error("JSON parse error: {0}")]
28 JsonParse(#[from] serde_json::Error),
29 #[error("Geometry extraction error: {0}")]
30 GeometryExtraction(#[from] geometry_extractor::Error),
31 #[error("Mesh build error: {0}")]
32 MeshBuild(#[from] draco_oxide_core::mesh::builder::Err),
33 #[error("Draco encode error: {0}")]
34 DracoEncode(#[from] crate::encode::Err),
35 #[error("IO error: {0}")]
36 Io(#[from] std::io::Error),
37 #[error("Unsupported: {0}")]
38 Unsupported(String),
39 #[error("Invalid input: {0}")]
40 InvalidInput(String),
41}
42
43#[derive(Debug, Clone)]
45pub struct TranscoderConfig {
46 pub draco: DracoConfig,
48}
49
50impl Default for TranscoderConfig {
51 fn default() -> Self {
52 Self {
53 draco: DracoConfig::default(),
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub enum OutputFormat {
61 Glb,
63 Gltf { bin_filename: String },
65}
66
67#[derive(Debug)]
69pub struct TranscodeResult {
70 pub json: Vec<u8>,
72 pub buffer: Vec<u8>,
74 pub warnings: Vec<String>,
76}
77
78pub struct GltfTranscoder {
82 config: TranscoderConfig,
83}
84
85impl Default for GltfTranscoder {
86 fn default() -> Self {
87 Self::new(TranscoderConfig::default())
88 }
89}
90
91impl GltfTranscoder {
92 pub fn new(config: TranscoderConfig) -> Self {
94 Self { config }
95 }
96
97 pub fn transcode_to_glb(&self, input: &[u8]) -> Result<(Vec<u8>, Vec<String>), Error> {
99 let result = self.transcode(input, &OutputFormat::Glb)?;
100
101 let mut output = Vec::new();
102 glb::write_glb(&mut output, &result.json, &result.buffer)?;
103
104 Ok((output, result.warnings))
105 }
106
107 pub fn transcode_to_file(
111 &self,
112 input: &[u8],
113 output_path: &Path,
114 ) -> Result<Vec<String>, Error> {
115 let extension = output_path
116 .extension()
117 .and_then(|e| e.to_str())
118 .map(|s| s.to_lowercase())
119 .unwrap_or_default();
120
121 let format = match extension.as_str() {
122 "glb" => OutputFormat::Glb,
123 "gltf" => {
124 let bin_name = output_path
125 .file_stem()
126 .and_then(|s| s.to_str())
127 .map(|s| format!("{}.bin", s))
128 .unwrap_or_else(|| "buffer.bin".to_string());
129 OutputFormat::Gltf {
130 bin_filename: bin_name,
131 }
132 }
133 _ => {
134 return Err(Error::InvalidInput(format!(
135 "Unknown output extension: {}",
136 extension
137 )))
138 }
139 };
140
141 let result = self.transcode(input, &format)?;
142
143 match &format {
144 OutputFormat::Glb => {
145 let mut file = std::fs::File::create(output_path)?;
146 glb::write_glb(&mut file, &result.json, &result.buffer)?;
147 }
148 OutputFormat::Gltf { bin_filename } => {
149 std::fs::write(output_path, &result.json)?;
151
152 if !result.buffer.is_empty() {
154 let bin_path = output_path
155 .parent()
156 .unwrap_or(Path::new("."))
157 .join(bin_filename);
158 std::fs::write(bin_path, &result.buffer)?;
159 }
160 }
161 }
162
163 Ok(result.warnings)
164 }
165
166 pub fn transcode(&self, input: &[u8], format: &OutputFormat) -> Result<TranscodeResult, Error> {
168 let glb_data = glb::parse_glb(input)?;
170 let mut json: Value = serde_json::from_slice(&glb_data.json)?;
171 let original_buffer = &glb_data.buffer;
172
173 let mut warnings = Vec::new();
174
175 if let Some(buffers) = json.get("buffers").and_then(|b| b.as_array()) {
177 for (i, buffer) in buffers.iter().enumerate() {
178 if buffer.get("uri").is_some() && i == 0 {
179 }
181 if i > 0 {
182 return Err(Error::Unsupported("Multiple buffers not supported".into()));
183 }
184 }
185 }
186
187 let (geometry_views, _non_geometry_views) = categorize_buffer_views(&json);
189
190 let mut new_buffer = BufferBuilder::new();
192 let mut compressed_data: Vec<CompressedPrimitive> = Vec::new();
193
194 if let Some(meshes) = json.get("meshes").and_then(|m| m.as_array()).cloned() {
195 for (mesh_idx, mesh) in meshes.iter().enumerate() {
196 if let Some(primitives) = mesh.get("primitives").and_then(|p| p.as_array()) {
197 for (prim_idx, primitive) in primitives.iter().enumerate() {
198 match self.process_primitive(
199 &json,
200 original_buffer,
201 primitive,
202 &mut new_buffer,
203 ) {
204 Ok(Some(compressed)) => {
205 compressed_data.push(CompressedPrimitive {
206 mesh_idx,
207 prim_idx,
208 buffer_view_offset: compressed.buffer_view_offset,
209 buffer_view_length: compressed.buffer_view_length,
210 attribute_ids: compressed.attribute_ids,
211 indices_accessor_idx: compressed.indices_accessor_idx,
212 feature_id_accessor_indices: compressed
213 .feature_id_accessor_indices,
214 original_accessor_indices: compressed.original_accessor_indices,
215 vertex_count: compressed.vertex_count,
216 indices_count: compressed.indices_count,
217 });
218 }
219 Ok(None) => {
220 }
222 Err(SkipReason::AlreadyCompressed) => {
223 warnings.push(format!(
224 "Mesh {} primitive {}: already Draco-compressed, skipping",
225 mesh_idx, prim_idx
226 ));
227 }
228 Err(SkipReason::NonTriangle(mode)) => {
229 warnings.push(format!(
230 "Mesh {} primitive {}: non-triangle mode ({}), skipping",
231 mesh_idx,
232 prim_idx,
233 draco_extension::primitive_mode_name(mode)
234 ));
235 }
236 Err(SkipReason::Error(e)) => {
237 return Err(e);
238 }
239 }
240 }
241 }
242 }
243 }
244
245 let mut accessor_remappings: HashMap<(usize, usize), HashMap<u64, usize>> = HashMap::new();
249
250 let mut accessor_usage: HashMap<u64, Vec<(usize, usize, usize)>> = HashMap::new();
253 for compressed in &compressed_data {
254 for &accessor_idx in compressed.original_accessor_indices.values() {
255 accessor_usage.entry(accessor_idx).or_default().push((
256 compressed.mesh_idx,
257 compressed.prim_idx,
258 compressed.vertex_count,
259 ));
260 }
261 if let Some(idx) = compressed.indices_accessor_idx {
263 accessor_usage.entry(idx).or_default().push((
264 compressed.mesh_idx,
265 compressed.prim_idx,
266 compressed.indices_count,
267 ));
268 }
269 }
270
271 for (accessor_idx, users) in &accessor_usage {
273 if users.len() > 1 {
274 for (i, &(mesh_idx, prim_idx, vertex_count)) in users.iter().enumerate() {
276 let remapping = accessor_remappings.entry((mesh_idx, prim_idx)).or_default();
277
278 if i == 0 {
279 if vertex_count > 0 {
281 draco_extension::update_accessor_count(
282 &mut json,
283 *accessor_idx as usize,
284 vertex_count,
285 );
286 }
287 remapping.insert(*accessor_idx, *accessor_idx as usize);
288 } else {
289 let new_idx = draco_extension::duplicate_accessor(
291 &mut json,
292 *accessor_idx as usize,
293 vertex_count,
294 );
295 remapping.insert(*accessor_idx, new_idx);
296 }
297 }
298 } else if let Some(&(mesh_idx, prim_idx, vertex_count)) = users.first() {
299 if vertex_count > 0 {
301 draco_extension::update_accessor_count(
302 &mut json,
303 *accessor_idx as usize,
304 vertex_count,
305 );
306 }
307 let remapping = accessor_remappings.entry((mesh_idx, prim_idx)).or_default();
308 remapping.insert(*accessor_idx, *accessor_idx as usize);
309 }
310 }
311
312 for compressed in &compressed_data {
314 if let Some(remapping) =
315 accessor_remappings.get(&(compressed.mesh_idx, compressed.prim_idx))
316 {
317 for (attr_name, &original_idx) in &compressed.original_accessor_indices {
318 if let Some(&new_idx) = remapping.get(&original_idx) {
319 if new_idx != original_idx as usize {
320 draco_extension::update_primitive_attribute(
321 &mut json,
322 compressed.mesh_idx,
323 compressed.prim_idx,
324 attr_name,
325 new_idx,
326 );
327 }
328 }
329 }
330 if let Some(original_indices_idx) = compressed.indices_accessor_idx {
332 if let Some(&new_idx) = remapping.get(&original_indices_idx) {
333 if new_idx != original_indices_idx as usize {
334 draco_extension::update_primitive_indices(
335 &mut json,
336 compressed.mesh_idx,
337 compressed.prim_idx,
338 new_idx,
339 );
340 }
341 }
342 }
343 }
344 }
345
346 let mut view_offset_map: HashMap<usize, usize> = HashMap::new();
348
349 let views_needing_8byte_align = get_8byte_aligned_buffer_views(&json);
351
352 if let Some(buffer_views) = json.get("bufferViews").and_then(|b| b.as_array()) {
353 for (old_idx, bv) in buffer_views.iter().enumerate() {
354 if !geometry_views.contains(&old_idx) {
355 let byte_offset =
357 bv.get("byteOffset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
358 let byte_length =
359 bv.get("byteLength").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
360
361 if byte_offset + byte_length <= original_buffer.len() {
362 let data = &original_buffer[byte_offset..byte_offset + byte_length];
363 let alignment = if views_needing_8byte_align.contains(&old_idx) {
365 8
366 } else {
367 4
368 };
369 let (new_offset, _) = new_buffer.append(data, alignment);
370 view_offset_map.insert(old_idx, new_offset);
371 }
372 }
373 }
374 }
375
376 for (old_idx, new_offset) in &view_offset_map {
380 draco_extension::update_buffer_view_offset(&mut json, *old_idx, *new_offset);
381 }
382
383 draco_extension::clear_accessors_referencing_views(&mut json, &geometry_views);
386
387 let _old_to_new = draco_extension::remove_buffer_views(&mut json, &geometry_views);
389
390 for compressed in &compressed_data {
392 let new_bv_idx = draco_extension::add_buffer_view(
393 &mut json,
394 0, compressed.buffer_view_offset,
396 compressed.buffer_view_length,
397 );
398
399 let remapping = accessor_remappings.get(&(compressed.mesh_idx, compressed.prim_idx));
401 let indices_accessor_idx = compressed.indices_accessor_idx.map(|orig| {
402 remapping
403 .and_then(|r| r.get(&orig))
404 .copied()
405 .map(|idx| idx as u64)
406 .unwrap_or(orig)
407 });
408
409 draco_extension::add_draco_extension(
410 &mut json,
411 compressed.mesh_idx,
412 compressed.prim_idx,
413 new_bv_idx,
414 &compressed.attribute_ids,
415 indices_accessor_idx,
416 );
417
418 let remapping = accessor_remappings.get(&(compressed.mesh_idx, compressed.prim_idx));
422 for &original_accessor_idx in &compressed.feature_id_accessor_indices {
423 let actual_idx = remapping
424 .and_then(|r| r.get(&original_accessor_idx))
425 .copied()
426 .unwrap_or(original_accessor_idx as usize);
427 draco_extension::update_accessor_component_type(
428 &mut json,
429 actual_idx as u64,
430 draco_extension::COMPONENT_TYPE_UNSIGNED_SHORT,
431 );
432 }
433 }
434
435 let mut final_buffer = new_buffer.finish();
437 let padding = (8 - (final_buffer.len() % 8)) % 8;
438 final_buffer.extend(std::iter::repeat_n(0u8, padding));
439 draco_extension::update_buffer_length(&mut json, 0, final_buffer.len());
440
441 if !compressed_data.is_empty() {
443 draco_extension::ensure_extension_declared(&mut json);
444 }
445
446 match format {
448 OutputFormat::Glb => {
449 draco_extension::set_buffer_uri(&mut json, 0, None);
450 }
451 OutputFormat::Gltf { bin_filename } => {
452 draco_extension::set_buffer_uri(&mut json, 0, Some(bin_filename));
453 }
454 }
455
456 let json_bytes = serde_json::to_vec(&json)?;
458
459 Ok(TranscodeResult {
460 json: json_bytes,
461 buffer: final_buffer,
462 warnings,
463 })
464 }
465
466 fn process_primitive(
468 &self,
469 json: &Value,
470 buffer: &[u8],
471 primitive: &Value,
472 output_buffer: &mut BufferBuilder,
473 ) -> Result<Option<CompressedPrimitiveData>, SkipReason> {
474 if draco_extension::is_draco_compressed(primitive) {
476 return Err(SkipReason::AlreadyCompressed);
477 }
478
479 let mode = primitive.get("mode").and_then(|m| m.as_u64()).unwrap_or(4);
481 if mode != 4 {
482 return Err(SkipReason::NonTriangle(mode));
483 }
484
485 let mut geometry = self.extract_geometry(json, buffer, primitive)?;
487
488 let vertex_count = geometry.positions.len();
490 let indices_count = geometry.indices.len();
491
492 let mesh = self.build_mesh(&mut geometry)?;
494
495 let mut compressed = Vec::new();
497 crate::encode::encode(mesh, &mut compressed, self.config.draco.clone())
498 .map_err(|e| SkipReason::Error(Error::DracoEncode(e)))?;
499
500 let (offset, length) = output_buffer.append(&compressed, 4);
502
503 Ok(Some(CompressedPrimitiveData {
504 buffer_view_offset: offset,
505 buffer_view_length: length,
506 attribute_ids: geometry.draco_attribute_ids,
507 indices_accessor_idx: geometry.indices_accessor_idx,
508 feature_id_accessor_indices: geometry
509 .feature_id_accessor_indices
510 .into_iter()
511 .map(|(_, idx)| idx)
512 .collect(),
513 original_accessor_indices: geometry.original_accessor_indices,
514 vertex_count,
515 indices_count,
516 }))
517 }
518
519 fn extract_geometry(
521 &self,
522 json: &Value,
523 buffer: &[u8],
524 primitive: &Value,
525 ) -> Result<ExtractedGeometry, SkipReason> {
526 let mut geometry = ExtractedGeometry::default();
527
528 if let Some(idx) = primitive.get("indices").and_then(|i| i.as_u64()) {
530 geometry.indices = read_accessor_as_u32(json, buffer, idx)
531 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?;
532 geometry.indices_accessor_idx = Some(idx);
533 }
534
535 if let Some(attrs) = primitive.get("attributes").and_then(|a| a.as_object()) {
537 for (name, accessor_idx) in attrs {
538 let idx = accessor_idx.as_u64().ok_or_else(|| {
539 SkipReason::Error(Error::InvalidInput(format!(
540 "Invalid accessor index for {}",
541 name
542 )))
543 })?;
544
545 geometry
547 .original_accessor_indices
548 .insert(name.to_string(), idx);
549
550 match name.as_str() {
551 "POSITION" => {
552 geometry.positions = read_accessor_as_vec3(json, buffer, idx)
553 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?;
554 }
555 "NORMAL" => {
556 geometry.normals = Some(
557 read_accessor_as_vec3(json, buffer, idx)
558 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?,
559 );
560 }
561 name if name.starts_with("TEXCOORD_") => {
562 let texcoords = read_accessor_as_vec2(json, buffer, idx)
563 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?;
564 geometry.texcoords.push((name.to_string(), texcoords));
565 }
566 name if name.starts_with("COLOR_") => {
567 let colors = read_accessor_as_vec4(json, buffer, idx)
569 .or_else(|_| {
570 read_accessor_as_vec3(json, buffer, idx).map(|v| {
571 v.into_iter().map(|c| [c[0], c[1], c[2], 1.0]).collect()
572 })
573 })
574 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?;
575 geometry.colors.push((name.to_string(), colors));
576 }
577 "TANGENT" => {
578 geometry.tangents = Some(
579 read_accessor_as_vec4(json, buffer, idx)
580 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?,
581 );
582 }
583 name if name.starts_with("_FEATURE_ID_") => {
584 let feature_ids = read_accessor_as_scalar_f32(json, buffer, idx)
585 .map_err(|e| SkipReason::Error(Error::GeometryExtraction(e)))?;
586 geometry.feature_ids.push((name.to_string(), feature_ids));
587 geometry
589 .feature_id_accessor_indices
590 .push((name.to_string(), idx));
591 }
592 _ => {
593 }
596 }
597 }
598 }
599
600 if geometry.positions.is_empty() {
601 return Err(SkipReason::Error(Error::InvalidInput(
602 "Primitive has no POSITION attribute".into(),
603 )));
604 }
605
606 Ok(geometry)
607 }
608
609 fn build_mesh(
612 &self,
613 geometry: &mut ExtractedGeometry,
614 ) -> Result<draco_oxide_core::mesh::Mesh, SkipReason> {
615 let mut builder = MeshBuilder::new();
616
617 let faces: Vec<[usize; 3]> = if geometry.indices.is_empty() {
619 (0..geometry.positions.len() / 3)
621 .map(|i| [i * 3, i * 3 + 1, i * 3 + 2])
622 .collect()
623 } else {
624 geometry
625 .indices
626 .chunks(3)
627 .map(|c| [c[0] as usize, c[1] as usize, c[2] as usize])
628 .collect()
629 };
630 builder.set_connectivity_attribute(faces);
631
632 geometry.draco_attribute_ids = DracoAttributeIds::new();
634 let mut draco_id = 0u32;
635
636 let positions: Vec<NdVector<3, f32>> = geometry
638 .positions
639 .iter()
640 .map(|p| NdVector::from(*p))
641 .collect();
642 let pos_id = builder.add_attribute(
643 positions,
644 AttributeType::Position,
645 AttributeDomain::Position,
646 vec![],
647 );
648 geometry.draco_attribute_ids.insert("POSITION", draco_id);
649 draco_id += 1;
650
651 if let Some(normals) = geometry.normals.as_ref() {
653 let normals: Vec<NdVector<3, f32>> =
654 normals.iter().map(|n| NdVector::from(*n)).collect();
655 builder.add_attribute(
656 normals,
657 AttributeType::Normal,
658 AttributeDomain::Corner,
659 vec![pos_id],
660 );
661 geometry.draco_attribute_ids.insert("NORMAL", draco_id);
662 draco_id += 1;
663 }
664
665 for (name, texcoords) in &geometry.texcoords {
667 let texcoords: Vec<NdVector<2, f32>> =
668 texcoords.iter().map(|t| NdVector::from(*t)).collect();
669 builder.add_attribute(
670 texcoords,
671 AttributeType::TextureCoordinate,
672 AttributeDomain::Corner,
673 vec![pos_id],
674 );
675 geometry.draco_attribute_ids.insert(name, draco_id);
676 draco_id += 1;
677 }
678
679 for (name, colors) in &geometry.colors {
681 let colors: Vec<NdVector<4, f32>> = colors.iter().map(|c| NdVector::from(*c)).collect();
682 builder.add_attribute(
683 colors,
684 AttributeType::Color,
685 AttributeDomain::Corner,
686 vec![pos_id],
687 );
688 geometry.draco_attribute_ids.insert(name, draco_id);
689 draco_id += 1;
690 }
691
692 if let Some(tangents) = geometry.tangents.as_ref() {
694 let tangents: Vec<NdVector<4, f32>> =
695 tangents.iter().map(|t| NdVector::from(*t)).collect();
696 builder.add_attribute(
697 tangents,
698 AttributeType::Tangent,
699 AttributeDomain::Corner,
700 vec![pos_id],
701 );
702 geometry.draco_attribute_ids.insert("TANGENT", draco_id);
703 draco_id += 1;
704 }
705
706 for (name, feature_ids) in &geometry.feature_ids {
710 let feature_ids: Vec<NdVector<1, u16>> = feature_ids
711 .iter()
712 .map(|&id| NdVector::from([id as u16]))
713 .collect();
714 builder.add_attribute(
715 feature_ids,
716 AttributeType::Custom,
717 AttributeDomain::Corner,
718 vec![pos_id],
719 );
720 geometry.draco_attribute_ids.insert(name, draco_id);
721 draco_id += 1;
722 }
723
724 let _ = draco_id;
726
727 builder
728 .build()
729 .map_err(|e| SkipReason::Error(Error::MeshBuild(e)))
730 }
731}
732
733fn categorize_buffer_views(json: &Value) -> (HashSet<usize>, HashSet<usize>) {
735 let mut geometry_views = HashSet::new();
736
737 if let Some(meshes) = json.get("meshes").and_then(|m| m.as_array()) {
739 for mesh in meshes {
740 if let Some(primitives) = mesh.get("primitives").and_then(|p| p.as_array()) {
741 for primitive in primitives {
742 if draco_extension::is_draco_compressed(primitive) {
744 continue;
745 }
746
747 if let Some(idx) = primitive.get("indices").and_then(|i| i.as_u64()) {
749 if let Some(bv) = get_accessor_buffer_view(json, idx as usize) {
750 geometry_views.insert(bv);
751 }
752 }
753
754 if let Some(attrs) = primitive.get("attributes").and_then(|a| a.as_object()) {
756 for (_, accessor_idx) in attrs {
757 if let Some(idx) = accessor_idx.as_u64() {
758 if let Some(bv) = get_accessor_buffer_view(json, idx as usize) {
759 geometry_views.insert(bv);
760 }
761 }
762 }
763 }
764 }
765 }
766 }
767 }
768
769 let num_views = json
771 .get("bufferViews")
772 .and_then(|b| b.as_array())
773 .map(|a| a.len())
774 .unwrap_or(0);
775
776 let non_geometry_views: HashSet<usize> = (0..num_views)
777 .filter(|i| !geometry_views.contains(i))
778 .collect();
779
780 (geometry_views, non_geometry_views)
781}
782
783fn get_accessor_buffer_view(json: &Value, accessor_idx: usize) -> Option<usize> {
785 json.get("accessors")
786 .and_then(|a| a.get(accessor_idx))
787 .and_then(|a| a.get("bufferView"))
788 .and_then(|v| v.as_u64())
789 .map(|v| v as usize)
790}
791
792fn get_8byte_aligned_buffer_views(json: &Value) -> HashSet<usize> {
796 let mut result = HashSet::new();
797
798 let ext = match json
799 .get("extensions")
800 .and_then(|e| e.get("EXT_structural_metadata"))
801 {
802 Some(ext) => ext,
803 None => return result,
804 };
805
806 let schema_classes = ext
808 .get("schema")
809 .and_then(|s| s.get("classes"))
810 .and_then(|c| c.as_object());
811
812 let schema_classes = match schema_classes {
813 Some(c) => c,
814 None => return result,
815 };
816
817 let tables = match ext.get("propertyTables").and_then(|t| t.as_array()) {
819 Some(t) => t,
820 None => return result,
821 };
822
823 for table in tables {
824 let class_name = match table.get("class").and_then(|c| c.as_str()) {
825 Some(c) => c,
826 None => continue,
827 };
828
829 let class_schema = match schema_classes.get(class_name) {
830 Some(c) => c,
831 None => continue,
832 };
833
834 let schema_props = match class_schema.get("properties").and_then(|p| p.as_object()) {
835 Some(p) => p,
836 None => continue,
837 };
838
839 let table_props = match table.get("properties").and_then(|p| p.as_object()) {
840 Some(p) => p,
841 None => continue,
842 };
843
844 for (prop_name, prop_data) in table_props {
845 let needs_8byte = schema_props
847 .get(prop_name)
848 .and_then(|s| s.get("componentType"))
849 .and_then(|c| c.as_str())
850 .map(|c| c == "INT64" || c == "FLOAT64")
851 .unwrap_or(false);
852
853 if needs_8byte {
854 if let Some(bv_idx) = prop_data.get("values").and_then(|v| v.as_u64()) {
856 result.insert(bv_idx as usize);
857 }
858 }
859 }
860 }
861
862 result
863}
864
865enum SkipReason {
867 AlreadyCompressed,
868 NonTriangle(u64),
869 Error(Error),
870}
871
872struct CompressedPrimitive {
874 mesh_idx: usize,
875 prim_idx: usize,
876 buffer_view_offset: usize,
877 buffer_view_length: usize,
878 attribute_ids: DracoAttributeIds,
879 indices_accessor_idx: Option<u64>,
880 feature_id_accessor_indices: Vec<u64>,
882 original_accessor_indices: HashMap<String, u64>,
884 vertex_count: usize,
886 indices_count: usize,
888}
889
890struct CompressedPrimitiveData {
891 buffer_view_offset: usize,
892 buffer_view_length: usize,
893 attribute_ids: DracoAttributeIds,
894 indices_accessor_idx: Option<u64>,
895 feature_id_accessor_indices: Vec<u64>,
897 original_accessor_indices: HashMap<String, u64>,
899 vertex_count: usize,
901 indices_count: usize,
903}
904
905#[derive(Default)]
907struct ExtractedGeometry {
908 positions: Vec<[f32; 3]>,
909 normals: Option<Vec<[f32; 3]>>,
910 texcoords: Vec<(String, Vec<[f32; 2]>)>,
911 colors: Vec<(String, Vec<[f32; 4]>)>,
912 tangents: Option<Vec<[f32; 4]>>,
913 feature_ids: Vec<(String, Vec<f32>)>,
914 feature_id_accessor_indices: Vec<(String, u64)>,
916 indices: Vec<u32>,
917 indices_accessor_idx: Option<u64>,
918 draco_attribute_ids: DracoAttributeIds,
919 original_accessor_indices: HashMap<String, u64>,
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926 use serde_json::json;
927
928 #[test]
929 fn test_categorize_buffer_views() {
930 let json = json!({
931 "meshes": [{
932 "primitives": [{
933 "attributes": { "POSITION": 0, "NORMAL": 1 },
934 "indices": 2
935 }]
936 }],
937 "accessors": [
938 { "bufferView": 0 },
939 { "bufferView": 1 },
940 { "bufferView": 2 }
941 ],
942 "bufferViews": [
943 { "buffer": 0, "byteOffset": 0, "byteLength": 100 },
944 { "buffer": 0, "byteOffset": 100, "byteLength": 100 },
945 { "buffer": 0, "byteOffset": 200, "byteLength": 50 },
946 { "buffer": 0, "byteOffset": 250, "byteLength": 1000 } ]
948 });
949
950 let (geometry, non_geometry) = categorize_buffer_views(&json);
951
952 assert!(geometry.contains(&0)); assert!(geometry.contains(&1)); assert!(geometry.contains(&2)); assert!(!geometry.contains(&3)); assert!(non_geometry.contains(&3));
958 assert!(!non_geometry.contains(&0));
959 }
960
961 #[test]
962 fn test_transcode_duck_glb() {
963 let test_path = "../tests/data/Duck/Duck.glb";
964 let input = match std::fs::read(test_path) {
965 Ok(data) => data,
966 Err(_) => {
967 println!("Test file {} not found, skipping", test_path);
968 return;
969 }
970 };
971
972 let transcoder = GltfTranscoder::default();
973 let (output, warnings) = transcoder
974 .transcode_to_glb(&input)
975 .expect("Transcoding failed");
976
977 assert!(!output.is_empty(), "Output should not be empty");
979
980 println!("Input size: {} bytes", input.len());
982 println!("Output size: {} bytes", output.len());
983 println!(
984 "Compression ratio: {:.2}%",
985 (output.len() as f64 / input.len() as f64) * 100.0
986 );
987
988 for warning in &warnings {
989 println!("Warning: {}", warning);
990 }
991
992 let parsed = super::glb::parse_glb(&output).expect("Output is not valid GLB");
994 assert!(!parsed.json.is_empty(), "JSON chunk should not be empty");
995
996 let json_str = String::from_utf8_lossy(&parsed.json);
998 assert!(
999 json_str.contains("KHR_draco_mesh_compression"),
1000 "Output should contain Draco extension"
1001 );
1002 }
1003
1004 #[test]
1005 fn test_transcode_deterministic() {
1006 let test_path = "../tests/data/Duck/Duck.glb";
1007 let input = match std::fs::read(test_path) {
1008 Ok(data) => data,
1009 Err(_) => {
1010 println!("Test file {} not found, skipping", test_path);
1011 return;
1012 }
1013 };
1014
1015 let transcoder = GltfTranscoder::default();
1016
1017 let mut outputs = Vec::new();
1019 for _ in 0..5 {
1020 let (output, _) = transcoder
1021 .transcode_to_glb(&input)
1022 .expect("Transcoding failed");
1023 outputs.push(output);
1024 }
1025
1026 for (i, output) in outputs.iter().enumerate().skip(1) {
1028 assert_eq!(
1029 outputs[0].len(),
1030 output.len(),
1031 "Output {} has different length",
1032 i
1033 );
1034 assert_eq!(&outputs[0], output, "Output {} differs", i);
1035 }
1036
1037 println!(
1038 "Determinism test passed: {} runs produced identical output",
1039 outputs.len()
1040 );
1041 }
1042}