Skip to main content

viewport_lib/geometry/
marching_cubes.rs

1//! CPU-side marching cubes isosurface extraction from volumetric scalar data.
2//!
3//! The output is a standard [`MeshData`](crate::resources::MeshData) that can be uploaded via
4//! [`upload_mesh_data()`](crate::ViewportGpuResources::upload_mesh_data) or
5//! [`replace_mesh_data()`](crate::ViewportGpuResources::replace_mesh_data).
6//!
7//! # Example
8//!
9//! ```ignore
10//! let volume = VolumeData {
11//!     data: vec![/* scalar values */],
12//!     dims: [32, 32, 32],
13//!     origin: [0.0, 0.0, 0.0],
14//!     spacing: [0.1, 0.1, 0.1],
15//! };
16//! let mesh = extract_isosurface(&volume, 0.5);
17//! // mesh.positions, mesh.normals, mesh.indices ready for upload.
18//! ```
19
20use crate::resources::MeshData;
21use rayon::prelude::*;
22use std::collections::HashMap;
23
24/// A structured 3D scalar field on a regular grid.
25#[derive(Debug, Clone)]
26pub struct VolumeData {
27    /// Flattened scalar values in x-fastest order: `index = x + y*nx + z*nx*ny`.
28    pub data: Vec<f32>,
29    /// Grid dimensions `[nx, ny, nz]`.
30    pub dims: [u32; 3],
31    /// World-space origin of the grid corner `(0, 0, 0)`.
32    pub origin: [f32; 3],
33    /// Cell size in each axis direction.
34    pub spacing: [f32; 3],
35}
36
37impl VolumeData {
38    /// Check whether grid indices are within bounds.
39    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    /// Read the scalar value at grid point `(ix, iy, iz)`.
44    ///
45    /// # Panics
46    ///
47    /// Panics if the index is out of bounds.
48    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
55/// Trilinear interpolation of the volume at an arbitrary world-space position.
56///
57/// Returns the interpolated scalar value. Points outside the grid are clamped
58/// to the boundary.
59pub fn trilinear_sample(volume: &VolumeData, world_pos: [f32; 3]) -> f32 {
60    let [nx, ny, nz] = volume.dims;
61
62    // Convert world position to continuous grid coordinates.
63    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    // Clamp to valid range.
68    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    // 8-corner trilinear interpolation.
85    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
105/// Compute the gradient at a world-space position via central differences.
106fn 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] // fallback normal
123    }
124}
125
126/// Extract an isosurface from a volume at the given `isovalue` using marching cubes.
127///
128/// Returns a [`MeshData`] with positions, normals (from volume gradient), and triangle
129/// indices. The mesh can be uploaded to the viewport via the standard mesh pipeline.
130///
131/// For volumes larger than 64x64x64 cells the extraction is parallelized via
132/// Z-slab decomposition. Edges on slab boundaries are independently interpolated
133/// by each adjacent slab, producing a small number of geometrically coincident
134/// but topologically disconnected vertices along each Z-boundary. For rendering
135/// this is invisible.
136pub 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        // Parallel Z-slab decomposition: divide the nz-1 cell layers into
154        // independent slabs, each with its own edge cache. Slab outputs are
155        // concatenated with adjusted index offsets after the parallel phase.
156        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
194/// Process one Z-slab of cells: `iz_start..iz_end` (exclusive end).
195fn 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                // 8 corner values in standard marching cubes order (Bourke numbering).
212                let corners = [
213                    volume.sample(ix, iy, iz),             // 0
214                    volume.sample(ix + 1, iy, iz),         // 1
215                    volume.sample(ix + 1, iy + 1, iz),     // 2
216                    volume.sample(ix, iy + 1, iz),         // 3
217                    volume.sample(ix, iy, iz + 1),         // 4
218                    volume.sample(ix + 1, iy, iz + 1),     // 5
219                    volume.sample(ix + 1, iy + 1, iz + 1), // 6
220                    volume.sample(ix, iy + 1, iz + 1),     // 7
221                ];
222
223                // Compute 8-bit cube index.
224                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                // Corner world positions.
237                let corner_pos = corner_positions(volume, ix, iy, iz);
238
239                // For each triangle edge in TRI_TABLE, get or create vertex.
240                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                        // Canonical edge key: lower-corner cell + axis.
249                        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                    // Emit triangle (swap v1/v2 to match renderer's CCW front-face convention).
278                    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
290/// Compute world-space positions for the 8 corners of a cell.
291fn 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],                // 0
301        [ox + dx, oy, oz],           // 1
302        [ox + dx, oy + dy, oz],      // 2
303        [ox, oy + dy, oz],           // 3
304        [ox, oy, oz + dz],           // 4
305        [ox + dx, oy, oz + dz],      // 5
306        [ox + dx, oy + dy, oz + dz], // 6
307        [ox, oy + dy, oz + dz],      // 7
308    ]
309}
310
311/// Canonical edge key for the vertex deduplication cache.
312///
313/// Shared edges between adjacent cells must produce the same key. We encode each
314/// edge as the lower-index corner cell coordinate + the axis of the edge.
315fn canonical_edge_key(cx: u32, cy: u32, cz: u32, edge_id: u8) -> (u32, u32, u32, u8) {
316    // Each edge is shared by up to 4 cells. We pick the canonical owner as the cell
317    // that has the smallest coordinates among all cells sharing this edge, encoding
318    // the edge as (cell_x, cell_y, cell_z, local_edge_axis).
319    //
320    // Edge axis encoding:
321    //   0-3: edges along X
322    //   4-7: edges along Y
323    //   8-11: edges along Z
324    match edge_id {
325        // X-axis edges
326        0 => (cx, cy, cz, 0),         // edge 0: corner 0-1, bottom-front
327        2 => (cx, cy + 1, cz, 0),     // edge 2: corner 3-2, top-front
328        4 => (cx, cy, cz + 1, 0),     // edge 4: corner 4-5, bottom-back
329        6 => (cx, cy + 1, cz + 1, 0), // edge 6: corner 7-6, top-back
330        // Y-axis edges
331        3 => (cx, cy, cz, 1),         // edge 3: corner 0-3, left-front
332        1 => (cx + 1, cy, cz, 1),     // edge 1: corner 1-2, right-front
333        7 => (cx, cy, cz + 1, 1),     // edge 7: corner 4-7, left-back
334        5 => (cx + 1, cy, cz + 1, 1), // edge 5: corner 5-6, right-back
335        // Z-axis edges
336        8 => (cx, cy, cz, 2),          // edge 8: corner 0-4, bottom-left
337        9 => (cx + 1, cy, cz, 2),      // edge 9: corner 1-5, bottom-right
338        10 => (cx + 1, cy + 1, cz, 2), // edge 10: corner 2-6, top-right
339        11 => (cx, cy + 1, cz, 2),     // edge 11: corner 3-7, top-left
340        _ => (cx, cy, cz, edge_id),    // fallback (should not happen)
341    }
342}
343
344/// Edge vertex pairs: for edge i, EDGE_VERTICES[i] = (corner_a, corner_b).
345const EDGE_VERTICES: [(u8, u8); 12] = [
346    (0, 1), // edge 0
347    (1, 2), // edge 1
348    (2, 3), // edge 2
349    (3, 0), // edge 3
350    (4, 5), // edge 4
351    (5, 6), // edge 5
352    (6, 7), // edge 6
353    (7, 4), // edge 7
354    (0, 4), // edge 8
355    (1, 5), // edge 9
356    (2, 6), // edge 10
357    (3, 7), // edge 11
358];
359
360// ---------------------------------------------------------------------------
361// Standard marching cubes lookup tables (Paul Bourke)
362// ---------------------------------------------------------------------------
363
364/// 12-bit edge intersection bitmask per cube configuration.
365#[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/// Triangle table: up to 5 triangles (15 edge indices) per configuration. -1 = sentinel.
402#[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        // Every edge index referenced in TRI_TABLE[i] must appear in EDGE_TABLE[i].
669        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; // one failure per cube_index is enough
682                }
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        // Mesh was generated.
723        assert!(
724            !mesh.positions.is_empty(),
725            "Sphere isosurface should produce vertices"
726        );
727
728        // Valid triangle list.
729        assert_eq!(mesh.indices.len() % 3, 0, "Indices must be a multiple of 3");
730
731        // Reasonable triangle count for a sphere.
732        assert!(
733            mesh.indices.len() > 100,
734            "Expected > 100 indices for sphere, got {}",
735            mesh.indices.len()
736        );
737
738        // All positions within expected bounding box (center 16 +/- radius 8 + margin).
739        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        // All normals approximately unit length.
750        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        // positions and normals must be same length.
760        assert_eq!(mesh.positions.len(), mesh.normals.len());
761    }
762
763    #[test]
764    fn test_sphere_winding_order() {
765        // Extract a sphere isosurface and verify geometric normals (cross product)
766        // align with the gradient-based vertex normals (which point outward for SDF).
767        // A winding mismatch means some fraction of triangles would be back-face culled.
768        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            // Geometric normal via cross product.
802            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            // Vertex normal (gradient-based, points outward for this SDF).
810            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        // All values above isovalue.
827        let data = vec![10.0f32; 8]; // 2x2x2, all 10.0
828        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, // z=0 row: (0,0,0)=1, (1,0,0)=2
848            3.0, 4.0, // z=0 row: (0,1,0)=3, (1,1,0)=4
849            5.0, 6.0, // z=1 row: (0,0,1)=5, (1,0,1)=6
850            7.0, 8.0, // z=1 row: (0,1,1)=7, (1,1,1)=8
851        ];
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,0)=0, (1,0,0)=1
873            0.0, 1.0, // (0,1,0)=0, (1,1,0)=1
874            0.0, 1.0, // (0,0,1)=0, (1,0,1)=1
875            0.0, 1.0, // (0,1,1)=0, (1,1,1)=1
876        ];
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        // At grid points.
885        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        // At midpoint along X: linear gradient means value = 0.5.
900        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        // At midpoint in all axes.
908        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}