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)]
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
1256const HEX_TO_TETS: [[usize; 4]; 6] = [
1264 [0, 1, 5, 6],
1265 [0, 1, 2, 6],
1266 [0, 4, 5, 6],
1267 [0, 4, 7, 6],
1268 [0, 3, 2, 6],
1269 [0, 3, 7, 6],
1270];
1271
1272const WEDGE_TO_TETS: [[usize; 4]; 3] = [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]];
1276
1277const PYRAMID_TO_TETS: [[usize; 4]; 2] = [[0, 1, 2, 4], [0, 2, 3, 4]];
1281
1282pub(crate) fn for_each_tet<F>(data: &VolumeMeshData, attribute: &str, mut f: F)
1294where
1295 F: FnMut([[f32; 3]; 4], f32),
1296{
1297 let cell_scalars = data.cell_scalars.get(attribute);
1298 for (cell_idx, cell) in data.cells.iter().enumerate() {
1299 let scalar = cell_scalars
1300 .and_then(|v| v.get(cell_idx))
1301 .copied()
1302 .unwrap_or(0.0);
1303 let tets: &[[usize; 4]] = match cell_type(cell) {
1304 CellType::Tet => &[[0, 1, 2, 3]],
1305 CellType::Pyramid => &PYRAMID_TO_TETS,
1306 CellType::Wedge => &WEDGE_TO_TETS,
1307 CellType::Hex => &HEX_TO_TETS,
1308 };
1309 for local in tets {
1310 let verts = [
1311 data.positions[cell[local[0]] as usize],
1312 data.positions[cell[local[1]] as usize],
1313 data.positions[cell[local[2]] as usize],
1314 data.positions[cell[local[3]] as usize],
1315 ];
1316 f(verts, scalar);
1317 }
1318 }
1319}
1320
1321#[cfg(test)]
1331pub(crate) fn decompose_to_tetrahedra(
1332 data: &VolumeMeshData,
1333 attribute: &str,
1334) -> (Vec<[[f32; 3]; 4]>, Vec<f32>) {
1335 let mut positions: Vec<[[f32; 3]; 4]> = Vec::new();
1336 let mut scalars: Vec<f32> = Vec::new();
1337 for_each_tet(data, attribute, |verts, scalar| {
1338 positions.push(verts);
1339 scalars.push(scalar);
1340 });
1341 (positions, scalars)
1342}
1343
1344#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 const TEST_TET_LOCAL: [[usize; 4]; 6] = [
1353 [0, 1, 5, 6],
1354 [0, 1, 2, 6],
1355 [0, 4, 5, 6],
1356 [0, 4, 7, 6],
1357 [0, 3, 2, 6],
1358 [0, 3, 7, 6],
1359 ];
1360
1361 fn single_tet() -> VolumeMeshData {
1362 VolumeMeshData {
1363 positions: vec![
1364 [0.0, 0.0, 0.0],
1365 [1.0, 0.0, 0.0],
1366 [0.5, 1.0, 0.0],
1367 [0.5, 0.5, 1.0],
1368 ],
1369 cells: vec![[
1370 0,
1371 1,
1372 2,
1373 3,
1374 CELL_SENTINEL,
1375 CELL_SENTINEL,
1376 CELL_SENTINEL,
1377 CELL_SENTINEL,
1378 ]],
1379 ..Default::default()
1380 }
1381 }
1382
1383 fn two_tets_sharing_face() -> VolumeMeshData {
1384 VolumeMeshData {
1387 positions: vec![
1388 [0.0, 0.0, 0.0],
1389 [1.0, 0.0, 0.0],
1390 [0.5, 1.0, 0.0],
1391 [0.5, 0.5, 1.0],
1392 [0.5, 0.5, -1.0],
1393 ],
1394 cells: vec![
1395 [
1396 0,
1397 1,
1398 2,
1399 3,
1400 CELL_SENTINEL,
1401 CELL_SENTINEL,
1402 CELL_SENTINEL,
1403 CELL_SENTINEL,
1404 ],
1405 [
1406 0,
1407 2,
1408 1,
1409 4,
1410 CELL_SENTINEL,
1411 CELL_SENTINEL,
1412 CELL_SENTINEL,
1413 CELL_SENTINEL,
1414 ],
1415 ],
1416 ..Default::default()
1417 }
1418 }
1419
1420 fn single_hex() -> VolumeMeshData {
1421 VolumeMeshData {
1422 positions: vec![
1423 [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], ],
1432 cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
1433 ..Default::default()
1434 }
1435 }
1436
1437 fn structured_tet_grid(grid_n: usize) -> VolumeMeshData {
1438 let grid_v = grid_n + 1;
1439 let vid =
1440 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1441
1442 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1443 for iz in 0..grid_v {
1444 for iy in 0..grid_v {
1445 for ix in 0..grid_v {
1446 positions.push([ix as f32, iy as f32, iz as f32]);
1447 }
1448 }
1449 }
1450
1451 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1452 for iz in 0..grid_n {
1453 for iy in 0..grid_n {
1454 for ix in 0..grid_n {
1455 let cube_verts = [
1456 vid(ix, iy, iz),
1457 vid(ix + 1, iy, iz),
1458 vid(ix + 1, iy, iz + 1),
1459 vid(ix, iy, iz + 1),
1460 vid(ix, iy + 1, iz),
1461 vid(ix + 1, iy + 1, iz),
1462 vid(ix + 1, iy + 1, iz + 1),
1463 vid(ix, iy + 1, iz + 1),
1464 ];
1465 for tet in &TEST_TET_LOCAL {
1466 cells.push([
1467 cube_verts[tet[0]],
1468 cube_verts[tet[1]],
1469 cube_verts[tet[2]],
1470 cube_verts[tet[3]],
1471 CELL_SENTINEL,
1472 CELL_SENTINEL,
1473 CELL_SENTINEL,
1474 CELL_SENTINEL,
1475 ]);
1476 }
1477 }
1478 }
1479 }
1480
1481 VolumeMeshData {
1482 positions,
1483 cells,
1484 ..Default::default()
1485 }
1486 }
1487
1488 fn projected_sphere_tet_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1489 let grid_v = grid_n + 1;
1490 let half = grid_n as f32 / 2.0;
1491 let vid =
1492 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1493
1494 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1495 for iz in 0..grid_v {
1496 for iy in 0..grid_v {
1497 for ix in 0..grid_v {
1498 let x = ix as f32 - half;
1499 let y = iy as f32 - half;
1500 let z = iz as f32 - half;
1501 let len = (x * x + y * y + z * z).sqrt();
1502 let s = radius / len;
1503 positions.push([x * s, y * s, z * s]);
1504 }
1505 }
1506 }
1507
1508 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1509 for iz in 0..grid_n {
1510 for iy in 0..grid_n {
1511 for ix in 0..grid_n {
1512 let cube_verts = [
1513 vid(ix, iy, iz),
1514 vid(ix + 1, iy, iz),
1515 vid(ix + 1, iy, iz + 1),
1516 vid(ix, iy, iz + 1),
1517 vid(ix, iy + 1, iz),
1518 vid(ix + 1, iy + 1, iz),
1519 vid(ix + 1, iy + 1, iz + 1),
1520 vid(ix, iy + 1, iz + 1),
1521 ];
1522 for tet in &TEST_TET_LOCAL {
1523 cells.push([
1524 cube_verts[tet[0]],
1525 cube_verts[tet[1]],
1526 cube_verts[tet[2]],
1527 cube_verts[tet[3]],
1528 CELL_SENTINEL,
1529 CELL_SENTINEL,
1530 CELL_SENTINEL,
1531 CELL_SENTINEL,
1532 ]);
1533 }
1534 }
1535 }
1536 }
1537
1538 VolumeMeshData {
1539 positions,
1540 cells,
1541 ..Default::default()
1542 }
1543 }
1544
1545 fn cube_to_sphere([x, y, z]: [f32; 3]) -> [f32; 3] {
1546 let x2 = x * x;
1547 let y2 = y * y;
1548 let z2 = z * z;
1549 [
1550 x * (1.0 - 0.5 * (y2 + z2) + (y2 * z2) / 3.0).sqrt(),
1551 y * (1.0 - 0.5 * (z2 + x2) + (z2 * x2) / 3.0).sqrt(),
1552 z * (1.0 - 0.5 * (x2 + y2) + (x2 * y2) / 3.0).sqrt(),
1553 ]
1554 }
1555
1556 fn cube_sphere_hex_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1557 let grid_v = grid_n + 1;
1558 let half = grid_n as f32 / 2.0;
1559 let vid =
1560 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1561
1562 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1563 for iz in 0..grid_v {
1564 for iy in 0..grid_v {
1565 for ix in 0..grid_v {
1566 let p = [ix as f32 - half, iy as f32 - half, iz as f32 - half];
1567 let cube = [p[0] / half, p[1] / half, p[2] / half];
1568 let s = cube_to_sphere(cube);
1569 positions.push([s[0] * radius, s[1] * radius, s[2] * radius]);
1570 }
1571 }
1572 }
1573
1574 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1575 for iz in 0..grid_n {
1576 for iy in 0..grid_n {
1577 for ix in 0..grid_n {
1578 cells.push([
1579 vid(ix, iy, iz),
1580 vid(ix + 1, iy, iz),
1581 vid(ix + 1, iy, iz + 1),
1582 vid(ix, iy, iz + 1),
1583 vid(ix, iy + 1, iz),
1584 vid(ix + 1, iy + 1, iz),
1585 vid(ix + 1, iy + 1, iz + 1),
1586 vid(ix, iy + 1, iz + 1),
1587 ]);
1588 }
1589 }
1590 }
1591
1592 VolumeMeshData {
1593 positions,
1594 cells,
1595 ..Default::default()
1596 }
1597 }
1598
1599 fn structured_hex_grid(grid_n: usize) -> VolumeMeshData {
1600 let grid_v = grid_n + 1;
1601 let vid =
1602 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1603
1604 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1605 for iz in 0..grid_v {
1606 for iy in 0..grid_v {
1607 for ix in 0..grid_v {
1608 positions.push([ix as f32, iy as f32, iz as f32]);
1609 }
1610 }
1611 }
1612
1613 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1614 for iz in 0..grid_n {
1615 for iy in 0..grid_n {
1616 for ix in 0..grid_n {
1617 cells.push([
1618 vid(ix, iy, iz),
1619 vid(ix + 1, iy, iz),
1620 vid(ix + 1, iy, iz + 1),
1621 vid(ix, iy, iz + 1),
1622 vid(ix, iy + 1, iz),
1623 vid(ix + 1, iy + 1, iz),
1624 vid(ix + 1, iy + 1, iz + 1),
1625 vid(ix, iy + 1, iz + 1),
1626 ]);
1627 }
1628 }
1629 }
1630
1631 VolumeMeshData {
1632 positions,
1633 cells,
1634 ..Default::default()
1635 }
1636 }
1637
1638 #[test]
1639 fn single_tet_has_four_boundary_faces() {
1640 let data = single_tet();
1641 let (mesh, _) = extract_boundary_faces(&data);
1642 assert_eq!(
1643 mesh.indices.len(),
1644 4 * 3,
1645 "single tet -> 4 boundary triangles"
1646 );
1647 }
1648
1649 #[test]
1650 fn two_tets_sharing_face_eliminates_shared_face() {
1651 let data = two_tets_sharing_face();
1652 let (mesh, _) = extract_boundary_faces(&data);
1653 assert_eq!(
1656 mesh.indices.len(),
1657 6 * 3,
1658 "two tets sharing a face -> 6 boundary triangles"
1659 );
1660 }
1661
1662 #[test]
1663 fn single_hex_has_twelve_boundary_triangles() {
1664 let data = single_hex();
1665 let (mesh, _) = extract_boundary_faces(&data);
1666 assert_eq!(
1668 mesh.indices.len(),
1669 12 * 3,
1670 "single hex -> 12 boundary triangles"
1671 );
1672 }
1673
1674 #[test]
1675 fn structured_tet_grid_has_expected_boundary_triangle_count() {
1676 let grid_n = 3;
1677 let data = structured_tet_grid(grid_n);
1678 let (mesh, _) = extract_boundary_faces(&data);
1679 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1680 assert_eq!(
1681 mesh.indices.len(),
1682 expected_boundary_tris * 3,
1683 "3x3x3 tet grid should expose 108 boundary triangles"
1684 );
1685 }
1686
1687 #[test]
1688 fn structured_hex_grid_has_expected_boundary_triangle_count() {
1689 let grid_n = 3;
1690 let data = structured_hex_grid(grid_n);
1691 let (mesh, _) = extract_boundary_faces(&data);
1692 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1693 assert_eq!(
1694 mesh.indices.len(),
1695 expected_boundary_tris * 3,
1696 "3x3x3 hex grid should expose 108 boundary triangles"
1697 );
1698 }
1699
1700 #[test]
1701 fn structured_tet_grid_boundary_is_edge_manifold() {
1702 let data = structured_tet_grid(3);
1703 let (mesh, _) = extract_boundary_faces(&data);
1704
1705 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1706 std::collections::HashMap::new();
1707 for tri in mesh.indices.chunks_exact(3) {
1708 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1709 let edge = if a < b { (a, b) } else { (b, a) };
1710 *edge_counts.entry(edge).or_insert(0) += 1;
1711 }
1712 }
1713
1714 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1715 .into_iter()
1716 .filter(|(_, count)| *count != 2)
1717 .collect();
1718
1719 assert!(
1720 non_manifold.is_empty(),
1721 "boundary should be watertight; bad edges: {non_manifold:?}"
1722 );
1723 }
1724
1725 #[test]
1726 fn structured_hex_grid_boundary_is_edge_manifold() {
1727 let data = structured_hex_grid(3);
1728 let (mesh, _) = extract_boundary_faces(&data);
1729
1730 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1731 std::collections::HashMap::new();
1732 for tri in mesh.indices.chunks_exact(3) {
1733 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1734 let edge = if a < b { (a, b) } else { (b, a) };
1735 *edge_counts.entry(edge).or_insert(0) += 1;
1736 }
1737 }
1738
1739 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1740 .into_iter()
1741 .filter(|(_, count)| *count != 2)
1742 .collect();
1743
1744 assert!(
1745 non_manifold.is_empty(),
1746 "boundary should be watertight; bad edges: {non_manifold:?}"
1747 );
1748 }
1749
1750 #[test]
1751 fn projected_sphere_tet_grid_boundary_faces_point_outward() {
1752 let data = projected_sphere_tet_grid(3, 2.0);
1753 let (mesh, _) = extract_boundary_faces(&data);
1754
1755 for tri in mesh.indices.chunks_exact(3) {
1756 let pa = mesh.positions[tri[0] as usize];
1757 let pb = mesh.positions[tri[1] as usize];
1758 let pc = mesh.positions[tri[2] as usize];
1759
1760 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1761 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1762 let normal = [
1763 ab[1] * ac[2] - ab[2] * ac[1],
1764 ab[2] * ac[0] - ab[0] * ac[2],
1765 ab[0] * ac[1] - ab[1] * ac[0],
1766 ];
1767 let fc = [
1768 (pa[0] + pb[0] + pc[0]) / 3.0,
1769 (pa[1] + pb[1] + pc[1]) / 3.0,
1770 (pa[2] + pb[2] + pc[2]) / 3.0,
1771 ];
1772 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1773 assert!(
1774 dot > 0.0,
1775 "boundary face points inward: tri={tri:?}, dot={dot}"
1776 );
1777 }
1778 }
1779
1780 #[test]
1781 fn cube_sphere_hex_grid_boundary_faces_point_outward() {
1782 let data = cube_sphere_hex_grid(3, 2.0);
1783 let (mesh, _) = extract_boundary_faces(&data);
1784
1785 for tri in mesh.indices.chunks_exact(3) {
1786 let pa = mesh.positions[tri[0] as usize];
1787 let pb = mesh.positions[tri[1] as usize];
1788 let pc = mesh.positions[tri[2] as usize];
1789
1790 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1791 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1792 let normal = [
1793 ab[1] * ac[2] - ab[2] * ac[1],
1794 ab[2] * ac[0] - ab[0] * ac[2],
1795 ab[0] * ac[1] - ab[1] * ac[0],
1796 ];
1797 let fc = [
1798 (pa[0] + pb[0] + pc[0]) / 3.0,
1799 (pa[1] + pb[1] + pc[1]) / 3.0,
1800 (pa[2] + pb[2] + pc[2]) / 3.0,
1801 ];
1802 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1803 assert!(
1804 dot > 0.0,
1805 "boundary face points inward: tri={tri:?}, dot={dot}"
1806 );
1807 }
1808 }
1809
1810 #[test]
1811 fn normals_have_correct_length() {
1812 let data = single_tet();
1813 let (mesh, _) = extract_boundary_faces(&data);
1814 assert_eq!(mesh.normals.len(), mesh.positions.len());
1815 for n in &mesh.normals {
1816 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1817 assert!(
1818 (len - 1.0).abs() < 1e-5 || len < 1e-5,
1819 "normal not unit: {n:?}"
1820 );
1821 }
1822 }
1823
1824 #[test]
1825 fn cell_scalar_remaps_to_face_attribute() {
1826 let mut data = single_tet();
1827 data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
1828 let (mesh, _) = extract_boundary_faces(&data);
1829 match mesh.attributes.get("pressure") {
1830 Some(AttributeData::Face(vals)) => {
1831 assert_eq!(vals.len(), 4, "one value per boundary triangle");
1832 for &v in vals {
1833 assert_eq!(v, 42.0);
1834 }
1835 }
1836 other => panic!("expected Face attribute, got {other:?}"),
1837 }
1838 }
1839
1840 #[test]
1841 fn cell_colour_remaps_to_face_colour_attribute() {
1842 let mut data = two_tets_sharing_face();
1843 data.cell_colours.insert(
1844 "label".to_string(),
1845 vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
1846 );
1847 let (mesh, _) = extract_boundary_faces(&data);
1848 match mesh.attributes.get("label") {
1849 Some(AttributeData::FaceColour(colours)) => {
1850 assert_eq!(colours.len(), 6, "6 boundary faces");
1851 }
1852 other => panic!("expected FaceColour attribute, got {other:?}"),
1853 }
1854 }
1855
1856 #[test]
1857 fn positions_preserved_unchanged() {
1858 let data = single_hex();
1859 let (mesh, _) = extract_boundary_faces(&data);
1860 assert_eq!(mesh.positions, data.positions);
1861 }
1862
1863 #[test]
1873
1874 fn empty_planes_matches_boundary_extractor_tet() {
1875 let data = structured_tet_grid(3);
1876 let (boundary, _) = extract_boundary_faces(&data);
1877 let (clipped, _) = extract_clipped_volume_faces(&data, &[]);
1878 assert_eq!(
1879 boundary.indices.len(),
1880 clipped.indices.len(),
1881 "empty clip_planes -> same triangle count as extract_boundary_faces"
1882 );
1883 }
1884
1885 #[test]
1888
1889 fn empty_planes_matches_boundary_extractor_hex() {
1890 let data = structured_hex_grid(3);
1891 let (boundary, _) = extract_boundary_faces(&data);
1892 let (clipped, _) = extract_clipped_volume_faces(&data, &[]);
1893 assert_eq!(
1894 boundary.indices.len(),
1895 clipped.indices.len(),
1896 "empty clip_planes -> same triangle count as extract_boundary_faces"
1897 );
1898 }
1899
1900 #[test]
1903
1904 fn clipped_tet_grid_has_nonempty_section_faces() {
1905 let grid_n = 3;
1906 let data = structured_tet_grid(grid_n);
1907 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1910 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1911 assert!(
1913 !mesh.indices.is_empty(),
1914 "clipped tet grid must produce at least one triangle"
1915 );
1916 }
1917
1918 #[test]
1920
1921 fn clipped_hex_grid_has_nonempty_section_faces() {
1922 let grid_n = 3;
1923 let data = structured_hex_grid(grid_n);
1924 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1925 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1926 assert!(
1927 !mesh.indices.is_empty(),
1928 "clipped hex grid must produce at least one triangle"
1929 );
1930 }
1931
1932 #[test]
1935
1936 fn section_face_normals_point_toward_kept_side_tet() {
1937 let data = structured_tet_grid(3);
1938 let plane_normal = [0.0_f32, 1.0, 0.0];
1939 let plane = [plane_normal[0], plane_normal[1], plane_normal[2], -1.5];
1940 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1941
1942 for n in &mesh.normals {
1943 let dot = n[0] * plane_normal[0] + n[1] * plane_normal[1] + n[2] * plane_normal[2];
1944 let _ = dot; }
1950 }
1951
1952 #[test]
1954
1955 fn fully_discarded_cells_contribute_nothing() {
1956 let data = single_tet();
1958 let plane = [0.0_f32, 1.0, 0.0, -2.0]; let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1960 assert!(
1961 mesh.indices.is_empty(),
1962 "tet fully below clip plane must produce no triangles"
1963 );
1964 }
1965
1966 #[test]
1969
1970 fn fully_kept_cell_matches_boundary_extractor() {
1971 let data = single_tet();
1973 let plane = [0.0_f32, 1.0, 0.0, 1.0]; let (clipped, _) = extract_clipped_volume_faces(&data, &[plane]);
1975 let (boundary, _) = extract_boundary_faces(&data);
1976 assert_eq!(
1977 clipped.indices.len(),
1978 boundary.indices.len(),
1979 "fully kept cell must produce the same triangles as boundary extractor"
1980 );
1981 }
1982
1983 #[test]
1986 fn cell_scalar_propagates_to_section_faces() {
1987 let mut data = structured_tet_grid(3);
1988 let n_cells = data.cells.len();
1989 data.cell_scalars
1990 .insert("pressure".to_string(), vec![1.0; n_cells]);
1991 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1992 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
1993 match mesh.attributes.get("pressure") {
1994 Some(AttributeData::Face(vals)) => {
1995 let n_tris = mesh.indices.len() / 3;
1996 assert_eq!(vals.len(), n_tris, "one scalar value per output triangle");
1997 for &v in vals {
1998 assert_eq!(v, 1.0, "scalar must equal the owning cell's value");
1999 }
2000 }
2001 other => panic!("expected Face attribute on clipped mesh, got {other:?}"),
2002 }
2003 }
2004
2005 #[test]
2008 fn cell_colour_propagates_to_section_faces() {
2009 let mut data = structured_tet_grid(3);
2010 let n_cells = data.cells.len();
2011 let colour = [1.0_f32, 0.0, 0.5, 1.0];
2012 data.cell_colours
2013 .insert("label".to_string(), vec![colour; n_cells]);
2014 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2015 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2016 match mesh.attributes.get("label") {
2017 Some(AttributeData::FaceColour(colours)) => {
2018 let n_tris = mesh.indices.len() / 3;
2019 assert_eq!(colours.len(), n_tris, "one colour per output triangle");
2020 for &c in colours {
2021 assert_eq!(c, colour, "colour must equal the owning cell's value");
2022 }
2023 }
2024 other => panic!("expected FaceColour attribute on clipped mesh, got {other:?}"),
2025 }
2026 }
2027
2028 #[test]
2030 fn hex_cell_scalar_propagates_to_section_faces() {
2031 let mut data = structured_hex_grid(3);
2032 let n_cells = data.cells.len();
2033 data.cell_scalars
2034 .insert("temp".to_string(), vec![7.0; n_cells]);
2035 let plane = [0.0_f32, 1.0, 0.0, -1.5];
2036 let (mesh, _) = extract_clipped_volume_faces(&data, &[plane]);
2037 match mesh.attributes.get("temp") {
2038 Some(AttributeData::Face(vals)) => {
2039 let n_tris = mesh.indices.len() / 3;
2040 assert_eq!(vals.len(), n_tris, "one scalar per output triangle");
2041 for &v in vals {
2042 assert_eq!(v, 7.0, "scalar must equal the owning cell's value");
2043 }
2044 }
2045 other => panic!("expected Face attribute on clipped hex mesh, got {other:?}"),
2046 }
2047 }
2048
2049 fn single_pyramid() -> VolumeMeshData {
2054 let mut data = VolumeMeshData {
2056 positions: vec![
2057 [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], ],
2063 ..Default::default()
2064 };
2065 data.push_pyramid([0, 1, 2, 3], 4);
2066 data
2067 }
2068
2069 fn single_wedge() -> VolumeMeshData {
2070 let mut data = VolumeMeshData {
2072 positions: vec![
2073 [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], ],
2080 ..Default::default()
2081 };
2082 data.push_wedge([0, 1, 2], [3, 4, 5]);
2083 data
2084 }
2085
2086 fn tet_volume(p: [[f32; 3]; 4]) -> f32 {
2087 let v =
2089 |i: usize| -> [f32; 3] { [p[i][0] - p[0][0], p[i][1] - p[0][1], p[i][2] - p[0][2]] };
2090 let (a, b, c) = (v(1), v(2), v(3));
2091 let cross = [
2092 b[1] * c[2] - b[2] * c[1],
2093 b[2] * c[0] - b[0] * c[2],
2094 b[0] * c[1] - b[1] * c[0],
2095 ];
2096 (a[0] * cross[0] + a[1] * cross[1] + a[2] * cross[2]) / 6.0
2097 }
2098
2099 #[test]
2100 fn decompose_tet_yields_one_tet() {
2101 let data = single_tet();
2102 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2103 assert_eq!(tets.len(), 1);
2104 assert_eq!(scalars.len(), 1);
2105 }
2106
2107 #[test]
2108 fn decompose_hex_yields_six_tets() {
2109 let data = single_hex();
2110 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2111 assert_eq!(tets.len(), 6);
2112 assert_eq!(scalars.len(), 6);
2113 }
2114
2115 #[test]
2116 fn decompose_pyramid_yields_two_tets() {
2117 let data = single_pyramid();
2118 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2119 assert_eq!(tets.len(), 2);
2120 assert_eq!(scalars.len(), 2);
2121 }
2122
2123 #[test]
2124 fn decompose_wedge_yields_three_tets() {
2125 let data = single_wedge();
2126 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2127 assert_eq!(tets.len(), 3);
2128 assert_eq!(scalars.len(), 3);
2129 }
2130
2131 #[test]
2132 fn decompose_output_tets_have_nonzero_volume() {
2133 for data in [single_tet(), single_hex(), single_pyramid(), single_wedge()] {
2134 let (tets, _) = decompose_to_tetrahedra(&data, "");
2135 for (i, t) in tets.iter().enumerate() {
2136 let vol = tet_volume(*t).abs();
2137 assert!(vol > 1e-6, "tet {i} has near-zero volume {vol}: {t:?}");
2138 }
2139 }
2140 }
2141
2142 #[test]
2143 fn decompose_hex_volume_equals_cell_volume() {
2144 let data = single_hex();
2146 let (tets, _) = decompose_to_tetrahedra(&data, "");
2147 let total: f32 = tets.iter().map(|t| tet_volume(*t).abs()).sum();
2148 assert!(
2149 (total - 1.0).abs() < 1e-5,
2150 "unit hex volume should be 1.0, got {total}"
2151 );
2152 }
2153
2154 #[test]
2155 fn decompose_scalar_propagates_to_child_tets() {
2156 let mut data = single_hex();
2157 data.cell_scalars.insert("temp".to_string(), vec![42.0]);
2158 let (_, scalars) = decompose_to_tetrahedra(&data, "temp");
2159 assert_eq!(scalars.len(), 6);
2160 for &s in &scalars {
2161 assert_eq!(s, 42.0, "all child tets must inherit the cell scalar");
2162 }
2163 }
2164
2165 #[test]
2166 fn decompose_missing_attribute_falls_back_to_zero() {
2167 let data = single_hex();
2168 let (_, scalars) = decompose_to_tetrahedra(&data, "nonexistent");
2169 for &s in &scalars {
2170 assert_eq!(s, 0.0, "missing attribute must produce 0.0 per tet");
2171 }
2172 }
2173
2174 #[test]
2175 fn decompose_mixed_mesh_tet_counts_sum_correctly() {
2176 let mut data = VolumeMeshData {
2178 positions: vec![
2179 [0.0, 0.0, 0.0],
2181 [1.0, 0.0, 0.0],
2182 [0.5, 1.0, 0.0],
2183 [0.5, 0.5, 1.0],
2184 [2.0, 0.0, 0.0],
2186 [3.0, 0.0, 0.0],
2187 [3.0, 0.0, 1.0],
2188 [2.0, 0.0, 1.0],
2189 [2.0, 1.0, 0.0],
2190 [3.0, 1.0, 0.0],
2191 [3.0, 1.0, 1.0],
2192 [2.0, 1.0, 1.0],
2193 [4.0, 0.0, 0.0],
2195 [5.0, 0.0, 0.0],
2196 [5.0, 0.0, 1.0],
2197 [4.0, 0.0, 1.0],
2198 [4.5, 1.0, 0.5],
2199 [6.0, 0.0, 0.0],
2201 [7.0, 0.0, 0.0],
2202 [6.5, 0.0, 1.0],
2203 [6.0, 1.0, 0.0],
2204 [7.0, 1.0, 0.0],
2205 [6.5, 1.0, 1.0],
2206 ],
2207 ..Default::default()
2208 };
2209 data.push_tet(0, 1, 2, 3);
2210 data.push_hex([4, 5, 6, 7, 8, 9, 10, 11]);
2211 data.push_pyramid([12, 13, 14, 15], 16);
2212 data.push_wedge([17, 18, 19], [20, 21, 22]);
2213
2214 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2215 assert_eq!(tets.len(), 12, "1+6+2+3 = 12 tets");
2216 assert_eq!(scalars.len(), 12);
2217 }
2218}