Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions src/export/gltf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,4 +398,189 @@ mod tests {
assert_eq!(&glb[0..4], b"glTF");
assert_eq!(glb.len() % 4, 0);
}

#[test]
fn test_gltf_from_marching_cubes_sphere() {
use crate::marching_cubes::marching_cubes;
let min = Point3D {
index: 0,
x: -2.0,
y: -2.0,
z: -2.0,
};
let max = Point3D {
index: 0,
x: 2.0,
y: 2.0,
z: 2.0,
};
let faces = marching_cubes(
6,
6,
6,
min,
max,
&|x, y, z| x * x + y * y + z * z - 1.0,
0.0,
);
assert!(!faces.is_empty());

let json = faces_to_gltf(&faces);
assert!(json.contains("\"version\":\"2.0\""));
assert!(json.contains("\"generator\":\"meshing\""));
assert!(json.contains("data:application/octet-stream;base64,"));
// Vertex count should match unique vertices
assert!(json.contains("\"componentType\":5126")); // FLOAT
assert!(json.contains("\"componentType\":5125")); // UNSIGNED_INT
}

#[test]
fn test_glb_from_marching_cubes_sphere() {
use crate::marching_cubes::marching_cubes;
let min = Point3D {
index: 0,
x: -2.0,
y: -2.0,
z: -2.0,
};
let max = Point3D {
index: 0,
x: 2.0,
y: 2.0,
z: 2.0,
};
let faces = marching_cubes(
6,
6,
6,
min,
max,
&|x, y, z| x * x + y * y + z * z - 1.0,
0.0,
);
assert!(!faces.is_empty());

let glb = faces_to_glb(&faces);

// Header validation
assert_eq!(&glb[0..4], b"glTF");
let version = u32::from_le_bytes([glb[4], glb[5], glb[6], glb[7]]);
assert_eq!(version, 2);
let total = u32::from_le_bytes([glb[8], glb[9], glb[10], glb[11]]);
assert_eq!(total as usize, glb.len());
assert_eq!(glb.len() % 4, 0);

// JSON chunk
let json_len = u32::from_le_bytes([glb[12], glb[13], glb[14], glb[15]]) as usize;
let json_type = u32::from_le_bytes([glb[16], glb[17], glb[18], glb[19]]);
assert_eq!(json_type, 0x4E4F534A); // "JSON"
assert_eq!(json_len % 4, 0); // padded

let json = std::str::from_utf8(&glb[20..20 + json_len]).unwrap().trim();
assert!(json.contains("\"POSITION\":0"));
assert!(json.contains("\"indices\":1"));

// BIN chunk
let bin_offset = 20 + json_len;
let bin_len = u32::from_le_bytes([
glb[bin_offset],
glb[bin_offset + 1],
glb[bin_offset + 2],
glb[bin_offset + 3],
]) as usize;
let bin_type = u32::from_le_bytes([
glb[bin_offset + 4],
glb[bin_offset + 5],
glb[bin_offset + 6],
glb[bin_offset + 7],
]);
assert_eq!(bin_type, 0x004E4942); // "BIN\0"
assert!(bin_len > 0);
}

#[test]
fn test_glb_accessor_counts_match_mesh() {
let faces = vec![
Face {
a: Point3D {
index: 0,
x: 0.0,
y: 0.0,
z: 0.0,
},
b: Point3D {
index: 1,
x: 1.0,
y: 0.0,
z: 0.0,
},
c: Point3D {
index: 2,
x: 0.0,
y: 1.0,
z: 0.0,
},
},
Face {
a: Point3D {
index: 0,
x: 0.0,
y: 0.0,
z: 0.0,
},
b: Point3D {
index: 2,
x: 0.0,
y: 1.0,
z: 0.0,
},
c: Point3D {
index: 3,
x: 0.0,
y: 0.0,
z: 1.0,
},
},
];
let json = faces_to_gltf(&faces);
// 4 unique vertices, 6 indices
assert!(json.contains("\"count\":4"));
assert!(json.contains("\"count\":6"));
}
Comment on lines +501 to +549
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_glb_accessor_counts_match_mesh calls faces_to_gltf (JSON .gltf) rather than faces_to_glb. Either rename the test to reflect that it validates the glTF JSON output, or switch it to parsing the GLB JSON chunk so the name matches the behavior.

Copilot uses AI. Check for mistakes.

#[test]
fn test_glb_min_max_bounds() {
let face = Face {
a: Point3D {
index: 0,
x: -1.0,
y: -2.0,
z: -3.0,
},
b: Point3D {
index: 1,
x: 4.0,
y: 5.0,
z: 6.0,
},
c: Point3D {
index: 2,
x: 0.0,
y: 0.0,
z: 0.0,
},
};
let json = faces_to_gltf(&[face]);
assert!(json.contains("\"min\":[-1,-2,-3]"));
assert!(json.contains("\"max\":[4,5,6]"));
}
Comment on lines +551 to +576
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_glb_min_max_bounds is validating min/max in the JSON produced by faces_to_gltf, not the GLB output. Rename the test (or validate bounds by extracting and inspecting the GLB JSON chunk) to avoid confusion.

Copilot uses AI. Check for mistakes.

#[test]
fn test_glb_two_chunks_present() {
let glb = faces_to_glb(&[test_face()]);
let json_len = u32::from_le_bytes([glb[12], glb[13], glb[14], glb[15]]) as usize;
// After JSON chunk (8 header + json_len), there should be a BIN chunk
let bin_offset = 20 + json_len;
assert!(glb.len() > bin_offset + 8); // BIN chunk header exists
}
}
141 changes: 141 additions & 0 deletions src/export/gltf_quantized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,4 +416,145 @@ mod tests {
let glb = tetrahedra_to_glb_quantized(&[tet]);
assert_eq!(&glb[0..4], b"glTF");
}

#[test]
fn test_quantized_from_marching_cubes() {
use crate::marching_cubes::marching_cubes;
let min = Point3D {
index: 0,
x: -2.0,
y: -2.0,
z: -2.0,
};
let max = Point3D {
index: 0,
x: 2.0,
y: 2.0,
z: 2.0,
};
let faces = marching_cubes(
6,
6,
6,
min,
max,
&|x, y, z| x * x + y * y + z * z - 1.0,
0.0,
);
assert!(!faces.is_empty());

let glb = faces_to_glb_quantized(&faces);
assert_eq!(&glb[0..4], b"glTF");
let version = u32::from_le_bytes([glb[4], glb[5], glb[6], glb[7]]);
assert_eq!(version, 2);
let total = u32::from_le_bytes([glb[8], glb[9], glb[10], glb[11]]);
assert_eq!(total as usize, glb.len());
assert_eq!(glb.len() % 4, 0);

// Verify extension in JSON
let json_len = u32::from_le_bytes([glb[12], glb[13], glb[14], glb[15]]) as usize;
let json = std::str::from_utf8(&glb[20..20 + json_len]).unwrap().trim();
assert!(json.contains("KHR_mesh_quantization"));
assert!(json.contains("\"componentType\":5122")); // SHORT
assert!(json.contains("\"extensionsRequired\""));
}

#[test]
fn test_quantized_has_node_matrix() {
let glb = faces_to_glb_quantized(&[test_face()]);
let json_len = u32::from_le_bytes([glb[12], glb[13], glb[14], glb[15]]) as usize;
let json = std::str::from_utf8(&glb[20..20 + json_len]).unwrap().trim();
// Node should have a matrix for dequantization
assert!(json.contains("\"matrix\""));
}

#[test]
fn test_quantized_i16_min_max() {
let glb = faces_to_glb_quantized(&[test_face()]);
let json_len = u32::from_le_bytes([glb[12], glb[13], glb[14], glb[15]]) as usize;
let json = std::str::from_utf8(&glb[20..20 + json_len]).unwrap().trim();
// i16 range: [-32767, 32767]
assert!(json.contains("\"max\":[32767,32767,32767]"));
assert!(json.contains("\"min\":[-32767,-32767,-32767]"));
}

#[test]
fn test_quantized_binary_buffer_size() {
// 3 vertices * 3 components * 2 bytes (i16) = 18 bytes, padded to 20
// 3 indices * 4 bytes (u32) = 12 bytes
// total binary = 32 bytes
let glb = faces_to_glb_quantized(&[test_face()]);

let json_len = u32::from_le_bytes([glb[12], glb[13], glb[14], glb[15]]) as usize;
let bin_offset = 20 + json_len;
let bin_len = u32::from_le_bytes([
glb[bin_offset],
glb[bin_offset + 1],
glb[bin_offset + 2],
glb[bin_offset + 3],
]) as usize;
// pos: 3*3*2=18 padded to 20, idx: 3*4=12, total=32
assert_eq!(bin_len, 32);
}

#[test]
fn test_quantized_vs_regular_both_valid() {
let faces = vec![
Face {
a: Point3D {
index: 0,
x: -5.0,
y: -5.0,
z: -5.0,
},
b: Point3D {
index: 1,
x: 5.0,
y: -5.0,
z: -5.0,
},
c: Point3D {
index: 2,
x: 0.0,
y: 5.0,
z: -5.0,
},
},
Face {
a: Point3D {
index: 0,
x: -5.0,
y: -5.0,
z: -5.0,
},
b: Point3D {
index: 2,
x: 0.0,
y: 5.0,
z: -5.0,
},
c: Point3D {
index: 3,
x: 0.0,
y: 0.0,
z: 5.0,
},
},
];
let regular = faces_to_glb(&faces);
let quantized = faces_to_glb_quantized(&faces);

// Both are valid GLB
assert_eq!(&regular[0..4], b"glTF");
assert_eq!(&quantized[0..4], b"glTF");
assert_eq!(regular.len() % 4, 0);
assert_eq!(quantized.len() % 4, 0);

// Lengths match declared size
let reg_total = u32::from_le_bytes([regular[8], regular[9], regular[10], regular[11]]);
assert_eq!(reg_total as usize, regular.len());
let q_total =
u32::from_le_bytes([quantized[8], quantized[9], quantized[10], quantized[11]]);
assert_eq!(q_total as usize, quantized.len());
}
}
Loading