1use std::collections::HashMap;
24
25use rayon::prelude::*;
26
27use crate::resources::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(
1262 &self,
1263 ) -> Result<(super::tetmesh::TetMesh, ConversionReport), ToTetMeshError> {
1264 use glam::Vec3;
1265
1266 let mut tet_indices: Vec<[u32; 4]> = Vec::new();
1267 let mut dropped = 0usize;
1268 for cell in &self.cells {
1269 if cell[4] == CELL_SENTINEL
1270 && cell[5] == CELL_SENTINEL
1271 && cell[6] == CELL_SENTINEL
1272 && cell[7] == CELL_SENTINEL
1273 {
1274 tet_indices.push([cell[0], cell[1], cell[2], cell[3]]);
1275 } else {
1276 dropped += 1;
1277 }
1278 }
1279
1280 if tet_indices.is_empty() {
1281 return Err(ToTetMeshError::NoTetCells);
1282 }
1283
1284 let vertex_count = self.positions.len();
1285 let mut remap = vec![u32::MAX; vertex_count];
1286 let mut positions: Vec<Vec3> = Vec::new();
1287 let mut tets: Vec<[u32; 4]> = Vec::with_capacity(tet_indices.len());
1288 for raw in tet_indices {
1289 let mut out = [0_u32; 4];
1290 for (slot, &idx) in raw.iter().enumerate() {
1291 let idx_usize = idx as usize;
1292 if idx_usize >= vertex_count {
1293 return Err(ToTetMeshError::OutOfRangeIndex(idx));
1294 }
1295 if remap[idx_usize] == u32::MAX {
1296 remap[idx_usize] = positions.len() as u32;
1297 let p = self.positions[idx_usize];
1298 positions.push(Vec3::new(p[0], p[1], p[2]));
1299 }
1300 out[slot] = remap[idx_usize];
1301 }
1302 tets.push(out);
1303 }
1304
1305 Ok((
1306 super::tetmesh::TetMesh::new(positions, tets),
1307 ConversionReport {
1308 dropped_non_tet_cells: dropped,
1309 },
1310 ))
1311 }
1312}
1313
1314#[derive(Clone, Debug, Default, PartialEq)]
1316pub struct ConversionReport {
1317 pub dropped_non_tet_cells: usize,
1319}
1320
1321#[derive(Debug)]
1323pub enum ToTetMeshError {
1324 NoTetCells,
1326 OutOfRangeIndex(u32),
1328}
1329
1330const HEX_TO_TETS: [[usize; 4]; 6] = [
1338 [0, 1, 5, 6],
1339 [0, 1, 2, 6],
1340 [0, 4, 5, 6],
1341 [0, 4, 7, 6],
1342 [0, 3, 2, 6],
1343 [0, 3, 7, 6],
1344];
1345
1346const WEDGE_TO_TETS: [[usize; 4]; 3] = [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]];
1350
1351const PYRAMID_TO_TETS: [[usize; 4]; 2] = [[0, 1, 2, 4], [0, 2, 3, 4]];
1355
1356pub(crate) fn for_each_tet<F>(data: &VolumeMeshData, attribute: &str, mut f: F)
1368where
1369 F: FnMut([[f32; 3]; 4], f32),
1370{
1371 let cell_scalars = data.cell_scalars.get(attribute);
1372 for (cell_idx, cell) in data.cells.iter().enumerate() {
1373 let scalar = cell_scalars
1374 .and_then(|v| v.get(cell_idx))
1375 .copied()
1376 .unwrap_or(0.0);
1377 let tets: &[[usize; 4]] = match cell_type(cell) {
1378 CellType::Tet => &[[0, 1, 2, 3]],
1379 CellType::Pyramid => &PYRAMID_TO_TETS,
1380 CellType::Wedge => &WEDGE_TO_TETS,
1381 CellType::Hex => &HEX_TO_TETS,
1382 };
1383 for local in tets {
1384 let verts = [
1385 data.positions[cell[local[0]] as usize],
1386 data.positions[cell[local[1]] as usize],
1387 data.positions[cell[local[2]] as usize],
1388 data.positions[cell[local[3]] as usize],
1389 ];
1390 f(verts, scalar);
1391 }
1392 }
1393}
1394
1395#[cfg(test)]
1405pub(crate) fn decompose_to_tetrahedra(
1406 data: &VolumeMeshData,
1407 attribute: &str,
1408) -> (Vec<[[f32; 3]; 4]>, Vec<f32>) {
1409 let mut positions: Vec<[[f32; 3]; 4]> = Vec::new();
1410 let mut scalars: Vec<f32> = Vec::new();
1411 for_each_tet(data, attribute, |verts, scalar| {
1412 positions.push(verts);
1413 scalars.push(scalar);
1414 });
1415 (positions, scalars)
1416}
1417
1418#[cfg(test)]
1423mod tests {
1424 use super::*;
1425
1426 const TEST_TET_LOCAL: [[usize; 4]; 6] = [
1427 [0, 1, 5, 6],
1428 [0, 1, 2, 6],
1429 [0, 4, 5, 6],
1430 [0, 4, 7, 6],
1431 [0, 3, 2, 6],
1432 [0, 3, 7, 6],
1433 ];
1434
1435 fn single_tet() -> VolumeMeshData {
1436 VolumeMeshData {
1437 positions: vec![
1438 [0.0, 0.0, 0.0],
1439 [1.0, 0.0, 0.0],
1440 [0.5, 1.0, 0.0],
1441 [0.5, 0.5, 1.0],
1442 ],
1443 cells: vec![[
1444 0,
1445 1,
1446 2,
1447 3,
1448 CELL_SENTINEL,
1449 CELL_SENTINEL,
1450 CELL_SENTINEL,
1451 CELL_SENTINEL,
1452 ]],
1453 ..Default::default()
1454 }
1455 }
1456
1457 fn two_tets_sharing_face() -> VolumeMeshData {
1458 VolumeMeshData {
1461 positions: vec![
1462 [0.0, 0.0, 0.0],
1463 [1.0, 0.0, 0.0],
1464 [0.5, 1.0, 0.0],
1465 [0.5, 0.5, 1.0],
1466 [0.5, 0.5, -1.0],
1467 ],
1468 cells: vec![
1469 [
1470 0,
1471 1,
1472 2,
1473 3,
1474 CELL_SENTINEL,
1475 CELL_SENTINEL,
1476 CELL_SENTINEL,
1477 CELL_SENTINEL,
1478 ],
1479 [
1480 0,
1481 2,
1482 1,
1483 4,
1484 CELL_SENTINEL,
1485 CELL_SENTINEL,
1486 CELL_SENTINEL,
1487 CELL_SENTINEL,
1488 ],
1489 ],
1490 ..Default::default()
1491 }
1492 }
1493
1494 fn single_hex() -> VolumeMeshData {
1495 VolumeMeshData {
1496 positions: vec![
1497 [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], ],
1506 cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
1507 ..Default::default()
1508 }
1509 }
1510
1511 fn structured_tet_grid(grid_n: usize) -> VolumeMeshData {
1512 let grid_v = grid_n + 1;
1513 let vid =
1514 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1515
1516 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1517 for iz in 0..grid_v {
1518 for iy in 0..grid_v {
1519 for ix in 0..grid_v {
1520 positions.push([ix as f32, iy as f32, iz as f32]);
1521 }
1522 }
1523 }
1524
1525 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1526 for iz in 0..grid_n {
1527 for iy in 0..grid_n {
1528 for ix in 0..grid_n {
1529 let cube_verts = [
1530 vid(ix, iy, iz),
1531 vid(ix + 1, iy, iz),
1532 vid(ix + 1, iy, iz + 1),
1533 vid(ix, iy, iz + 1),
1534 vid(ix, iy + 1, iz),
1535 vid(ix + 1, iy + 1, iz),
1536 vid(ix + 1, iy + 1, iz + 1),
1537 vid(ix, iy + 1, iz + 1),
1538 ];
1539 for tet in &TEST_TET_LOCAL {
1540 cells.push([
1541 cube_verts[tet[0]],
1542 cube_verts[tet[1]],
1543 cube_verts[tet[2]],
1544 cube_verts[tet[3]],
1545 CELL_SENTINEL,
1546 CELL_SENTINEL,
1547 CELL_SENTINEL,
1548 CELL_SENTINEL,
1549 ]);
1550 }
1551 }
1552 }
1553 }
1554
1555 VolumeMeshData {
1556 positions,
1557 cells,
1558 ..Default::default()
1559 }
1560 }
1561
1562 fn projected_sphere_tet_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1563 let grid_v = grid_n + 1;
1564 let half = grid_n as f32 / 2.0;
1565 let vid =
1566 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1567
1568 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1569 for iz in 0..grid_v {
1570 for iy in 0..grid_v {
1571 for ix in 0..grid_v {
1572 let x = ix as f32 - half;
1573 let y = iy as f32 - half;
1574 let z = iz as f32 - half;
1575 let len = (x * x + y * y + z * z).sqrt();
1576 let s = radius / len;
1577 positions.push([x * s, y * s, z * s]);
1578 }
1579 }
1580 }
1581
1582 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1583 for iz in 0..grid_n {
1584 for iy in 0..grid_n {
1585 for ix in 0..grid_n {
1586 let cube_verts = [
1587 vid(ix, iy, iz),
1588 vid(ix + 1, iy, iz),
1589 vid(ix + 1, iy, iz + 1),
1590 vid(ix, iy, iz + 1),
1591 vid(ix, iy + 1, iz),
1592 vid(ix + 1, iy + 1, iz),
1593 vid(ix + 1, iy + 1, iz + 1),
1594 vid(ix, iy + 1, iz + 1),
1595 ];
1596 for tet in &TEST_TET_LOCAL {
1597 cells.push([
1598 cube_verts[tet[0]],
1599 cube_verts[tet[1]],
1600 cube_verts[tet[2]],
1601 cube_verts[tet[3]],
1602 CELL_SENTINEL,
1603 CELL_SENTINEL,
1604 CELL_SENTINEL,
1605 CELL_SENTINEL,
1606 ]);
1607 }
1608 }
1609 }
1610 }
1611
1612 VolumeMeshData {
1613 positions,
1614 cells,
1615 ..Default::default()
1616 }
1617 }
1618
1619 fn cube_to_sphere([x, y, z]: [f32; 3]) -> [f32; 3] {
1620 let x2 = x * x;
1621 let y2 = y * y;
1622 let z2 = z * z;
1623 [
1624 x * (1.0 - 0.5 * (y2 + z2) + (y2 * z2) / 3.0).sqrt(),
1625 y * (1.0 - 0.5 * (z2 + x2) + (z2 * x2) / 3.0).sqrt(),
1626 z * (1.0 - 0.5 * (x2 + y2) + (x2 * y2) / 3.0).sqrt(),
1627 ]
1628 }
1629
1630 fn cube_sphere_hex_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1631 let grid_v = grid_n + 1;
1632 let half = grid_n as f32 / 2.0;
1633 let vid =
1634 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1635
1636 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1637 for iz in 0..grid_v {
1638 for iy in 0..grid_v {
1639 for ix in 0..grid_v {
1640 let p = [ix as f32 - half, iy as f32 - half, iz as f32 - half];
1641 let cube = [p[0] / half, p[1] / half, p[2] / half];
1642 let s = cube_to_sphere(cube);
1643 positions.push([s[0] * radius, s[1] * radius, s[2] * radius]);
1644 }
1645 }
1646 }
1647
1648 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1649 for iz in 0..grid_n {
1650 for iy in 0..grid_n {
1651 for ix in 0..grid_n {
1652 cells.push([
1653 vid(ix, iy, iz),
1654 vid(ix + 1, iy, iz),
1655 vid(ix + 1, iy, iz + 1),
1656 vid(ix, iy, iz + 1),
1657 vid(ix, iy + 1, iz),
1658 vid(ix + 1, iy + 1, iz),
1659 vid(ix + 1, iy + 1, iz + 1),
1660 vid(ix, iy + 1, iz + 1),
1661 ]);
1662 }
1663 }
1664 }
1665
1666 VolumeMeshData {
1667 positions,
1668 cells,
1669 ..Default::default()
1670 }
1671 }
1672
1673 fn structured_hex_grid(grid_n: usize) -> VolumeMeshData {
1674 let grid_v = grid_n + 1;
1675 let vid =
1676 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1677
1678 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1679 for iz in 0..grid_v {
1680 for iy in 0..grid_v {
1681 for ix in 0..grid_v {
1682 positions.push([ix as f32, iy as f32, iz as f32]);
1683 }
1684 }
1685 }
1686
1687 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1688 for iz in 0..grid_n {
1689 for iy in 0..grid_n {
1690 for ix in 0..grid_n {
1691 cells.push([
1692 vid(ix, iy, iz),
1693 vid(ix + 1, iy, iz),
1694 vid(ix + 1, iy, iz + 1),
1695 vid(ix, iy, iz + 1),
1696 vid(ix, iy + 1, iz),
1697 vid(ix + 1, iy + 1, iz),
1698 vid(ix + 1, iy + 1, iz + 1),
1699 vid(ix, iy + 1, iz + 1),
1700 ]);
1701 }
1702 }
1703 }
1704
1705 VolumeMeshData {
1706 positions,
1707 cells,
1708 ..Default::default()
1709 }
1710 }
1711
1712 #[test]
1713 fn single_tet_has_four_boundary_faces() {
1714 let data = single_tet();
1715 let (mesh, _) = extract_boundary_faces(&data);
1716 assert_eq!(
1717 mesh.indices.len(),
1718 4 * 3,
1719 "single tet -> 4 boundary triangles"
1720 );
1721 }
1722
1723 #[test]
1724 fn two_tets_sharing_face_eliminates_shared_face() {
1725 let data = two_tets_sharing_face();
1726 let (mesh, _) = extract_boundary_faces(&data);
1727 assert_eq!(
1730 mesh.indices.len(),
1731 6 * 3,
1732 "two tets sharing a face -> 6 boundary triangles"
1733 );
1734 }
1735
1736 #[test]
1737 fn single_hex_has_twelve_boundary_triangles() {
1738 let data = single_hex();
1739 let (mesh, _) = extract_boundary_faces(&data);
1740 assert_eq!(
1742 mesh.indices.len(),
1743 12 * 3,
1744 "single hex -> 12 boundary triangles"
1745 );
1746 }
1747
1748 #[test]
1749 fn structured_tet_grid_has_expected_boundary_triangle_count() {
1750 let grid_n = 3;
1751 let data = structured_tet_grid(grid_n);
1752 let (mesh, _) = extract_boundary_faces(&data);
1753 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1754 assert_eq!(
1755 mesh.indices.len(),
1756 expected_boundary_tris * 3,
1757 "3x3x3 tet grid should expose 108 boundary triangles"
1758 );
1759 }
1760
1761 #[test]
1762 fn structured_hex_grid_has_expected_boundary_triangle_count() {
1763 let grid_n = 3;
1764 let data = structured_hex_grid(grid_n);
1765 let (mesh, _) = extract_boundary_faces(&data);
1766 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1767 assert_eq!(
1768 mesh.indices.len(),
1769 expected_boundary_tris * 3,
1770 "3x3x3 hex grid should expose 108 boundary triangles"
1771 );
1772 }
1773
1774 #[test]
1775 fn structured_tet_grid_boundary_is_edge_manifold() {
1776 let data = structured_tet_grid(3);
1777 let (mesh, _) = extract_boundary_faces(&data);
1778
1779 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1780 std::collections::HashMap::new();
1781 for tri in mesh.indices.chunks_exact(3) {
1782 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1783 let edge = if a < b { (a, b) } else { (b, a) };
1784 *edge_counts.entry(edge).or_insert(0) += 1;
1785 }
1786 }
1787
1788 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1789 .into_iter()
1790 .filter(|(_, count)| *count != 2)
1791 .collect();
1792
1793 assert!(
1794 non_manifold.is_empty(),
1795 "boundary should be watertight; bad edges: {non_manifold:?}"
1796 );
1797 }
1798
1799 #[test]
1800 fn structured_hex_grid_boundary_is_edge_manifold() {
1801 let data = structured_hex_grid(3);
1802 let (mesh, _) = extract_boundary_faces(&data);
1803
1804 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1805 std::collections::HashMap::new();
1806 for tri in mesh.indices.chunks_exact(3) {
1807 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1808 let edge = if a < b { (a, b) } else { (b, a) };
1809 *edge_counts.entry(edge).or_insert(0) += 1;
1810 }
1811 }
1812
1813 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1814 .into_iter()
1815 .filter(|(_, count)| *count != 2)
1816 .collect();
1817
1818 assert!(
1819 non_manifold.is_empty(),
1820 "boundary should be watertight; bad edges: {non_manifold:?}"
1821 );
1822 }
1823
1824 #[test]
1825 fn projected_sphere_tet_grid_boundary_faces_point_outward() {
1826 let data = projected_sphere_tet_grid(3, 2.0);
1827 let (mesh, _) = extract_boundary_faces(&data);
1828
1829 for tri in mesh.indices.chunks_exact(3) {
1830 let pa = mesh.positions[tri[0] as usize];
1831 let pb = mesh.positions[tri[1] as usize];
1832 let pc = mesh.positions[tri[2] as usize];
1833
1834 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1835 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1836 let normal = [
1837 ab[1] * ac[2] - ab[2] * ac[1],
1838 ab[2] * ac[0] - ab[0] * ac[2],
1839 ab[0] * ac[1] - ab[1] * ac[0],
1840 ];
1841 let fc = [
1842 (pa[0] + pb[0] + pc[0]) / 3.0,
1843 (pa[1] + pb[1] + pc[1]) / 3.0,
1844 (pa[2] + pb[2] + pc[2]) / 3.0,
1845 ];
1846 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1847 assert!(
1848 dot > 0.0,
1849 "boundary face points inward: tri={tri:?}, dot={dot}"
1850 );
1851 }
1852 }
1853
1854 #[test]
1855 fn cube_sphere_hex_grid_boundary_faces_point_outward() {
1856 let data = cube_sphere_hex_grid(3, 2.0);
1857 let (mesh, _) = extract_boundary_faces(&data);
1858
1859 for tri in mesh.indices.chunks_exact(3) {
1860 let pa = mesh.positions[tri[0] as usize];
1861 let pb = mesh.positions[tri[1] as usize];
1862 let pc = mesh.positions[tri[2] as usize];
1863
1864 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1865 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1866 let normal = [
1867 ab[1] * ac[2] - ab[2] * ac[1],
1868 ab[2] * ac[0] - ab[0] * ac[2],
1869 ab[0] * ac[1] - ab[1] * ac[0],
1870 ];
1871 let fc = [
1872 (pa[0] + pb[0] + pc[0]) / 3.0,
1873 (pa[1] + pb[1] + pc[1]) / 3.0,
1874 (pa[2] + pb[2] + pc[2]) / 3.0,
1875 ];
1876 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1877 assert!(
1878 dot > 0.0,
1879 "boundary face points inward: tri={tri:?}, dot={dot}"
1880 );
1881 }
1882 }
1883
1884 #[test]
1885 fn normals_have_correct_length() {
1886 let data = single_tet();
1887 let (mesh, _) = extract_boundary_faces(&data);
1888 assert_eq!(mesh.normals.len(), mesh.positions.len());
1889 for n in &mesh.normals {
1890 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1891 assert!(
1892 (len - 1.0).abs() < 1e-5 || len < 1e-5,
1893 "normal not unit: {n:?}"
1894 );
1895 }
1896 }
1897
1898 #[test]
1899 fn cell_scalar_remaps_to_face_attribute() {
1900 let mut data = single_tet();
1901 data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
1902 let (mesh, _) = extract_boundary_faces(&data);
1903 match mesh.attributes.get("pressure") {
1904 Some(AttributeData::Face(vals)) => {
1905 assert_eq!(vals.len(), 4, "one value per boundary triangle");
1906 for &v in vals {
1907 assert_eq!(v, 42.0);
1908 }
1909 }
1910 other => panic!("expected Face attribute, got {other:?}"),
1911 }
1912 }
1913
1914 #[test]
1915 fn cell_colour_remaps_to_face_colour_attribute() {
1916 let mut data = two_tets_sharing_face();
1917 data.cell_colours.insert(
1918 "label".to_string(),
1919 vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
1920 );
1921 let (mesh, _) = extract_boundary_faces(&data);
1922 match mesh.attributes.get("label") {
1923 Some(AttributeData::FaceColour(colours)) => {
1924 assert_eq!(colours.len(), 6, "6 boundary faces");
1925 }
1926 other => panic!("expected FaceColour attribute, got {other:?}"),
1927 }
1928 }
1929
1930 #[test]
1931 fn positions_preserved_unchanged() {
1932 let data = single_hex();
1933 let (mesh, _) = extract_boundary_faces(&data);
1934 assert_eq!(mesh.positions, data.positions);
1935 }
1936
1937 #[test]
1947
1948 fn empty_planes_matches_boundary_extractor_tet() {
1949 let data = structured_tet_grid(3);
1950 let (boundary, _) = extract_boundary_faces(&data);
1951 let (clipped, _) = extract_clipped_volume_faces(&data, &[]);
1952 assert_eq!(
1953 boundary.indices.len(),
1954 clipped.indices.len(),
1955 "empty clip_planes -> same triangle count as extract_boundary_faces"
1956 );
1957 }
1958
1959 #[test]
1962
1963 fn empty_planes_matches_boundary_extractor_hex() {
1964 let data = structured_hex_grid(3);
1965 let (boundary, _) = extract_boundary_faces(&data);
1966 let (clipped, _) = extract_clipped_volume_faces(&data, &[]);
1967 assert_eq!(
1968 boundary.indices.len(),
1969 clipped.indices.len(),
1970 "empty clip_planes -> same triangle count as extract_boundary_faces"
1971 );
1972 }
1973
1974 #[test]
1977
1978 fn clipped_tet_grid_has_nonempty_section_faces() {
1979 let grid_n = 3;
1980 let data = structured_tet_grid(grid_n);
1981 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1984 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1985 assert!(
1987 !mesh.indices.is_empty(),
1988 "clipped tet grid must produce at least one triangle"
1989 );
1990 }
1991
1992 #[test]
1994
1995 fn clipped_hex_grid_has_nonempty_section_faces() {
1996 let grid_n = 3;
1997 let data = structured_hex_grid(grid_n);
1998 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1999 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2000 assert!(
2001 !mesh.indices.is_empty(),
2002 "clipped hex grid must produce at least one triangle"
2003 );
2004 }
2005
2006 #[test]
2009
2010 fn section_face_normals_point_toward_kept_side_tet() {
2011 let data = structured_tet_grid(3);
2012 let plane_normal = [0.0_f32, 1.0, 0.0];
2013 let plane = [plane_normal[0], plane_normal[1], plane_normal[2], -1.5];
2014 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2015
2016 for n in &mesh.normals {
2017 let dot = n[0] * plane_normal[0] + n[1] * plane_normal[1] + n[2] * plane_normal[2];
2018 let _ = dot; }
2024 }
2025
2026 #[test]
2028
2029 fn fully_discarded_cells_contribute_nothing() {
2030 let data = single_tet();
2032 let plane = [0.0_f32, 1.0, 0.0, -2.0]; let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2034 assert!(
2035 mesh.indices.is_empty(),
2036 "tet fully below clip plane must produce no triangles"
2037 );
2038 }
2039
2040 #[test]
2043
2044 fn fully_kept_cell_matches_boundary_extractor() {
2045 let data = single_tet();
2047 let plane = [0.0_f32, 1.0, 0.0, 1.0]; let (clipped, _) = extract_clipped_volume_faces(&data, &[plane]);
2049 let (boundary, _) = extract_boundary_faces(&data);
2050 assert_eq!(
2051 clipped.indices.len(),
2052 boundary.indices.len(),
2053 "fully kept cell must produce the same triangles as boundary extractor"
2054 );
2055 }
2056
2057 #[test]
2060 fn cell_scalar_propagates_to_section_faces() {
2061 let mut data = structured_tet_grid(3);
2062 let n_cells = data.cells.len();
2063 data.cell_scalars
2064 .insert("pressure".to_string(), vec![1.0; n_cells]);
2065 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2066 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2067 match mesh.attributes.get("pressure") {
2068 Some(AttributeData::Face(vals)) => {
2069 let n_tris = mesh.indices.len() / 3;
2070 assert_eq!(vals.len(), n_tris, "one scalar value per output triangle");
2071 for &v in vals {
2072 assert_eq!(v, 1.0, "scalar must equal the owning cell's value");
2073 }
2074 }
2075 other => panic!("expected Face attribute on clipped mesh, got {other:?}"),
2076 }
2077 }
2078
2079 #[test]
2082 fn cell_colour_propagates_to_section_faces() {
2083 let mut data = structured_tet_grid(3);
2084 let n_cells = data.cells.len();
2085 let colour = [1.0_f32, 0.0, 0.5, 1.0];
2086 data.cell_colours
2087 .insert("label".to_string(), vec![colour; n_cells]);
2088 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2089 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2090 match mesh.attributes.get("label") {
2091 Some(AttributeData::FaceColour(colours)) => {
2092 let n_tris = mesh.indices.len() / 3;
2093 assert_eq!(colours.len(), n_tris, "one colour per output triangle");
2094 for &c in colours {
2095 assert_eq!(c, colour, "colour must equal the owning cell's value");
2096 }
2097 }
2098 other => panic!("expected FaceColour attribute on clipped mesh, got {other:?}"),
2099 }
2100 }
2101
2102 #[test]
2104 fn hex_cell_scalar_propagates_to_section_faces() {
2105 let mut data = structured_hex_grid(3);
2106 let n_cells = data.cells.len();
2107 data.cell_scalars
2108 .insert("temp".to_string(), vec![7.0; n_cells]);
2109 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2110 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2111 match mesh.attributes.get("temp") {
2112 Some(AttributeData::Face(vals)) => {
2113 let n_tris = mesh.indices.len() / 3;
2114 assert_eq!(vals.len(), n_tris, "one scalar per output triangle");
2115 for &v in vals {
2116 assert_eq!(v, 7.0, "scalar must equal the owning cell's value");
2117 }
2118 }
2119 other => panic!("expected Face attribute on clipped hex mesh, got {other:?}"),
2120 }
2121 }
2122
2123 fn single_pyramid() -> VolumeMeshData {
2128 let mut data = VolumeMeshData {
2130 positions: vec![
2131 [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], ],
2137 ..Default::default()
2138 };
2139 data.push_pyramid([0, 1, 2, 3], 4);
2140 data
2141 }
2142
2143 fn single_wedge() -> VolumeMeshData {
2144 let mut data = VolumeMeshData {
2146 positions: vec![
2147 [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], ],
2154 ..Default::default()
2155 };
2156 data.push_wedge([0, 1, 2], [3, 4, 5]);
2157 data
2158 }
2159
2160 fn tet_volume(p: [[f32; 3]; 4]) -> f32 {
2161 let v =
2163 |i: usize| -> [f32; 3] { [p[i][0] - p[0][0], p[i][1] - p[0][1], p[i][2] - p[0][2]] };
2164 let (a, b, c) = (v(1), v(2), v(3));
2165 let cross = [
2166 b[1] * c[2] - b[2] * c[1],
2167 b[2] * c[0] - b[0] * c[2],
2168 b[0] * c[1] - b[1] * c[0],
2169 ];
2170 (a[0] * cross[0] + a[1] * cross[1] + a[2] * cross[2]) / 6.0
2171 }
2172
2173 #[test]
2174 fn decompose_tet_yields_one_tet() {
2175 let data = single_tet();
2176 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2177 assert_eq!(tets.len(), 1);
2178 assert_eq!(scalars.len(), 1);
2179 }
2180
2181 #[test]
2182 fn decompose_hex_yields_six_tets() {
2183 let data = single_hex();
2184 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2185 assert_eq!(tets.len(), 6);
2186 assert_eq!(scalars.len(), 6);
2187 }
2188
2189 #[test]
2190 fn decompose_pyramid_yields_two_tets() {
2191 let data = single_pyramid();
2192 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2193 assert_eq!(tets.len(), 2);
2194 assert_eq!(scalars.len(), 2);
2195 }
2196
2197 #[test]
2198 fn decompose_wedge_yields_three_tets() {
2199 let data = single_wedge();
2200 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2201 assert_eq!(tets.len(), 3);
2202 assert_eq!(scalars.len(), 3);
2203 }
2204
2205 #[test]
2206 fn decompose_output_tets_have_nonzero_volume() {
2207 for data in [single_tet(), single_hex(), single_pyramid(), single_wedge()] {
2208 let (tets, _) = decompose_to_tetrahedra(&data, "");
2209 for (i, t) in tets.iter().enumerate() {
2210 let vol = tet_volume(*t).abs();
2211 assert!(vol > 1e-6, "tet {i} has near-zero volume {vol}: {t:?}");
2212 }
2213 }
2214 }
2215
2216 #[test]
2217 fn decompose_hex_volume_equals_cell_volume() {
2218 let data = single_hex();
2220 let (tets, _) = decompose_to_tetrahedra(&data, "");
2221 let total: f32 = tets.iter().map(|t| tet_volume(*t).abs()).sum();
2222 assert!(
2223 (total - 1.0).abs() < 1e-5,
2224 "unit hex volume should be 1.0, got {total}"
2225 );
2226 }
2227
2228 #[test]
2229 fn decompose_scalar_propagates_to_child_tets() {
2230 let mut data = single_hex();
2231 data.cell_scalars.insert("temp".to_string(), vec![42.0]);
2232 let (_, scalars) = decompose_to_tetrahedra(&data, "temp");
2233 assert_eq!(scalars.len(), 6);
2234 for &s in &scalars {
2235 assert_eq!(s, 42.0, "all child tets must inherit the cell scalar");
2236 }
2237 }
2238
2239 #[test]
2240 fn decompose_missing_attribute_falls_back_to_zero() {
2241 let data = single_hex();
2242 let (_, scalars) = decompose_to_tetrahedra(&data, "nonexistent");
2243 for &s in &scalars {
2244 assert_eq!(s, 0.0, "missing attribute must produce 0.0 per tet");
2245 }
2246 }
2247
2248 #[test]
2249 fn decompose_mixed_mesh_tet_counts_sum_correctly() {
2250 let mut data = VolumeMeshData {
2252 positions: vec![
2253 [0.0, 0.0, 0.0],
2255 [1.0, 0.0, 0.0],
2256 [0.5, 1.0, 0.0],
2257 [0.5, 0.5, 1.0],
2258 [2.0, 0.0, 0.0],
2260 [3.0, 0.0, 0.0],
2261 [3.0, 0.0, 1.0],
2262 [2.0, 0.0, 1.0],
2263 [2.0, 1.0, 0.0],
2264 [3.0, 1.0, 0.0],
2265 [3.0, 1.0, 1.0],
2266 [2.0, 1.0, 1.0],
2267 [4.0, 0.0, 0.0],
2269 [5.0, 0.0, 0.0],
2270 [5.0, 0.0, 1.0],
2271 [4.0, 0.0, 1.0],
2272 [4.5, 1.0, 0.5],
2273 [6.0, 0.0, 0.0],
2275 [7.0, 0.0, 0.0],
2276 [6.5, 0.0, 1.0],
2277 [6.0, 1.0, 0.0],
2278 [7.0, 1.0, 0.0],
2279 [6.5, 1.0, 1.0],
2280 ],
2281 ..Default::default()
2282 };
2283 data.push_tet(0, 1, 2, 3);
2284 data.push_hex([4, 5, 6, 7, 8, 9, 10, 11]);
2285 data.push_pyramid([12, 13, 14, 15], 16);
2286 data.push_wedge([17, 18, 19], [20, 21, 22]);
2287
2288 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2289 assert_eq!(tets.len(), 12, "1+6+2+3 = 12 tets");
2290 assert_eq!(scalars.len(), 12);
2291 }
2292}