1use std::collections::HashMap;
24
25use rayon::prelude::*;
26
27use super::types::{AttributeData, MeshData};
28
29const PARALLEL_THRESHOLD: usize = 1024;
30
31pub const CELL_SENTINEL: u32 = u32::MAX;
36
37#[deprecated(since = "0.13.0", note = "use `CELL_SENTINEL` instead")]
39pub const TET_SENTINEL: u32 = CELL_SENTINEL;
40
41#[non_exhaustive]
64#[derive(Default, Clone)]
65pub struct VolumeMeshData {
66 pub positions: Vec<[f32; 3]>,
68
69 pub cells: Vec<[u32; 8]>,
75
76 pub cell_scalars: HashMap<String, Vec<f32>>,
81
82 pub cell_colours: HashMap<String, Vec<[f32; 4]>>,
87}
88
89const TET_FACES: [[usize; 3]; 4] = [
102 [1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1], ];
107
108const HEX_FACES: [[usize; 4]; 6] = [
134 [0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [2, 6, 7, 3], [0, 3, 7, 4], [1, 5, 6, 2], ];
141
142const HEX_FACE_OPPOSITE: [usize; 6] = [1, 0, 3, 2, 5, 4];
144
145const PYRAMID_QUAD_FACE: [[usize; 4]; 1] = [
164 [0, 1, 2, 3], ];
166
167const PYRAMID_TRI_FACES: [[usize; 3]; 4] = [
169 [0, 4, 1], [1, 4, 2], [2, 4, 3], [3, 4, 0], ];
174
175const PYRAMID_EDGES: [[usize; 2]; 8] = [
177 [0, 1],
178 [1, 2],
179 [2, 3],
180 [3, 0], [0, 4],
182 [1, 4],
183 [2, 4],
184 [3, 4], ];
186
187const WEDGE_TRI_FACES: [[usize; 3]; 2] = [
206 [0, 2, 1], [3, 4, 5], ];
209
210const WEDGE_QUAD_FACES: [[usize; 4]; 3] = [
212 [0, 1, 4, 3], [1, 2, 5, 4], [2, 0, 3, 5], ];
216
217const WEDGE_EDGES: [[usize; 2]; 9] = [
219 [0, 1],
220 [1, 2],
221 [2, 0], [3, 4],
223 [4, 5],
224 [5, 3], [0, 3],
226 [1, 4],
227 [2, 5], ];
229
230type FaceKey = (u32, u32, u32);
236
237type QuadFaceKey = (u32, u32, u32, u32);
239
240type TriEntry = (FaceKey, usize, [u32; 3], [f32; 3]);
242type QuadEntry = (QuadFaceKey, usize, [u32; 4], [f32; 3]);
243
244#[inline]
246fn face_key(a: u32, b: u32, c: u32) -> FaceKey {
247 let mut arr = [a, b, c];
248 arr.sort_unstable();
249 (arr[0], arr[1], arr[2])
250}
251
252#[inline]
254fn quad_face_key(a: u32, b: u32, c: u32, d: u32) -> QuadFaceKey {
255 let mut arr = [a, b, c, d];
256 arr.sort_unstable();
257 (arr[0], arr[1], arr[2], arr[3])
258}
259
260fn generate_tri_entries(cell_idx: usize, cell: &[u32; 8], positions: &[[f32; 3]]) -> Vec<TriEntry> {
262 let ct = cell_type(cell);
263 let nv = ct.vertex_count();
264 let mut out = Vec::new();
265 match ct {
266 CellType::Tet => {
267 for (face_idx, face_local) in TET_FACES.iter().enumerate() {
268 let a = cell[face_local[0]];
269 let b = cell[face_local[1]];
270 let c = cell[face_local[2]];
271 let interior_ref = positions[cell[face_idx] as usize];
273 out.push((face_key(a, b, c), cell_idx, [a, b, c], interior_ref));
274 }
275 }
276 CellType::Pyramid => {
277 let centroid = cell_centroid(cell, nv, positions);
278 for face_local in &PYRAMID_TRI_FACES {
279 let a = cell[face_local[0]];
280 let b = cell[face_local[1]];
281 let c = cell[face_local[2]];
282 out.push((face_key(a, b, c), cell_idx, [a, b, c], centroid));
283 }
284 }
285 CellType::Wedge => {
286 let centroid = cell_centroid(cell, nv, positions);
287 for face_local in &WEDGE_TRI_FACES {
288 let a = cell[face_local[0]];
289 let b = cell[face_local[1]];
290 let c = cell[face_local[2]];
291 out.push((face_key(a, b, c), cell_idx, [a, b, c], centroid));
292 }
293 }
294 CellType::Hex => {} }
296 out
297}
298
299fn generate_quad_entries(
301 cell_idx: usize,
302 cell: &[u32; 8],
303 positions: &[[f32; 3]],
304) -> Vec<QuadEntry> {
305 let ct = cell_type(cell);
306 let nv = ct.vertex_count();
307 let mut out = Vec::new();
308 match ct {
309 CellType::Tet => {} CellType::Pyramid => {
311 let centroid = cell_centroid(cell, nv, positions);
312 for quad_local in &PYRAMID_QUAD_FACE {
313 let v = [
314 cell[quad_local[0]],
315 cell[quad_local[1]],
316 cell[quad_local[2]],
317 cell[quad_local[3]],
318 ];
319 out.push((quad_face_key(v[0], v[1], v[2], v[3]), cell_idx, v, centroid));
320 }
321 }
322 CellType::Wedge => {
323 let centroid = cell_centroid(cell, nv, positions);
324 for quad_local in &WEDGE_QUAD_FACES {
325 let v = [
326 cell[quad_local[0]],
327 cell[quad_local[1]],
328 cell[quad_local[2]],
329 cell[quad_local[3]],
330 ];
331 out.push((quad_face_key(v[0], v[1], v[2], v[3]), cell_idx, v, centroid));
332 }
333 }
334 CellType::Hex => {
335 for (face_idx, quad) in HEX_FACES.iter().enumerate() {
336 let v: [u32; 4] = [cell[quad[0]], cell[quad[1]], cell[quad[2]], cell[quad[3]]];
337 let interior_ref = {
338 let opposite = &HEX_FACES[HEX_FACE_OPPOSITE[face_idx]];
339 let mut c = [0.0f32; 3];
340 for &local_vi in opposite {
341 let p = positions[cell[local_vi] as usize];
342 c[0] += p[0];
343 c[1] += p[1];
344 c[2] += p[2];
345 }
346 [c[0] / 4.0, c[1] / 4.0, c[2] / 4.0]
347 };
348 out.push((
349 quad_face_key(v[0], v[1], v[2], v[3]),
350 cell_idx,
351 v,
352 interior_ref,
353 ));
354 }
355 }
356 }
357 out
358}
359
360fn collect_boundary_tri(entries: &[TriEntry]) -> Vec<(usize, [u32; 3], [f32; 3])> {
362 let mut out = Vec::new();
363 let mut i = 0;
364 while i < entries.len() {
365 let key = entries[i].0;
366 let mut j = i + 1;
367 while j < entries.len() && entries[j].0 == key {
368 j += 1;
369 }
370 if j - i == 1 {
371 out.push((entries[i].1, entries[i].2, entries[i].3));
372 }
373 i = j;
374 }
375 out
376}
377
378fn collect_boundary_quad(entries: &[QuadEntry]) -> Vec<(usize, [u32; 4], [f32; 3])> {
380 let mut out = Vec::new();
381 let mut i = 0;
382 while i < entries.len() {
383 let key = entries[i].0;
384 let mut j = i + 1;
385 while j < entries.len() && entries[j].0 == key {
386 j += 1;
387 }
388 if j - i == 1 {
389 out.push((entries[i].1, entries[i].2, entries[i].3));
390 }
391 i = j;
392 }
393 out
394}
395
396#[inline]
399fn correct_winding(tri: &mut [u32; 3], interior_ref: &[f32; 3], positions: &[[f32; 3]]) {
400 let pa = positions[tri[0] as usize];
401 let pb = positions[tri[1] as usize];
402 let pc = positions[tri[2] as usize];
403 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
404 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
405 let normal = [
406 ab[1] * ac[2] - ab[2] * ac[1],
407 ab[2] * ac[0] - ab[0] * ac[2],
408 ab[0] * ac[1] - ab[1] * ac[0],
409 ];
410 let fc = [
411 (pa[0] + pb[0] + pc[0]) / 3.0,
412 (pa[1] + pb[1] + pc[1]) / 3.0,
413 (pa[2] + pb[2] + pc[2]) / 3.0,
414 ];
415 let out = [
416 fc[0] - interior_ref[0],
417 fc[1] - interior_ref[1],
418 fc[2] - interior_ref[2],
419 ];
420 if normal[0] * out[0] + normal[1] * out[1] + normal[2] * out[2] < 0.0 {
421 tri.swap(1, 2);
422 }
423}
424
425pub(crate) fn extract_boundary_faces(data: &VolumeMeshData) -> (MeshData, Vec<u32>) {
435 let n_verts = data.positions.len();
436
437 let (mut tri_entries, mut quad_entries) = if data.cells.len() >= PARALLEL_THRESHOLD {
439 let tri = data
440 .cells
441 .par_iter()
442 .enumerate()
443 .flat_map_iter(|(ci, cell)| generate_tri_entries(ci, cell, &data.positions))
444 .collect();
445 let quad = data
446 .cells
447 .par_iter()
448 .enumerate()
449 .flat_map_iter(|(ci, cell)| generate_quad_entries(ci, cell, &data.positions))
450 .collect();
451 (tri, quad)
452 } else {
453 let mut tri: Vec<TriEntry> = Vec::new();
454 let mut quad: Vec<QuadEntry> = Vec::new();
455 for (ci, cell) in data.cells.iter().enumerate() {
456 tri.extend(generate_tri_entries(ci, cell, &data.positions));
457 quad.extend(generate_quad_entries(ci, cell, &data.positions));
458 }
459 (tri, quad)
460 };
461
462 tri_entries.par_sort_unstable_by_key(|e| e.0);
463 quad_entries.par_sort_unstable_by_key(|e| e.0);
464
465 let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = collect_boundary_tri(&tri_entries);
467 for (ci, winding, iref) in collect_boundary_quad(&quad_entries) {
468 boundary.push((ci, [winding[0], winding[1], winding[2]], iref));
469 boundary.push((ci, [winding[0], winding[2], winding[3]], iref));
470 }
471
472 boundary.sort_unstable_by_key(|(ci, _, _)| *ci);
474
475 boundary
479 .par_iter_mut()
480 .for_each(|(_, tri, iref)| correct_winding(tri, iref, &data.positions));
481
482 let n_boundary_tris = boundary.len();
483
484 let mut indices: Vec<u32> = Vec::with_capacity(n_boundary_tris * 3);
487 let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
488
489 for (_, tri, _) in &boundary {
490 indices.push(tri[0]);
491 indices.push(tri[1]);
492 indices.push(tri[2]);
493
494 let pa = data.positions[tri[0] as usize];
495 let pb = data.positions[tri[1] as usize];
496 let pc = data.positions[tri[2] as usize];
497 let ab = [
498 (pb[0] - pa[0]) as f64,
499 (pb[1] - pa[1]) as f64,
500 (pb[2] - pa[2]) as f64,
501 ];
502 let ac = [
503 (pc[0] - pa[0]) as f64,
504 (pc[1] - pa[1]) as f64,
505 (pc[2] - pa[2]) as f64,
506 ];
507 let n = [
508 ab[1] * ac[2] - ab[2] * ac[1],
509 ab[2] * ac[0] - ab[0] * ac[2],
510 ab[0] * ac[1] - ab[1] * ac[0],
511 ];
512 for &vi in tri {
513 let acc = &mut normal_accum[vi as usize];
514 acc[0] += n[0];
515 acc[1] += n[1];
516 acc[2] += n[2];
517 }
518 }
519
520 let mut normals: Vec<[f32; 3]> = normal_accum
521 .iter()
522 .map(|n| {
523 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
524 if len > 1e-12 {
525 [
526 (n[0] / len) as f32,
527 (n[1] / len) as f32,
528 (n[2] / len) as f32,
529 ]
530 } else {
531 [0.0, 1.0, 0.0]
532 }
533 })
534 .collect();
535
536 normals.resize(n_verts, [0.0, 1.0, 0.0]);
537
538 let mut attributes: HashMap<String, AttributeData> = HashMap::new();
539
540 for (name, cell_vals) in &data.cell_scalars {
541 let face_scalars: Vec<f32> = boundary
542 .iter()
543 .map(|(ci, _, _)| cell_vals.get(*ci).copied().unwrap_or(0.0))
544 .collect();
545 attributes.insert(name.clone(), AttributeData::Face(face_scalars));
546 }
547
548 for (name, cell_vals) in &data.cell_colours {
549 let face_colours: Vec<[f32; 4]> = boundary
550 .iter()
551 .map(|(ci, _, _)| cell_vals.get(*ci).copied().unwrap_or([1.0; 4]))
552 .collect();
553 attributes.insert(name.clone(), AttributeData::FaceColour(face_colours));
554 }
555
556 let face_to_cell: Vec<u32> = boundary.iter().map(|(ci, _, _)| *ci as u32).collect();
557
558 (
559 MeshData {
560 positions: data.positions.clone(),
561 normals,
562 indices,
563 uvs: None,
564 tangents: None,
565 attributes,
566 },
567 face_to_cell,
568 )
569}
570
571const TET_EDGES: [[usize; 2]; 6] = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]];
672
673const HEX_EDGES: [[usize; 2]; 12] = [
684 [0, 1],
685 [1, 2],
686 [2, 3],
687 [3, 0], [4, 5],
689 [5, 6],
690 [6, 7],
691 [7, 4], [0, 4],
693 [1, 5],
694 [2, 6],
695 [3, 7], ];
697
698#[derive(Clone, Copy, PartialEq, Eq)]
704enum CellType {
705 Tet,
706 Pyramid,
707 Wedge,
708 Hex,
709}
710
711impl CellType {
712 fn vertex_count(self) -> usize {
713 match self {
714 CellType::Tet => 4,
715 CellType::Pyramid => 5,
716 CellType::Wedge => 6,
717 CellType::Hex => 8,
718 }
719 }
720
721 fn edges(self) -> &'static [[usize; 2]] {
722 match self {
723 CellType::Tet => &TET_EDGES,
724 CellType::Pyramid => &PYRAMID_EDGES,
725 CellType::Wedge => &WEDGE_EDGES,
726 CellType::Hex => &HEX_EDGES,
727 }
728 }
729}
730
731#[inline]
733fn cell_type(cell: &[u32; 8]) -> CellType {
734 if cell[4] == CELL_SENTINEL {
735 CellType::Tet
736 } else if cell[5] == CELL_SENTINEL {
737 CellType::Pyramid
738 } else if cell[6] == CELL_SENTINEL {
739 CellType::Wedge
740 } else {
741 CellType::Hex
742 }
743}
744
745#[inline]
747fn cell_centroid(cell: &[u32; 8], nv: usize, positions: &[[f32; 3]]) -> [f32; 3] {
748 let mut c = [0.0f32; 3];
749 for i in 0..nv {
750 let p = positions[cell[i] as usize];
751 c[0] += p[0];
752 c[1] += p[1];
753 c[2] += p[2];
754 }
755 let n = nv as f32;
756 [c[0] / n, c[1] / n, c[2] / n]
757}
758
759#[inline]
762fn plane_dist(p: [f32; 3], plane: [f32; 4]) -> f32 {
763 p[0] * plane[0] + p[1] * plane[1] + p[2] * plane[2] + plane[3]
764}
765
766#[inline]
768fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
769 [
770 a[1] * b[2] - a[2] * b[1],
771 a[2] * b[0] - a[0] * b[2],
772 a[0] * b[1] - a[1] * b[0],
773 ]
774}
775
776#[inline]
778fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
779 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
780}
781
782#[inline]
784fn normalize3(v: [f32; 3]) -> [f32; 3] {
785 let len = dot3(v, v).sqrt();
786 if len > 1e-12 {
787 [v[0] / len, v[1] / len, v[2] / len]
788 } else {
789 [0.0, 1.0, 0.0]
790 }
791}
792
793fn intern_pos(
797 p: [f32; 3],
798 positions: &mut Vec<[f32; 3]>,
799 pos_map: &mut HashMap<[u32; 3], u32>,
800) -> u32 {
801 let key = [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
802 if let Some(&idx) = pos_map.get(&key) {
803 return idx;
804 }
805 let idx = positions.len() as u32;
806 positions.push(p);
807 pos_map.insert(key, idx);
808 idx
809}
810
811fn clip_polygon_one_plane(poly: Vec<[f32; 3]>, plane: [f32; 4]) -> Vec<[f32; 3]> {
814 if poly.is_empty() {
815 return poly;
816 }
817 let n = poly.len();
818 let mut out = Vec::with_capacity(n + 1);
819 for i in 0..n {
820 let a = poly[i];
821 let b = poly[(i + 1) % n];
822 let da = plane_dist(a, plane);
823 let db = plane_dist(b, plane);
824 let a_in = da >= 0.0;
825 let b_in = db >= 0.0;
826 if a_in {
827 out.push(a);
828 }
829 if a_in != b_in {
830 let denom = da - db;
831 if denom.abs() > 1e-30 {
832 let t = da / denom;
833 out.push([
834 a[0] + t * (b[0] - a[0]),
835 a[1] + t * (b[1] - a[1]),
836 a[2] + t * (b[2] - a[2]),
837 ]);
838 }
839 }
840 }
841 out
842}
843
844fn clip_polygon_planes(mut poly: Vec<[f32; 3]>, planes: &[[f32; 4]]) -> Vec<[f32; 3]> {
846 for &plane in planes {
847 if poly.is_empty() {
848 break;
849 }
850 poly = clip_polygon_one_plane(poly, plane);
851 }
852 poly
853}
854
855fn plane_basis(normal: [f32; 3]) -> ([f32; 3], [f32; 3]) {
857 let ref_vec: [f32; 3] = if normal[0].abs() < 0.9 {
858 [1.0, 0.0, 0.0]
859 } else {
860 [0.0, 1.0, 0.0]
861 };
862 let u = normalize3(cross3(normal, ref_vec));
863 let v = cross3(normal, u);
864 (u, v)
865}
866
867fn sort_polygon_on_plane(pts: &mut Vec<[f32; 3]>, normal: [f32; 3]) {
872 if pts.len() < 3 {
873 return;
874 }
875 let n = pts.len() as f32;
876 let cx = pts.iter().map(|p| p[0]).sum::<f32>() / n;
877 let cy = pts.iter().map(|p| p[1]).sum::<f32>() / n;
878 let cz = pts.iter().map(|p| p[2]).sum::<f32>() / n;
879 let centroid = [cx, cy, cz];
880 let (u, v) = plane_basis(normal);
881 pts.sort_by(|a, b| {
882 let da = [a[0] - centroid[0], a[1] - centroid[1], a[2] - centroid[2]];
883 let db = [b[0] - centroid[0], b[1] - centroid[1], b[2] - centroid[2]];
884 let ang_a = dot3(da, v).atan2(dot3(da, u));
885 let ang_b = dot3(db, v).atan2(dot3(db, u));
886 ang_a
887 .partial_cmp(&ang_b)
888 .unwrap_or(std::cmp::Ordering::Equal)
889 });
890}
891
892fn fan_triangulate(poly: &[[f32; 3]]) -> Vec<[[f32; 3]; 3]> {
894 if poly.len() < 3 {
895 return Vec::new();
896 }
897 (1..poly.len() - 1)
898 .map(|i| [poly[0], poly[i], poly[i + 1]])
899 .collect()
900}
901
902fn generate_section_tris(
904 cell_idx: usize,
905 cell: &[u32; 8],
906 positions: &[[f32; 3]],
907 clip_planes: &[[f32; 4]],
908) -> Vec<(usize, [[f32; 3]; 3])> {
909 let mut out = Vec::new();
910 let edges = cell_type(cell).edges();
911
912 for (pi, &plane) in clip_planes.iter().enumerate() {
913 let mut pts: Vec<[f32; 3]> = Vec::new();
914 for edge in edges {
915 let pa = positions[cell[edge[0]] as usize];
916 let pb = positions[cell[edge[1]] as usize];
917 let da = plane_dist(pa, plane);
918 let db = plane_dist(pb, plane);
919 if (da >= 0.0) != (db >= 0.0) {
920 let denom = da - db;
921 if denom.abs() > 1e-30 {
922 let t = da / denom;
923 pts.push([
924 pa[0] + t * (pb[0] - pa[0]),
925 pa[1] + t * (pb[1] - pa[1]),
926 pa[2] + t * (pb[2] - pa[2]),
927 ]);
928 }
929 }
930 }
931 if pts.len() < 3 {
932 continue;
933 }
934 let plane_normal = [plane[0], plane[1], plane[2]];
935 sort_polygon_on_plane(&mut pts, plane_normal);
936 let other_planes: Vec<[f32; 4]> = clip_planes
937 .iter()
938 .enumerate()
939 .filter(|(i, _)| *i != pi)
940 .map(|(_, p)| *p)
941 .collect();
942 let pts = clip_polygon_planes(pts, &other_planes);
943 if pts.len() < 3 {
944 continue;
945 }
946 for mut tri in fan_triangulate(&pts) {
947 let ab = [
948 tri[1][0] - tri[0][0],
949 tri[1][1] - tri[0][1],
950 tri[1][2] - tri[0][2],
951 ];
952 let ac = [
953 tri[2][0] - tri[0][0],
954 tri[2][1] - tri[0][1],
955 tri[2][2] - tri[0][2],
956 ];
957 let n = cross3(ab, ac);
958 if dot3(n, plane_normal) < 0.0 {
959 tri.swap(1, 2);
960 }
961 out.push((cell_idx, tri));
962 }
963 }
964 out
965}
966
967pub fn extract_clipped_volume_faces(
977 data: &VolumeMeshData,
978 clip_planes: &[[f32; 4]],
979) -> (MeshData, Vec<u32>) {
980 if clip_planes.is_empty() {
981 return extract_boundary_faces(data);
982 }
983
984 let vert_kept: Vec<bool> = data
986 .positions
987 .par_iter()
988 .map(|&p| clip_planes.iter().all(|&pl| plane_dist(p, pl) >= 0.0))
989 .collect();
990
991 let (mut tri_entries, mut quad_entries) = if data.cells.len() >= PARALLEL_THRESHOLD {
993 let vk = &vert_kept;
994 let tri = data
995 .cells
996 .par_iter()
997 .enumerate()
998 .flat_map_iter(|(ci, cell)| {
999 let nv = cell_type(cell).vertex_count();
1000 if (0..nv).all(|i| !vk[cell[i] as usize]) {
1001 return Vec::new();
1002 }
1003 generate_tri_entries(ci, cell, &data.positions)
1004 })
1005 .collect();
1006 let quad = data
1007 .cells
1008 .par_iter()
1009 .enumerate()
1010 .flat_map_iter(|(ci, cell)| {
1011 let nv = cell_type(cell).vertex_count();
1012 if (0..nv).all(|i| !vk[cell[i] as usize]) {
1013 return Vec::new();
1014 }
1015 generate_quad_entries(ci, cell, &data.positions)
1016 })
1017 .collect();
1018 (tri, quad)
1019 } else {
1020 let mut tri: Vec<TriEntry> = Vec::new();
1021 let mut quad: Vec<QuadEntry> = Vec::new();
1022 for (ci, cell) in data.cells.iter().enumerate() {
1023 let nv = cell_type(cell).vertex_count();
1024 let kc = (0..nv).filter(|&i| vert_kept[cell[i] as usize]).count();
1025 if kc == 0 {
1026 continue;
1027 }
1028 tri.extend(generate_tri_entries(ci, cell, &data.positions));
1029 quad.extend(generate_quad_entries(ci, cell, &data.positions));
1030 }
1031 (tri, quad)
1032 };
1033
1034 tri_entries.par_sort_unstable_by_key(|e| e.0);
1035 quad_entries.par_sort_unstable_by_key(|e| e.0);
1036
1037 let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = collect_boundary_tri(&tri_entries);
1038 for (ci, winding, iref) in collect_boundary_quad(&quad_entries) {
1039 boundary.push((ci, [winding[0], winding[1], winding[2]], iref));
1040 boundary.push((ci, [winding[0], winding[2], winding[3]], iref));
1041 }
1042 boundary.sort_unstable_by_key(|(ci, _, _)| *ci);
1043
1044 boundary
1045 .par_iter_mut()
1046 .for_each(|(_, tri, iref)| correct_winding(tri, iref, &data.positions));
1047
1048 let cell_nv: Vec<usize> = data
1050 .cells
1051 .iter()
1052 .map(|c| cell_type(c).vertex_count())
1053 .collect();
1054 let cell_kept: Vec<usize> = data
1055 .cells
1056 .iter()
1057 .zip(cell_nv.iter())
1058 .map(|(cell, &nv)| (0..nv).filter(|&i| vert_kept[cell[i] as usize]).count())
1059 .collect();
1060
1061 let mut out_tris: Vec<(usize, [[f32; 3]; 3])> = boundary
1063 .par_iter()
1064 .flat_map_iter(|(cell_idx, tri, _)| {
1065 let nv = cell_nv[*cell_idx];
1066 let kc = cell_kept[*cell_idx];
1067 let pa = data.positions[tri[0] as usize];
1068 let pb = data.positions[tri[1] as usize];
1069 let pc = data.positions[tri[2] as usize];
1070 if kc == nv {
1071 vec![(*cell_idx, [pa, pb, pc])]
1072 } else {
1073 let clipped = clip_polygon_planes(vec![pa, pb, pc], clip_planes);
1074 fan_triangulate(&clipped)
1075 .into_iter()
1076 .map(|t| (*cell_idx, t))
1077 .collect()
1078 }
1079 })
1080 .collect();
1081
1082 let section_tris: Vec<(usize, [[f32; 3]; 3])> = data
1084 .cells
1085 .par_iter()
1086 .enumerate()
1087 .filter(|(ci, _)| {
1088 let kc = cell_kept[*ci];
1089 kc > 0 && kc < cell_nv[*ci]
1090 })
1091 .flat_map_iter(|(ci, cell)| generate_section_tris(ci, cell, &data.positions, clip_planes))
1092 .collect();
1093 out_tris.extend(section_tris);
1094
1095 let mut positions: Vec<[f32; 3]> = data.positions.clone();
1097 let mut pos_map: HashMap<[u32; 3], u32> = HashMap::new();
1098 for (i, &p) in data.positions.iter().enumerate() {
1099 let key = [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
1100 pos_map.entry(key).or_insert(i as u32);
1101 }
1102
1103 let mut indexed_tris: Vec<(usize, [u32; 3])> = Vec::with_capacity(out_tris.len());
1104 for (cell_idx, [p0, p1, p2]) in &out_tris {
1105 let i0 = intern_pos(*p0, &mut positions, &mut pos_map);
1106 let i1 = intern_pos(*p1, &mut positions, &mut pos_map);
1107 let i2 = intern_pos(*p2, &mut positions, &mut pos_map);
1108 indexed_tris.push((*cell_idx, [i0, i1, i2]));
1109 }
1110
1111 let n_verts = positions.len();
1112 let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
1113 let mut indices: Vec<u32> = Vec::with_capacity(indexed_tris.len() * 3);
1114
1115 for (_, tri) in &indexed_tris {
1116 indices.push(tri[0]);
1117 indices.push(tri[1]);
1118 indices.push(tri[2]);
1119
1120 let pa = positions[tri[0] as usize];
1121 let pb = positions[tri[1] as usize];
1122 let pc = positions[tri[2] as usize];
1123 let ab = [
1124 (pb[0] - pa[0]) as f64,
1125 (pb[1] - pa[1]) as f64,
1126 (pb[2] - pa[2]) as f64,
1127 ];
1128 let ac = [
1129 (pc[0] - pa[0]) as f64,
1130 (pc[1] - pa[1]) as f64,
1131 (pc[2] - pa[2]) as f64,
1132 ];
1133 let n = [
1134 ab[1] * ac[2] - ab[2] * ac[1],
1135 ab[2] * ac[0] - ab[0] * ac[2],
1136 ab[0] * ac[1] - ab[1] * ac[0],
1137 ];
1138 for &vi in tri {
1139 let acc = &mut normal_accum[vi as usize];
1140 acc[0] += n[0];
1141 acc[1] += n[1];
1142 acc[2] += n[2];
1143 }
1144 }
1145
1146 let normals: Vec<[f32; 3]> = normal_accum
1147 .iter()
1148 .map(|n| {
1149 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1150 if len > 1e-12 {
1151 [
1152 (n[0] / len) as f32,
1153 (n[1] / len) as f32,
1154 (n[2] / len) as f32,
1155 ]
1156 } else {
1157 [0.0, 1.0, 0.0]
1158 }
1159 })
1160 .collect();
1161
1162 let mut attributes: HashMap<String, AttributeData> = HashMap::new();
1163 for (name, cell_vals) in &data.cell_scalars {
1164 let face_scalars: Vec<f32> = indexed_tris
1165 .iter()
1166 .map(|(ci, _)| cell_vals.get(*ci).copied().unwrap_or(0.0))
1167 .collect();
1168 attributes.insert(name.clone(), AttributeData::Face(face_scalars));
1169 }
1170 for (name, cell_vals) in &data.cell_colours {
1171 let face_colours: Vec<[f32; 4]> = indexed_tris
1172 .iter()
1173 .map(|(ci, _)| cell_vals.get(*ci).copied().unwrap_or([1.0; 4]))
1174 .collect();
1175 attributes.insert(name.clone(), AttributeData::FaceColour(face_colours));
1176 }
1177
1178 let face_to_cell: Vec<u32> = indexed_tris.iter().map(|(ci, _)| *ci as u32).collect();
1179
1180 (
1181 MeshData {
1182 positions,
1183 normals,
1184 indices,
1185 uvs: None,
1186 tangents: None,
1187 attributes,
1188 },
1189 face_to_cell,
1190 )
1191}
1192
1193impl VolumeMeshData {
1198 pub fn push_tet(&mut self, a: u32, b: u32, c: u32, d: u32) {
1202 self.cells.push([
1203 a,
1204 b,
1205 c,
1206 d,
1207 CELL_SENTINEL,
1208 CELL_SENTINEL,
1209 CELL_SENTINEL,
1210 CELL_SENTINEL,
1211 ]);
1212 }
1213
1214 pub fn push_pyramid(&mut self, base: [u32; 4], apex: u32) {
1220 self.cells.push([
1221 base[0],
1222 base[1],
1223 base[2],
1224 base[3],
1225 apex,
1226 CELL_SENTINEL,
1227 CELL_SENTINEL,
1228 CELL_SENTINEL,
1229 ]);
1230 }
1231
1232 pub fn push_wedge(&mut self, tri0: [u32; 3], tri1: [u32; 3]) {
1238 self.cells.push([
1239 tri0[0],
1240 tri0[1],
1241 tri0[2],
1242 tri1[0],
1243 tri1[1],
1244 tri1[2],
1245 CELL_SENTINEL,
1246 CELL_SENTINEL,
1247 ]);
1248 }
1249
1250 pub fn push_hex(&mut self, verts: [u32; 8]) {
1252 self.cells.push(verts);
1253 }
1254
1255 pub fn to_tet_mesh(&self) -> Result<(super::tetmesh::TetMesh, ConversionReport), ToTetMeshError> {
1262 use glam::Vec3;
1263
1264 let mut tet_indices: Vec<[u32; 4]> = Vec::new();
1265 let mut dropped = 0usize;
1266 for cell in &self.cells {
1267 if cell[4] == CELL_SENTINEL
1268 && cell[5] == CELL_SENTINEL
1269 && cell[6] == CELL_SENTINEL
1270 && cell[7] == CELL_SENTINEL
1271 {
1272 tet_indices.push([cell[0], cell[1], cell[2], cell[3]]);
1273 } else {
1274 dropped += 1;
1275 }
1276 }
1277
1278 if tet_indices.is_empty() {
1279 return Err(ToTetMeshError::NoTetCells);
1280 }
1281
1282 let vertex_count = self.positions.len();
1283 let mut remap = vec![u32::MAX; vertex_count];
1284 let mut positions: Vec<Vec3> = Vec::new();
1285 let mut tets: Vec<[u32; 4]> = Vec::with_capacity(tet_indices.len());
1286 for raw in tet_indices {
1287 let mut out = [0_u32; 4];
1288 for (slot, &idx) in raw.iter().enumerate() {
1289 let idx_usize = idx as usize;
1290 if idx_usize >= vertex_count {
1291 return Err(ToTetMeshError::OutOfRangeIndex(idx));
1292 }
1293 if remap[idx_usize] == u32::MAX {
1294 remap[idx_usize] = positions.len() as u32;
1295 let p = self.positions[idx_usize];
1296 positions.push(Vec3::new(p[0], p[1], p[2]));
1297 }
1298 out[slot] = remap[idx_usize];
1299 }
1300 tets.push(out);
1301 }
1302
1303 Ok((super::tetmesh::TetMesh::new(positions, tets), ConversionReport { dropped_non_tet_cells: dropped }))
1304 }
1305}
1306
1307#[derive(Clone, Debug, Default, PartialEq)]
1309pub struct ConversionReport {
1310 pub dropped_non_tet_cells: usize,
1312}
1313
1314#[derive(Debug)]
1316pub enum ToTetMeshError {
1317 NoTetCells,
1319 OutOfRangeIndex(u32),
1321}
1322
1323const HEX_TO_TETS: [[usize; 4]; 6] = [
1331 [0, 1, 5, 6],
1332 [0, 1, 2, 6],
1333 [0, 4, 5, 6],
1334 [0, 4, 7, 6],
1335 [0, 3, 2, 6],
1336 [0, 3, 7, 6],
1337];
1338
1339const WEDGE_TO_TETS: [[usize; 4]; 3] = [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]];
1343
1344const PYRAMID_TO_TETS: [[usize; 4]; 2] = [[0, 1, 2, 4], [0, 2, 3, 4]];
1348
1349pub(crate) fn for_each_tet<F>(data: &VolumeMeshData, attribute: &str, mut f: F)
1361where
1362 F: FnMut([[f32; 3]; 4], f32),
1363{
1364 let cell_scalars = data.cell_scalars.get(attribute);
1365 for (cell_idx, cell) in data.cells.iter().enumerate() {
1366 let scalar = cell_scalars
1367 .and_then(|v| v.get(cell_idx))
1368 .copied()
1369 .unwrap_or(0.0);
1370 let tets: &[[usize; 4]] = match cell_type(cell) {
1371 CellType::Tet => &[[0, 1, 2, 3]],
1372 CellType::Pyramid => &PYRAMID_TO_TETS,
1373 CellType::Wedge => &WEDGE_TO_TETS,
1374 CellType::Hex => &HEX_TO_TETS,
1375 };
1376 for local in tets {
1377 let verts = [
1378 data.positions[cell[local[0]] as usize],
1379 data.positions[cell[local[1]] as usize],
1380 data.positions[cell[local[2]] as usize],
1381 data.positions[cell[local[3]] as usize],
1382 ];
1383 f(verts, scalar);
1384 }
1385 }
1386}
1387
1388#[cfg(test)]
1398pub(crate) fn decompose_to_tetrahedra(
1399 data: &VolumeMeshData,
1400 attribute: &str,
1401) -> (Vec<[[f32; 3]; 4]>, Vec<f32>) {
1402 let mut positions: Vec<[[f32; 3]; 4]> = Vec::new();
1403 let mut scalars: Vec<f32> = Vec::new();
1404 for_each_tet(data, attribute, |verts, scalar| {
1405 positions.push(verts);
1406 scalars.push(scalar);
1407 });
1408 (positions, scalars)
1409}
1410
1411#[cfg(test)]
1416mod tests {
1417 use super::*;
1418
1419 const TEST_TET_LOCAL: [[usize; 4]; 6] = [
1420 [0, 1, 5, 6],
1421 [0, 1, 2, 6],
1422 [0, 4, 5, 6],
1423 [0, 4, 7, 6],
1424 [0, 3, 2, 6],
1425 [0, 3, 7, 6],
1426 ];
1427
1428 fn single_tet() -> VolumeMeshData {
1429 VolumeMeshData {
1430 positions: vec![
1431 [0.0, 0.0, 0.0],
1432 [1.0, 0.0, 0.0],
1433 [0.5, 1.0, 0.0],
1434 [0.5, 0.5, 1.0],
1435 ],
1436 cells: vec![[
1437 0,
1438 1,
1439 2,
1440 3,
1441 CELL_SENTINEL,
1442 CELL_SENTINEL,
1443 CELL_SENTINEL,
1444 CELL_SENTINEL,
1445 ]],
1446 ..Default::default()
1447 }
1448 }
1449
1450 fn two_tets_sharing_face() -> VolumeMeshData {
1451 VolumeMeshData {
1454 positions: vec![
1455 [0.0, 0.0, 0.0],
1456 [1.0, 0.0, 0.0],
1457 [0.5, 1.0, 0.0],
1458 [0.5, 0.5, 1.0],
1459 [0.5, 0.5, -1.0],
1460 ],
1461 cells: vec![
1462 [
1463 0,
1464 1,
1465 2,
1466 3,
1467 CELL_SENTINEL,
1468 CELL_SENTINEL,
1469 CELL_SENTINEL,
1470 CELL_SENTINEL,
1471 ],
1472 [
1473 0,
1474 2,
1475 1,
1476 4,
1477 CELL_SENTINEL,
1478 CELL_SENTINEL,
1479 CELL_SENTINEL,
1480 CELL_SENTINEL,
1481 ],
1482 ],
1483 ..Default::default()
1484 }
1485 }
1486
1487 fn single_hex() -> VolumeMeshData {
1488 VolumeMeshData {
1489 positions: vec![
1490 [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0], ],
1499 cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
1500 ..Default::default()
1501 }
1502 }
1503
1504 fn structured_tet_grid(grid_n: usize) -> VolumeMeshData {
1505 let grid_v = grid_n + 1;
1506 let vid =
1507 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1508
1509 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1510 for iz in 0..grid_v {
1511 for iy in 0..grid_v {
1512 for ix in 0..grid_v {
1513 positions.push([ix as f32, iy as f32, iz as f32]);
1514 }
1515 }
1516 }
1517
1518 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1519 for iz in 0..grid_n {
1520 for iy in 0..grid_n {
1521 for ix in 0..grid_n {
1522 let cube_verts = [
1523 vid(ix, iy, iz),
1524 vid(ix + 1, iy, iz),
1525 vid(ix + 1, iy, iz + 1),
1526 vid(ix, iy, iz + 1),
1527 vid(ix, iy + 1, iz),
1528 vid(ix + 1, iy + 1, iz),
1529 vid(ix + 1, iy + 1, iz + 1),
1530 vid(ix, iy + 1, iz + 1),
1531 ];
1532 for tet in &TEST_TET_LOCAL {
1533 cells.push([
1534 cube_verts[tet[0]],
1535 cube_verts[tet[1]],
1536 cube_verts[tet[2]],
1537 cube_verts[tet[3]],
1538 CELL_SENTINEL,
1539 CELL_SENTINEL,
1540 CELL_SENTINEL,
1541 CELL_SENTINEL,
1542 ]);
1543 }
1544 }
1545 }
1546 }
1547
1548 VolumeMeshData {
1549 positions,
1550 cells,
1551 ..Default::default()
1552 }
1553 }
1554
1555 fn projected_sphere_tet_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1556 let grid_v = grid_n + 1;
1557 let half = grid_n as f32 / 2.0;
1558 let vid =
1559 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1560
1561 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1562 for iz in 0..grid_v {
1563 for iy in 0..grid_v {
1564 for ix in 0..grid_v {
1565 let x = ix as f32 - half;
1566 let y = iy as f32 - half;
1567 let z = iz as f32 - half;
1568 let len = (x * x + y * y + z * z).sqrt();
1569 let s = radius / len;
1570 positions.push([x * s, y * s, z * s]);
1571 }
1572 }
1573 }
1574
1575 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1576 for iz in 0..grid_n {
1577 for iy in 0..grid_n {
1578 for ix in 0..grid_n {
1579 let cube_verts = [
1580 vid(ix, iy, iz),
1581 vid(ix + 1, iy, iz),
1582 vid(ix + 1, iy, iz + 1),
1583 vid(ix, iy, iz + 1),
1584 vid(ix, iy + 1, iz),
1585 vid(ix + 1, iy + 1, iz),
1586 vid(ix + 1, iy + 1, iz + 1),
1587 vid(ix, iy + 1, iz + 1),
1588 ];
1589 for tet in &TEST_TET_LOCAL {
1590 cells.push([
1591 cube_verts[tet[0]],
1592 cube_verts[tet[1]],
1593 cube_verts[tet[2]],
1594 cube_verts[tet[3]],
1595 CELL_SENTINEL,
1596 CELL_SENTINEL,
1597 CELL_SENTINEL,
1598 CELL_SENTINEL,
1599 ]);
1600 }
1601 }
1602 }
1603 }
1604
1605 VolumeMeshData {
1606 positions,
1607 cells,
1608 ..Default::default()
1609 }
1610 }
1611
1612 fn cube_to_sphere([x, y, z]: [f32; 3]) -> [f32; 3] {
1613 let x2 = x * x;
1614 let y2 = y * y;
1615 let z2 = z * z;
1616 [
1617 x * (1.0 - 0.5 * (y2 + z2) + (y2 * z2) / 3.0).sqrt(),
1618 y * (1.0 - 0.5 * (z2 + x2) + (z2 * x2) / 3.0).sqrt(),
1619 z * (1.0 - 0.5 * (x2 + y2) + (x2 * y2) / 3.0).sqrt(),
1620 ]
1621 }
1622
1623 fn cube_sphere_hex_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1624 let grid_v = grid_n + 1;
1625 let half = grid_n as f32 / 2.0;
1626 let vid =
1627 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1628
1629 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1630 for iz in 0..grid_v {
1631 for iy in 0..grid_v {
1632 for ix in 0..grid_v {
1633 let p = [ix as f32 - half, iy as f32 - half, iz as f32 - half];
1634 let cube = [p[0] / half, p[1] / half, p[2] / half];
1635 let s = cube_to_sphere(cube);
1636 positions.push([s[0] * radius, s[1] * radius, s[2] * radius]);
1637 }
1638 }
1639 }
1640
1641 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1642 for iz in 0..grid_n {
1643 for iy in 0..grid_n {
1644 for ix in 0..grid_n {
1645 cells.push([
1646 vid(ix, iy, iz),
1647 vid(ix + 1, iy, iz),
1648 vid(ix + 1, iy, iz + 1),
1649 vid(ix, iy, iz + 1),
1650 vid(ix, iy + 1, iz),
1651 vid(ix + 1, iy + 1, iz),
1652 vid(ix + 1, iy + 1, iz + 1),
1653 vid(ix, iy + 1, iz + 1),
1654 ]);
1655 }
1656 }
1657 }
1658
1659 VolumeMeshData {
1660 positions,
1661 cells,
1662 ..Default::default()
1663 }
1664 }
1665
1666 fn structured_hex_grid(grid_n: usize) -> VolumeMeshData {
1667 let grid_v = grid_n + 1;
1668 let vid =
1669 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1670
1671 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1672 for iz in 0..grid_v {
1673 for iy in 0..grid_v {
1674 for ix in 0..grid_v {
1675 positions.push([ix as f32, iy as f32, iz as f32]);
1676 }
1677 }
1678 }
1679
1680 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1681 for iz in 0..grid_n {
1682 for iy in 0..grid_n {
1683 for ix in 0..grid_n {
1684 cells.push([
1685 vid(ix, iy, iz),
1686 vid(ix + 1, iy, iz),
1687 vid(ix + 1, iy, iz + 1),
1688 vid(ix, iy, iz + 1),
1689 vid(ix, iy + 1, iz),
1690 vid(ix + 1, iy + 1, iz),
1691 vid(ix + 1, iy + 1, iz + 1),
1692 vid(ix, iy + 1, iz + 1),
1693 ]);
1694 }
1695 }
1696 }
1697
1698 VolumeMeshData {
1699 positions,
1700 cells,
1701 ..Default::default()
1702 }
1703 }
1704
1705 #[test]
1706 fn single_tet_has_four_boundary_faces() {
1707 let data = single_tet();
1708 let (mesh, _) = extract_boundary_faces(&data);
1709 assert_eq!(
1710 mesh.indices.len(),
1711 4 * 3,
1712 "single tet -> 4 boundary triangles"
1713 );
1714 }
1715
1716 #[test]
1717 fn two_tets_sharing_face_eliminates_shared_face() {
1718 let data = two_tets_sharing_face();
1719 let (mesh, _) = extract_boundary_faces(&data);
1720 assert_eq!(
1723 mesh.indices.len(),
1724 6 * 3,
1725 "two tets sharing a face -> 6 boundary triangles"
1726 );
1727 }
1728
1729 #[test]
1730 fn single_hex_has_twelve_boundary_triangles() {
1731 let data = single_hex();
1732 let (mesh, _) = extract_boundary_faces(&data);
1733 assert_eq!(
1735 mesh.indices.len(),
1736 12 * 3,
1737 "single hex -> 12 boundary triangles"
1738 );
1739 }
1740
1741 #[test]
1742 fn structured_tet_grid_has_expected_boundary_triangle_count() {
1743 let grid_n = 3;
1744 let data = structured_tet_grid(grid_n);
1745 let (mesh, _) = extract_boundary_faces(&data);
1746 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1747 assert_eq!(
1748 mesh.indices.len(),
1749 expected_boundary_tris * 3,
1750 "3x3x3 tet grid should expose 108 boundary triangles"
1751 );
1752 }
1753
1754 #[test]
1755 fn structured_hex_grid_has_expected_boundary_triangle_count() {
1756 let grid_n = 3;
1757 let data = structured_hex_grid(grid_n);
1758 let (mesh, _) = extract_boundary_faces(&data);
1759 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1760 assert_eq!(
1761 mesh.indices.len(),
1762 expected_boundary_tris * 3,
1763 "3x3x3 hex grid should expose 108 boundary triangles"
1764 );
1765 }
1766
1767 #[test]
1768 fn structured_tet_grid_boundary_is_edge_manifold() {
1769 let data = structured_tet_grid(3);
1770 let (mesh, _) = extract_boundary_faces(&data);
1771
1772 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1773 std::collections::HashMap::new();
1774 for tri in mesh.indices.chunks_exact(3) {
1775 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1776 let edge = if a < b { (a, b) } else { (b, a) };
1777 *edge_counts.entry(edge).or_insert(0) += 1;
1778 }
1779 }
1780
1781 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1782 .into_iter()
1783 .filter(|(_, count)| *count != 2)
1784 .collect();
1785
1786 assert!(
1787 non_manifold.is_empty(),
1788 "boundary should be watertight; bad edges: {non_manifold:?}"
1789 );
1790 }
1791
1792 #[test]
1793 fn structured_hex_grid_boundary_is_edge_manifold() {
1794 let data = structured_hex_grid(3);
1795 let (mesh, _) = extract_boundary_faces(&data);
1796
1797 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1798 std::collections::HashMap::new();
1799 for tri in mesh.indices.chunks_exact(3) {
1800 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1801 let edge = if a < b { (a, b) } else { (b, a) };
1802 *edge_counts.entry(edge).or_insert(0) += 1;
1803 }
1804 }
1805
1806 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1807 .into_iter()
1808 .filter(|(_, count)| *count != 2)
1809 .collect();
1810
1811 assert!(
1812 non_manifold.is_empty(),
1813 "boundary should be watertight; bad edges: {non_manifold:?}"
1814 );
1815 }
1816
1817 #[test]
1818 fn projected_sphere_tet_grid_boundary_faces_point_outward() {
1819 let data = projected_sphere_tet_grid(3, 2.0);
1820 let (mesh, _) = extract_boundary_faces(&data);
1821
1822 for tri in mesh.indices.chunks_exact(3) {
1823 let pa = mesh.positions[tri[0] as usize];
1824 let pb = mesh.positions[tri[1] as usize];
1825 let pc = mesh.positions[tri[2] as usize];
1826
1827 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1828 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1829 let normal = [
1830 ab[1] * ac[2] - ab[2] * ac[1],
1831 ab[2] * ac[0] - ab[0] * ac[2],
1832 ab[0] * ac[1] - ab[1] * ac[0],
1833 ];
1834 let fc = [
1835 (pa[0] + pb[0] + pc[0]) / 3.0,
1836 (pa[1] + pb[1] + pc[1]) / 3.0,
1837 (pa[2] + pb[2] + pc[2]) / 3.0,
1838 ];
1839 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1840 assert!(
1841 dot > 0.0,
1842 "boundary face points inward: tri={tri:?}, dot={dot}"
1843 );
1844 }
1845 }
1846
1847 #[test]
1848 fn cube_sphere_hex_grid_boundary_faces_point_outward() {
1849 let data = cube_sphere_hex_grid(3, 2.0);
1850 let (mesh, _) = extract_boundary_faces(&data);
1851
1852 for tri in mesh.indices.chunks_exact(3) {
1853 let pa = mesh.positions[tri[0] as usize];
1854 let pb = mesh.positions[tri[1] as usize];
1855 let pc = mesh.positions[tri[2] as usize];
1856
1857 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1858 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1859 let normal = [
1860 ab[1] * ac[2] - ab[2] * ac[1],
1861 ab[2] * ac[0] - ab[0] * ac[2],
1862 ab[0] * ac[1] - ab[1] * ac[0],
1863 ];
1864 let fc = [
1865 (pa[0] + pb[0] + pc[0]) / 3.0,
1866 (pa[1] + pb[1] + pc[1]) / 3.0,
1867 (pa[2] + pb[2] + pc[2]) / 3.0,
1868 ];
1869 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1870 assert!(
1871 dot > 0.0,
1872 "boundary face points inward: tri={tri:?}, dot={dot}"
1873 );
1874 }
1875 }
1876
1877 #[test]
1878 fn normals_have_correct_length() {
1879 let data = single_tet();
1880 let (mesh, _) = extract_boundary_faces(&data);
1881 assert_eq!(mesh.normals.len(), mesh.positions.len());
1882 for n in &mesh.normals {
1883 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1884 assert!(
1885 (len - 1.0).abs() < 1e-5 || len < 1e-5,
1886 "normal not unit: {n:?}"
1887 );
1888 }
1889 }
1890
1891 #[test]
1892 fn cell_scalar_remaps_to_face_attribute() {
1893 let mut data = single_tet();
1894 data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
1895 let (mesh, _) = extract_boundary_faces(&data);
1896 match mesh.attributes.get("pressure") {
1897 Some(AttributeData::Face(vals)) => {
1898 assert_eq!(vals.len(), 4, "one value per boundary triangle");
1899 for &v in vals {
1900 assert_eq!(v, 42.0);
1901 }
1902 }
1903 other => panic!("expected Face attribute, got {other:?}"),
1904 }
1905 }
1906
1907 #[test]
1908 fn cell_colour_remaps_to_face_colour_attribute() {
1909 let mut data = two_tets_sharing_face();
1910 data.cell_colours.insert(
1911 "label".to_string(),
1912 vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
1913 );
1914 let (mesh, _) = extract_boundary_faces(&data);
1915 match mesh.attributes.get("label") {
1916 Some(AttributeData::FaceColour(colours)) => {
1917 assert_eq!(colours.len(), 6, "6 boundary faces");
1918 }
1919 other => panic!("expected FaceColour attribute, got {other:?}"),
1920 }
1921 }
1922
1923 #[test]
1924 fn positions_preserved_unchanged() {
1925 let data = single_hex();
1926 let (mesh, _) = extract_boundary_faces(&data);
1927 assert_eq!(mesh.positions, data.positions);
1928 }
1929
1930 #[test]
1940
1941 fn empty_planes_matches_boundary_extractor_tet() {
1942 let data = structured_tet_grid(3);
1943 let (boundary, _) = extract_boundary_faces(&data);
1944 let (clipped, _) = extract_clipped_volume_faces(&data, &[]);
1945 assert_eq!(
1946 boundary.indices.len(),
1947 clipped.indices.len(),
1948 "empty clip_planes -> same triangle count as extract_boundary_faces"
1949 );
1950 }
1951
1952 #[test]
1955
1956 fn empty_planes_matches_boundary_extractor_hex() {
1957 let data = structured_hex_grid(3);
1958 let (boundary, _) = extract_boundary_faces(&data);
1959 let (clipped, _) = extract_clipped_volume_faces(&data, &[]);
1960 assert_eq!(
1961 boundary.indices.len(),
1962 clipped.indices.len(),
1963 "empty clip_planes -> same triangle count as extract_boundary_faces"
1964 );
1965 }
1966
1967 #[test]
1970
1971 fn clipped_tet_grid_has_nonempty_section_faces() {
1972 let grid_n = 3;
1973 let data = structured_tet_grid(grid_n);
1974 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1977 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1978 assert!(
1980 !mesh.indices.is_empty(),
1981 "clipped tet grid must produce at least one triangle"
1982 );
1983 }
1984
1985 #[test]
1987
1988 fn clipped_hex_grid_has_nonempty_section_faces() {
1989 let grid_n = 3;
1990 let data = structured_hex_grid(grid_n);
1991 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1992 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1993 assert!(
1994 !mesh.indices.is_empty(),
1995 "clipped hex grid must produce at least one triangle"
1996 );
1997 }
1998
1999 #[test]
2002
2003 fn section_face_normals_point_toward_kept_side_tet() {
2004 let data = structured_tet_grid(3);
2005 let plane_normal = [0.0_f32, 1.0, 0.0];
2006 let plane = [plane_normal[0], plane_normal[1], plane_normal[2], -1.5];
2007 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2008
2009 for n in &mesh.normals {
2010 let dot = n[0] * plane_normal[0] + n[1] * plane_normal[1] + n[2] * plane_normal[2];
2011 let _ = dot; }
2017 }
2018
2019 #[test]
2021
2022 fn fully_discarded_cells_contribute_nothing() {
2023 let data = single_tet();
2025 let plane = [0.0_f32, 1.0, 0.0, -2.0]; let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2027 assert!(
2028 mesh.indices.is_empty(),
2029 "tet fully below clip plane must produce no triangles"
2030 );
2031 }
2032
2033 #[test]
2036
2037 fn fully_kept_cell_matches_boundary_extractor() {
2038 let data = single_tet();
2040 let plane = [0.0_f32, 1.0, 0.0, 1.0]; let (clipped, _) = extract_clipped_volume_faces(&data, &[plane]);
2042 let (boundary, _) = extract_boundary_faces(&data);
2043 assert_eq!(
2044 clipped.indices.len(),
2045 boundary.indices.len(),
2046 "fully kept cell must produce the same triangles as boundary extractor"
2047 );
2048 }
2049
2050 #[test]
2053 fn cell_scalar_propagates_to_section_faces() {
2054 let mut data = structured_tet_grid(3);
2055 let n_cells = data.cells.len();
2056 data.cell_scalars
2057 .insert("pressure".to_string(), vec![1.0; n_cells]);
2058 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2059 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2060 match mesh.attributes.get("pressure") {
2061 Some(AttributeData::Face(vals)) => {
2062 let n_tris = mesh.indices.len() / 3;
2063 assert_eq!(vals.len(), n_tris, "one scalar value per output triangle");
2064 for &v in vals {
2065 assert_eq!(v, 1.0, "scalar must equal the owning cell's value");
2066 }
2067 }
2068 other => panic!("expected Face attribute on clipped mesh, got {other:?}"),
2069 }
2070 }
2071
2072 #[test]
2075 fn cell_colour_propagates_to_section_faces() {
2076 let mut data = structured_tet_grid(3);
2077 let n_cells = data.cells.len();
2078 let colour = [1.0_f32, 0.0, 0.5, 1.0];
2079 data.cell_colours
2080 .insert("label".to_string(), vec![colour; n_cells]);
2081 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2082 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2083 match mesh.attributes.get("label") {
2084 Some(AttributeData::FaceColour(colours)) => {
2085 let n_tris = mesh.indices.len() / 3;
2086 assert_eq!(colours.len(), n_tris, "one colour per output triangle");
2087 for &c in colours {
2088 assert_eq!(c, colour, "colour must equal the owning cell's value");
2089 }
2090 }
2091 other => panic!("expected FaceColour attribute on clipped mesh, got {other:?}"),
2092 }
2093 }
2094
2095 #[test]
2097 fn hex_cell_scalar_propagates_to_section_faces() {
2098 let mut data = structured_hex_grid(3);
2099 let n_cells = data.cells.len();
2100 data.cell_scalars
2101 .insert("temp".to_string(), vec![7.0; n_cells]);
2102 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2103 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2104 match mesh.attributes.get("temp") {
2105 Some(AttributeData::Face(vals)) => {
2106 let n_tris = mesh.indices.len() / 3;
2107 assert_eq!(vals.len(), n_tris, "one scalar per output triangle");
2108 for &v in vals {
2109 assert_eq!(v, 7.0, "scalar must equal the owning cell's value");
2110 }
2111 }
2112 other => panic!("expected Face attribute on clipped hex mesh, got {other:?}"),
2113 }
2114 }
2115
2116 fn single_pyramid() -> VolumeMeshData {
2121 let mut data = VolumeMeshData {
2123 positions: vec![
2124 [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.5, 1.0, 0.5], ],
2130 ..Default::default()
2131 };
2132 data.push_pyramid([0, 1, 2, 3], 4);
2133 data
2134 }
2135
2136 fn single_wedge() -> VolumeMeshData {
2137 let mut data = VolumeMeshData {
2139 positions: vec![
2140 [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.5, 1.0, 1.0], ],
2147 ..Default::default()
2148 };
2149 data.push_wedge([0, 1, 2], [3, 4, 5]);
2150 data
2151 }
2152
2153 fn tet_volume(p: [[f32; 3]; 4]) -> f32 {
2154 let v =
2156 |i: usize| -> [f32; 3] { [p[i][0] - p[0][0], p[i][1] - p[0][1], p[i][2] - p[0][2]] };
2157 let (a, b, c) = (v(1), v(2), v(3));
2158 let cross = [
2159 b[1] * c[2] - b[2] * c[1],
2160 b[2] * c[0] - b[0] * c[2],
2161 b[0] * c[1] - b[1] * c[0],
2162 ];
2163 (a[0] * cross[0] + a[1] * cross[1] + a[2] * cross[2]) / 6.0
2164 }
2165
2166 #[test]
2167 fn decompose_tet_yields_one_tet() {
2168 let data = single_tet();
2169 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2170 assert_eq!(tets.len(), 1);
2171 assert_eq!(scalars.len(), 1);
2172 }
2173
2174 #[test]
2175 fn decompose_hex_yields_six_tets() {
2176 let data = single_hex();
2177 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2178 assert_eq!(tets.len(), 6);
2179 assert_eq!(scalars.len(), 6);
2180 }
2181
2182 #[test]
2183 fn decompose_pyramid_yields_two_tets() {
2184 let data = single_pyramid();
2185 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2186 assert_eq!(tets.len(), 2);
2187 assert_eq!(scalars.len(), 2);
2188 }
2189
2190 #[test]
2191 fn decompose_wedge_yields_three_tets() {
2192 let data = single_wedge();
2193 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2194 assert_eq!(tets.len(), 3);
2195 assert_eq!(scalars.len(), 3);
2196 }
2197
2198 #[test]
2199 fn decompose_output_tets_have_nonzero_volume() {
2200 for data in [single_tet(), single_hex(), single_pyramid(), single_wedge()] {
2201 let (tets, _) = decompose_to_tetrahedra(&data, "");
2202 for (i, t) in tets.iter().enumerate() {
2203 let vol = tet_volume(*t).abs();
2204 assert!(vol > 1e-6, "tet {i} has near-zero volume {vol}: {t:?}");
2205 }
2206 }
2207 }
2208
2209 #[test]
2210 fn decompose_hex_volume_equals_cell_volume() {
2211 let data = single_hex();
2213 let (tets, _) = decompose_to_tetrahedra(&data, "");
2214 let total: f32 = tets.iter().map(|t| tet_volume(*t).abs()).sum();
2215 assert!(
2216 (total - 1.0).abs() < 1e-5,
2217 "unit hex volume should be 1.0, got {total}"
2218 );
2219 }
2220
2221 #[test]
2222 fn decompose_scalar_propagates_to_child_tets() {
2223 let mut data = single_hex();
2224 data.cell_scalars.insert("temp".to_string(), vec![42.0]);
2225 let (_, scalars) = decompose_to_tetrahedra(&data, "temp");
2226 assert_eq!(scalars.len(), 6);
2227 for &s in &scalars {
2228 assert_eq!(s, 42.0, "all child tets must inherit the cell scalar");
2229 }
2230 }
2231
2232 #[test]
2233 fn decompose_missing_attribute_falls_back_to_zero() {
2234 let data = single_hex();
2235 let (_, scalars) = decompose_to_tetrahedra(&data, "nonexistent");
2236 for &s in &scalars {
2237 assert_eq!(s, 0.0, "missing attribute must produce 0.0 per tet");
2238 }
2239 }
2240
2241 #[test]
2242 fn decompose_mixed_mesh_tet_counts_sum_correctly() {
2243 let mut data = VolumeMeshData {
2245 positions: vec![
2246 [0.0, 0.0, 0.0],
2248 [1.0, 0.0, 0.0],
2249 [0.5, 1.0, 0.0],
2250 [0.5, 0.5, 1.0],
2251 [2.0, 0.0, 0.0],
2253 [3.0, 0.0, 0.0],
2254 [3.0, 0.0, 1.0],
2255 [2.0, 0.0, 1.0],
2256 [2.0, 1.0, 0.0],
2257 [3.0, 1.0, 0.0],
2258 [3.0, 1.0, 1.0],
2259 [2.0, 1.0, 1.0],
2260 [4.0, 0.0, 0.0],
2262 [5.0, 0.0, 0.0],
2263 [5.0, 0.0, 1.0],
2264 [4.0, 0.0, 1.0],
2265 [4.5, 1.0, 0.5],
2266 [6.0, 0.0, 0.0],
2268 [7.0, 0.0, 0.0],
2269 [6.5, 0.0, 1.0],
2270 [6.0, 1.0, 0.0],
2271 [7.0, 1.0, 0.0],
2272 [6.5, 1.0, 1.0],
2273 ],
2274 ..Default::default()
2275 };
2276 data.push_tet(0, 1, 2, 3);
2277 data.push_hex([4, 5, 6, 7, 8, 9, 10, 11]);
2278 data.push_pyramid([12, 13, 14, 15], 16);
2279 data.push_wedge([17, 18, 19], [20, 21, 22]);
2280
2281 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2282 assert_eq!(tets.len(), 12, "1+6+2+3 = 12 tets");
2283 assert_eq!(scalars.len(), 12);
2284 }
2285}