From 850273c0390864c0f6bd00c34f9ef27d03b2807f Mon Sep 17 00:00:00 2001 From: nash1111 Date: Sat, 7 Feb 2026 23:19:50 +0900 Subject: [PATCH] fix: resolve CI failures with fontconfig dependency and code formatting - Add libfontconfig1-dev to both CI workflows (required by plotters) - Update lint workflow Rust toolchain from 1.70.0 to stable - Update actions/checkout to v4 and rust-cache to v2 - Run cargo fmt to fix all formatting issues Closes #41 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/2dtest.yml | 11 +- .github/workflows/lint.yml | 5 +- benches/mesh_benchmarks.rs | 236 +++++++++++++++++++++++----- src/advancing_front.rs | 268 +++++++++++++++++++++++++++----- src/delaunay_refinement.rs | 246 ++++++++++++++++++++++++------ src/export/gltf.rs | 120 ++++++++++++--- src/export/gltf_quantized.rs | 140 ++++++++++++++--- src/export/mod.rs | 4 +- src/export/obj.rs | 146 +++++++++++++++--- src/export/stl.rs | 154 ++++++++++++++++--- src/export/vtk.rs | 127 ++++++++++++--- src/lib.rs | 15 +- src/marching_cubes.rs | 130 ++++++++++++---- src/model/tetrahedron.rs | 3 +- src/octree.rs | 288 ++++++++++++++++++++++++++++++----- src/pipeline.rs | 126 ++++++++++++--- src/voxel_mesh.rs | 138 ++++++++++++++--- src/wasm.rs | 81 +++++++--- 18 files changed, 1861 insertions(+), 377 deletions(-) diff --git a/.github/workflows/2dtest.yml b/.github/workflows/2dtest.yml index 2669c92..f0fe061 100644 --- a/.github/workflows/2dtest.yml +++ b/.github/workflows/2dtest.yml @@ -11,7 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev - name: Set up Rust toolchain uses: dtolnay/rust-toolchain@master @@ -19,11 +22,11 @@ jobs: toolchain: stable components: rustfmt, clippy + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + - name: Check with cargo run: cargo check - name: Test with cargo run: cargo test - - - name: Cache dependencies - uses: Swatinem/rust-cache@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 234ca62..0d5c17f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,10 +11,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev + - name: Set up Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: 1.70.0 + toolchain: stable components: rustfmt, clippy - name: Cache dependencies diff --git a/benches/mesh_benchmarks.rs b/benches/mesh_benchmarks.rs index 96cc63c..f1afb63 100644 --- a/benches/mesh_benchmarks.rs +++ b/benches/mesh_benchmarks.rs @@ -35,22 +35,116 @@ fn bench_bowyer_watson_3d(c: &mut Criterion) { fn bench_advancing_front(c: &mut Criterion) { let p = [ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 1.0, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 0.0, y: 1.0, z: 0.0 }, - Point3D { index: 4, x: 0.0, y: 0.0, z: 1.0 }, - Point3D { index: 5, x: 1.0, y: 0.0, z: 1.0 }, - Point3D { index: 6, x: 1.0, y: 1.0, z: 1.0 }, - Point3D { index: 7, x: 0.0, y: 1.0, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 1.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 0.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 5, + x: 1.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 6, + x: 1.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 7, + x: 0.0, + y: 1.0, + z: 1.0, + }, ]; let faces = vec![ - Face { a: p[0], b: p[1], c: p[2] }, Face { a: p[0], b: p[2], c: p[3] }, - Face { a: p[4], b: p[6], c: p[5] }, Face { a: p[4], b: p[7], c: p[6] }, - Face { a: p[0], b: p[5], c: p[1] }, Face { a: p[0], b: p[4], c: p[5] }, - Face { a: p[2], b: p[7], c: p[3] }, Face { a: p[2], b: p[6], c: p[7] }, - Face { a: p[0], b: p[3], c: p[7] }, Face { a: p[0], b: p[7], c: p[4] }, - Face { a: p[1], b: p[5], c: p[6] }, Face { a: p[1], b: p[6], c: p[2] }, + Face { + a: p[0], + b: p[1], + c: p[2], + }, + Face { + a: p[0], + b: p[2], + c: p[3], + }, + Face { + a: p[4], + b: p[6], + c: p[5], + }, + Face { + a: p[4], + b: p[7], + c: p[6], + }, + Face { + a: p[0], + b: p[5], + c: p[1], + }, + Face { + a: p[0], + b: p[4], + c: p[5], + }, + Face { + a: p[2], + b: p[7], + c: p[3], + }, + Face { + a: p[2], + b: p[6], + c: p[7], + }, + Face { + a: p[0], + b: p[3], + c: p[7], + }, + Face { + a: p[0], + b: p[7], + c: p[4], + }, + Face { + a: p[1], + b: p[5], + c: p[6], + }, + Face { + a: p[1], + b: p[6], + c: p[2], + }, ]; let points = p.to_vec(); c.bench_function("advancing_front (cube)", |b| { @@ -59,18 +153,40 @@ fn bench_advancing_front(c: &mut Criterion) { } fn bench_octree(c: &mut Criterion) { - let min = Point3D { index: 0, x: -1.0, y: -1.0, z: -1.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: -1.0, + y: -1.0, + z: -1.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; c.bench_function("octree_mesh (depth=3, sphere)", |b| { - b.iter(|| octree_mesh(black_box(min), black_box(max), 3, &|p| { - p.x * p.x + p.y * p.y + p.z * p.z <= 1.0 - })) + b.iter(|| { + octree_mesh(black_box(min), black_box(max), 3, &|p| { + p.x * p.x + p.y * p.y + p.z * p.z <= 1.0 + }) + }) }); } fn bench_marching_cubes(c: &mut Criterion) { - 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 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 field = |x: f64, y: f64, z: f64| x * x + y * y + z * z - 1.0; c.bench_function("marching_cubes (20^3, sphere)", |b| { b.iter(|| marching_cubes(20, 20, 20, black_box(min), black_box(max), &field, 0.0)) @@ -78,25 +194,77 @@ fn bench_marching_cubes(c: &mut Criterion) { } fn bench_voxel_mesh(c: &mut Criterion) { - let min = Point3D { index: 0, x: -1.0, y: -1.0, z: -1.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: -1.0, + y: -1.0, + z: -1.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; c.bench_function("voxel_mesh (8^3, sphere)", |b| { - b.iter(|| voxel_mesh(black_box(min), black_box(max), 8, 8, 8, &|p| { - p.x * p.x + p.y * p.y + p.z * p.z <= 1.0 - })) + b.iter(|| { + voxel_mesh(black_box(min), black_box(max), 8, 8, 8, &|p| { + p.x * p.x + p.y * p.y + p.z * p.z <= 1.0 + }) + }) }); } fn bench_delaunay_refinement(c: &mut Criterion) { let points = vec![ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 1.0, y: 1.0, z: 0.0 }, - Point3D { index: 4, x: 0.0, y: 0.0, z: 1.0 }, - Point3D { index: 5, x: 1.0, y: 0.0, z: 1.0 }, - Point3D { index: 6, x: 0.0, y: 1.0, z: 1.0 }, - Point3D { index: 7, x: 1.0, y: 1.0, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 1.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 5, + x: 1.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 6, + x: 0.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 7, + x: 1.0, + y: 1.0, + z: 1.0, + }, ]; c.bench_function("delaunay_refinement (cube, ratio=2.0)", |b| { b.iter(|| delaunay_refinement(black_box(points.clone()), 2.0)) diff --git a/src/advancing_front.rs b/src/advancing_front.rs index e580d96..5b8d579 100644 --- a/src/advancing_front.rs +++ b/src/advancing_front.rs @@ -135,7 +135,9 @@ pub fn advancing_front(faces: Vec, points: Vec) -> Vec 1e-15, "Degenerate tetrahedron found"); + assert!( + tet.signed_volume().abs() > 1e-15, + "Degenerate tetrahedron found" + ); } } #[test] fn test_faces_no_points() { // Faces provided but no points — should still generate via normal projection - let p0 = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let p1 = Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }; - let p2 = Point3D { index: 2, x: 0.5, y: 1.0, z: 0.0 }; - let faces = vec![Face { a: p0, b: p1, c: p2 }]; + let p0 = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let p1 = Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }; + let p2 = Point3D { + index: 2, + x: 0.5, + y: 1.0, + z: 0.0, + }; + let faces = vec![Face { + a: p0, + b: p1, + c: p2, + }]; let result = advancing_front(faces, Vec::new()); assert!(!result.is_empty()); } @@ -263,36 +359,124 @@ mod tests { fn test_cube_surface() { // 8 vertices of a unit cube let p = [ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 1.0, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 0.0, y: 1.0, z: 0.0 }, - Point3D { index: 4, x: 0.0, y: 0.0, z: 1.0 }, - Point3D { index: 5, x: 1.0, y: 0.0, z: 1.0 }, - Point3D { index: 6, x: 1.0, y: 1.0, z: 1.0 }, - Point3D { index: 7, x: 0.0, y: 1.0, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 1.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 0.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 5, + x: 1.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 6, + x: 1.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 7, + x: 0.0, + y: 1.0, + z: 1.0, + }, ]; // 12 triangular faces (2 per cube face, normals pointing outward) let faces = vec![ // Bottom (z=0), normal -z - Face { a: p[0], b: p[1], c: p[2] }, - Face { a: p[0], b: p[2], c: p[3] }, + Face { + a: p[0], + b: p[1], + c: p[2], + }, + Face { + a: p[0], + b: p[2], + c: p[3], + }, // Top (z=1), normal +z - Face { a: p[4], b: p[6], c: p[5] }, - Face { a: p[4], b: p[7], c: p[6] }, + Face { + a: p[4], + b: p[6], + c: p[5], + }, + Face { + a: p[4], + b: p[7], + c: p[6], + }, // Front (y=0), normal -y - Face { a: p[0], b: p[5], c: p[1] }, - Face { a: p[0], b: p[4], c: p[5] }, + Face { + a: p[0], + b: p[5], + c: p[1], + }, + Face { + a: p[0], + b: p[4], + c: p[5], + }, // Back (y=1), normal +y - Face { a: p[2], b: p[7], c: p[3] }, - Face { a: p[2], b: p[6], c: p[7] }, + Face { + a: p[2], + b: p[7], + c: p[3], + }, + Face { + a: p[2], + b: p[6], + c: p[7], + }, // Left (x=0), normal -x - Face { a: p[0], b: p[3], c: p[7] }, - Face { a: p[0], b: p[7], c: p[4] }, + Face { + a: p[0], + b: p[3], + c: p[7], + }, + Face { + a: p[0], + b: p[7], + c: p[4], + }, // Right (x=1), normal +x - Face { a: p[1], b: p[5], c: p[6] }, - Face { a: p[1], b: p[6], c: p[2] }, + Face { + a: p[1], + b: p[5], + c: p[6], + }, + Face { + a: p[1], + b: p[6], + c: p[2], + }, ]; let points = p.to_vec(); diff --git a/src/delaunay_refinement.rs b/src/delaunay_refinement.rs index f4ef4f9..12f6176 100644 --- a/src/delaunay_refinement.rs +++ b/src/delaunay_refinement.rs @@ -51,22 +51,17 @@ fn radius_edge_ratio(tet: &Tetrahedron) -> f64 { /// let refined = delaunay_refinement(points, 2.0); /// assert!(!refined.is_empty()); /// ``` -pub fn delaunay_refinement( - points: Vec, - max_radius_edge_ratio: f64, -) -> Vec { +pub fn delaunay_refinement(points: Vec, max_radius_edge_ratio: f64) -> Vec { let mut refined_points = points.clone(); let mut mesh = bowyer_watson_3d(points); let max_iterations = 100 * refined_points.len(); for _ in 0..max_iterations { - let worst = mesh - .iter() - .max_by(|a, b| { - radius_edge_ratio(a) - .partial_cmp(&radius_edge_ratio(b)) - .unwrap_or(std::cmp::Ordering::Equal) - }); + let worst = mesh.iter().max_by(|a, b| { + radius_edge_ratio(a) + .partial_cmp(&radius_edge_ratio(b)) + .unwrap_or(std::cmp::Ordering::Equal) + }); let worst = match worst { Some(t) => *t, @@ -98,10 +93,30 @@ mod tests { #[test] fn test_regular_tetrahedron_with_loose_threshold() { let points = vec![ - Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }, - Point3D { index: 1, x: 1.0, y: -1.0, z: -1.0 }, - Point3D { index: 2, x: -1.0, y: 1.0, z: -1.0 }, - Point3D { index: 3, x: -1.0, y: -1.0, z: 1.0 }, + Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 1, + x: 1.0, + y: -1.0, + z: -1.0, + }, + Point3D { + index: 2, + x: -1.0, + y: 1.0, + z: -1.0, + }, + Point3D { + index: 3, + x: -1.0, + y: -1.0, + z: 1.0, + }, ]; let result = delaunay_refinement(points, 10.0); assert_eq!(result.len(), 1); @@ -110,14 +125,54 @@ mod tests { #[test] fn test_cube_vertices_produce_valid_mesh() { let points = vec![ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 1.0, y: 1.0, z: 0.0 }, - Point3D { index: 4, x: 0.0, y: 0.0, z: 1.0 }, - Point3D { index: 5, x: 1.0, y: 0.0, z: 1.0 }, - Point3D { index: 6, x: 0.0, y: 1.0, z: 1.0 }, - Point3D { index: 7, x: 1.0, y: 1.0, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 1.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 5, + x: 1.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 6, + x: 0.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 7, + x: 1.0, + y: 1.0, + z: 1.0, + }, ]; let result = delaunay_refinement(points, 2.0); assert!(!result.is_empty()); @@ -126,14 +181,54 @@ mod tests { #[test] fn test_tight_threshold_adds_points() { let points = vec![ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 1.0, y: 1.0, z: 0.0 }, - Point3D { index: 4, x: 0.0, y: 0.0, z: 1.0 }, - Point3D { index: 5, x: 1.0, y: 0.0, z: 1.0 }, - Point3D { index: 6, x: 0.0, y: 1.0, z: 1.0 }, - Point3D { index: 7, x: 1.0, y: 1.0, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 1.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 5, + x: 1.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 6, + x: 0.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 7, + x: 1.0, + y: 1.0, + z: 1.0, + }, ]; let initial = bowyer_watson_3d(points.clone()); // Moderate threshold triggers some refinement @@ -144,28 +239,91 @@ mod tests { #[test] fn test_all_refined_tetrahedra_have_nonzero_volume() { let points = vec![ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 0.5, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 0.5, y: 0.5, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 0.5, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 0.5, + y: 0.5, + z: 1.0, + }, ]; let result = delaunay_refinement(points, 2.0); for tet in &result { - assert!(tet.signed_volume().abs() > 1e-15, "Degenerate tetrahedron found"); + assert!( + tet.signed_volume().abs() > 1e-15, + "Degenerate tetrahedron found" + ); } } #[test] fn test_refinement_produces_at_least_as_many_tets() { let points = vec![ - Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }, - Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }, - Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }, - Point3D { index: 3, x: 1.0, y: 1.0, z: 0.0 }, - Point3D { index: 4, x: 0.0, y: 0.0, z: 1.0 }, - Point3D { index: 5, x: 1.0, y: 0.0, z: 1.0 }, - Point3D { index: 6, x: 0.0, y: 1.0, z: 1.0 }, - Point3D { index: 7, x: 1.0, y: 1.0, z: 1.0 }, + Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 3, + x: 1.0, + y: 1.0, + z: 0.0, + }, + Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 5, + x: 1.0, + y: 0.0, + z: 1.0, + }, + Point3D { + index: 6, + x: 0.0, + y: 1.0, + z: 1.0, + }, + Point3D { + index: 7, + x: 1.0, + y: 1.0, + z: 1.0, + }, ]; let initial = bowyer_watson_3d(points.clone()); let refined = delaunay_refinement(points, 2.0); diff --git a/src/export/gltf.rs b/src/export/gltf.rs index 9243ad7..00e7709 100644 --- a/src/export/gltf.rs +++ b/src/export/gltf.rs @@ -49,17 +49,30 @@ fn collect_mesh_data(faces: &[Face]) -> MeshData { for (_, v) in &vertices { let coords = [v.x as f32, v.y as f32, v.z as f32]; for i in 0..3 { - if coords[i] < min[i] { min[i] = coords[i]; } - if coords[i] > max[i] { max[i] = coords[i]; } + if coords[i] < min[i] { + min[i] = coords[i]; + } + if coords[i] > max[i] { + max[i] = coords[i]; + } } positions.extend_from_slice(&coords); } let mut indices = Vec::with_capacity(faces.len() * 3); for face in faces { - let a = vertices.iter().position(|(idx, _)| *idx == face.a.index).unwrap() as u32; - let b = vertices.iter().position(|(idx, _)| *idx == face.b.index).unwrap() as u32; - let c = vertices.iter().position(|(idx, _)| *idx == face.c.index).unwrap() as u32; + let a = vertices + .iter() + .position(|(idx, _)| *idx == face.a.index) + .unwrap() as u32; + let b = vertices + .iter() + .position(|(idx, _)| *idx == face.b.index) + .unwrap() as u32; + let c = vertices + .iter() + .position(|(idx, _)| *idx == face.c.index) + .unwrap() as u32; indices.push(a); indices.push(b); indices.push(c); @@ -70,7 +83,12 @@ fn collect_mesh_data(faces: &[Face]) -> MeshData { max = [0.0; 3]; } - MeshData { positions, indices, min, max } + MeshData { + positions, + indices, + min, + max, + } } fn build_binary_buffer(data: &MeshData) -> Vec { @@ -195,19 +213,22 @@ pub fn faces_to_glb(faces: &[Face]) -> Vec { let mut glb = Vec::with_capacity(total_length); // GLB Header - glb.extend_from_slice(b"glTF"); // magic - glb.extend_from_slice(&2u32.to_le_bytes()); // version + glb.extend_from_slice(b"glTF"); // magic + glb.extend_from_slice(&2u32.to_le_bytes()); // version glb.extend_from_slice(&(total_length as u32).to_le_bytes()); // total length // JSON chunk glb.extend_from_slice(&(json_padded_len as u32).to_le_bytes()); // chunk length - glb.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); // chunk type "JSON" + glb.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); // chunk type "JSON" glb.extend_from_slice(json_bytes); - glb.extend(std::iter::repeat_n(b' ', json_padded_len - json_bytes.len())); + glb.extend(std::iter::repeat_n( + b' ', + json_padded_len - json_bytes.len(), + )); // Binary chunk glb.extend_from_slice(&(bin_padded_len as u32).to_le_bytes()); // chunk length - glb.extend_from_slice(&0x004E4942u32.to_le_bytes()); // chunk type "BIN\0" + glb.extend_from_slice(&0x004E4942u32.to_le_bytes()); // chunk type "BIN\0" glb.extend_from_slice(&bin_buffer); glb.extend(std::iter::repeat_n(0u8, bin_padded_len - bin_buffer.len())); @@ -232,9 +253,24 @@ mod tests { fn test_face() -> Face { 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 }, + 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, + }, } } @@ -301,10 +337,30 @@ mod tests { #[test] fn test_tetrahedra_to_gltf() { let tet = Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, }; let json = tetrahedra_to_gltf(&[tet]); assert!(json.contains("\"version\":\"2.0\"")); @@ -313,10 +369,30 @@ mod tests { #[test] fn test_tetrahedra_to_glb() { let tet = Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, }; let glb = tetrahedra_to_glb(&[tet]); assert_eq!(&glb[0..4], b"glTF"); diff --git a/src/export/gltf_quantized.rs b/src/export/gltf_quantized.rs index f5f1676..1c7d05e 100644 --- a/src/export/gltf_quantized.rs +++ b/src/export/gltf_quantized.rs @@ -45,9 +45,21 @@ pub fn faces_to_glb_quantized(faces: &[Face]) -> Vec { // Compute scale and offset for quantization let scale = [ - if bb_max[0] > bb_min[0] { bb_max[0] - bb_min[0] } else { 1.0 }, - if bb_max[1] > bb_min[1] { bb_max[1] - bb_min[1] } else { 1.0 }, - if bb_max[2] > bb_min[2] { bb_max[2] - bb_min[2] } else { 1.0 }, + if bb_max[0] > bb_min[0] { + bb_max[0] - bb_min[0] + } else { + 1.0 + }, + if bb_max[1] > bb_min[1] { + bb_max[1] - bb_min[1] + } else { + 1.0 + }, + if bb_max[2] > bb_min[2] { + bb_max[2] - bb_min[2] + } else { + 1.0 + }, ]; let offset = bb_min; @@ -109,7 +121,10 @@ fn collect_unique_vertices(faces: &[Face]) -> (Vec<[f32; 3]>, Vec) { let mut indices = Vec::with_capacity(faces.len() * 3); for face in faces { for pt in [face.a, face.b, face.c] { - let pos = vertex_list.iter().position(|(idx, _)| *idx == pt.index).unwrap(); + let pos = vertex_list + .iter() + .position(|(idx, _)| *idx == pt.index) + .unwrap(); indices.push(pos as u32); } } @@ -123,8 +138,12 @@ fn bounding_box(vertices: &[[f32; 3]]) -> ([f32; 3], [f32; 3]) { let mut max = [f32::MIN; 3]; for v in vertices { for i in 0..3 { - if v[i] < min[i] { min[i] = v[i]; } - if v[i] > max[i] { max[i] = v[i]; } + if v[i] < min[i] { + min[i] = v[i]; + } + if v[i] > max[i] { + max[i] = v[i]; + } } } (min, max) @@ -189,7 +208,14 @@ fn build_glb(json_str: &str, bin_buffer: &[u8]) -> Vec { let json_padded_len = (json_bytes.len() + 3) & !3; let bin_padded_len = (bin_buffer.len() + 3) & !3; - let total_length = 12 + 8 + json_padded_len + if bin_buffer.is_empty() { 0 } else { 8 + bin_padded_len }; + let total_length = 12 + + 8 + + json_padded_len + + if bin_buffer.is_empty() { + 0 + } else { + 8 + bin_padded_len + }; let mut glb = Vec::with_capacity(total_length); @@ -202,7 +228,10 @@ fn build_glb(json_str: &str, bin_buffer: &[u8]) -> Vec { glb.extend_from_slice(&(json_padded_len as u32).to_le_bytes()); glb.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); glb.extend_from_slice(json_bytes); - glb.extend(std::iter::repeat_n(b' ', json_padded_len - json_bytes.len())); + glb.extend(std::iter::repeat_n( + b' ', + json_padded_len - json_bytes.len(), + )); // Binary chunk (only if there's data) if !bin_buffer.is_empty() { @@ -223,9 +252,24 @@ mod tests { fn test_face() -> Face { 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 }, + 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, + }, } } @@ -263,14 +307,44 @@ mod tests { // may make the total GLB larger, but vertex data savings scale. 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 }, + 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: 1, x: 1.0, y: 0.0, z: 0.0 }, - b: Point3D { index: 3, x: 1.0, y: 1.0, z: 0.0 }, - c: Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }, + a: Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }, + b: Point3D { + index: 3, + x: 1.0, + y: 1.0, + z: 0.0, + }, + c: Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }, }, ]; let regular = faces_to_glb(&faces); @@ -279,7 +353,9 @@ mod tests { assert_eq!(®ular[0..4], b"glTF"); assert_eq!(&quantized[0..4], b"glTF"); // Quantized uses int16 component type - let q_json_len = u32::from_le_bytes([quantized[12], quantized[13], quantized[14], quantized[15]]) as usize; + let q_json_len = + u32::from_le_bytes([quantized[12], quantized[13], quantized[14], quantized[15]]) + as usize; let q_json = std::str::from_utf8(&quantized[20..20 + q_json_len]).unwrap(); assert!(q_json.contains("5122")); // SHORT component type } @@ -312,10 +388,30 @@ mod tests { #[test] fn test_tetrahedra_to_glb_quantized() { let tet = Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, }; let glb = tetrahedra_to_glb_quantized(&[tet]); assert_eq!(&glb[0..4], b"glTF"); diff --git a/src/export/mod.rs b/src/export/mod.rs index 8c9d7e2..ad8ce38 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -7,7 +7,5 @@ mod vtk; pub use gltf::{faces_to_glb, faces_to_gltf, tetrahedra_to_glb, tetrahedra_to_gltf}; pub use gltf_quantized::{faces_to_glb_quantized, tetrahedra_to_glb_quantized}; pub use obj::{faces_to_obj, tetrahedra_to_obj, triangles_to_obj}; -pub use stl::{ - extract_surface_faces, faces_to_stl, tetrahedra_to_stl, triangles_to_stl, -}; +pub use stl::{extract_surface_faces, faces_to_stl, tetrahedra_to_stl, triangles_to_stl}; pub use vtk::tetrahedra_to_vtk; diff --git a/src/export/obj.rs b/src/export/obj.rs index b648c02..c5d4ee3 100644 --- a/src/export/obj.rs +++ b/src/export/obj.rs @@ -24,9 +24,21 @@ pub fn triangles_to_obj(triangles: &[Triangle]) -> String { } for triangle in triangles { - let a_pos = vertices.iter().position(|(idx, _, _)| *idx == triangle.a.index).unwrap() + 1; - let b_pos = vertices.iter().position(|(idx, _, _)| *idx == triangle.b.index).unwrap() + 1; - let c_pos = vertices.iter().position(|(idx, _, _)| *idx == triangle.c.index).unwrap() + 1; + let a_pos = vertices + .iter() + .position(|(idx, _, _)| *idx == triangle.a.index) + .unwrap() + + 1; + let b_pos = vertices + .iter() + .position(|(idx, _, _)| *idx == triangle.b.index) + .unwrap() + + 1; + let c_pos = vertices + .iter() + .position(|(idx, _, _)| *idx == triangle.c.index) + .unwrap() + + 1; result.push_str(&format!("f {} {} {}\n", a_pos, b_pos, c_pos)); } @@ -61,9 +73,21 @@ pub fn faces_to_obj(faces: &[Face]) -> String { } for face in faces { - let a_pos = vertices.iter().position(|(idx, _)| *idx == face.a.index).unwrap() + 1; - let b_pos = vertices.iter().position(|(idx, _)| *idx == face.b.index).unwrap() + 1; - let c_pos = vertices.iter().position(|(idx, _)| *idx == face.c.index).unwrap() + 1; + let a_pos = vertices + .iter() + .position(|(idx, _)| *idx == face.a.index) + .unwrap() + + 1; + let b_pos = vertices + .iter() + .position(|(idx, _)| *idx == face.b.index) + .unwrap() + + 1; + let c_pos = vertices + .iter() + .position(|(idx, _)| *idx == face.c.index) + .unwrap() + + 1; result.push_str(&format!("f {} {} {}\n", a_pos, b_pos, c_pos)); } @@ -84,9 +108,21 @@ mod tests { #[test] fn test_triangles_to_obj_single_triangle() { let triangles = vec![Triangle { - a: Point2D { index: 0, x: 0.0, y: 0.0 }, - b: Point2D { index: 1, x: 1.0, y: 0.0 }, - c: Point2D { index: 2, x: 0.0, y: 1.0 }, + a: Point2D { + index: 0, + x: 0.0, + y: 0.0, + }, + b: Point2D { + index: 1, + x: 1.0, + y: 0.0, + }, + c: Point2D { + index: 2, + x: 0.0, + y: 1.0, + }, }]; let result = triangles_to_obj(&triangles); @@ -100,20 +136,45 @@ mod tests { fn test_triangles_to_obj_shared_vertices() { let triangles = vec![ Triangle { - a: Point2D { index: 0, x: 0.0, y: 0.0 }, - b: Point2D { index: 1, x: 1.0, y: 0.0 }, - c: Point2D { index: 2, x: 0.0, y: 1.0 }, + a: Point2D { + index: 0, + x: 0.0, + y: 0.0, + }, + b: Point2D { + index: 1, + x: 1.0, + y: 0.0, + }, + c: Point2D { + index: 2, + x: 0.0, + y: 1.0, + }, }, Triangle { - a: Point2D { index: 1, x: 1.0, y: 0.0 }, - b: Point2D { index: 3, x: 1.0, y: 1.0 }, - c: Point2D { index: 2, x: 0.0, y: 1.0 }, + a: Point2D { + index: 1, + x: 1.0, + y: 0.0, + }, + b: Point2D { + index: 3, + x: 1.0, + y: 1.0, + }, + c: Point2D { + index: 2, + x: 0.0, + y: 1.0, + }, }, ]; let result = triangles_to_obj(&triangles); // Should have 4 unique vertices, not 6 - let vertex_count = result.matches("\nv ").count() + if result.starts_with("v ") { 1 } else { 0 }; + let vertex_count = + result.matches("\nv ").count() + if result.starts_with("v ") { 1 } else { 0 }; assert_eq!(vertex_count, 4); // Two face lines let face_count = result.matches("f ").count(); @@ -123,14 +184,34 @@ mod tests { #[test] fn test_tetrahedra_to_obj_single() { let tet = Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, }; let result = tetrahedra_to_obj(&[tet]); - let vertex_count = result.matches("\nv ").count() - + if result.starts_with("v ") { 1 } else { 0 }; + let vertex_count = + result.matches("\nv ").count() + if result.starts_with("v ") { 1 } else { 0 }; assert_eq!(vertex_count, 4); let face_count = result.matches("f ").count(); assert_eq!(face_count, 4); @@ -145,9 +226,24 @@ mod tests { #[test] fn test_faces_to_obj_3d_vertices() { 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: 7.0, y: 8.0, z: 9.0 }, + 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: 7.0, + y: 8.0, + z: 9.0, + }, }; let result = faces_to_obj(&[face]); assert!(result.contains("v 1 2 3")); diff --git a/src/export/stl.rs b/src/export/stl.rs index d526a71..5fe30e9 100644 --- a/src/export/stl.rs +++ b/src/export/stl.rs @@ -96,9 +96,21 @@ mod tests { #[test] fn test_triangles_to_stl_single_triangle() { let triangles = vec![Triangle { - a: Point2D { index: 0, x: 0.0, y: 0.0 }, - b: Point2D { index: 1, x: 1.0, y: 0.0 }, - c: Point2D { index: 2, x: 0.0, y: 1.0 }, + a: Point2D { + index: 0, + x: 0.0, + y: 0.0, + }, + b: Point2D { + index: 1, + x: 1.0, + y: 0.0, + }, + c: Point2D { + index: 2, + x: 0.0, + y: 1.0, + }, }]; let result = triangles_to_stl(&triangles, "mesh"); @@ -114,14 +126,38 @@ mod tests { fn test_triangles_to_stl_multiple_triangles() { let triangles = vec![ Triangle { - a: Point2D { index: 0, x: 0.0, y: 0.0 }, - b: Point2D { index: 1, x: 1.0, y: 0.0 }, - c: Point2D { index: 2, x: 0.0, y: 1.0 }, + a: Point2D { + index: 0, + x: 0.0, + y: 0.0, + }, + b: Point2D { + index: 1, + x: 1.0, + y: 0.0, + }, + c: Point2D { + index: 2, + x: 0.0, + y: 1.0, + }, }, Triangle { - a: Point2D { index: 1, x: 1.0, y: 0.0 }, - b: Point2D { index: 3, x: 1.0, y: 1.0 }, - c: Point2D { index: 2, x: 0.0, y: 1.0 }, + a: Point2D { + index: 1, + x: 1.0, + y: 0.0, + }, + b: Point2D { + index: 3, + x: 1.0, + y: 1.0, + }, + c: Point2D { + index: 2, + x: 0.0, + y: 1.0, + }, }, ]; @@ -132,10 +168,30 @@ mod tests { fn single_tet() -> Tetrahedron { Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, } } @@ -158,9 +214,24 @@ mod tests { #[test] fn test_faces_to_stl_computes_normal() { let face = 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 }, + 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, + }, }; let result = faces_to_stl(&[face], "test"); // Normal should be (0, 0, 1) for a face in the XY plane @@ -176,14 +247,49 @@ mod tests { #[test] fn test_extract_surface_shared_face_excluded() { // Two tetrahedra sharing a face — the shared face should be excluded - let p0 = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let p1 = Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }; - let p2 = Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }; - let p3 = Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }; - let p4 = Point3D { index: 4, x: 0.0, y: 0.0, z: -1.0 }; - - let tet1 = Tetrahedron { a: p0, b: p1, c: p2, d: p3 }; - let tet2 = Tetrahedron { a: p0, b: p1, c: p2, d: p4 }; + let p0 = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let p1 = Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }; + let p2 = Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }; + let p3 = Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }; + let p4 = Point3D { + index: 4, + x: 0.0, + y: 0.0, + z: -1.0, + }; + + let tet1 = Tetrahedron { + a: p0, + b: p1, + c: p2, + d: p3, + }; + let tet2 = Tetrahedron { + a: p0, + b: p1, + c: p2, + d: p4, + }; let surface = extract_surface_faces(&[tet1, tet2]); // 2 tets * 4 faces = 8 total, minus 2 (shared face counted in both) = 6 assert_eq!(surface.len(), 6); diff --git a/src/export/vtk.rs b/src/export/vtk.rs index f5ad08a..d2f0c56 100644 --- a/src/export/vtk.rs +++ b/src/export/vtk.rs @@ -63,10 +63,22 @@ pub fn tetrahedra_to_vtk(tetrahedra: &[Tetrahedron], title: &str) -> String { let cell_list_size = num_cells * 5; result.push_str(&format!("CELLS {} {}\n", num_cells, cell_list_size)); for tet in tetrahedra { - let a = vertices.iter().position(|(idx, _)| *idx == tet.a.index).unwrap(); - let b = vertices.iter().position(|(idx, _)| *idx == tet.b.index).unwrap(); - let c = vertices.iter().position(|(idx, _)| *idx == tet.c.index).unwrap(); - let d = vertices.iter().position(|(idx, _)| *idx == tet.d.index).unwrap(); + let a = vertices + .iter() + .position(|(idx, _)| *idx == tet.a.index) + .unwrap(); + let b = vertices + .iter() + .position(|(idx, _)| *idx == tet.b.index) + .unwrap(); + let c = vertices + .iter() + .position(|(idx, _)| *idx == tet.c.index) + .unwrap(); + let d = vertices + .iter() + .position(|(idx, _)| *idx == tet.d.index) + .unwrap(); result.push_str(&format!("4 {} {} {} {}\n", a, b, c, d)); } @@ -85,10 +97,30 @@ mod tests { fn single_tet() -> Tetrahedron { Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, } } @@ -121,14 +153,49 @@ mod tests { #[test] fn test_vtk_shared_vertices() { - let p0 = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let p1 = Point3D { index: 1, x: 1.0, y: 0.0, z: 0.0 }; - let p2 = Point3D { index: 2, x: 0.0, y: 1.0, z: 0.0 }; - let p3 = Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }; - let p4 = Point3D { index: 4, x: 1.0, y: 1.0, z: 1.0 }; - - let tet1 = Tetrahedron { a: p0, b: p1, c: p2, d: p3 }; - let tet2 = Tetrahedron { a: p1, b: p2, c: p3, d: p4 }; + let p0 = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let p1 = Point3D { + index: 1, + x: 1.0, + y: 0.0, + z: 0.0, + }; + let p2 = Point3D { + index: 2, + x: 0.0, + y: 1.0, + z: 0.0, + }; + let p3 = Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }; + let p4 = Point3D { + index: 4, + x: 1.0, + y: 1.0, + z: 1.0, + }; + + let tet1 = Tetrahedron { + a: p0, + b: p1, + c: p2, + d: p3, + }; + let tet2 = Tetrahedron { + a: p1, + b: p2, + c: p3, + d: p4, + }; let result = tetrahedra_to_vtk(&[tet1, tet2], "shared"); assert!(result.contains("POINTS 5 double")); assert!(result.contains("CELLS 2 10")); @@ -138,10 +205,30 @@ mod tests { #[test] fn test_vtk_coordinates() { let tet = Tetrahedron { - a: Point3D { index: 0, x: 1.5, y: 2.5, z: 3.5 }, - b: Point3D { index: 1, x: 4.0, y: 5.0, z: 6.0 }, - c: Point3D { index: 2, x: 7.0, y: 8.0, z: 9.0 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 0.0 }, + a: Point3D { + index: 0, + x: 1.5, + y: 2.5, + z: 3.5, + }, + b: Point3D { + index: 1, + x: 4.0, + y: 5.0, + z: 6.0, + }, + c: Point3D { + index: 2, + x: 7.0, + y: 8.0, + z: 9.0, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 0.0, + }, }; let result = tetrahedra_to_vtk(&[tet], "coords"); assert!(result.contains("1.5 2.5 3.5")); diff --git a/src/lib.rs b/src/lib.rs index 0b467f7..20dccb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,18 +22,18 @@ pub use model::{Circle, Edge, Face, Point2D, Point3D, Sphere, Tetrahedron, Trian use tetrahedron_utils::remove_tetrahedra_with_vertices_from_super_tetrahedron; use triangle_utils::remove_triangles_with_vertices_from_super_triangle; +pub mod advancing_front; +pub mod delaunay_refinement; pub mod error; pub mod export; mod geometry; mod geometry_3d; -mod model; -mod tetrahedron_utils; -mod triangle_utils; -pub mod advancing_front; -pub mod delaunay_refinement; pub mod marching_cubes; +mod model; pub mod octree; pub mod pipeline; +mod tetrahedron_utils; +mod triangle_utils; pub mod voxel_mesh; #[cfg(target_arch = "wasm32")] pub mod wasm; @@ -172,10 +172,7 @@ pub fn bowyer_watson_3d(points: Vec) -> Vec { } } - remove_tetrahedra_with_vertices_from_super_tetrahedron( - &tetrahedralization, - &super_tetrahedron, - ) + remove_tetrahedra_with_vertices_from_super_tetrahedron(&tetrahedralization, &super_tetrahedron) } #[cfg(test)] diff --git a/src/marching_cubes.rs b/src/marching_cubes.rs index 5e7367e..0a08f38 100644 --- a/src/marching_cubes.rs +++ b/src/marching_cubes.rs @@ -396,20 +396,19 @@ pub fn marching_cubes( // 8 corner positions of this cube (standard ordering) let corners = [ - (x, y, z), // 0 - (x + dx, y, z), // 1 - (x + dx, y + dy, z), // 2 - (x, y + dy, z), // 3 - (x, y, z + dz), // 4 - (x + dx, y, z + dz), // 5 - (x + dx, y + dy, z + dz), // 6 - (x, y + dy, z + dz), // 7 + (x, y, z), // 0 + (x + dx, y, z), // 1 + (x + dx, y + dy, z), // 2 + (x, y + dy, z), // 3 + (x, y, z + dz), // 4 + (x + dx, y, z + dz), // 5 + (x + dx, y + dy, z + dz), // 6 + (x, y + dy, z + dz), // 7 ]; // Sample scalar field at each corner - let values: [f64; 8] = std::array::from_fn(|n| { - scalar_field(corners[n].0, corners[n].1, corners[n].2) - }); + let values: [f64; 8] = + std::array::from_fn(|n| scalar_field(corners[n].0, corners[n].1, corners[n].2)); // Build cube index: bit n is set if corner n is above the iso-value let mut cube_index: usize = 0; @@ -487,8 +486,18 @@ mod tests { #[test] fn test_sphere_isosurface() { - 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 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 field = |x: f64, y: f64, z: f64| x * x + y * y + z * z - 1.0; let faces = marching_cubes(10, 10, 10, min, max, &field, 0.0); assert!(!faces.is_empty(), "Sphere should produce faces"); @@ -496,8 +505,18 @@ mod tests { #[test] fn test_all_above_iso_returns_empty() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; // Field always returns 10.0, iso_value is 0.0 → all corners above iso let field = |_x: f64, _y: f64, _z: f64| 10.0; let faces = marching_cubes(5, 5, 5, min, max, &field, 0.0); @@ -506,8 +525,18 @@ mod tests { #[test] fn test_faces_have_distinct_vertices() { - 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 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 field = |x: f64, y: f64, z: f64| x * x + y * y + z * z - 1.0; let faces = marching_cubes(10, 10, 10, min, max, &field, 0.0); for face in &faces { @@ -515,15 +544,27 @@ mod tests { let ab = (a.x - b.x).abs() + (a.y - b.y).abs() + (a.z - b.z).abs(); let ac = (a.x - c.x).abs() + (a.y - c.y).abs() + (a.z - c.z).abs(); let bc = (b.x - c.x).abs() + (b.y - c.y).abs() + (b.z - c.z).abs(); - assert!(ab > 1e-15 || ac > 1e-15 || bc > 1e-15, - "Face should have at least two distinct vertices"); + assert!( + ab > 1e-15 || ac > 1e-15 || bc > 1e-15, + "Face should have at least two distinct vertices" + ); } } #[test] fn test_all_below_iso_returns_empty() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let field = |_x: f64, _y: f64, _z: f64| -10.0; let faces = marching_cubes(5, 5, 5, min, max, &field, 0.0); assert!(faces.is_empty(), "All below iso should produce no faces"); @@ -531,18 +572,41 @@ mod tests { #[test] fn test_higher_resolution_produces_more_faces() { - 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 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 field = |x: f64, y: f64, z: f64| x * x + y * y + z * z - 1.0; let low = marching_cubes(5, 5, 5, min, max, &field, 0.0); let high = marching_cubes(10, 10, 10, min, max, &field, 0.0); - assert!(high.len() > low.len(), "Higher resolution should produce more faces"); + assert!( + high.len() > low.len(), + "Higher resolution should produce more faces" + ); } #[test] fn test_torus_isosurface() { - let min = Point3D { index: 0, x: -3.0, y: -3.0, z: -1.5 }; - let max = Point3D { index: 0, x: 3.0, y: 3.0, z: 1.5 }; + let min = Point3D { + index: 0, + x: -3.0, + y: -3.0, + z: -1.5, + }; + let max = Point3D { + index: 0, + x: 3.0, + y: 3.0, + z: 1.5, + }; // Torus: (sqrt(x²+y²) - R)² + z² - r² = 0, R=2, r=0.5 let field = |x: f64, y: f64, z: f64| { let r_big = 2.0; @@ -556,8 +620,18 @@ mod tests { #[test] fn test_plane_isosurface() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let field = |_x: f64, _y: f64, z: f64| z; let faces = marching_cubes(5, 5, 5, min, max, &field, 0.5); assert!(!faces.is_empty(), "Plane isosurface should produce faces"); diff --git a/src/model/tetrahedron.rs b/src/model/tetrahedron.rs index 10fd4a4..15aabb4 100644 --- a/src/model/tetrahedron.rs +++ b/src/model/tetrahedron.rs @@ -31,8 +31,7 @@ impl Tetrahedron { let c_sq = cx * cx + cy * cy + cz * cz; let d_sq = dx * dx + dy * dy + dz * dz; - let det = bx * (cy * dz - cz * dy) - by * (cx * dz - cz * dx) - + bz * (cx * dy - cy * dx); + let det = bx * (cy * dz - cz * dy) - by * (cx * dz - cz * dx) + bz * (cx * dy - cy * dx); let inv_det = 1.0 / (2.0 * det); let ux = (b_sq * (cy * dz - cz * dy) - c_sq * (by * dz - bz * dy) diff --git a/src/octree.rs b/src/octree.rs index f39be9b..403e469 100644 --- a/src/octree.rs +++ b/src/octree.rs @@ -32,22 +32,87 @@ fn subdivide( let idx = *next_index; *next_index += 8; let p = [ - Point3D { index: idx, x: b.min_x, y: b.min_y, z: b.min_z }, - Point3D { index: idx + 1, x: b.max_x, y: b.min_y, z: b.min_z }, - Point3D { index: idx + 2, x: b.max_x, y: b.max_y, z: b.min_z }, - Point3D { index: idx + 3, x: b.min_x, y: b.max_y, z: b.min_z }, - Point3D { index: idx + 4, x: b.min_x, y: b.min_y, z: b.max_z }, - Point3D { index: idx + 5, x: b.max_x, y: b.min_y, z: b.max_z }, - Point3D { index: idx + 6, x: b.max_x, y: b.max_y, z: b.max_z }, - Point3D { index: idx + 7, x: b.min_x, y: b.max_y, z: b.max_z }, + Point3D { + index: idx, + x: b.min_x, + y: b.min_y, + z: b.min_z, + }, + Point3D { + index: idx + 1, + x: b.max_x, + y: b.min_y, + z: b.min_z, + }, + Point3D { + index: idx + 2, + x: b.max_x, + y: b.max_y, + z: b.min_z, + }, + Point3D { + index: idx + 3, + x: b.min_x, + y: b.max_y, + z: b.min_z, + }, + Point3D { + index: idx + 4, + x: b.min_x, + y: b.min_y, + z: b.max_z, + }, + Point3D { + index: idx + 5, + x: b.max_x, + y: b.min_y, + z: b.max_z, + }, + Point3D { + index: idx + 6, + x: b.max_x, + y: b.max_y, + z: b.max_z, + }, + Point3D { + index: idx + 7, + x: b.min_x, + y: b.max_y, + z: b.max_z, + }, ]; // Standard 5-tetrahedra decomposition of a hexahedron - tetrahedra.push(Tetrahedron { a: p[0], b: p[1], c: p[3], d: p[4] }); - tetrahedra.push(Tetrahedron { a: p[1], b: p[2], c: p[3], d: p[6] }); - tetrahedra.push(Tetrahedron { a: p[1], b: p[4], c: p[5], d: p[6] }); - tetrahedra.push(Tetrahedron { a: p[3], b: p[4], c: p[6], d: p[7] }); - tetrahedra.push(Tetrahedron { a: p[1], b: p[3], c: p[4], d: p[6] }); + tetrahedra.push(Tetrahedron { + a: p[0], + b: p[1], + c: p[3], + d: p[4], + }); + tetrahedra.push(Tetrahedron { + a: p[1], + b: p[2], + c: p[3], + d: p[6], + }); + tetrahedra.push(Tetrahedron { + a: p[1], + b: p[4], + c: p[5], + d: p[6], + }); + tetrahedra.push(Tetrahedron { + a: p[3], + b: p[4], + c: p[6], + d: p[7], + }); + tetrahedra.push(Tetrahedron { + a: p[1], + b: p[3], + c: p[4], + d: p[6], + }); return; } @@ -56,18 +121,81 @@ fn subdivide( let mid_z = (b.min_z + b.max_z) / 2.0; let octants = [ - Bounds { min_x: b.min_x, min_y: b.min_y, min_z: b.min_z, max_x: mid_x, max_y: mid_y, max_z: mid_z }, - Bounds { min_x: mid_x, min_y: b.min_y, min_z: b.min_z, max_x: b.max_x, max_y: mid_y, max_z: mid_z }, - Bounds { min_x: b.min_x, min_y: mid_y, min_z: b.min_z, max_x: mid_x, max_y: b.max_y, max_z: mid_z }, - Bounds { min_x: mid_x, min_y: mid_y, min_z: b.min_z, max_x: b.max_x, max_y: b.max_y, max_z: mid_z }, - Bounds { min_x: b.min_x, min_y: b.min_y, min_z: mid_z, max_x: mid_x, max_y: mid_y, max_z: b.max_z }, - Bounds { min_x: mid_x, min_y: b.min_y, min_z: mid_z, max_x: b.max_x, max_y: mid_y, max_z: b.max_z }, - Bounds { min_x: b.min_x, min_y: mid_y, min_z: mid_z, max_x: mid_x, max_y: b.max_y, max_z: b.max_z }, - Bounds { min_x: mid_x, min_y: mid_y, min_z: mid_z, max_x: b.max_x, max_y: b.max_y, max_z: b.max_z }, + Bounds { + min_x: b.min_x, + min_y: b.min_y, + min_z: b.min_z, + max_x: mid_x, + max_y: mid_y, + max_z: mid_z, + }, + Bounds { + min_x: mid_x, + min_y: b.min_y, + min_z: b.min_z, + max_x: b.max_x, + max_y: mid_y, + max_z: mid_z, + }, + Bounds { + min_x: b.min_x, + min_y: mid_y, + min_z: b.min_z, + max_x: mid_x, + max_y: b.max_y, + max_z: mid_z, + }, + Bounds { + min_x: mid_x, + min_y: mid_y, + min_z: b.min_z, + max_x: b.max_x, + max_y: b.max_y, + max_z: mid_z, + }, + Bounds { + min_x: b.min_x, + min_y: b.min_y, + min_z: mid_z, + max_x: mid_x, + max_y: mid_y, + max_z: b.max_z, + }, + Bounds { + min_x: mid_x, + min_y: b.min_y, + min_z: mid_z, + max_x: b.max_x, + max_y: mid_y, + max_z: b.max_z, + }, + Bounds { + min_x: b.min_x, + min_y: mid_y, + min_z: mid_z, + max_x: mid_x, + max_y: b.max_y, + max_z: b.max_z, + }, + Bounds { + min_x: mid_x, + min_y: mid_y, + min_z: mid_z, + max_x: b.max_x, + max_y: b.max_y, + max_z: b.max_z, + }, ]; for octant in &octants { - subdivide(*octant, depth + 1, max_depth, is_inside, next_index, tetrahedra); + subdivide( + *octant, + depth + 1, + max_depth, + is_inside, + next_index, + tetrahedra, + ); } } @@ -115,7 +243,14 @@ pub fn octree_mesh( max_y: max.y, max_z: max.z, }; - subdivide(bounds, 0, max_depth, is_inside, &mut next_index, &mut tetrahedra); + subdivide( + bounds, + 0, + max_depth, + is_inside, + &mut next_index, + &mut tetrahedra, + ); tetrahedra } @@ -125,45 +260,96 @@ mod tests { #[test] fn test_single_cell_always_inside() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = octree_mesh(min, max, 0, &|_| true); assert_eq!(result.len(), 5); } #[test] fn test_depth_1_all_inside() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = octree_mesh(min, max, 1, &|_| true); assert_eq!(result.len(), 40); } #[test] fn test_sphere_containment() { - let min = Point3D { index: 0, x: -1.0, y: -1.0, z: -1.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; - let result = octree_mesh(min, max, 2, &|p| { - p.x * p.x + p.y * p.y + p.z * p.z <= 1.0 - }); + let min = Point3D { + index: 0, + x: -1.0, + y: -1.0, + z: -1.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; + let result = octree_mesh(min, max, 2, &|p| p.x * p.x + p.y * p.y + p.z * p.z <= 1.0); assert!(!result.is_empty()); assert!(result.len() < 64 * 5); } #[test] fn test_all_tetrahedra_have_nonzero_volume() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = octree_mesh(min, max, 2, &|_| true); for tet in &result { - assert!(tet.signed_volume().abs() > 1e-15, "Degenerate tetrahedron found"); + assert!( + tet.signed_volume().abs() > 1e-15, + "Degenerate tetrahedron found" + ); } } #[test] fn test_depth_2_cell_count() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = octree_mesh(min, max, 2, &|_| true); // depth=2 → 64 leaf cells × 5 tets = 320 assert_eq!(result.len(), 320); @@ -171,8 +357,18 @@ mod tests { #[test] fn test_partial_containment() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 2.0, y: 2.0, z: 2.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 2.0, + y: 2.0, + z: 2.0, + }; // Only accept cells whose center x < 1.0 (half the domain) let result = octree_mesh(min, max, 1, &|p| p.x < 1.0); // 8 octants at depth 1, 4 have center.x < 1.0 @@ -181,8 +377,18 @@ mod tests { #[test] fn test_empty_domain() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = octree_mesh(min, max, 2, &|_| false); assert!(result.is_empty()); } diff --git a/src/pipeline.rs b/src/pipeline.rs index 9409dd5..e77eb6d 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -144,16 +144,36 @@ mod tests { #[test] fn test_surface_to_volume_sphere() { - 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 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 result = surface_to_volume(8, 8, 8, min, max, &sphere_field, 0.0); assert!(!result.is_empty()); } #[test] fn test_surface_to_volume_empty_field() { - let min = Point3D { index: 0, x: -1.0, y: -1.0, z: -1.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: -1.0, + y: -1.0, + z: -1.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; // Field always positive → no isosurface let result = surface_to_volume(4, 4, 4, min, max, &|_, _, _| 10.0, 0.0); assert!(result.is_empty()); @@ -161,8 +181,18 @@ mod tests { #[test] fn test_octree_refined() { - let min = Point3D { index: 0, x: -1.0, y: -1.0, z: -1.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: -1.0, + y: -1.0, + z: -1.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; // Use depth=1 and loose ratio to keep test fast let result = octree_refined(min, max, 1, &|_| true, 2.0); assert!(!result.is_empty()); @@ -170,8 +200,18 @@ mod tests { #[test] fn test_voxel_refined() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; // Use 2x2x2 and loose ratio to keep test fast let result = voxel_refined(min, max, 2, 2, 2, &|_| true, 2.0); assert!(!result.is_empty()); @@ -186,10 +226,30 @@ mod tests { #[test] fn test_refine_tetrahedra_single() { let tet = Tetrahedron { - 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.5, y: 1.0, z: 0.0 }, - d: Point3D { index: 3, x: 0.5, y: 0.5, z: 1.0 }, + 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.5, + y: 1.0, + z: 0.0, + }, + d: Point3D { + index: 3, + x: 0.5, + y: 0.5, + z: 1.0, + }, }; let result = refine_tetrahedra(&[tet], 2.0); assert!(!result.is_empty()); @@ -197,8 +257,18 @@ mod tests { #[test] fn test_octree_refined_empty_domain() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = octree_refined(min, max, 2, &|_| false, 2.0); assert!(result.is_empty()); } @@ -206,10 +276,30 @@ mod tests { #[test] fn test_extract_unique_points() { let tet = Tetrahedron { - 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 }, - d: Point3D { index: 3, x: 0.0, y: 0.0, z: 1.0 }, + 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, + }, + d: Point3D { + index: 3, + x: 0.0, + y: 0.0, + z: 1.0, + }, }; let points = extract_unique_points(&[tet]); assert_eq!(points.len(), 4); diff --git a/src/voxel_mesh.rs b/src/voxel_mesh.rs index 2ddd4f7..32556a7 100644 --- a/src/voxel_mesh.rs +++ b/src/voxel_mesh.rs @@ -111,11 +111,36 @@ pub fn voxel_mesh( }; // Standard 5-tetrahedra decomposition of a hexahedron - tetrahedra.push(Tetrahedron { a: p0, b: p1, c: p3, d: p4 }); - tetrahedra.push(Tetrahedron { a: p1, b: p2, c: p3, d: p6 }); - tetrahedra.push(Tetrahedron { a: p1, b: p4, c: p5, d: p6 }); - tetrahedra.push(Tetrahedron { a: p3, b: p4, c: p6, d: p7 }); - tetrahedra.push(Tetrahedron { a: p1, b: p3, c: p4, d: p6 }); + tetrahedra.push(Tetrahedron { + a: p0, + b: p1, + c: p3, + d: p4, + }); + tetrahedra.push(Tetrahedron { + a: p1, + b: p2, + c: p3, + d: p6, + }); + tetrahedra.push(Tetrahedron { + a: p1, + b: p4, + c: p5, + d: p6, + }); + tetrahedra.push(Tetrahedron { + a: p3, + b: p4, + c: p6, + d: p7, + }); + tetrahedra.push(Tetrahedron { + a: p1, + b: p3, + c: p4, + d: p6, + }); } } } @@ -129,24 +154,54 @@ mod tests { #[test] fn test_single_cell_always_inside() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 1, 1, 1, &|_| true); assert_eq!(result.len(), 5); } #[test] fn test_2x2x2_all_inside() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 2, 2, 2, &|_| true); assert_eq!(result.len(), 40); } #[test] fn test_sphere_containment() { - let min = Point3D { index: 0, x: -1.0, y: -1.0, z: -1.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: -1.0, + y: -1.0, + z: -1.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 4, 4, 4, &|p| { p.x * p.x + p.y * p.y + p.z * p.z <= 1.0 }); @@ -156,26 +211,59 @@ mod tests { #[test] fn test_empty_domain() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 4, 4, 4, &|_| false); assert!(result.is_empty()); } #[test] fn test_all_tetrahedra_have_nonzero_volume() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 3, 3, 3, &|_| true); for tet in &result { - assert!(tet.signed_volume().abs() > 1e-15, "Degenerate tetrahedron found"); + assert!( + tet.signed_volume().abs() > 1e-15, + "Degenerate tetrahedron found" + ); } } #[test] fn test_asymmetric_resolution() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 1, 2, 3, &|_| true); // 1×2×3 = 6 cells × 5 tets = 30 assert_eq!(result.len(), 30); @@ -183,8 +271,18 @@ mod tests { #[test] fn test_shared_vertex_indices() { - let min = Point3D { index: 0, x: 0.0, y: 0.0, z: 0.0 }; - let max = Point3D { index: 0, x: 1.0, y: 1.0, z: 1.0 }; + let min = Point3D { + index: 0, + x: 0.0, + y: 0.0, + z: 0.0, + }; + let max = Point3D { + index: 0, + x: 1.0, + y: 1.0, + z: 1.0, + }; let result = voxel_mesh(min, max, 2, 2, 2, &|_| true); // Collect all unique vertex indices let mut indices: Vec = Vec::new(); diff --git a/src/wasm.rs b/src/wasm.rs index 847a3f8..60b6ee7 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -130,16 +130,31 @@ pub fn octree_mesh_generate( depth: usize, is_inside_fn: &Function, ) -> Result { - let min = Point3D { index: 0, x: min_x, y: min_y, z: min_z }; - let max = Point3D { index: 0, x: max_x, y: max_y, z: max_z }; + let min = Point3D { + index: 0, + x: min_x, + y: min_y, + z: min_z, + }; + let max = Point3D { + index: 0, + x: max_x, + y: max_y, + z: max_z, + }; let func = is_inside_fn.clone(); let is_inside = move |p: &Point3D| -> bool { let this = JsValue::null(); - func.call3(&this, &JsValue::from(p.x), &JsValue::from(p.y), &JsValue::from(p.z)) - .ok() - .and_then(|v| v.as_bool()) - .unwrap_or(false) + func.call3( + &this, + &JsValue::from(p.x), + &JsValue::from(p.y), + &JsValue::from(p.z), + ) + .ok() + .and_then(|v| v.as_bool()) + .unwrap_or(false) }; let tets = octree_mesh(min, max, depth, &is_inside); @@ -167,16 +182,31 @@ pub fn marching_cubes_generate( scalar_field_fn: &Function, iso_value: f64, ) -> Result { - let min = Point3D { index: 0, x: min_x, y: min_y, z: min_z }; - let max = Point3D { index: 0, x: max_x, y: max_y, z: max_z }; + let min = Point3D { + index: 0, + x: min_x, + y: min_y, + z: min_z, + }; + let max = Point3D { + index: 0, + x: max_x, + y: max_y, + z: max_z, + }; let func = scalar_field_fn.clone(); let field = move |x: f64, y: f64, z: f64| -> f64 { let this = JsValue::null(); - func.call3(&this, &JsValue::from(x), &JsValue::from(y), &JsValue::from(z)) - .ok() - .and_then(|v| v.as_f64()) - .unwrap_or(0.0) + func.call3( + &this, + &JsValue::from(x), + &JsValue::from(y), + &JsValue::from(z), + ) + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) }; let faces = marching_cubes(nx, ny, nz, min, max, &field, iso_value); @@ -232,16 +262,31 @@ pub fn voxel_mesh_generate( nz: usize, is_inside_fn: &Function, ) -> Result { - let min = Point3D { index: 0, x: min_x, y: min_y, z: min_z }; - let max = Point3D { index: 0, x: max_x, y: max_y, z: max_z }; + let min = Point3D { + index: 0, + x: min_x, + y: min_y, + z: min_z, + }; + let max = Point3D { + index: 0, + x: max_x, + y: max_y, + z: max_z, + }; let func = is_inside_fn.clone(); let is_inside = move |p: &Point3D| -> bool { let this = JsValue::null(); - func.call3(&this, &JsValue::from(p.x), &JsValue::from(p.y), &JsValue::from(p.z)) - .ok() - .and_then(|v| v.as_bool()) - .unwrap_or(false) + func.call3( + &this, + &JsValue::from(p.x), + &JsValue::from(p.y), + &JsValue::from(p.z), + ) + .ok() + .and_then(|v| v.as_bool()) + .unwrap_or(false) }; let tets = voxel_mesh(min, max, nx, ny, nz, &is_inside);