1use crate::core::attribute::{AttributeDomain, AttributeType};
7use crate::core::mesh::builder::MeshBuilder;
8use crate::core::shared::NdVector;
9use crate::encode::Config as DracoConfig;
10use crate::prelude::ConfigType;
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] crate::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_pretty(&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<crate::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 geometry.normals.is_some() {
653 let normals: Vec<NdVector<3, f32>> = geometry
654 .normals
655 .as_ref()
656 .unwrap()
657 .iter()
658 .map(|n| NdVector::from(*n))
659 .collect();
660 builder.add_attribute(
661 normals,
662 AttributeType::Normal,
663 AttributeDomain::Corner,
664 vec![pos_id],
665 );
666 geometry.draco_attribute_ids.insert("NORMAL", draco_id);
667 draco_id += 1;
668 }
669
670 for (name, texcoords) in &geometry.texcoords {
672 let texcoords: Vec<NdVector<2, f32>> =
673 texcoords.iter().map(|t| NdVector::from(*t)).collect();
674 builder.add_attribute(
675 texcoords,
676 AttributeType::TextureCoordinate,
677 AttributeDomain::Corner,
678 vec![pos_id],
679 );
680 geometry.draco_attribute_ids.insert(name, draco_id);
681 draco_id += 1;
682 }
683
684 for (name, colors) in &geometry.colors {
686 let colors: Vec<NdVector<4, f32>> = colors.iter().map(|c| NdVector::from(*c)).collect();
687 builder.add_attribute(
688 colors,
689 AttributeType::Color,
690 AttributeDomain::Corner,
691 vec![pos_id],
692 );
693 geometry.draco_attribute_ids.insert(name, draco_id);
694 draco_id += 1;
695 }
696
697 if geometry.tangents.is_some() {
699 let tangents: Vec<NdVector<4, f32>> = geometry
700 .tangents
701 .as_ref()
702 .unwrap()
703 .iter()
704 .map(|t| NdVector::from(*t))
705 .collect();
706 builder.add_attribute(
707 tangents,
708 AttributeType::Tangent,
709 AttributeDomain::Corner,
710 vec![pos_id],
711 );
712 geometry.draco_attribute_ids.insert("TANGENT", draco_id);
713 draco_id += 1;
714 }
715
716 for (name, feature_ids) in &geometry.feature_ids {
720 let feature_ids: Vec<NdVector<1, u16>> = feature_ids
721 .iter()
722 .map(|&id| NdVector::from([id as u16]))
723 .collect();
724 builder.add_attribute(
725 feature_ids,
726 AttributeType::Custom,
727 AttributeDomain::Corner,
728 vec![pos_id],
729 );
730 geometry.draco_attribute_ids.insert(name, draco_id);
731 draco_id += 1;
732 }
733
734 let _ = draco_id;
736
737 builder
738 .build()
739 .map_err(|e| SkipReason::Error(Error::MeshBuild(e)))
740 }
741}
742
743fn categorize_buffer_views(json: &Value) -> (HashSet<usize>, HashSet<usize>) {
745 let mut geometry_views = HashSet::new();
746
747 if let Some(meshes) = json.get("meshes").and_then(|m| m.as_array()) {
749 for mesh in meshes {
750 if let Some(primitives) = mesh.get("primitives").and_then(|p| p.as_array()) {
751 for primitive in primitives {
752 if draco_extension::is_draco_compressed(primitive) {
754 continue;
755 }
756
757 if let Some(idx) = primitive.get("indices").and_then(|i| i.as_u64()) {
759 if let Some(bv) = get_accessor_buffer_view(json, idx as usize) {
760 geometry_views.insert(bv);
761 }
762 }
763
764 if let Some(attrs) = primitive.get("attributes").and_then(|a| a.as_object()) {
766 for (_, accessor_idx) in attrs {
767 if let Some(idx) = accessor_idx.as_u64() {
768 if let Some(bv) = get_accessor_buffer_view(json, idx as usize) {
769 geometry_views.insert(bv);
770 }
771 }
772 }
773 }
774 }
775 }
776 }
777 }
778
779 let num_views = json
781 .get("bufferViews")
782 .and_then(|b| b.as_array())
783 .map(|a| a.len())
784 .unwrap_or(0);
785
786 let non_geometry_views: HashSet<usize> = (0..num_views)
787 .filter(|i| !geometry_views.contains(i))
788 .collect();
789
790 (geometry_views, non_geometry_views)
791}
792
793fn get_accessor_buffer_view(json: &Value, accessor_idx: usize) -> Option<usize> {
795 json.get("accessors")
796 .and_then(|a| a.get(accessor_idx))
797 .and_then(|a| a.get("bufferView"))
798 .and_then(|v| v.as_u64())
799 .map(|v| v as usize)
800}
801
802fn get_8byte_aligned_buffer_views(json: &Value) -> HashSet<usize> {
806 let mut result = HashSet::new();
807
808 let ext = match json
809 .get("extensions")
810 .and_then(|e| e.get("EXT_structural_metadata"))
811 {
812 Some(ext) => ext,
813 None => return result,
814 };
815
816 let schema_classes = ext
818 .get("schema")
819 .and_then(|s| s.get("classes"))
820 .and_then(|c| c.as_object());
821
822 let schema_classes = match schema_classes {
823 Some(c) => c,
824 None => return result,
825 };
826
827 let tables = match ext.get("propertyTables").and_then(|t| t.as_array()) {
829 Some(t) => t,
830 None => return result,
831 };
832
833 for table in tables {
834 let class_name = match table.get("class").and_then(|c| c.as_str()) {
835 Some(c) => c,
836 None => continue,
837 };
838
839 let class_schema = match schema_classes.get(class_name) {
840 Some(c) => c,
841 None => continue,
842 };
843
844 let schema_props = match class_schema.get("properties").and_then(|p| p.as_object()) {
845 Some(p) => p,
846 None => continue,
847 };
848
849 let table_props = match table.get("properties").and_then(|p| p.as_object()) {
850 Some(p) => p,
851 None => continue,
852 };
853
854 for (prop_name, prop_data) in table_props {
855 let needs_8byte = schema_props
857 .get(prop_name)
858 .and_then(|s| s.get("componentType"))
859 .and_then(|c| c.as_str())
860 .map(|c| c == "INT64" || c == "FLOAT64")
861 .unwrap_or(false);
862
863 if needs_8byte {
864 if let Some(bv_idx) = prop_data.get("values").and_then(|v| v.as_u64()) {
866 result.insert(bv_idx as usize);
867 }
868 }
869 }
870 }
871
872 result
873}
874
875enum SkipReason {
877 AlreadyCompressed,
878 NonTriangle(u64),
879 Error(Error),
880}
881
882struct CompressedPrimitive {
884 mesh_idx: usize,
885 prim_idx: usize,
886 buffer_view_offset: usize,
887 buffer_view_length: usize,
888 attribute_ids: DracoAttributeIds,
889 indices_accessor_idx: Option<u64>,
890 feature_id_accessor_indices: Vec<u64>,
892 original_accessor_indices: HashMap<String, u64>,
894 vertex_count: usize,
896 indices_count: usize,
898}
899
900struct CompressedPrimitiveData {
901 buffer_view_offset: usize,
902 buffer_view_length: usize,
903 attribute_ids: DracoAttributeIds,
904 indices_accessor_idx: Option<u64>,
905 feature_id_accessor_indices: Vec<u64>,
907 original_accessor_indices: HashMap<String, u64>,
909 vertex_count: usize,
911 indices_count: usize,
913}
914
915#[derive(Default)]
917struct ExtractedGeometry {
918 positions: Vec<[f32; 3]>,
919 normals: Option<Vec<[f32; 3]>>,
920 texcoords: Vec<(String, Vec<[f32; 2]>)>,
921 colors: Vec<(String, Vec<[f32; 4]>)>,
922 tangents: Option<Vec<[f32; 4]>>,
923 feature_ids: Vec<(String, Vec<f32>)>,
924 feature_id_accessor_indices: Vec<(String, u64)>,
926 indices: Vec<u32>,
927 indices_accessor_idx: Option<u64>,
928 draco_attribute_ids: DracoAttributeIds,
929 original_accessor_indices: HashMap<String, u64>,
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936 use serde_json::json;
937
938 #[test]
939 fn test_categorize_buffer_views() {
940 let json = json!({
941 "meshes": [{
942 "primitives": [{
943 "attributes": { "POSITION": 0, "NORMAL": 1 },
944 "indices": 2
945 }]
946 }],
947 "accessors": [
948 { "bufferView": 0 },
949 { "bufferView": 1 },
950 { "bufferView": 2 }
951 ],
952 "bufferViews": [
953 { "buffer": 0, "byteOffset": 0, "byteLength": 100 },
954 { "buffer": 0, "byteOffset": 100, "byteLength": 100 },
955 { "buffer": 0, "byteOffset": 200, "byteLength": 50 },
956 { "buffer": 0, "byteOffset": 250, "byteLength": 1000 } ]
958 });
959
960 let (geometry, non_geometry) = categorize_buffer_views(&json);
961
962 assert!(geometry.contains(&0)); assert!(geometry.contains(&1)); assert!(geometry.contains(&2)); assert!(!geometry.contains(&3)); assert!(non_geometry.contains(&3));
968 assert!(!non_geometry.contains(&0));
969 }
970
971 #[test]
972 fn test_transcode_duck_glb() {
973 let test_path = "tests/data/Duck/Duck.glb";
974 let input = match std::fs::read(test_path) {
975 Ok(data) => data,
976 Err(_) => {
977 println!("Test file {} not found, skipping", test_path);
978 return;
979 }
980 };
981
982 let transcoder = GltfTranscoder::default();
983 let (output, warnings) = transcoder
984 .transcode_to_glb(&input)
985 .expect("Transcoding failed");
986
987 assert!(!output.is_empty(), "Output should not be empty");
989
990 println!("Input size: {} bytes", input.len());
992 println!("Output size: {} bytes", output.len());
993 println!(
994 "Compression ratio: {:.2}%",
995 (output.len() as f64 / input.len() as f64) * 100.0
996 );
997
998 for warning in &warnings {
999 println!("Warning: {}", warning);
1000 }
1001
1002 let parsed = super::glb::parse_glb(&output).expect("Output is not valid GLB");
1004 assert!(!parsed.json.is_empty(), "JSON chunk should not be empty");
1005
1006 let json_str = String::from_utf8_lossy(&parsed.json);
1008 assert!(
1009 json_str.contains("KHR_draco_mesh_compression"),
1010 "Output should contain Draco extension"
1011 );
1012 }
1013
1014 #[test]
1015 fn test_transcode_deterministic() {
1016 let test_path = "tests/data/Duck/Duck.glb";
1017 let input = match std::fs::read(test_path) {
1018 Ok(data) => data,
1019 Err(_) => {
1020 println!("Test file {} not found, skipping", test_path);
1021 return;
1022 }
1023 };
1024
1025 let transcoder = GltfTranscoder::default();
1026
1027 let mut outputs = Vec::new();
1029 for _ in 0..5 {
1030 let (output, _) = transcoder
1031 .transcode_to_glb(&input)
1032 .expect("Transcoding failed");
1033 outputs.push(output);
1034 }
1035
1036 for (i, output) in outputs.iter().enumerate().skip(1) {
1038 assert_eq!(
1039 outputs[0].len(),
1040 output.len(),
1041 "Output {} has different length",
1042 i
1043 );
1044 assert_eq!(&outputs[0], output, "Output {} differs", i);
1045 }
1046
1047 println!(
1048 "Determinism test passed: {} runs produced identical output",
1049 outputs.len()
1050 );
1051 }
1052}