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 {
813 correct += 1;
814 } else {
815 flipped += 1;
816 }
817 }
818
819 let total = correct + flipped;
820 let flipped_pct = flipped as f32 / total as f32 * 100.0;
821 assert!(
822 flipped_pct < 5.0,
823 "{}/{} triangles ({:.1}%) have geometric normal opposing the gradient — winding is wrong",
824 flipped,
825 total,
826 flipped_pct
827 );
828 }
829
830 #[test]
831 fn test_empty_volume() {
832 let data = vec![10.0f32; 8]; let volume = VolumeData {
835 data,
836 dims: [2, 2, 2],
837 origin: [0.0, 0.0, 0.0],
838 spacing: [1.0, 1.0, 1.0],
839 };
840
841 let mesh = extract_isosurface(&volume, 5.0);
842 assert_eq!(
843 mesh.positions.len(),
844 0,
845 "All-above should produce empty mesh"
846 );
847 assert_eq!(mesh.indices.len(), 0);
848 }
849
850 #[test]
851 fn test_volume_data_sample() {
852 let data = vec![
853 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, ];
858 let vol = VolumeData {
859 data,
860 dims: [2, 2, 2],
861 origin: [0.0, 0.0, 0.0],
862 spacing: [1.0, 1.0, 1.0],
863 };
864
865 assert_eq!(vol.sample(0, 0, 0), 1.0);
866 assert_eq!(vol.sample(1, 0, 0), 2.0);
867 assert_eq!(vol.sample(0, 1, 0), 3.0);
868 assert_eq!(vol.sample(1, 1, 0), 4.0);
869 assert_eq!(vol.sample(0, 0, 1), 5.0);
870 assert_eq!(vol.sample(1, 0, 1), 6.0);
871 assert_eq!(vol.sample(0, 1, 1), 7.0);
872 assert_eq!(vol.sample(1, 1, 1), 8.0);
873 }
874
875 #[test]
876 fn test_trilinear_interpolation() {
877 let data = vec![
878 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, ];
883 let vol = VolumeData {
884 data,
885 dims: [2, 2, 2],
886 origin: [0.0, 0.0, 0.0],
887 spacing: [1.0, 1.0, 1.0],
888 };
889
890 let v00 = trilinear_sample(&vol, [0.0, 0.0, 0.0]);
892 assert!(
893 (v00 - 0.0).abs() < 0.01,
894 "Expected 0.0 at origin, got {}",
895 v00
896 );
897
898 let v10 = trilinear_sample(&vol, [0.999, 0.0, 0.0]);
899 assert!(
900 (v10 - 1.0).abs() < 0.02,
901 "Expected ~1.0 at (1,0,0), got {}",
902 v10
903 );
904
905 let mid = trilinear_sample(&vol, [0.5, 0.0, 0.0]);
907 assert!(
908 (mid - 0.5).abs() < 0.01,
909 "Expected 0.5 at midpoint, got {}",
910 mid
911 );
912
913 let center = trilinear_sample(&vol, [0.5, 0.5, 0.5]);
915 assert!(
916 (center - 0.5).abs() < 0.01,
917 "Expected 0.5 at center, got {}",
918 center
919 );
920 }
921}