1#![allow(clippy::type_complexity)]
6use super::types::{GltfScene, ValidationIssue};
7
8pub fn escape_json(s: &str) -> String {
10 s.replace('\\', "\\\\").replace('"', "\\\"")
11}
12pub fn decode_f32_le(bytes: &[u8]) -> Vec<f32> {
14 let mut out = Vec::with_capacity(bytes.len() / 4);
15 let mut i = 0;
16 while i + 3 < bytes.len() {
17 let v = f32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
18 out.push(v);
19 i += 4;
20 }
21 out
22}
23pub fn decode_u16_le(bytes: &[u8]) -> Vec<u16> {
25 let mut out = Vec::with_capacity(bytes.len() / 2);
26 let mut i = 0;
27 while i + 1 < bytes.len() {
28 let v = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
29 out.push(v);
30 i += 2;
31 }
32 out
33}
34pub fn decode_u32_le(bytes: &[u8]) -> Vec<u32> {
36 let mut out = Vec::with_capacity(bytes.len() / 4);
37 let mut i = 0;
38 while i + 3 < bytes.len() {
39 let v = u32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
40 out.push(v);
41 i += 4;
42 }
43 out
44}
45pub fn decode_vec3(floats: &[f32]) -> Vec<[f32; 3]> {
47 let mut out = Vec::with_capacity(floats.len() / 3);
48 let mut i = 0;
49 while i + 2 < floats.len() {
50 out.push([floats[i], floats[i + 1], floats[i + 2]]);
51 i += 3;
52 }
53 out
54}
55pub fn decode_vec4(floats: &[f32]) -> Vec<[f32; 4]> {
57 let mut out = Vec::with_capacity(floats.len() / 4);
58 let mut i = 0;
59 while i + 3 < floats.len() {
60 out.push([floats[i], floats[i + 1], floats[i + 2], floats[i + 3]]);
61 i += 4;
62 }
63 out
64}
65pub fn write_obj(positions: &[[f32; 3]], normals: &[[f32; 3]], indices: &[u32]) -> String {
71 let mut out = String::new();
72 out.push_str("# OxiPhysics OBJ export\n");
73 for p in positions {
74 out.push_str(&format!("v {} {} {}\n", p[0], p[1], p[2]));
75 }
76 for n in normals {
77 out.push_str(&format!("vn {} {} {}\n", n[0], n[1], n[2]));
78 }
79 let mut i = 0;
80 while i + 2 < indices.len() {
81 let a = indices[i] + 1;
82 let b = indices[i + 1] + 1;
83 let c = indices[i + 2] + 1;
84 out.push_str(&format!("f {a}//{a} {b}//{b} {c}//{c}\n"));
85 i += 3;
86 }
87 out
88}
89pub fn parse_obj(content: &str) -> Result<(Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<u32>), String> {
95 let mut positions: Vec<[f32; 3]> = Vec::new();
96 let mut normals: Vec<[f32; 3]> = Vec::new();
97 let mut indices: Vec<u32> = Vec::new();
98 for line in content.lines() {
99 let line = line.trim();
100 if line.is_empty() || line.starts_with('#') {
101 continue;
102 }
103 if line.starts_with("vn ") {
104 let parts: Vec<&str> = line.split_whitespace().collect();
105 if parts.len() >= 4 {
106 let x = parts[1].parse::<f32>().map_err(|e| e.to_string())?;
107 let y = parts[2].parse::<f32>().map_err(|e| e.to_string())?;
108 let z = parts[3].parse::<f32>().map_err(|e| e.to_string())?;
109 normals.push([x, y, z]);
110 }
111 } else if line.starts_with("v ") {
112 let parts: Vec<&str> = line.split_whitespace().collect();
113 if parts.len() >= 4 {
114 let x = parts[1].parse::<f32>().map_err(|e| e.to_string())?;
115 let y = parts[2].parse::<f32>().map_err(|e| e.to_string())?;
116 let z = parts[3].parse::<f32>().map_err(|e| e.to_string())?;
117 positions.push([x, y, z]);
118 }
119 } else if line.starts_with("f ") {
120 let parts: Vec<&str> = line.split_whitespace().collect();
121 if parts.len() >= 4 {
122 for token in &parts[1..4] {
123 let vi = token
124 .split('/')
125 .next()
126 .ok_or_else(|| format!("bad face token: {token}"))?
127 .parse::<u32>()
128 .map_err(|e| e.to_string())?;
129 indices.push(vi - 1);
130 }
131 }
132 }
133 }
134 Ok((positions, normals, indices))
135}
136pub fn compute_flat_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
138 let mut normals = vec![[0.0f32; 3]; positions.len()];
139 let mut i = 0;
140 while i + 2 < indices.len() {
141 let a = indices[i] as usize;
142 let b = indices[i + 1] as usize;
143 let c = indices[i + 2] as usize;
144 if a < positions.len() && b < positions.len() && c < positions.len() {
145 let edge1 = [
146 positions[b][0] - positions[a][0],
147 positions[b][1] - positions[a][1],
148 positions[b][2] - positions[a][2],
149 ];
150 let edge2 = [
151 positions[c][0] - positions[a][0],
152 positions[c][1] - positions[a][1],
153 positions[c][2] - positions[a][2],
154 ];
155 let normal = [
156 edge1[1] * edge2[2] - edge1[2] * edge2[1],
157 edge1[2] * edge2[0] - edge1[0] * edge2[2],
158 edge1[0] * edge2[1] - edge1[1] * edge2[0],
159 ];
160 let len =
161 (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
162 let n = if len > 1e-10 {
163 [normal[0] / len, normal[1] / len, normal[2] / len]
164 } else {
165 [0.0, 0.0, 1.0]
166 };
167 normals[a] = n;
168 normals[b] = n;
169 normals[c] = n;
170 }
171 i += 3;
172 }
173 normals
174}
175pub fn validate_scene(scene: &GltfScene) -> Result<(), String> {
180 let issues = validate_scene_detailed(scene);
181 if let Some(err) = issues.iter().find(|i| i.is_error) {
182 Err(err.message.clone())
183 } else {
184 Ok(())
185 }
186}
187pub fn validate_scene_detailed(scene: &GltfScene) -> Vec<ValidationIssue> {
189 let mut issues = Vec::new();
190 for (ni, node) in scene.nodes.iter().enumerate() {
191 if let Some(mesh_idx) = node.mesh
192 && mesh_idx >= scene.meshes.len()
193 {
194 issues
195 .push(
196 ValidationIssue::error(
197 format!(
198 "Node {ni} ('{}') references mesh {mesh_idx} which does not exist (scene has {} meshes)",
199 node.name, scene.meshes.len()
200 ),
201 ),
202 );
203 }
204 for &child_idx in &node.children {
205 if child_idx >= scene.nodes.len() {
206 issues
207 .push(
208 ValidationIssue::error(
209 format!(
210 "Node {ni} ('{}') has child index {child_idx} which does not exist (scene has {} nodes)",
211 node.name, scene.nodes.len()
212 ),
213 ),
214 );
215 }
216 }
217 }
218 for (mi, mesh) in scene.meshes.iter().enumerate() {
219 for (pi, prim) in mesh.primitives.iter().enumerate() {
220 if prim.positions.is_empty() {
221 issues.push(ValidationIssue::warning(format!(
222 "Mesh {mi} ('{}'), primitive {pi}: has no positions (degenerate primitive)",
223 mesh.name
224 )));
225 }
226 if !prim.positions.is_empty() {
227 for (ii, &idx) in prim.indices.iter().enumerate() {
228 if idx as usize >= prim.positions.len() {
229 issues.push(ValidationIssue::error(format!(
230 "Mesh {mi}, primitive {pi}, index {ii}: index {idx} >= vertex count {}",
231 prim.positions.len()
232 )));
233 break;
234 }
235 }
236 }
237 if !prim.normals.is_empty() && prim.normals.len() != prim.positions.len() {
238 issues.push(ValidationIssue::warning(format!(
239 "Mesh {mi}, primitive {pi}: normal count ({}) != position count ({})",
240 prim.normals.len(),
241 prim.positions.len()
242 )));
243 }
244 }
245 }
246 for (mi, mat) in scene.materials.iter().enumerate() {
247 match mat.alpha_mode.as_str() {
248 "OPAQUE" | "MASK" | "BLEND" => {}
249 other => {
250 issues.push(ValidationIssue::error(format!(
251 "Material {mi} ('{}') has invalid alphaMode '{other}'",
252 mat.name
253 )));
254 }
255 }
256 for (ci, &c) in mat.base_color_factor.iter().enumerate() {
257 if !(0.0..=1.0).contains(&c) {
258 issues.push(ValidationIssue::warning(format!(
259 "Material {mi} ('{}') baseColorFactor[{ci}] = {c} is outside [0,1]",
260 mat.name
261 )));
262 }
263 }
264 }
265 for (ai, anim) in scene.animations.iter().enumerate() {
266 for (ci, ch) in anim.channels.iter().enumerate() {
267 let target_node = ch.target.node;
268 if target_node >= scene.nodes.len() {
269 issues.push(ValidationIssue::error(format!(
270 "Animation {ai} ('{}'), channel {ci}: target node {target_node} does not exist",
271 anim.name
272 )));
273 }
274 if ch.input_times.is_empty() {
275 issues.push(ValidationIssue::warning(format!(
276 "Animation {ai} ('{}'), channel {ci}: no keyframes",
277 anim.name
278 )));
279 }
280 }
281 }
282 issues
283}
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 use crate::gltf::CameraType;
289
290 use crate::gltf::GlbWriter;
291 use crate::gltf::GltfAccessor;
292 use crate::gltf::GltfAnimation;
293 use crate::gltf::GltfAnimationChannel;
294 use crate::gltf::GltfAnimationTarget;
295 use crate::gltf::GltfBufferView;
296 use crate::gltf::GltfMaterial;
297 use crate::gltf::GltfMesh;
298 use crate::gltf::GltfNode;
299 use crate::gltf::GltfPrimitive;
300
301 use crate::gltf::Joint;
302 use crate::gltf::JointChannel;
303 use crate::gltf::LightType;
304 use crate::gltf::MorphPrimitive;
305 use crate::gltf::MorphTarget;
306
307 use crate::gltf::SceneCamera;
308 use crate::gltf::SceneLight;
309 use crate::gltf::Skeleton;
310 use crate::gltf::SkeletonAnimation;
311
312 #[test]
313 fn test_gltf_scene_to_json_contains_asset_and_meshes() {
314 let mut scene = GltfScene::new();
315 let prim = GltfPrimitive {
316 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
317 normals: vec![[0.0, 0.0, 1.0]; 3],
318 texcoords: vec![],
319 indices: vec![0, 1, 2],
320 };
321 scene.add_mesh(GltfMesh {
322 name: "TestMesh".to_string(),
323 primitives: vec![prim],
324 });
325 let json = scene.to_json();
326 assert!(json.contains("\"asset\""), "JSON must contain 'asset' key");
327 assert!(
328 json.contains("\"meshes\""),
329 "JSON must contain 'meshes' key"
330 );
331 assert!(json.contains("\"version\""), "JSON must contain version");
332 assert!(json.contains("2.0"), "version must be 2.0");
333 }
334 #[test]
335 fn test_add_mesh_returns_correct_index() {
336 let mut scene = GltfScene::new();
337 let idx0 = scene.add_mesh(GltfMesh {
338 name: "Mesh0".to_string(),
339 primitives: vec![],
340 });
341 let idx1 = scene.add_mesh(GltfMesh {
342 name: "Mesh1".to_string(),
343 primitives: vec![],
344 });
345 assert_eq!(idx0, 0);
346 assert_eq!(idx1, 1);
347 assert_eq!(scene.mesh_count(), 2);
348 }
349 #[test]
350 fn test_write_obj_single_triangle() {
351 let positions = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
352 let normals = vec![[0.0f32, 0.0, 1.0]; 3];
353 let indices = vec![0u32, 1, 2];
354 let obj = write_obj(&positions, &normals, &indices);
355 let v_lines: Vec<&str> = obj.lines().filter(|l| l.starts_with("v ")).collect();
356 let vn_lines: Vec<&str> = obj.lines().filter(|l| l.starts_with("vn ")).collect();
357 let f_lines: Vec<&str> = obj.lines().filter(|l| l.starts_with("f ")).collect();
358 assert_eq!(v_lines.len(), 3, "expected 3 vertex lines");
359 assert_eq!(vn_lines.len(), 3, "expected 3 normal lines");
360 assert_eq!(f_lines.len(), 1, "expected 1 face line");
361 assert!(f_lines[0].contains("1//1"), "face must use v//vn format");
362 }
363 #[test]
364 fn test_parse_obj_roundtrip() {
365 let positions = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
366 let normals = vec![[0.0f32, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]];
367 let indices = vec![0u32, 1, 2];
368 let obj_str = write_obj(&positions, &normals, &indices);
369 let (parsed_pos, parsed_nrm, parsed_idx) = parse_obj(&obj_str).unwrap();
370 assert_eq!(parsed_pos.len(), 3);
371 assert_eq!(parsed_nrm.len(), 3);
372 assert_eq!(parsed_idx, vec![0, 1, 2]);
373 assert!((parsed_pos[1][0] - 1.0).abs() < 1e-6);
374 assert!((parsed_nrm[0][2] - 1.0).abs() < 1e-6);
375 }
376 #[test]
377 fn test_gltf_node_default_translation() {
378 let node = GltfNode::default();
379 assert_eq!(node.translation, [0.0, 0.0, 0.0]);
380 assert_eq!(node.scale, [1.0, 1.0, 1.0]);
381 assert_eq!(node.rotation, [0.0, 0.0, 0.0, 1.0]);
382 assert!(node.mesh.is_none());
383 assert!(node.children.is_empty());
384 }
385 #[test]
386 fn test_primitive_bounding_box() {
387 let prim = GltfPrimitive {
388 positions: vec![[-1.0, -2.0, -3.0], [4.0, 5.0, 6.0], [0.0, 0.0, 0.0]],
389 normals: vec![[0.0, 0.0, 1.0]; 3],
390 texcoords: vec![],
391 indices: vec![0, 1, 2],
392 };
393 let (min, max) = prim.bounding_box();
394 assert!((min[0] - (-1.0)).abs() < 1e-6);
395 assert!((min[1] - (-2.0)).abs() < 1e-6);
396 assert!((min[2] - (-3.0)).abs() < 1e-6);
397 assert!((max[0] - 4.0).abs() < 1e-6);
398 assert!((max[1] - 5.0).abs() < 1e-6);
399 assert!((max[2] - 6.0).abs() < 1e-6);
400 }
401 #[test]
402 fn test_primitive_triangle_and_vertex_count() {
403 let prim = GltfPrimitive {
404 positions: vec![[0.0; 3]; 6],
405 normals: vec![[0.0, 0.0, 1.0]; 6],
406 texcoords: vec![],
407 indices: vec![0, 1, 2, 3, 4, 5],
408 };
409 assert_eq!(prim.triangle_count(), 2);
410 assert_eq!(prim.vertex_count(), 6);
411 }
412 #[test]
413 fn test_extract_triangles() {
414 let prim = GltfPrimitive {
415 positions: vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
416 normals: vec![[0.0, 0.0, 1.0]; 3],
417 texcoords: vec![],
418 indices: vec![0, 1, 2],
419 };
420 let tris = prim.extract_triangles();
421 assert_eq!(tris.len(), 1);
422 assert!((tris[0][0][0] - 1.0).abs() < 1e-6);
423 assert!((tris[0][1][1] - 1.0).abs() < 1e-6);
424 assert!((tris[0][2][2] - 1.0).abs() < 1e-6);
425 }
426 #[test]
427 fn test_mesh_total_counts() {
428 let mesh = GltfMesh {
429 name: "test".into(),
430 primitives: vec![
431 GltfPrimitive {
432 positions: vec![[0.0; 3]; 3],
433 normals: vec![[0.0, 0.0, 1.0]; 3],
434 texcoords: vec![],
435 indices: vec![0, 1, 2],
436 },
437 GltfPrimitive {
438 positions: vec![[0.0; 3]; 4],
439 normals: vec![[0.0, 0.0, 1.0]; 4],
440 texcoords: vec![],
441 indices: vec![0, 1, 2, 1, 2, 3],
442 },
443 ],
444 };
445 assert_eq!(mesh.total_vertex_count(), 7);
446 assert_eq!(mesh.total_triangle_count(), 3);
447 }
448 #[test]
449 fn test_accessor_components_and_size() {
450 let acc = GltfAccessor {
451 buffer_view: 0,
452 byte_offset: 0,
453 component_type: 5126,
454 count: 10,
455 element_type: "VEC3".to_string(),
456 };
457 assert_eq!(acc.components_per_element(), 3);
458 assert_eq!(acc.component_size(), 4);
459 assert_eq!(acc.byte_length(), 10 * 3 * 4);
460 }
461 #[test]
462 fn test_accessor_scalar_u16() {
463 let acc = GltfAccessor {
464 buffer_view: 0,
465 byte_offset: 0,
466 component_type: 5123,
467 count: 5,
468 element_type: "SCALAR".to_string(),
469 };
470 assert_eq!(acc.components_per_element(), 1);
471 assert_eq!(acc.component_size(), 2);
472 assert_eq!(acc.byte_length(), 10);
473 }
474 #[test]
475 fn test_accessor_mat4() {
476 let acc = GltfAccessor {
477 buffer_view: 0,
478 byte_offset: 0,
479 component_type: 5126,
480 count: 1,
481 element_type: "MAT4".to_string(),
482 };
483 assert_eq!(acc.components_per_element(), 16);
484 assert_eq!(acc.byte_length(), 64);
485 }
486 #[test]
487 fn test_decode_f32_le() {
488 let val: f32 = 3.125;
489 let bytes = val.to_le_bytes();
490 let decoded = decode_f32_le(&bytes);
491 assert_eq!(decoded.len(), 1);
492 assert!((decoded[0] - 3.125).abs() < 1e-5);
493 }
494 #[test]
495 fn test_decode_u16_le() {
496 let val: u16 = 12345;
497 let bytes = val.to_le_bytes();
498 let decoded = decode_u16_le(&bytes);
499 assert_eq!(decoded, vec![12345]);
500 }
501 #[test]
502 fn test_decode_u32_le() {
503 let val: u32 = 999999;
504 let bytes = val.to_le_bytes();
505 let decoded = decode_u32_le(&bytes);
506 assert_eq!(decoded, vec![999999]);
507 }
508 #[test]
509 fn test_decode_vec3() {
510 let floats = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
511 let vecs = decode_vec3(&floats);
512 assert_eq!(vecs.len(), 2);
513 assert_eq!(vecs[0], [1.0, 2.0, 3.0]);
514 assert_eq!(vecs[1], [4.0, 5.0, 6.0]);
515 }
516 #[test]
517 fn test_decode_vec4() {
518 let floats = vec![1.0, 2.0, 3.0, 4.0];
519 let vecs = decode_vec4(&floats);
520 assert_eq!(vecs.len(), 1);
521 assert_eq!(vecs[0], [1.0, 2.0, 3.0, 4.0]);
522 }
523 #[test]
524 fn test_animation_duration() {
525 let anim = GltfAnimation {
526 name: "walk".into(),
527 channels: vec![
528 GltfAnimationChannel {
529 target: GltfAnimationTarget {
530 node: 0,
531 path: "translation".into(),
532 },
533 interpolation: "LINEAR".into(),
534 input_times: vec![0.0, 0.5, 1.0],
535 output_values: vec![0.0; 9],
536 },
537 GltfAnimationChannel {
538 target: GltfAnimationTarget {
539 node: 1,
540 path: "rotation".into(),
541 },
542 interpolation: "LINEAR".into(),
543 input_times: vec![0.0, 2.0],
544 output_values: vec![0.0; 8],
545 },
546 ],
547 };
548 assert!((anim.duration() - 2.0).abs() < 1e-6);
549 assert_eq!(anim.total_keyframe_count(), 5);
550 }
551 #[test]
552 fn test_animation_empty() {
553 let anim = GltfAnimation {
554 name: "empty".into(),
555 channels: vec![],
556 };
557 assert!((anim.duration()).abs() < 1e-6);
558 assert_eq!(anim.total_keyframe_count(), 0);
559 }
560 #[test]
561 fn test_material_default() {
562 let mat = GltfMaterial::default();
563 assert!(mat.is_opaque());
564 assert!(!mat.is_emissive());
565 assert_eq!(mat.alpha_mode, "OPAQUE");
566 assert!((mat.metallic_factor - 1.0).abs() < 1e-6);
567 }
568 #[test]
569 fn test_material_emissive() {
570 let mat = GltfMaterial {
571 emissive_factor: [1.0, 0.0, 0.0],
572 ..Default::default()
573 };
574 assert!(mat.is_emissive());
575 }
576 #[test]
577 fn test_material_blend() {
578 let mut mat = GltfMaterial {
579 alpha_mode: "BLEND".to_string(),
580 ..Default::default()
581 };
582 mat.base_color_factor[3] = 0.5;
583 assert!(!mat.is_opaque());
584 }
585 #[test]
586 fn test_scene_graph_traversal() {
587 let mut scene = GltfScene::new();
588 scene.add_node(GltfNode {
589 name: "root".into(),
590 translation: [1.0, 0.0, 0.0],
591 children: vec![1],
592 ..GltfNode::default()
593 });
594 scene.add_node(GltfNode {
595 name: "child".into(),
596 translation: [0.0, 2.0, 0.0],
597 children: vec![2],
598 ..GltfNode::default()
599 });
600 scene.add_node(GltfNode {
601 name: "grandchild".into(),
602 translation: [0.0, 0.0, 3.0],
603 ..GltfNode::default()
604 });
605 let mut visited = Vec::new();
606 scene.traverse_depth_first(|idx, depth, acc_trans| {
607 visited.push((idx, depth, acc_trans));
608 });
609 assert_eq!(visited.len(), 3);
610 assert_eq!(visited[0].0, 0);
611 assert_eq!(visited[0].1, 0);
612 assert!((visited[0].2[0] - 1.0).abs() < 1e-12);
613 assert_eq!(visited[1].0, 1);
614 assert_eq!(visited[1].1, 1);
615 assert!((visited[1].2[0] - 1.0).abs() < 1e-12);
616 assert!((visited[1].2[1] - 2.0).abs() < 1e-12);
617 assert_eq!(visited[2].0, 2);
618 assert_eq!(visited[2].1, 2);
619 assert!((visited[2].2[2] - 3.0).abs() < 1e-12);
620 }
621 #[test]
622 fn test_collect_mesh_primitives() {
623 let mut scene = GltfScene::new();
624 let mesh_idx = scene.add_mesh(GltfMesh {
625 name: "m0".into(),
626 primitives: vec![GltfPrimitive {
627 positions: vec![[0.0; 3]; 3],
628 normals: vec![[0.0, 0.0, 1.0]; 3],
629 texcoords: vec![],
630 indices: vec![0, 1, 2],
631 }],
632 });
633 scene.add_node(GltfNode {
634 name: "node0".into(),
635 mesh: Some(mesh_idx),
636 ..GltfNode::default()
637 });
638 let prims = scene.collect_mesh_primitives();
639 assert_eq!(prims.len(), 1);
640 assert_eq!(prims[0].0, "node0");
641 assert_eq!(prims[0].1.vertex_count(), 3);
642 }
643 #[test]
644 fn test_nodes_using_mesh() {
645 let mut scene = GltfScene::new();
646 scene.add_mesh(GltfMesh {
647 name: "m".into(),
648 primitives: vec![],
649 });
650 scene.add_node(GltfNode {
651 name: "a".into(),
652 mesh: Some(0),
653 ..GltfNode::default()
654 });
655 scene.add_node(GltfNode {
656 name: "b".into(),
657 mesh: None,
658 ..GltfNode::default()
659 });
660 scene.add_node(GltfNode {
661 name: "c".into(),
662 mesh: Some(0),
663 ..GltfNode::default()
664 });
665 let users = scene.nodes_using_mesh(0);
666 assert_eq!(users, vec![0, 2]);
667 }
668 #[test]
669 fn test_scene_total_counts() {
670 let mut scene = GltfScene::new();
671 scene.add_mesh(GltfMesh {
672 name: "m".into(),
673 primitives: vec![GltfPrimitive {
674 positions: vec![[0.0; 3]; 4],
675 normals: vec![[0.0, 0.0, 1.0]; 4],
676 texcoords: vec![],
677 indices: vec![0, 1, 2, 1, 2, 3],
678 }],
679 });
680 assert_eq!(scene.total_vertex_count(), 4);
681 assert_eq!(scene.total_triangle_count(), 2);
682 }
683 #[test]
684 fn test_compute_flat_normals() {
685 let positions = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
686 let indices = vec![0u32, 1, 2];
687 let normals = compute_flat_normals(&positions, &indices);
688 assert_eq!(normals.len(), 3);
689 for n in &normals {
690 assert!((n[2] - 1.0).abs() < 1e-5);
691 }
692 }
693 #[test]
694 fn test_json_with_materials() {
695 let mut scene = GltfScene::new();
696 scene.add_node(GltfNode::default());
697 scene.add_mesh(GltfMesh {
698 name: "m".into(),
699 primitives: vec![],
700 });
701 scene.add_material(GltfMaterial {
702 name: "red".into(),
703 base_color_factor: [1.0, 0.0, 0.0, 1.0],
704 ..GltfMaterial::default()
705 });
706 let json = scene.to_json();
707 assert!(json.contains("\"materials\""));
708 assert!(json.contains("\"red\""));
709 assert!(json.contains("pbrMetallicRoughness"));
710 }
711 #[test]
712 fn test_json_with_children() {
713 let mut scene = GltfScene::new();
714 scene.add_node(GltfNode {
715 name: "parent".into(),
716 children: vec![1],
717 ..GltfNode::default()
718 });
719 scene.add_node(GltfNode {
720 name: "child".into(),
721 ..GltfNode::default()
722 });
723 let json = scene.to_json();
724 assert!(json.contains("\"children\""));
725 }
726 #[test]
727 fn test_buffer_view() {
728 let bv = GltfBufferView {
729 buffer: 0,
730 byte_offset: 100,
731 byte_length: 240,
732 byte_stride: Some(12),
733 };
734 assert_eq!(bv.byte_offset, 100);
735 assert_eq!(bv.byte_stride, Some(12));
736 }
737 #[test]
738 fn test_morph_target_blend() {
739 let base = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
740 let target = MorphTarget {
741 name: "smile".into(),
742 position_deltas: vec![[0.0, 0.1, 0.0], [0.0, 0.2, 0.0], [0.0, 0.15, 0.0]],
743 };
744 let blended = target.apply(&base, 0.5);
745 assert_eq!(blended.len(), 3);
746 assert!((blended[0][1] - 0.05).abs() < 1e-6, "y should be 0.05");
747 assert!((blended[1][0] - 1.0).abs() < 1e-6, "x unchanged");
748 }
749 #[test]
750 fn test_morph_target_zero_weight() {
751 let base = vec![[1.0f32, 2.0, 3.0]];
752 let target = MorphTarget {
753 name: "t".into(),
754 position_deltas: vec![[10.0, 10.0, 10.0]],
755 };
756 let result = target.apply(&base, 0.0);
757 assert!((result[0][0] - 1.0).abs() < 1e-6);
758 }
759 #[test]
760 fn test_morph_target_full_weight() {
761 let base = vec![[1.0f32, 2.0, 3.0]];
762 let target = MorphTarget {
763 name: "t".into(),
764 position_deltas: vec![[1.0, -1.0, 0.5]],
765 };
766 let result = target.apply(&base, 1.0);
767 assert!((result[0][0] - 2.0).abs() < 1e-6);
768 assert!((result[0][1] - 1.0).abs() < 1e-6);
769 assert!((result[0][2] - 3.5).abs() < 1e-6);
770 }
771 #[test]
772 fn test_primitive_morph_targets_blend() {
773 let mut prim = MorphPrimitive {
774 base: GltfPrimitive {
775 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
776 normals: vec![[0.0, 0.0, 1.0]; 3],
777 texcoords: vec![],
778 indices: vec![0, 1, 2],
779 },
780 targets: vec![
781 MorphTarget {
782 name: "A".into(),
783 position_deltas: vec![[0.0, 1.0, 0.0]; 3],
784 },
785 MorphTarget {
786 name: "B".into(),
787 position_deltas: vec![[0.0, 0.0, 2.0]; 3],
788 },
789 ],
790 weights: vec![0.5, 0.0],
791 };
792 let blended = prim.blend();
793 assert!((blended[0][1] - 0.5).abs() < 1e-6);
794 prim.set_weight(1, 1.0);
795 let blended2 = prim.blend();
796 assert!((blended2[0][2] - 2.0).abs() < 1e-6);
797 }
798 #[test]
799 fn test_joint_default() {
800 let j = Joint::default();
801 assert_eq!(j.translation, [0.0, 0.0, 0.0]);
802 assert_eq!(j.rotation, [0.0, 0.0, 0.0, 1.0]);
803 assert_eq!(j.scale, [1.0, 1.0, 1.0]);
804 assert!(j.children.is_empty());
805 }
806 #[test]
807 fn test_skeleton_add_and_count() {
808 let mut skel = Skeleton::new();
809 skel.add_joint(Joint {
810 name: "root".into(),
811 ..Joint::default()
812 });
813 skel.add_joint(Joint {
814 name: "hip".into(),
815 ..Joint::default()
816 });
817 assert_eq!(skel.joint_count(), 2);
818 assert_eq!(skel.joints[0].name, "root");
819 }
820 #[test]
821 fn test_skeleton_animation_export_json() {
822 let mut skel = Skeleton::new();
823 skel.add_joint(Joint {
824 name: "bone0".into(),
825 ..Joint::default()
826 });
827 let anim = SkeletonAnimation {
828 name: "run".into(),
829 joint_channels: vec![JointChannel {
830 joint_index: 0,
831 path: "rotation".into(),
832 times: vec![0.0, 0.5, 1.0],
833 values: vec![
834 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.707, 0.707, 0.0, 0.0, 0.0, 1.0,
835 ],
836 interpolation: "LINEAR".into(),
837 }],
838 };
839 let json = skel.export_animation_json(&anim);
840 assert!(json.contains("run"), "should contain animation name");
841 assert!(json.contains("rotation"), "should contain channel path");
842 assert!(json.contains("bone0"), "should contain joint name");
843 }
844 #[test]
845 fn test_skeleton_animation_duration() {
846 let anim = SkeletonAnimation {
847 name: "walk".into(),
848 joint_channels: vec![
849 JointChannel {
850 joint_index: 0,
851 path: "translation".into(),
852 times: vec![0.0, 1.5],
853 values: vec![0.0; 6],
854 interpolation: "LINEAR".into(),
855 },
856 JointChannel {
857 joint_index: 1,
858 path: "rotation".into(),
859 times: vec![0.0, 0.5],
860 values: vec![0.0; 8],
861 interpolation: "STEP".into(),
862 },
863 ],
864 };
865 assert!((anim.duration() - 1.5).abs() < 1e-6);
866 }
867 #[test]
868 fn test_glb_magic_and_version() {
869 let writer = GlbWriter::new();
870 let scene = GltfScene::new();
871 let bytes = writer.write(&scene);
872 assert!(bytes.len() >= 12, "GLB must be at least 12 bytes");
873 assert_eq!(&bytes[0..4], b"glTF", "magic bytes must be 'glTF'");
874 let version = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
875 assert_eq!(version, 2, "GLB version must be 2");
876 }
877 #[test]
878 fn test_glb_header_length_consistent() {
879 let writer = GlbWriter::new();
880 let scene = GltfScene::new();
881 let bytes = writer.write(&scene);
882 let total_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
883 assert_eq!(
884 total_len as usize,
885 bytes.len(),
886 "header length field must equal actual length"
887 );
888 }
889 #[test]
890 fn test_glb_with_mesh_round_trip() {
891 let mut scene = GltfScene::new();
892 scene.add_mesh(GltfMesh {
893 name: "cube".into(),
894 primitives: vec![GltfPrimitive {
895 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
896 normals: vec![[0.0, 0.0, 1.0]; 3],
897 texcoords: vec![],
898 indices: vec![0, 1, 2],
899 }],
900 });
901 let writer = GlbWriter::new();
902 let bytes = writer.write(&scene);
903 assert!(bytes.len() > 28, "GLB must have JSON chunk");
904 let json_chunk_type = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
905 assert_eq!(json_chunk_type, 0x4E4F534A, "first chunk must be JSON");
906 }
907 #[test]
908 fn test_camera_node_json() {
909 let mut scene = GltfScene::new();
910 let cam = SceneCamera {
911 name: "main_cam".into(),
912 camera_type: CameraType::Perspective {
913 yfov: 0.785,
914 znear: 0.01,
915 zfar: Some(1000.0),
916 aspect_ratio: Some(1.778),
917 },
918 };
919 scene.add_camera(cam);
920 let json = scene.to_json_with_hierarchy();
921 assert!(json.contains("cameras"), "JSON should contain 'cameras'");
922 assert!(
923 json.contains("perspective"),
924 "JSON should contain 'perspective'"
925 );
926 }
927 #[test]
928 fn test_light_node_json() {
929 let mut scene = GltfScene::new();
930 let light = SceneLight {
931 name: "sun".into(),
932 light_type: LightType::Directional,
933 color: [1.0, 0.98, 0.9],
934 intensity: 100_000.0,
935 };
936 scene.add_light(light);
937 let json = scene.to_json_with_hierarchy();
938 assert!(
939 json.contains("extensions"),
940 "should contain 'extensions' for lights"
941 );
942 assert!(
943 json.contains("KHR_lights_punctual"),
944 "should contain KHR extension"
945 );
946 assert!(json.contains("directional"), "should contain light type");
947 }
948 #[test]
949 fn test_validate_empty_scene_passes() {
950 let scene = GltfScene::new();
951 let result = validate_scene(&scene);
952 assert!(result.is_ok(), "empty scene should pass validation");
953 }
954 #[test]
955 fn test_validate_dangling_node_mesh_reference_fails() {
956 let mut scene = GltfScene::new();
957 scene.add_node(GltfNode {
958 name: "bad".into(),
959 mesh: Some(5),
960 ..GltfNode::default()
961 });
962 let result = validate_scene(&scene);
963 assert!(
964 result.is_err(),
965 "dangling mesh reference should fail validation"
966 );
967 }
968 #[test]
969 fn test_validate_valid_mesh_ref_passes() {
970 let mut scene = GltfScene::new();
971 let idx = scene.add_mesh(GltfMesh {
972 name: "m".into(),
973 primitives: vec![],
974 });
975 scene.add_node(GltfNode {
976 name: "good".into(),
977 mesh: Some(idx),
978 ..GltfNode::default()
979 });
980 let result = validate_scene(&scene);
981 assert!(result.is_ok(), "valid mesh reference should pass");
982 }
983 #[test]
984 fn test_validate_dangling_child_reference_fails() {
985 let mut scene = GltfScene::new();
986 scene.add_node(GltfNode {
987 name: "parent".into(),
988 children: vec![99],
989 ..GltfNode::default()
990 });
991 let result = validate_scene(&scene);
992 assert!(result.is_err(), "dangling child reference should fail");
993 }
994 #[test]
995 fn test_validate_degenerate_primitive_warns() {
996 let mut scene = GltfScene::new();
997 scene.add_mesh(GltfMesh {
998 name: "degen".into(),
999 primitives: vec![GltfPrimitive {
1000 positions: vec![],
1001 normals: vec![],
1002 texcoords: vec![],
1003 indices: vec![],
1004 }],
1005 });
1006 let issues = validate_scene_detailed(&scene);
1007 assert!(
1008 !issues.is_empty(),
1009 "degenerate primitive should produce issues"
1010 );
1011 }
1012 #[test]
1013 fn test_validate_index_out_of_range_fails() {
1014 let scene_with_bad_prim = GltfScene {
1015 nodes: vec![],
1016 meshes: vec![GltfMesh {
1017 name: "m".into(),
1018 primitives: vec![GltfPrimitive {
1019 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
1020 normals: vec![[0.0, 0.0, 1.0]; 2],
1021 texcoords: vec![],
1022 indices: vec![0, 1, 5],
1023 }],
1024 }],
1025 accessors: vec![],
1026 buffer_views: vec![],
1027 animations: vec![],
1028 materials: vec![],
1029 cameras: vec![],
1030 lights: vec![],
1031 };
1032 let issues = validate_scene_detailed(&scene_with_bad_prim);
1033 assert!(
1034 !issues.is_empty(),
1035 "out-of-range index should produce issues"
1036 );
1037 }
1038}
1039pub fn scene_to_gltf_json(scene: &GltfScene, embed_buffers: bool) -> String {
1044 let mut buffer_data: Vec<u8> = Vec::new();
1045 let mut buffer_views_json: Vec<String> = Vec::new();
1046 let mut accessors_json: Vec<String> = Vec::new();
1047 let mut mesh_json_items: Vec<String> = Vec::new();
1048 for mesh in &scene.meshes {
1049 for prim in &mesh.primitives {
1050 let pos_bv_start = buffer_data.len();
1051 for p in &prim.positions {
1052 for c in p {
1053 buffer_data.extend_from_slice(&c.to_le_bytes());
1054 }
1055 }
1056 let pos_bv_len = buffer_data.len() - pos_bv_start;
1057 let pos_bv_idx = buffer_views_json.len();
1058 buffer_views_json.push(format!(
1059 r#"{{ "buffer": 0, "byteOffset": {start}, "byteLength": {len}, "target": 34962 }}"#,
1060 start = pos_bv_start,
1061 len = pos_bv_len
1062 ));
1063 let pos_acc_idx = accessors_json.len();
1064 accessors_json
1065 .push(
1066 format!(
1067 r#"{{ "bufferView": {bv}, "byteOffset": 0, "componentType": 5126, "type": "VEC3", "count": {count} }}"#,
1068 bv = pos_bv_idx, count = prim.positions.len()
1069 ),
1070 );
1071 let idx_bv_start = buffer_data.len();
1072 while !buffer_data.len().is_multiple_of(4) {
1073 buffer_data.push(0u8);
1074 }
1075 let idx_bv_start = if buffer_data.len() == idx_bv_start {
1076 idx_bv_start
1077 } else {
1078 buffer_data.len()
1079 };
1080 let _ = idx_bv_start;
1081 let idx_real_start = buffer_data.len();
1082 for &i in &prim.indices {
1083 buffer_data.extend_from_slice(&i.to_le_bytes());
1084 }
1085 let idx_bv_len = buffer_data.len() - idx_real_start;
1086 let idx_bv_idx = buffer_views_json.len();
1087 buffer_views_json.push(format!(
1088 r#"{{ "buffer": 0, "byteOffset": {start}, "byteLength": {len}, "target": 34963 }}"#,
1089 start = idx_real_start,
1090 len = idx_bv_len
1091 ));
1092 let idx_acc_idx = accessors_json.len();
1093 accessors_json
1094 .push(
1095 format!(
1096 r#"{{ "bufferView": {bv}, "byteOffset": 0, "componentType": 5125, "type": "SCALAR", "count": {count} }}"#,
1097 bv = idx_bv_idx, count = prim.indices.len()
1098 ),
1099 );
1100 let prim_json = format!(
1101 r#"{{ "attributes": {{ "POSITION": {pa} }}, "indices": {ia} }}"#,
1102 pa = pos_acc_idx,
1103 ia = idx_acc_idx
1104 );
1105 mesh_json_items.push(format!(
1106 r#"{{ "name": "{}", "primitives": [{}] }}"#,
1107 mesh.name, prim_json
1108 ));
1109 }
1110 }
1111 let buf_json = if embed_buffers {
1112 let b64 = base64_encode(&buffer_data);
1113 format!(
1114 r#"{{ "uri": "data:application/octet-stream;base64,{}", "byteLength": {} }}"#,
1115 b64,
1116 buffer_data.len()
1117 )
1118 } else {
1119 format!(
1120 r#"{{ "uri": "buffer.bin", "byteLength": {} }}"#,
1121 buffer_data.len()
1122 )
1123 };
1124 let bvs = buffer_views_json.join(", ");
1125 let accs = accessors_json.join(", ");
1126 let meshes = mesh_json_items.join(", ");
1127 format!(
1128 r#"{{
1129 "asset": {{ "version": "2.0", "generator": "OxiPhysics" }},
1130 "scene": 0,
1131 "scenes": [{{ "nodes": [] }}],
1132 "nodes": [],
1133 "meshes": [{meshes}],
1134 "accessors": [{accs}],
1135 "bufferViews": [{bvs}],
1136 "buffers": [{buf}]
1137}}"#,
1138 meshes = meshes,
1139 accs = accs,
1140 bvs = bvs,
1141 buf = buf_json,
1142 )
1143}
1144pub(super) fn base64_encode(data: &[u8]) -> String {
1146 pub(super) const CHARS: &[u8] =
1147 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1148 let mut out = String::new();
1149 let mut i = 0;
1150 while i < data.len() {
1151 let b0 = data[i] as u32;
1152 let b1 = if i + 1 < data.len() {
1153 data[i + 1] as u32
1154 } else {
1155 0
1156 };
1157 let b2 = if i + 2 < data.len() {
1158 data[i + 2] as u32
1159 } else {
1160 0
1161 };
1162 let n = (b0 << 16) | (b1 << 8) | b2;
1163 out.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
1164 out.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
1165 if i + 1 < data.len() {
1166 out.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
1167 } else {
1168 out.push('=');
1169 }
1170 if i + 2 < data.len() {
1171 out.push(CHARS[(n & 0x3F) as usize] as char);
1172 } else {
1173 out.push('=');
1174 }
1175 i += 3;
1176 }
1177 out
1178}
1179#[cfg(test)]
1180mod tests_gltf_additions {
1181 use super::*;
1182 use crate::gltf::AccessorType;
1183 use crate::gltf::AnimationChannelBuilder;
1184 use crate::gltf::ComponentType;
1185 use crate::gltf::GlbWriter;
1186 use crate::gltf::GltfMesh;
1187 use crate::gltf::GltfPrimitive;
1188 use crate::gltf::Interpolation;
1189 use crate::gltf::PbrMaterialBuilder;
1190 use crate::gltf::TypedAccessor;
1191
1192 #[test]
1193 fn test_component_type_byte_size() {
1194 assert_eq!(ComponentType::UnsignedByte.byte_size(), 1);
1195 assert_eq!(ComponentType::UnsignedShort.byte_size(), 2);
1196 assert_eq!(ComponentType::UnsignedInt.byte_size(), 4);
1197 assert_eq!(ComponentType::Float.byte_size(), 4);
1198 }
1199 #[test]
1200 fn test_accessor_type_num_components() {
1201 assert_eq!(AccessorType::Scalar.num_components(), 1);
1202 assert_eq!(AccessorType::Vec3.num_components(), 3);
1203 assert_eq!(AccessorType::Vec4.num_components(), 4);
1204 assert_eq!(AccessorType::Mat4.num_components(), 16);
1205 }
1206 #[test]
1207 fn test_accessor_type_as_str() {
1208 assert_eq!(AccessorType::Vec3.as_str(), "VEC3");
1209 assert_eq!(AccessorType::Mat4.as_str(), "MAT4");
1210 assert_eq!(AccessorType::Scalar.as_str(), "SCALAR");
1211 }
1212 #[test]
1213 fn test_component_type_code() {
1214 assert_eq!(ComponentType::Float.component_type_code(), 5126);
1215 assert_eq!(ComponentType::UnsignedInt.component_type_code(), 5125);
1216 }
1217 #[test]
1218 fn test_typed_accessor_byte_length() {
1219 let acc = TypedAccessor::new("pos", 0, 0, ComponentType::Float, AccessorType::Vec3, 10);
1220 assert_eq!(acc.byte_length(), 120);
1221 }
1222 #[test]
1223 fn test_typed_accessor_to_json_contains_type() {
1224 let acc = TypedAccessor::new("vel", 1, 0, ComponentType::Float, AccessorType::Vec3, 5);
1225 let json = acc.to_json();
1226 assert!(json.contains("VEC3"), "JSON should contain type");
1227 assert!(json.contains("5126"), "JSON should contain componentType");
1228 }
1229 #[test]
1230 fn test_typed_accessor_decode_f32() {
1231 let values: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1232 let mut buffer: Vec<u8> = Vec::new();
1233 for v in &values {
1234 buffer.extend_from_slice(&v.to_le_bytes());
1235 }
1236 let acc = TypedAccessor::new("v", 0, 0, ComponentType::Float, AccessorType::Vec3, 2);
1237 let decoded = acc.decode_f32(&buffer).unwrap();
1238 assert_eq!(decoded.len(), 6);
1239 assert!((decoded[0] - 1.0).abs() < 1e-6);
1240 assert!((decoded[5] - 6.0).abs() < 1e-6);
1241 }
1242 #[test]
1243 fn test_typed_accessor_decode_u32() {
1244 let indices: Vec<u32> = vec![0, 1, 2, 3, 4, 5];
1245 let mut buffer: Vec<u8> = Vec::new();
1246 for i in &indices {
1247 buffer.extend_from_slice(&i.to_le_bytes());
1248 }
1249 let acc = TypedAccessor::new(
1250 "idx",
1251 0,
1252 0,
1253 ComponentType::UnsignedInt,
1254 AccessorType::Scalar,
1255 6,
1256 );
1257 let decoded = acc.decode_u32(&buffer).unwrap();
1258 assert_eq!(decoded, indices);
1259 }
1260 #[test]
1261 fn test_typed_accessor_wrong_type_returns_none() {
1262 let buffer = vec![0u8; 24];
1263 let acc = TypedAccessor::new(
1264 "idx",
1265 0,
1266 0,
1267 ComponentType::UnsignedInt,
1268 AccessorType::Scalar,
1269 6,
1270 );
1271 assert!(acc.decode_f32(&buffer).is_none());
1272 }
1273 #[test]
1274 fn test_pbr_builder_default() {
1275 let mat = PbrMaterialBuilder::default();
1276 assert_eq!(mat.metallic_factor, 0.0);
1277 assert_eq!(mat.roughness_factor, 0.5);
1278 assert_eq!(mat.alpha_mode, "OPAQUE");
1279 }
1280 #[test]
1281 fn test_pbr_builder_chain() {
1282 let mat = PbrMaterialBuilder::new("metal")
1283 .base_color(0.8, 0.8, 0.8, 1.0)
1284 .metallic_roughness(0.9, 0.1)
1285 .emissive(0.0, 0.0, 0.0)
1286 .double_sided(true)
1287 .alpha_mode("BLEND");
1288 assert!((mat.metallic_factor - 0.9).abs() < 1e-6);
1289 assert!(mat.double_sided);
1290 assert_eq!(mat.alpha_mode, "BLEND");
1291 }
1292 #[test]
1293 fn test_pbr_to_json_contains_keys() {
1294 let mat = PbrMaterialBuilder::new("glass")
1295 .base_color(0.2, 0.4, 0.8, 0.5)
1296 .alpha_mode("BLEND");
1297 let json = mat.to_json();
1298 assert!(
1299 json.contains("pbrMetallicRoughness"),
1300 "should contain PBR key"
1301 );
1302 assert!(
1303 json.contains("baseColorFactor"),
1304 "should contain baseColorFactor"
1305 );
1306 assert!(json.contains("BLEND"), "should contain alpha mode");
1307 assert!(json.contains("glass"), "should contain name");
1308 }
1309 #[test]
1310 fn test_pbr_build_to_gltf_material() {
1311 let mat = PbrMaterialBuilder::new("copper")
1312 .metallic_roughness(0.8, 0.3)
1313 .build();
1314 assert_eq!(mat.name, "copper");
1315 assert!((mat.metallic_factor - 0.8).abs() < 1e-6);
1316 assert!((mat.roughness_factor - 0.3).abs() < 1e-6);
1317 }
1318 #[test]
1319 fn test_interpolation_as_str() {
1320 assert_eq!(Interpolation::Linear.as_str(), "LINEAR");
1321 assert_eq!(Interpolation::Step.as_str(), "STEP");
1322 assert_eq!(Interpolation::CubicSpline.as_str(), "CUBICSPLINE");
1323 }
1324 #[test]
1325 fn test_animation_channel_builder_push_and_duration() {
1326 let ch = AnimationChannelBuilder::new(0, "translation", Interpolation::Linear)
1327 .push(0.0, vec![0.0, 0.0, 0.0])
1328 .push(0.5, vec![0.0, 1.0, 0.0])
1329 .push(1.0, vec![0.0, 2.0, 0.0]);
1330 assert_eq!(ch.len(), 3);
1331 assert!((ch.duration() - 1.0).abs() < 1e-6);
1332 assert!(!ch.is_empty());
1333 }
1334 #[test]
1335 fn test_animation_channel_times_and_values() {
1336 let ch = AnimationChannelBuilder::new(1, "rotation", Interpolation::Step)
1337 .push(0.0, vec![0.0, 0.0, 0.0, 1.0])
1338 .push(1.0, vec![0.0, 0.707, 0.0, 0.707]);
1339 let times = ch.times();
1340 let vals = ch.values_flat();
1341 assert_eq!(times.len(), 2);
1342 assert_eq!(vals.len(), 8);
1343 assert!((times[0] - 0.0).abs() < 1e-6);
1344 assert!((times[1] - 1.0).abs() < 1e-6);
1345 }
1346 #[test]
1347 fn test_animation_channel_json_fragments() {
1348 let ch = AnimationChannelBuilder::new(0, "scale", Interpolation::Linear)
1349 .push(0.0, vec![1.0, 1.0, 1.0])
1350 .push(1.0, vec![2.0, 2.0, 2.0]);
1351 let (sampler, channel) = ch.to_json_fragments(0, 10, 11);
1352 assert!(
1353 sampler.contains("LINEAR"),
1354 "sampler should contain interpolation"
1355 );
1356 assert!(channel.contains("scale"), "channel should contain path");
1357 assert!(
1358 channel.contains("\"node\": 0"),
1359 "channel should contain node index"
1360 );
1361 }
1362 #[test]
1363 fn test_animation_channel_empty() {
1364 let ch = AnimationChannelBuilder::new(0, "translation", Interpolation::Linear);
1365 assert!(ch.is_empty());
1366 assert_eq!(ch.len(), 0);
1367 assert!((ch.duration() - 0.0).abs() < 1e-6);
1368 }
1369 #[test]
1370 fn test_base64_encode_hello() {
1371 let encoded = base64_encode(b"Hello");
1372 assert_eq!(encoded, "SGVsbG8=");
1373 }
1374 #[test]
1375 fn test_base64_encode_empty() {
1376 assert_eq!(base64_encode(b""), "");
1377 }
1378 #[test]
1379 fn test_base64_encode_length_multiple_3() {
1380 let encoded = base64_encode(b"Man");
1381 assert_eq!(encoded, "TWFu");
1382 }
1383 #[test]
1384 fn test_scene_to_gltf_json_empty_scene() {
1385 let scene = GltfScene::new();
1386 let json = scene_to_gltf_json(&scene, false);
1387 assert!(
1388 json.contains("\"version\": \"2.0\""),
1389 "should contain asset version"
1390 );
1391 assert!(json.contains("OxiPhysics"), "should contain generator");
1392 }
1393 #[test]
1394 fn test_scene_to_gltf_json_with_mesh() {
1395 let mut scene = GltfScene::new();
1396 scene.add_mesh(GltfMesh {
1397 name: "triangle".into(),
1398 primitives: vec![GltfPrimitive {
1399 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1400 normals: vec![[0.0, 0.0, 1.0]; 3],
1401 texcoords: vec![],
1402 indices: vec![0, 1, 2],
1403 }],
1404 });
1405 let json = scene_to_gltf_json(&scene, false);
1406 assert!(json.contains("triangle"), "should contain mesh name");
1407 assert!(json.contains("VEC3"), "should contain VEC3 accessor type");
1408 assert!(json.contains("SCALAR"), "should contain SCALAR for indices");
1409 }
1410 #[test]
1411 fn test_scene_to_gltf_json_embed_buffer() {
1412 let mut scene = GltfScene::new();
1413 scene.add_mesh(GltfMesh {
1414 name: "cube".into(),
1415 primitives: vec![GltfPrimitive {
1416 positions: vec![[0.0, 0.0, 0.0]],
1417 normals: vec![[0.0, 1.0, 0.0]],
1418 texcoords: vec![],
1419 indices: vec![0],
1420 }],
1421 });
1422 let json = scene_to_gltf_json(&scene, true);
1423 assert!(
1424 json.contains("data:application/octet-stream;base64,"),
1425 "embedded buffer should use data URI"
1426 );
1427 }
1428
1429 fn make_triangle_scene() -> GltfScene {
1435 let mut scene = GltfScene::new();
1436 let mesh_idx = scene.add_mesh(GltfMesh {
1437 name: "triangle".into(),
1438 primitives: vec![GltfPrimitive {
1439 positions: vec![[0.0_f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1440 normals: vec![[0.0_f32, 0.0, 1.0]; 3],
1441 texcoords: vec![[0.0_f32, 0.0], [1.0, 0.0], [0.0, 1.0]],
1442 indices: vec![0u32, 1, 2],
1443 }],
1444 });
1445 scene.add_node(crate::gltf::GltfNode {
1446 name: "node0".into(),
1447 mesh: Some(mesh_idx),
1448 ..crate::gltf::GltfNode::default()
1449 });
1450 scene
1451 }
1452
1453 #[test]
1454 fn test_write_glb_header_magic() {
1455 let scene = make_triangle_scene();
1456 let writer = GlbWriter::new();
1457 let bytes = writer.write_glb(&scene);
1458 assert!(bytes.len() >= 12, "GLB must have at least a 12-byte header");
1459 assert_eq!(&bytes[0..4], b"glTF", "magic bytes must be 'glTF'");
1460 let version = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
1461 assert_eq!(version, 2, "GLB version must be 2");
1462 }
1463
1464 #[test]
1465 fn test_write_glb_total_length_consistent() {
1466 let scene = make_triangle_scene();
1467 let writer = GlbWriter::new();
1468 let bytes = writer.write_glb(&scene);
1469 let total_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
1470 assert_eq!(
1471 total_len as usize,
1472 bytes.len(),
1473 "total_length field must match actual byte length"
1474 );
1475 }
1476
1477 #[test]
1478 fn test_write_glb_chunk_types() {
1479 let scene = make_triangle_scene();
1480 let writer = GlbWriter::new();
1481 let bytes = writer.write_glb(&scene);
1482 let json_type = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
1484 assert_eq!(
1485 json_type, 0x4E4F534A,
1486 "first chunk must be JSON (0x4E4F534A)"
1487 );
1488 let json_chunk_len =
1490 u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1491 let bin_start = 12 + 8 + json_chunk_len;
1493 assert!(
1494 bin_start + 8 <= bytes.len(),
1495 "must have room for BIN chunk header"
1496 );
1497 let bin_type = u32::from_le_bytes([
1498 bytes[bin_start + 4],
1499 bytes[bin_start + 5],
1500 bytes[bin_start + 6],
1501 bytes[bin_start + 7],
1502 ]);
1503 assert_eq!(
1504 bin_type, 0x004E4942,
1505 "second chunk must be BIN\\0 (0x004E4942)"
1506 );
1507 }
1508
1509 #[test]
1510 fn test_write_glb_vertex_positions_roundtrip() {
1511 let expected_positions: [[f32; 3]; 3] = [
1512 [0.0_f32, 0.0, 0.0],
1513 [1.0_f32, 0.0, 0.0],
1514 [0.0_f32, 1.0, 0.0],
1515 ];
1516 let scene = make_triangle_scene();
1517 let writer = GlbWriter::new();
1518 let bytes = writer.write_glb(&scene);
1519
1520 let json_chunk_len =
1522 u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1523 let bin_start = 12 + 8 + json_chunk_len;
1524 let bin_chunk_len = u32::from_le_bytes([
1525 bytes[bin_start],
1526 bytes[bin_start + 1],
1527 bytes[bin_start + 2],
1528 bytes[bin_start + 3],
1529 ]) as usize;
1530 let bin_data = &bytes[bin_start + 8..bin_start + 8 + bin_chunk_len];
1531
1532 let n_verts = expected_positions.len();
1534 let pos_size = n_verts * 12; assert!(
1536 bin_data.len() >= pos_size,
1537 "BIN buffer must be large enough for position data"
1538 );
1539
1540 for (i, expected) in expected_positions.iter().enumerate() {
1541 let off = i * 12;
1542 let x = f32::from_le_bytes([
1543 bin_data[off],
1544 bin_data[off + 1],
1545 bin_data[off + 2],
1546 bin_data[off + 3],
1547 ]);
1548 let y = f32::from_le_bytes([
1549 bin_data[off + 4],
1550 bin_data[off + 5],
1551 bin_data[off + 6],
1552 bin_data[off + 7],
1553 ]);
1554 let z = f32::from_le_bytes([
1555 bin_data[off + 8],
1556 bin_data[off + 9],
1557 bin_data[off + 10],
1558 bin_data[off + 11],
1559 ]);
1560 assert!(
1561 (x - expected[0]).abs() < f32::EPSILON,
1562 "position[{i}].x mismatch: {x} vs {}",
1563 expected[0]
1564 );
1565 assert!(
1566 (y - expected[1]).abs() < f32::EPSILON,
1567 "position[{i}].y mismatch: {y} vs {}",
1568 expected[1]
1569 );
1570 assert!(
1571 (z - expected[2]).abs() < f32::EPSILON,
1572 "position[{i}].z mismatch: {z} vs {}",
1573 expected[2]
1574 );
1575 }
1576 }
1577
1578 #[test]
1579 fn test_write_glb_bufferview_offsets_layout() {
1580 let scene = make_triangle_scene();
1587 let writer = GlbWriter::new();
1588 let bytes = writer.write_glb(&scene);
1589
1590 let json_chunk_len =
1592 u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1593 let json_bytes = &bytes[20..20 + json_chunk_len];
1594 let json_str = std::str::from_utf8(json_bytes)
1596 .expect("valid UTF-8")
1597 .trim_end();
1598
1599 assert!(
1601 json_str.contains("\"byteOffset\": 0"),
1602 "position bufferView byteOffset must be 0"
1603 );
1604 assert!(
1605 json_str.contains("\"byteOffset\": 36"),
1606 "normal bufferView byteOffset must be 36"
1607 );
1608 assert!(
1609 json_str.contains("\"byteOffset\": 72"),
1610 "uv bufferView byteOffset must be 72"
1611 );
1612 assert!(
1613 json_str.contains("\"byteOffset\": 96"),
1614 "index bufferView byteOffset must be 96"
1615 );
1616 }
1617
1618 #[test]
1619 fn test_write_glb_json_contains_accessors() {
1620 let scene = make_triangle_scene();
1621 let writer = GlbWriter::new();
1622 let bytes = writer.write_glb(&scene);
1623 let json_chunk_len =
1624 u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;
1625 let json_str = std::str::from_utf8(&bytes[20..20 + json_chunk_len])
1626 .expect("UTF-8")
1627 .trim_end();
1628 assert!(
1629 json_str.contains("\"accessors\""),
1630 "JSON must contain accessors array"
1631 );
1632 assert!(
1633 json_str.contains("\"bufferViews\""),
1634 "JSON must contain bufferViews array"
1635 );
1636 assert!(
1637 json_str.contains("\"buffers\""),
1638 "JSON must contain buffers array"
1639 );
1640 assert!(
1641 json_str.contains("VEC3"),
1642 "accessors must include VEC3 type"
1643 );
1644 assert!(
1645 json_str.contains("VEC2"),
1646 "accessors must include VEC2 type for UV"
1647 );
1648 assert!(
1649 json_str.contains("SCALAR"),
1650 "accessors must include SCALAR type for indices"
1651 );
1652 assert!(
1654 json_str.contains("\"min\""),
1655 "POSITION accessor must have min bounds"
1656 );
1657 assert!(
1658 json_str.contains("\"max\""),
1659 "POSITION accessor must have max bounds"
1660 );
1661 }
1662}