1use crate::resources::MeshData;
21use rayon::prelude::*;
22use std::collections::HashMap;
23
24#[derive(Debug, Clone)]
26pub struct VolumeData {
27 pub data: Vec<f32>,
29 pub dims: [u32; 3],
31 pub origin: [f32; 3],
33 pub spacing: [f32; 3],
35}
36
37impl VolumeData {
38 pub fn in_bounds(&self, ix: u32, iy: u32, iz: u32) -> bool {
40 ix < self.dims[0] && iy < self.dims[1] && iz < self.dims[2]
41 }
42
43 pub fn sample(&self, ix: u32, iy: u32, iz: u32) -> f32 {
49 let nx = self.dims[0] as usize;
50 let ny = self.dims[1] as usize;
51 self.data[ix as usize + iy as usize * nx + iz as usize * nx * ny]
52 }
53}
54
55pub fn trilinear_sample(volume: &VolumeData, world_pos: [f32; 3]) -> f32 {
60 let [nx, ny, nz] = volume.dims;
61
62 let gx = (world_pos[0] - volume.origin[0]) / volume.spacing[0];
64 let gy = (world_pos[1] - volume.origin[1]) / volume.spacing[1];
65 let gz = (world_pos[2] - volume.origin[2]) / volume.spacing[2];
66
67 let gx = gx.clamp(0.0, (nx as f32) - 1.001);
69 let gy = gy.clamp(0.0, (ny as f32) - 1.001);
70 let gz = gz.clamp(0.0, (nz as f32) - 1.001);
71
72 let ix = gx.floor() as u32;
73 let iy = gy.floor() as u32;
74 let iz = gz.floor() as u32;
75
76 let fx = gx - ix as f32;
77 let fy = gy - iy as f32;
78 let fz = gz - iz as f32;
79
80 let ix1 = (ix + 1).min(nx - 1);
81 let iy1 = (iy + 1).min(ny - 1);
82 let iz1 = (iz + 1).min(nz - 1);
83
84 let c000 = volume.sample(ix, iy, iz);
86 let c100 = volume.sample(ix1, iy, iz);
87 let c010 = volume.sample(ix, iy1, iz);
88 let c110 = volume.sample(ix1, iy1, iz);
89 let c001 = volume.sample(ix, iy, iz1);
90 let c101 = volume.sample(ix1, iy, iz1);
91 let c011 = volume.sample(ix, iy1, iz1);
92 let c111 = volume.sample(ix1, iy1, iz1);
93
94 let c00 = c000 * (1.0 - fx) + c100 * fx;
95 let c10 = c010 * (1.0 - fx) + c110 * fx;
96 let c01 = c001 * (1.0 - fx) + c101 * fx;
97 let c11 = c011 * (1.0 - fx) + c111 * fx;
98
99 let c0 = c00 * (1.0 - fy) + c10 * fy;
100 let c1 = c01 * (1.0 - fy) + c11 * fy;
101
102 c0 * (1.0 - fz) + c1 * fz
103}
104
105fn gradient_at(volume: &VolumeData, pos: [f32; 3]) -> [f32; 3] {
107 let hx = volume.spacing[0] * 0.5;
108 let hy = volume.spacing[1] * 0.5;
109 let hz = volume.spacing[2] * 0.5;
110
111 let gx = trilinear_sample(volume, [pos[0] + hx, pos[1], pos[2]])
112 - trilinear_sample(volume, [pos[0] - hx, pos[1], pos[2]]);
113 let gy = trilinear_sample(volume, [pos[0], pos[1] + hy, pos[2]])
114 - trilinear_sample(volume, [pos[0], pos[1] - hy, pos[2]]);
115 let gz = trilinear_sample(volume, [pos[0], pos[1], pos[2] + hz])
116 - trilinear_sample(volume, [pos[0], pos[1], pos[2] - hz]);
117
118 let len = (gx * gx + gy * gy + gz * gz).sqrt();
119 if len > 1e-10 {
120 [gx / len, gy / len, gz / len]
121 } else {
122 [0.0, 1.0, 0.0] }
124}
125
126pub fn extract_isosurface(volume: &VolumeData, isovalue: f32) -> MeshData {
137 let [nx, ny, nz] = volume.dims;
138 if nx < 2 || ny < 2 || nz < 2 {
139 return MeshData {
140 positions: Vec::new(),
141 normals: Vec::new(),
142 indices: Vec::new(),
143 uvs: None,
144 tangents: None,
145 attributes: HashMap::new(),
146 };
147 }
148
149 const SLAB_THRESHOLD: usize = 64 * 64 * 64;
150 let cell_count = (nx - 1) as usize * (ny - 1) as usize * (nz - 1) as usize;
151
152 let (positions, normals, indices) = if cell_count >= SLAB_THRESHOLD {
153 let nz_cells = (nz - 1) as usize;
157 let num_threads = rayon::current_num_threads();
158 let slab_height = (nz_cells / num_threads).max(1);
159
160 let slabs: Vec<_> = (0..nz_cells)
161 .step_by(slab_height)
162 .collect::<Vec<_>>()
163 .par_iter()
164 .map(|&iz_start| {
165 let iz_end = (iz_start + slab_height).min(nz_cells);
166 extract_isosurface_slab(volume, isovalue, iz_start as u32, iz_end as u32)
167 })
168 .collect();
169
170 let mut positions: Vec<[f32; 3]> = Vec::new();
171 let mut normals: Vec<[f32; 3]> = Vec::new();
172 let mut indices: Vec<u32> = Vec::new();
173 for (slab_pos, slab_nrm, slab_idx) in slabs {
174 let offset = positions.len() as u32;
175 positions.extend_from_slice(&slab_pos);
176 normals.extend_from_slice(&slab_nrm);
177 indices.extend(slab_idx.into_iter().map(|i| i + offset));
178 }
179 (positions, normals, indices)
180 } else {
181 extract_isosurface_slab(volume, isovalue, 0, nz - 1)
182 };
183
184 MeshData {
185 positions,
186 normals,
187 indices,
188 uvs: None,
189 tangents: None,
190 attributes: HashMap::new(),
191 }
192}
193
194fn extract_isosurface_slab(
196 volume: &VolumeData,
197 isovalue: f32,
198 iz_start: u32,
199 iz_end: u32,
200) -> (Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<u32>) {
201 let [nx, ny, _nz] = volume.dims;
202
203 let mut positions: Vec<[f32; 3]> = Vec::new();
204 let mut normals: Vec<[f32; 3]> = Vec::new();
205 let mut indices: Vec<u32> = Vec::new();
206 let mut edge_cache: HashMap<(u32, u32, u32, u8), u32> = HashMap::new();
207
208 for iz in iz_start..iz_end {
209 for iy in 0..(ny - 1) {
210 for ix in 0..(nx - 1) {
211 let corners = [
213 volume.sample(ix, iy, iz), volume.sample(ix + 1, iy, iz), volume.sample(ix + 1, iy + 1, iz), volume.sample(ix, iy + 1, iz), volume.sample(ix, iy, iz + 1), volume.sample(ix + 1, iy, iz + 1), volume.sample(ix + 1, iy + 1, iz + 1), volume.sample(ix, iy + 1, iz + 1), ];
222
223 let mut cube_index = 0u8;
225 for (i, &val) in corners.iter().enumerate() {
226 if val < isovalue {
227 cube_index |= 1 << i;
228 }
229 }
230
231 let edge_bits = EDGE_TABLE[cube_index as usize];
232 if edge_bits == 0 {
233 continue;
234 }
235
236 let corner_pos = corner_positions(volume, ix, iy, iz);
238
239 let tri_row = &TRI_TABLE[cube_index as usize];
241 let mut i = 0;
242 while i < 16 && tri_row[i] >= 0 {
243 let mut tri_verts = [0u32; 3];
244 for v in 0..3 {
245 let edge_id = tri_row[i + v] as u8;
246 let (a, b) = EDGE_VERTICES[edge_id as usize];
247
248 let cache_key = canonical_edge_key(ix, iy, iz, edge_id);
250
251 tri_verts[v] = *edge_cache.entry(cache_key).or_insert_with(|| {
252 let va = corners[a as usize];
253 let vb = corners[b as usize];
254 let t = if (va - vb).abs() > 1e-10 {
255 (isovalue - va) / (vb - va)
256 } else {
257 0.5
258 };
259 let t = t.clamp(0.0, 1.0);
260
261 let pa = corner_pos[a as usize];
262 let pb = corner_pos[b as usize];
263 let pos = [
264 pa[0] + t * (pb[0] - pa[0]),
265 pa[1] + t * (pb[1] - pa[1]),
266 pa[2] + t * (pb[2] - pa[2]),
267 ];
268
269 let normal = gradient_at(volume, pos);
270
271 let idx = positions.len() as u32;
272 positions.push(pos);
273 normals.push(normal);
274 idx
275 });
276 }
277 indices.push(tri_verts[0]);
279 indices.push(tri_verts[2]);
280 indices.push(tri_verts[1]);
281 i += 3;
282 }
283 }
284 }
285 }
286
287 (positions, normals, indices)
288}
289
290fn corner_positions(volume: &VolumeData, ix: u32, iy: u32, iz: u32) -> [[f32; 3]; 8] {
292 let ox = volume.origin[0] + ix as f32 * volume.spacing[0];
293 let oy = volume.origin[1] + iy as f32 * volume.spacing[1];
294 let oz = volume.origin[2] + iz as f32 * volume.spacing[2];
295 let dx = volume.spacing[0];
296 let dy = volume.spacing[1];
297 let dz = volume.spacing[2];
298
299 [
300 [ox, oy, oz], [ox + dx, oy, oz], [ox + dx, oy + dy, oz], [ox, oy + dy, oz], [ox, oy, oz + dz], [ox + dx, oy, oz + dz], [ox + dx, oy + dy, oz + dz], [ox, oy + dy, oz + dz], ]
309}
310
311fn canonical_edge_key(cx: u32, cy: u32, cz: u32, edge_id: u8) -> (u32, u32, u32, u8) {
316 match edge_id {
325 0 => (cx, cy, cz, 0), 2 => (cx, cy + 1, cz, 0), 4 => (cx, cy, cz + 1, 0), 6 => (cx, cy + 1, cz + 1, 0), 3 => (cx, cy, cz, 1), 1 => (cx + 1, cy, cz, 1), 7 => (cx, cy, cz + 1, 1), 5 => (cx + 1, cy, cz + 1, 1), 8 => (cx, cy, cz, 2), 9 => (cx + 1, cy, cz, 2), 10 => (cx + 1, cy + 1, cz, 2), 11 => (cx, cy + 1, cz, 2), _ => (cx, cy, cz, edge_id), }
342}
343
344const EDGE_VERTICES: [(u8, u8); 12] = [
346 (0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7), ];
359
360#[rustfmt::skip]
366const EDGE_TABLE: [u16; 256] = [
367 0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
368 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
369 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
370 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
371 0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
372 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
373 0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
374 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
375 0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
376 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
377 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
378 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
379 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
380 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
381 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
382 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
383 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
384 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
385 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
386 0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
387 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
388 0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
389 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
390 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
391 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
392 0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
393 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
394 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
395 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
396 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
397 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
398 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000,
399];
400
401#[rustfmt::skip]
403pub(crate) const TRI_TABLE: [[i8; 16]; 256] = [
404 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
405 [ 0, 8, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
406 [ 0, 1, 9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
407 [ 1, 8, 3, 9, 8, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
408 [ 1, 2,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
409 [ 0, 8, 3, 1, 2,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
410 [ 9, 2,10, 0, 2, 9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
411 [ 2, 8, 3, 2,10, 8,10, 9, 8,-1,-1,-1,-1,-1,-1,-1],
412 [ 3,11, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
413 [ 0,11, 2, 8,11, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
414 [ 1, 9, 0, 2, 3,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
415 [ 1,11, 2, 1, 9,11, 9, 8,11,-1,-1,-1,-1,-1,-1,-1],
416 [ 3,10, 1,11,10, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
417 [ 0,10, 1, 0, 8,10, 8,11,10,-1,-1,-1,-1,-1,-1,-1],
418 [ 3, 9, 0, 3,11, 9,11,10, 9,-1,-1,-1,-1,-1,-1,-1],
419 [ 9, 8,10,10, 8,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
420 [ 4, 7, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
421 [ 4, 3, 0, 7, 3, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
422 [ 0, 1, 9, 8, 4, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
423 [ 4, 1, 9, 4, 7, 1, 7, 3, 1,-1,-1,-1,-1,-1,-1,-1],
424 [ 1, 2,10, 8, 4, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
425 [ 3, 4, 7, 3, 0, 4, 1, 2,10,-1,-1,-1,-1,-1,-1,-1],
426 [ 9, 2,10, 9, 0, 2, 8, 4, 7,-1,-1,-1,-1,-1,-1,-1],
427 [ 2,10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4,-1,-1,-1,-1],
428 [ 8, 4, 7, 3,11, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
429 [11, 4, 7,11, 2, 4, 2, 0, 4,-1,-1,-1,-1,-1,-1,-1],
430 [ 9, 0, 1, 8, 4, 7, 2, 3,11,-1,-1,-1,-1,-1,-1,-1],
431 [ 4, 7,11, 9, 4,11, 9,11, 2, 9, 2, 1,-1,-1,-1,-1],
432 [ 3,10, 1, 3,11,10, 7, 8, 4,-1,-1,-1,-1,-1,-1,-1],
433 [ 1,11,10, 1, 4,11, 1, 0, 4, 7,11, 4,-1,-1,-1,-1],
434 [ 4, 7, 8, 9, 0,11, 9,11,10,11, 0, 3,-1,-1,-1,-1],
435 [ 4, 7,11, 4,11, 9, 9,11,10,-1,-1,-1,-1,-1,-1,-1],
436 [ 9, 5, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
437 [ 9, 5, 4, 0, 8, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
438 [ 0, 5, 4, 1, 5, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
439 [ 8, 5, 4, 8, 3, 5, 3, 1, 5,-1,-1,-1,-1,-1,-1,-1],
440 [ 1, 2,10, 9, 5, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
441 [ 3, 0, 8, 1, 2,10, 4, 9, 5,-1,-1,-1,-1,-1,-1,-1],
442 [ 5, 2,10, 5, 4, 2, 4, 0, 2,-1,-1,-1,-1,-1,-1,-1],
443 [ 2,10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8,-1,-1,-1,-1],
444 [ 9, 5, 4, 2, 3,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
445 [ 0,11, 2, 0, 8,11, 4, 9, 5,-1,-1,-1,-1,-1,-1,-1],
446 [ 0, 5, 4, 0, 1, 5, 2, 3,11,-1,-1,-1,-1,-1,-1,-1],
447 [ 2, 1, 5, 2, 5, 8, 2, 8,11, 4, 8, 5,-1,-1,-1,-1],
448 [10, 3,11,10, 1, 3, 9, 5, 4,-1,-1,-1,-1,-1,-1,-1],
449 [ 4, 9, 5, 0, 8, 1, 8,10, 1, 8,11,10,-1,-1,-1,-1],
450 [ 5, 4, 0, 5, 0,11, 5,11,10,11, 0, 3,-1,-1,-1,-1],
451 [ 5, 4, 8, 5, 8,10,10, 8,11,-1,-1,-1,-1,-1,-1,-1],
452 [ 9, 7, 8, 5, 7, 9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
453 [ 9, 3, 0, 9, 5, 3, 5, 7, 3,-1,-1,-1,-1,-1,-1,-1],
454 [ 0, 7, 8, 0, 1, 7, 1, 5, 7,-1,-1,-1,-1,-1,-1,-1],
455 [ 1, 5, 3, 3, 5, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
456 [ 9, 7, 8, 9, 5, 7,10, 1, 2,-1,-1,-1,-1,-1,-1,-1],
457 [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3,-1,-1,-1,-1],
458 [ 8, 0, 2, 8, 2, 5, 8, 5, 7,10, 5, 2,-1,-1,-1,-1],
459 [ 2,10, 5, 2, 5, 3, 3, 5, 7,-1,-1,-1,-1,-1,-1,-1],
460 [ 7, 9, 5, 7, 8, 9, 3,11, 2,-1,-1,-1,-1,-1,-1,-1],
461 [ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7,11,-1,-1,-1,-1],
462 [ 2, 3,11, 0, 1, 8, 1, 7, 8, 1, 5, 7,-1,-1,-1,-1],
463 [11, 2, 1,11, 1, 7, 7, 1, 5,-1,-1,-1,-1,-1,-1,-1],
464 [ 9, 5, 8, 8, 5, 7,10, 1, 3,10, 3,11,-1,-1,-1,-1],
465 [ 5, 7, 0, 5, 0, 9, 7,11, 0, 1, 0,10,11,10, 0,-1],
466 [11,10, 0,11, 0, 3,10, 5, 0, 8, 0, 7, 5, 7, 0,-1],
467 [11,10, 5, 7,11, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
468 [10, 6, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
469 [ 0, 8, 3, 5,10, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
470 [ 9, 0, 1, 5,10, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
471 [ 1, 8, 3, 1, 9, 8, 5,10, 6,-1,-1,-1,-1,-1,-1,-1],
472 [ 1, 6, 5, 2, 6, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
473 [ 1, 6, 5, 1, 2, 6, 3, 0, 8,-1,-1,-1,-1,-1,-1,-1],
474 [ 9, 6, 5, 9, 0, 6, 0, 2, 6,-1,-1,-1,-1,-1,-1,-1],
475 [ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8,-1,-1,-1,-1],
476 [ 2, 3,11,10, 6, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
477 [11, 0, 8,11, 2, 0,10, 6, 5,-1,-1,-1,-1,-1,-1,-1],
478 [ 0, 1, 9, 2, 3,11, 5,10, 6,-1,-1,-1,-1,-1,-1,-1],
479 [ 5,10, 6, 1, 9, 2, 9,11, 2, 9, 8,11,-1,-1,-1,-1],
480 [ 6, 3,11, 6, 5, 3, 5, 1, 3,-1,-1,-1,-1,-1,-1,-1],
481 [ 0, 8,11, 0,11, 5, 0, 5, 1, 5,11, 6,-1,-1,-1,-1],
482 [ 3,11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9,-1,-1,-1,-1],
483 [ 6, 5, 9, 6, 9,11,11, 9, 8,-1,-1,-1,-1,-1,-1,-1],
484 [ 5,10, 6, 4, 7, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
485 [ 4, 3, 0, 4, 7, 3, 6, 5,10,-1,-1,-1,-1,-1,-1,-1],
486 [ 1, 9, 0, 5,10, 6, 8, 4, 7,-1,-1,-1,-1,-1,-1,-1],
487 [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4,-1,-1,-1,-1],
488 [ 6, 1, 2, 6, 5, 1, 4, 7, 8,-1,-1,-1,-1,-1,-1,-1],
489 [ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7,-1,-1,-1,-1],
490 [ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6,-1,-1,-1,-1],
491 [ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9,-1],
492 [ 3,11, 2, 7, 8, 4,10, 6, 5,-1,-1,-1,-1,-1,-1,-1],
493 [ 5,10, 6, 4, 7, 2, 4, 2, 0, 2, 7,11,-1,-1,-1,-1],
494 [ 0, 1, 9, 4, 7, 8, 2, 3,11, 5,10, 6,-1,-1,-1,-1],
495 [ 9, 2, 1, 9,11, 2, 9, 4,11, 7,11, 4, 5,10, 6,-1],
496 [ 8, 4, 7, 3,11, 5, 3, 5, 1, 5,11, 6,-1,-1,-1,-1],
497 [ 5, 1,11, 5,11, 6, 1, 0,11, 7,11, 4, 0, 4,11,-1],
498 [ 0, 5, 9, 0, 6, 5, 0, 3, 6,11, 6, 3, 8, 4, 7,-1],
499 [ 6, 5, 9, 6, 9,11, 4, 7, 9, 7,11, 9,-1,-1,-1,-1],
500 [10, 4, 9, 6, 4,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
501 [ 4,10, 6, 4, 9,10, 0, 8, 3,-1,-1,-1,-1,-1,-1,-1],
502 [10, 0, 1,10, 6, 0, 6, 4, 0,-1,-1,-1,-1,-1,-1,-1],
503 [ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1,10,-1,-1,-1,-1],
504 [ 1, 4, 9, 1, 2, 4, 2, 6, 4,-1,-1,-1,-1,-1,-1,-1],
505 [ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4,-1,-1,-1,-1],
506 [ 0, 2, 4, 4, 2, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
507 [ 8, 3, 2, 8, 2, 4, 4, 2, 6,-1,-1,-1,-1,-1,-1,-1],
508 [10, 4, 9,10, 6, 4,11, 2, 3,-1,-1,-1,-1,-1,-1,-1],
509 [ 0, 8, 2, 2, 8,11, 4, 9,10, 4,10, 6,-1,-1,-1,-1],
510 [ 3,11, 2, 0, 1, 6, 0, 6, 4, 6, 1,10,-1,-1,-1,-1],
511 [ 6, 4, 1, 6, 1,10, 4, 8, 1, 2, 1,11, 8,11, 1,-1],
512 [ 9, 6, 4, 9, 3, 6, 9, 1, 3,11, 6, 3,-1,-1,-1,-1],
513 [ 8,11, 1, 8, 1, 0,11, 6, 1, 9, 1, 4, 6, 4, 1,-1],
514 [ 3,11, 6, 3, 6, 0, 0, 6, 4,-1,-1,-1,-1,-1,-1,-1],
515 [ 6, 4, 8,11, 6, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
516 [ 7,10, 6, 7, 8,10, 8, 9,10,-1,-1,-1,-1,-1,-1,-1],
517 [ 0, 7, 3, 0,10, 7, 0, 9,10, 6, 7,10,-1,-1,-1,-1],
518 [10, 6, 7, 1,10, 7, 1, 7, 8, 1, 8, 0,-1,-1,-1,-1],
519 [10, 6, 7,10, 7, 1, 1, 7, 3,-1,-1,-1,-1,-1,-1,-1],
520 [ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7,-1,-1,-1,-1],
521 [ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9,-1],
522 [ 7, 8, 0, 7, 0, 6, 6, 0, 2,-1,-1,-1,-1,-1,-1,-1],
523 [ 7, 3, 2, 6, 7, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
524 [ 2, 3,11,10, 6, 8,10, 8, 9, 8, 6, 7,-1,-1,-1,-1],
525 [ 2, 0, 7, 2, 7,11, 0, 9, 7, 6, 7,10, 9,10, 7,-1],
526 [ 1, 8, 0, 1, 7, 8, 1,10, 7, 6, 7,10, 2, 3,11,-1],
527 [11, 2, 1,11, 1, 7,10, 6, 1, 6, 7, 1,-1,-1,-1,-1],
528 [ 8, 9, 6, 8, 6, 7, 9, 1, 6,11, 6, 3, 1, 3, 6,-1],
529 [ 0, 9, 1,11, 6, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
530 [ 7, 8, 0, 7, 0, 6, 3,11, 0,11, 6, 0,-1,-1,-1,-1],
531 [ 7,11, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
532 [ 7, 6,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
533 [ 3, 0, 8,11, 7, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
534 [ 0, 1, 9,11, 7, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
535 [ 8, 1, 9, 8, 3, 1,11, 7, 6,-1,-1,-1,-1,-1,-1,-1],
536 [10, 1, 2, 6,11, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
537 [ 1, 2,10, 3, 0, 8, 6,11, 7,-1,-1,-1,-1,-1,-1,-1],
538 [ 2, 9, 0, 2,10, 9, 6,11, 7,-1,-1,-1,-1,-1,-1,-1],
539 [ 6,11, 7, 2,10, 3,10, 8, 3,10, 9, 8,-1,-1,-1,-1],
540 [ 7, 2, 3, 6, 2, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
541 [ 7, 0, 8, 7, 6, 0, 6, 2, 0,-1,-1,-1,-1,-1,-1,-1],
542 [ 2, 7, 6, 2, 3, 7, 0, 1, 9,-1,-1,-1,-1,-1,-1,-1],
543 [ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6,-1,-1,-1,-1],
544 [10, 7, 6,10, 1, 7, 1, 3, 7,-1,-1,-1,-1,-1,-1,-1],
545 [10, 7, 6, 1, 7,10, 1, 8, 7, 1, 0, 8,-1,-1,-1,-1],
546 [ 0, 3, 7, 0, 7,10, 0,10, 9, 6,10, 7,-1,-1,-1,-1],
547 [ 7, 6,10, 7,10, 8, 8,10, 9,-1,-1,-1,-1,-1,-1,-1],
548 [ 6, 8, 4,11, 8, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
549 [ 3, 6,11, 3, 0, 6, 0, 4, 6,-1,-1,-1,-1,-1,-1,-1],
550 [ 8, 6,11, 8, 4, 6, 9, 0, 1,-1,-1,-1,-1,-1,-1,-1],
551 [ 9, 4, 6, 9, 6, 3, 9, 3, 1,11, 3, 6,-1,-1,-1,-1],
552 [ 6, 8, 4, 6,11, 8, 2,10, 1,-1,-1,-1,-1,-1,-1,-1],
553 [ 1, 2,10, 3, 0,11, 0, 6,11, 0, 4, 6,-1,-1,-1,-1],
554 [ 4,11, 8, 4, 6,11, 0, 2, 9, 2,10, 9,-1,-1,-1,-1],
555 [10, 9, 3,10, 3, 2, 9, 4, 3,11, 3, 6, 4, 6, 3,-1],
556 [ 8, 2, 3, 8, 4, 2, 4, 6, 2,-1,-1,-1,-1,-1,-1,-1],
557 [ 0, 4, 2, 4, 6, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
558 [ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8,-1,-1,-1,-1],
559 [ 1, 9, 4, 1, 4, 2, 2, 4, 6,-1,-1,-1,-1,-1,-1,-1],
560 [ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6,10, 1,-1,-1,-1,-1],
561 [10, 1, 0,10, 0, 6, 6, 0, 4,-1,-1,-1,-1,-1,-1,-1],
562 [ 4, 6, 3, 4, 3, 8, 6,10, 3, 0, 3, 9,10, 9, 3,-1],
563 [10, 9, 4, 6,10, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
564 [ 4, 9, 5, 7, 6,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
565 [ 0, 8, 3, 4, 9, 5,11, 7, 6,-1,-1,-1,-1,-1,-1,-1],
566 [ 5, 0, 1, 5, 4, 0, 7, 6,11,-1,-1,-1,-1,-1,-1,-1],
567 [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5,-1,-1,-1,-1],
568 [ 9, 5, 4,10, 1, 2, 7, 6,11,-1,-1,-1,-1,-1,-1,-1],
569 [ 6,11, 7, 1, 2,10, 0, 8, 3, 4, 9, 5,-1,-1,-1,-1],
570 [ 7, 6,11, 5, 4,10, 4, 2,10, 4, 0, 2,-1,-1,-1,-1],
571 [ 3, 4, 8, 3, 5, 4, 3, 2, 5,10, 5, 2,11, 7, 6,-1],
572 [ 7, 2, 3, 7, 6, 2, 5, 4, 9,-1,-1,-1,-1,-1,-1,-1],
573 [ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7,-1,-1,-1,-1],
574 [ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0,-1,-1,-1,-1],
575 [ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8,-1],
576 [ 9, 5, 4,10, 1, 6, 1, 7, 6, 1, 3, 7,-1,-1,-1,-1],
577 [ 1, 6,10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4,-1],
578 [ 4, 0,10, 4,10, 5, 0, 3,10, 6,10, 7, 3, 7,10,-1],
579 [ 7, 6,10, 7,10, 8, 5, 4,10, 4, 8,10,-1,-1,-1,-1],
580 [ 6, 9, 5, 6,11, 9,11, 8, 9,-1,-1,-1,-1,-1,-1,-1],
581 [ 3, 6,11, 0, 6, 3, 0, 5, 6, 0, 9, 5,-1,-1,-1,-1],
582 [ 0,11, 8, 0, 5,11, 0, 1, 5, 5, 6,11,-1,-1,-1,-1],
583 [ 6,11, 3, 6, 3, 5, 5, 3, 1,-1,-1,-1,-1,-1,-1,-1],
584 [ 1, 2,10, 9, 5,11, 9,11, 8,11, 5, 6,-1,-1,-1,-1],
585 [ 0,11, 3, 0, 6,11, 0, 9, 6, 5, 6, 9, 1, 2,10,-1],
586 [11, 8, 5,11, 5, 6, 8, 0, 5,10, 5, 2, 0, 2, 5,-1],
587 [ 6,11, 3, 6, 3, 5, 2,10, 3,10, 5, 3,-1,-1,-1,-1],
588 [ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2,-1,-1,-1,-1],
589 [ 9, 5, 6, 9, 6, 0, 0, 6, 2,-1,-1,-1,-1,-1,-1,-1],
590 [ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8,-1],
591 [ 1, 5, 6, 2, 1, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
592 [ 1, 3, 6, 1, 6,10, 3, 8, 6, 5, 6, 9, 8, 9, 6,-1],
593 [10, 1, 0,10, 0, 6, 9, 5, 0, 5, 6, 0,-1,-1,-1,-1],
594 [ 0, 3, 8, 5, 6,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
595 [10, 5, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
596 [11, 5,10, 7, 5,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
597 [11, 5,10,11, 7, 5, 8, 3, 0,-1,-1,-1,-1,-1,-1,-1],
598 [ 5,11, 7, 5,10,11, 1, 9, 0,-1,-1,-1,-1,-1,-1,-1],
599 [10, 7, 5,10,11, 7, 9, 8, 1, 8, 3, 1,-1,-1,-1,-1],
600 [11, 1, 2,11, 7, 1, 7, 5, 1,-1,-1,-1,-1,-1,-1,-1],
601 [ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2,11,-1,-1,-1,-1],
602 [ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2,11, 7,-1,-1,-1,-1],
603 [ 7, 5, 2, 7, 2,11, 5, 9, 2, 3, 2, 8, 9, 8, 2,-1],
604 [ 2, 5,10, 2, 3, 5, 3, 7, 5,-1,-1,-1,-1,-1,-1,-1],
605 [ 8, 2, 0, 8, 5, 2, 8, 7, 5,10, 2, 5,-1,-1,-1,-1],
606 [ 9, 0, 1, 5,10, 3, 5, 3, 7, 3,10, 2,-1,-1,-1,-1],
607 [ 9, 8, 2, 9, 2, 1, 8, 7, 2,10, 2, 5, 7, 5, 2,-1],
608 [ 1, 3, 5, 3, 7, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
609 [ 0, 8, 7, 0, 7, 1, 1, 7, 5,-1,-1,-1,-1,-1,-1,-1],
610 [ 9, 0, 3, 9, 3, 5, 5, 3, 7,-1,-1,-1,-1,-1,-1,-1],
611 [ 9, 8, 7, 5, 9, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
612 [ 5, 8, 4, 5,10, 8,10,11, 8,-1,-1,-1,-1,-1,-1,-1],
613 [ 5, 0, 4, 5,11, 0, 5,10,11,11, 3, 0,-1,-1,-1,-1],
614 [ 0, 1, 9, 8, 4,10, 8,10,11,10, 4, 5,-1,-1,-1,-1],
615 [10,11, 4,10, 4, 5,11, 3, 4, 9, 4, 1, 3, 1, 4,-1],
616 [ 2, 5, 1, 2, 8, 5, 2,11, 8, 4, 5, 8,-1,-1,-1,-1],
617 [ 0, 4,11, 0,11, 3, 4, 5,11, 2,11, 1, 5, 1,11,-1],
618 [ 0, 2, 5, 0, 5, 9, 2,11, 5, 4, 5, 8,11, 8, 5,-1],
619 [ 9, 4, 5, 2,11, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
620 [ 2, 5,10, 3, 5, 2, 3, 4, 5, 3, 8, 4,-1,-1,-1,-1],
621 [ 5,10, 2, 5, 2, 4, 4, 2, 0,-1,-1,-1,-1,-1,-1,-1],
622 [ 3,10, 2, 3, 5,10, 3, 8, 5, 4, 5, 8, 0, 1, 9,-1],
623 [ 5,10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2,-1,-1,-1,-1],
624 [ 8, 4, 5, 8, 5, 3, 3, 5, 1,-1,-1,-1,-1,-1,-1,-1],
625 [ 0, 4, 5, 1, 0, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
626 [ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5,-1,-1,-1,-1],
627 [ 9, 4, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
628 [ 4,11, 7, 4, 9,11, 9,10,11,-1,-1,-1,-1,-1,-1,-1],
629 [ 0, 8, 3, 4, 9, 7, 9,11, 7, 9,10,11,-1,-1,-1,-1],
630 [ 1,10,11, 1,11, 4, 1, 4, 0, 7, 4,11,-1,-1,-1,-1],
631 [ 3, 1, 4, 3, 4, 8, 1,10, 4, 7, 4,11,10,11, 4,-1],
632 [ 4,11, 7, 9,11, 4, 9, 2,11, 9, 1, 2,-1,-1,-1,-1],
633 [ 9, 7, 4, 9,11, 7, 9, 1,11, 2,11, 1, 0, 8, 3,-1],
634 [11, 7, 4,11, 4, 2, 2, 4, 0,-1,-1,-1,-1,-1,-1,-1],
635 [11, 7, 4,11, 4, 2, 8, 3, 4, 3, 2, 4,-1,-1,-1,-1],
636 [ 2, 9,10, 2, 7, 9, 2, 3, 7, 7, 4, 9,-1,-1,-1,-1],
637 [ 9,10, 7, 9, 7, 4,10, 2, 7, 8, 7, 0, 2, 0, 7,-1],
638 [ 3, 7,10, 3,10, 2, 7, 4,10, 1,10, 0, 4, 0,10,-1],
639 [ 1,10, 2, 8, 7, 4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
640 [ 4, 9, 1, 4, 1, 7, 7, 1, 3,-1,-1,-1,-1,-1,-1,-1],
641 [ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1,-1,-1,-1,-1],
642 [ 4, 0, 3, 7, 4, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
643 [ 4, 8, 7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
644 [ 9,10, 8,10,11, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
645 [ 3, 0, 9, 3, 9,11,11, 9,10,-1,-1,-1,-1,-1,-1,-1],
646 [ 0, 1,10, 0,10, 8, 8,10,11,-1,-1,-1,-1,-1,-1,-1],
647 [ 3, 1,10,11, 3,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
648 [ 1, 2,11, 1,11, 9, 9,11, 8,-1,-1,-1,-1,-1,-1,-1],
649 [ 3, 0, 9, 3, 9,11, 1, 2, 9, 2,11, 9,-1,-1,-1,-1],
650 [ 0, 2,11, 8, 0,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
651 [ 3, 2,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
652 [ 2, 3, 8, 2, 8,10,10, 8, 9,-1,-1,-1,-1,-1,-1,-1],
653 [ 9,10, 2, 0, 9, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
654 [ 2, 3, 8, 2, 8,10, 0, 1, 8, 1,10, 8,-1,-1,-1,-1],
655 [ 1,10, 2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
656 [ 1, 3, 8, 9, 1, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
657 [ 0, 9, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
658 [ 0, 3, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
659 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
660];
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn test_tri_table_edge_consistency() {
668 let mut failures = Vec::new();
670 for cube_index in 0u16..256 {
671 let edge_bits = EDGE_TABLE[cube_index as usize];
672 let tri_row = &TRI_TABLE[cube_index as usize];
673 let mut j = 0;
674 while j < 16 && tri_row[j] >= 0 {
675 let edge_id = tri_row[j] as u8;
676 if edge_bits & (1 << edge_id) == 0 {
677 failures.push(format!(
678 "TRI_TABLE[{}]: edge {} not in EDGE_TABLE ({:#014b})",
679 cube_index, edge_id, edge_bits
680 ));
681 break; }
683 j += 1;
684 }
685 }
686 if !failures.is_empty() {
687 panic!(
688 "{} TRI_TABLE entries inconsistent with EDGE_TABLE:\n{}",
689 failures.len(),
690 failures.join("\n")
691 );
692 }
693 }
694
695 #[test]
696 fn test_sphere_isosurface() {
697 let n = 32u32;
698 let mut data = vec![0.0f32; (n * n * n) as usize];
699 let center = n as f32 / 2.0;
700
701 for iz in 0..n {
702 for iy in 0..n {
703 for ix in 0..n {
704 let dx = ix as f32 - center;
705 let dy = iy as f32 - center;
706 let dz = iz as f32 - center;
707 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
708 data[(ix + iy * n + iz * n * n) as usize] = dist;
709 }
710 }
711 }
712
713 let volume = VolumeData {
714 data,
715 dims: [n, n, n],
716 origin: [0.0, 0.0, 0.0],
717 spacing: [1.0, 1.0, 1.0],
718 };
719
720 let mesh = extract_isosurface(&volume, 8.0);
721
722 assert!(
724 !mesh.positions.is_empty(),
725 "Sphere isosurface should produce vertices"
726 );
727
728 assert_eq!(mesh.indices.len() % 3, 0, "Indices must be a multiple of 3");
730
731 assert!(
733 mesh.indices.len() > 100,
734 "Expected > 100 indices for sphere, got {}",
735 mesh.indices.len()
736 );
737
738 for pos in &mesh.positions {
740 for c in pos {
741 assert!(
742 *c >= 4.0 && *c <= 28.0,
743 "Position component {} out of expected range [4, 28]",
744 c
745 );
746 }
747 }
748
749 for n in &mesh.normals {
751 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
752 assert!(
753 len > 0.95 && len < 1.05,
754 "Normal length {} not approximately 1.0",
755 len
756 );
757 }
758
759 assert_eq!(mesh.positions.len(), mesh.normals.len());
761 }
762
763 #[test]
764 fn test_sphere_winding_order() {
765 let n = 32u32;
769 let center = n as f32 / 2.0;
770 let mut data = vec![0.0f32; (n * n * n) as usize];
771 for iz in 0..n {
772 for iy in 0..n {
773 for ix in 0..n {
774 let dx = ix as f32 - center;
775 let dy = iy as f32 - center;
776 let dz = iz as f32 - center;
777 data[(ix + iy * n + iz * n * n) as usize] =
778 (dx * dx + dy * dy + dz * dz).sqrt();
779 }
780 }
781 }
782 let volume = VolumeData {
783 data,
784 dims: [n, n, n],
785 origin: [0.0, 0.0, 0.0],
786 spacing: [1.0, 1.0, 1.0],
787 };
788 let mesh = extract_isosurface(&volume, 8.0);
789 assert!(!mesh.positions.is_empty(), "expected vertices");
790
791 let mut correct = 0usize;
792 let mut flipped = 0usize;
793 let tri_count = mesh.indices.len() / 3;
794 for t in 0..tri_count {
795 let i0 = mesh.indices[t * 3] as usize;
796 let i1 = mesh.indices[t * 3 + 1] as usize;
797 let i2 = mesh.indices[t * 3 + 2] as usize;
798 let p0 = mesh.positions[i0];
799 let p1 = mesh.positions[i1];
800 let p2 = mesh.positions[i2];
801 let e1 = [p1[0]-p0[0], p1[1]-p0[1], p1[2]-p0[2]];
803 let e2 = [p2[0]-p0[0], p2[1]-p0[1], p2[2]-p0[2]];
804 let gn = [
805 e1[1]*e2[2] - e1[2]*e2[1],
806 e1[2]*e2[0] - e1[0]*e2[2],
807 e1[0]*e2[1] - e1[1]*e2[0],
808 ];
809 let vn = mesh.normals[i0];
811 let dot = gn[0]*vn[0] + gn[1]*vn[1] + gn[2]*vn[2];
812 if dot >= 0.0 { correct += 1; } else { flipped += 1; }
813 }
814
815 let total = correct + flipped;
816 let flipped_pct = flipped as f32 / total as f32 * 100.0;
817 assert!(
818 flipped_pct < 5.0,
819 "{}/{} triangles ({:.1}%) have geometric normal opposing the gradient — winding is wrong",
820 flipped, total, flipped_pct
821 );
822 }
823
824 #[test]
825 fn test_empty_volume() {
826 let data = vec![10.0f32; 8]; let volume = VolumeData {
829 data,
830 dims: [2, 2, 2],
831 origin: [0.0, 0.0, 0.0],
832 spacing: [1.0, 1.0, 1.0],
833 };
834
835 let mesh = extract_isosurface(&volume, 5.0);
836 assert_eq!(
837 mesh.positions.len(),
838 0,
839 "All-above should produce empty mesh"
840 );
841 assert_eq!(mesh.indices.len(), 0);
842 }
843
844 #[test]
845 fn test_volume_data_sample() {
846 let data = vec![
847 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, ];
852 let vol = VolumeData {
853 data,
854 dims: [2, 2, 2],
855 origin: [0.0, 0.0, 0.0],
856 spacing: [1.0, 1.0, 1.0],
857 };
858
859 assert_eq!(vol.sample(0, 0, 0), 1.0);
860 assert_eq!(vol.sample(1, 0, 0), 2.0);
861 assert_eq!(vol.sample(0, 1, 0), 3.0);
862 assert_eq!(vol.sample(1, 1, 0), 4.0);
863 assert_eq!(vol.sample(0, 0, 1), 5.0);
864 assert_eq!(vol.sample(1, 0, 1), 6.0);
865 assert_eq!(vol.sample(0, 1, 1), 7.0);
866 assert_eq!(vol.sample(1, 1, 1), 8.0);
867 }
868
869 #[test]
870 fn test_trilinear_interpolation() {
871 let data = vec![
872 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, ];
877 let vol = VolumeData {
878 data,
879 dims: [2, 2, 2],
880 origin: [0.0, 0.0, 0.0],
881 spacing: [1.0, 1.0, 1.0],
882 };
883
884 let v00 = trilinear_sample(&vol, [0.0, 0.0, 0.0]);
886 assert!(
887 (v00 - 0.0).abs() < 0.01,
888 "Expected 0.0 at origin, got {}",
889 v00
890 );
891
892 let v10 = trilinear_sample(&vol, [0.999, 0.0, 0.0]);
893 assert!(
894 (v10 - 1.0).abs() < 0.02,
895 "Expected ~1.0 at (1,0,0), got {}",
896 v10
897 );
898
899 let mid = trilinear_sample(&vol, [0.5, 0.0, 0.0]);
901 assert!(
902 (mid - 0.5).abs() < 0.01,
903 "Expected 0.5 at midpoint, got {}",
904 mid
905 );
906
907 let center = trilinear_sample(&vol, [0.5, 0.5, 0.5]);
909 assert!(
910 (center - 0.5).abs() < 0.01,
911 "Expected 0.5 at center, got {}",
912 center
913 );
914 }
915}