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_colors: 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], [1, 2], [2, 3], [3, 0], [0, 4], [1, 4], [2, 4], [3, 4], ];
180
181const WEDGE_TRI_FACES: [[usize; 3]; 2] = [
200 [0, 2, 1], [3, 4, 5], ];
203
204const WEDGE_QUAD_FACES: [[usize; 4]; 3] = [
206 [0, 1, 4, 3], [1, 2, 5, 4], [2, 0, 3, 5], ];
210
211const WEDGE_EDGES: [[usize; 2]; 9] = [
213 [0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3], [0, 3], [1, 4], [2, 5], ];
217
218type FaceKey = (u32, u32, u32);
224
225type QuadFaceKey = (u32, u32, u32, u32);
227
228type TriEntry = (FaceKey, usize, [u32; 3], [f32; 3]);
230type QuadEntry = (QuadFaceKey, usize, [u32; 4], [f32; 3]);
231
232#[inline]
234fn face_key(a: u32, b: u32, c: u32) -> FaceKey {
235 let mut arr = [a, b, c];
236 arr.sort_unstable();
237 (arr[0], arr[1], arr[2])
238}
239
240#[inline]
242fn quad_face_key(a: u32, b: u32, c: u32, d: u32) -> QuadFaceKey {
243 let mut arr = [a, b, c, d];
244 arr.sort_unstable();
245 (arr[0], arr[1], arr[2], arr[3])
246}
247
248fn generate_tri_entries(cell_idx: usize, cell: &[u32; 8], positions: &[[f32; 3]]) -> Vec<TriEntry> {
250 let ct = cell_type(cell);
251 let nv = ct.vertex_count();
252 let mut out = Vec::new();
253 match ct {
254 CellType::Tet => {
255 for (face_idx, face_local) in TET_FACES.iter().enumerate() {
256 let a = cell[face_local[0]];
257 let b = cell[face_local[1]];
258 let c = cell[face_local[2]];
259 let interior_ref = positions[cell[face_idx] as usize];
261 out.push((face_key(a, b, c), cell_idx, [a, b, c], interior_ref));
262 }
263 }
264 CellType::Pyramid => {
265 let centroid = cell_centroid(cell, nv, positions);
266 for face_local in &PYRAMID_TRI_FACES {
267 let a = cell[face_local[0]];
268 let b = cell[face_local[1]];
269 let c = cell[face_local[2]];
270 out.push((face_key(a, b, c), cell_idx, [a, b, c], centroid));
271 }
272 }
273 CellType::Wedge => {
274 let centroid = cell_centroid(cell, nv, positions);
275 for face_local in &WEDGE_TRI_FACES {
276 let a = cell[face_local[0]];
277 let b = cell[face_local[1]];
278 let c = cell[face_local[2]];
279 out.push((face_key(a, b, c), cell_idx, [a, b, c], centroid));
280 }
281 }
282 CellType::Hex => {} }
284 out
285}
286
287fn generate_quad_entries(cell_idx: usize, cell: &[u32; 8], positions: &[[f32; 3]]) -> Vec<QuadEntry> {
289 let ct = cell_type(cell);
290 let nv = ct.vertex_count();
291 let mut out = Vec::new();
292 match ct {
293 CellType::Tet => {} CellType::Pyramid => {
295 let centroid = cell_centroid(cell, nv, positions);
296 for quad_local in &PYRAMID_QUAD_FACE {
297 let v = [
298 cell[quad_local[0]],
299 cell[quad_local[1]],
300 cell[quad_local[2]],
301 cell[quad_local[3]],
302 ];
303 out.push((quad_face_key(v[0], v[1], v[2], v[3]), cell_idx, v, centroid));
304 }
305 }
306 CellType::Wedge => {
307 let centroid = cell_centroid(cell, nv, positions);
308 for quad_local in &WEDGE_QUAD_FACES {
309 let v = [
310 cell[quad_local[0]],
311 cell[quad_local[1]],
312 cell[quad_local[2]],
313 cell[quad_local[3]],
314 ];
315 out.push((quad_face_key(v[0], v[1], v[2], v[3]), cell_idx, v, centroid));
316 }
317 }
318 CellType::Hex => {
319 for (face_idx, quad) in HEX_FACES.iter().enumerate() {
320 let v: [u32; 4] =
321 [cell[quad[0]], cell[quad[1]], cell[quad[2]], cell[quad[3]]];
322 let interior_ref = {
323 let opposite = &HEX_FACES[HEX_FACE_OPPOSITE[face_idx]];
324 let mut c = [0.0f32; 3];
325 for &local_vi in opposite {
326 let p = positions[cell[local_vi] as usize];
327 c[0] += p[0];
328 c[1] += p[1];
329 c[2] += p[2];
330 }
331 [c[0] / 4.0, c[1] / 4.0, c[2] / 4.0]
332 };
333 out.push((quad_face_key(v[0], v[1], v[2], v[3]), cell_idx, v, interior_ref));
334 }
335 }
336 }
337 out
338}
339
340fn collect_boundary_tri(entries: &[TriEntry]) -> Vec<(usize, [u32; 3], [f32; 3])> {
342 let mut out = Vec::new();
343 let mut i = 0;
344 while i < entries.len() {
345 let key = entries[i].0;
346 let mut j = i + 1;
347 while j < entries.len() && entries[j].0 == key {
348 j += 1;
349 }
350 if j - i == 1 {
351 out.push((entries[i].1, entries[i].2, entries[i].3));
352 }
353 i = j;
354 }
355 out
356}
357
358fn collect_boundary_quad(entries: &[QuadEntry]) -> Vec<(usize, [u32; 4], [f32; 3])> {
360 let mut out = Vec::new();
361 let mut i = 0;
362 while i < entries.len() {
363 let key = entries[i].0;
364 let mut j = i + 1;
365 while j < entries.len() && entries[j].0 == key {
366 j += 1;
367 }
368 if j - i == 1 {
369 out.push((entries[i].1, entries[i].2, entries[i].3));
370 }
371 i = j;
372 }
373 out
374}
375
376#[inline]
379fn correct_winding(tri: &mut [u32; 3], interior_ref: &[f32; 3], positions: &[[f32; 3]]) {
380 let pa = positions[tri[0] as usize];
381 let pb = positions[tri[1] as usize];
382 let pc = positions[tri[2] as usize];
383 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
384 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
385 let normal = [
386 ab[1] * ac[2] - ab[2] * ac[1],
387 ab[2] * ac[0] - ab[0] * ac[2],
388 ab[0] * ac[1] - ab[1] * ac[0],
389 ];
390 let fc = [
391 (pa[0] + pb[0] + pc[0]) / 3.0,
392 (pa[1] + pb[1] + pc[1]) / 3.0,
393 (pa[2] + pb[2] + pc[2]) / 3.0,
394 ];
395 let out = [
396 fc[0] - interior_ref[0],
397 fc[1] - interior_ref[1],
398 fc[2] - interior_ref[2],
399 ];
400 if normal[0] * out[0] + normal[1] * out[1] + normal[2] * out[2] < 0.0 {
401 tri.swap(1, 2);
402 }
403}
404
405pub(crate) fn extract_boundary_faces(data: &VolumeMeshData) -> MeshData {
412 let n_verts = data.positions.len();
413
414 let (mut tri_entries, mut quad_entries) = if data.cells.len() >= PARALLEL_THRESHOLD {
416 let tri = data
417 .cells
418 .par_iter()
419 .enumerate()
420 .flat_map_iter(|(ci, cell)| generate_tri_entries(ci, cell, &data.positions))
421 .collect();
422 let quad = data
423 .cells
424 .par_iter()
425 .enumerate()
426 .flat_map_iter(|(ci, cell)| generate_quad_entries(ci, cell, &data.positions))
427 .collect();
428 (tri, quad)
429 } else {
430 let mut tri: Vec<TriEntry> = Vec::new();
431 let mut quad: Vec<QuadEntry> = Vec::new();
432 for (ci, cell) in data.cells.iter().enumerate() {
433 tri.extend(generate_tri_entries(ci, cell, &data.positions));
434 quad.extend(generate_quad_entries(ci, cell, &data.positions));
435 }
436 (tri, quad)
437 };
438
439 tri_entries.par_sort_unstable_by_key(|e| e.0);
440 quad_entries.par_sort_unstable_by_key(|e| e.0);
441
442 let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = collect_boundary_tri(&tri_entries);
444 for (ci, winding, iref) in collect_boundary_quad(&quad_entries) {
445 boundary.push((ci, [winding[0], winding[1], winding[2]], iref));
446 boundary.push((ci, [winding[0], winding[2], winding[3]], iref));
447 }
448
449 boundary.sort_unstable_by_key(|(ci, _, _)| *ci);
451
452 boundary
456 .par_iter_mut()
457 .for_each(|(_, tri, iref)| correct_winding(tri, iref, &data.positions));
458
459 let n_boundary_tris = boundary.len();
460
461 let mut indices: Vec<u32> = Vec::with_capacity(n_boundary_tris * 3);
464 let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
465
466 for (_, tri, _) in &boundary {
467 indices.push(tri[0]);
468 indices.push(tri[1]);
469 indices.push(tri[2]);
470
471 let pa = data.positions[tri[0] as usize];
472 let pb = data.positions[tri[1] as usize];
473 let pc = data.positions[tri[2] as usize];
474 let ab = [
475 (pb[0] - pa[0]) as f64,
476 (pb[1] - pa[1]) as f64,
477 (pb[2] - pa[2]) as f64,
478 ];
479 let ac = [
480 (pc[0] - pa[0]) as f64,
481 (pc[1] - pa[1]) as f64,
482 (pc[2] - pa[2]) as f64,
483 ];
484 let n = [
485 ab[1] * ac[2] - ab[2] * ac[1],
486 ab[2] * ac[0] - ab[0] * ac[2],
487 ab[0] * ac[1] - ab[1] * ac[0],
488 ];
489 for &vi in tri {
490 let acc = &mut normal_accum[vi as usize];
491 acc[0] += n[0];
492 acc[1] += n[1];
493 acc[2] += n[2];
494 }
495 }
496
497 let mut normals: Vec<[f32; 3]> = normal_accum
498 .iter()
499 .map(|n| {
500 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
501 if len > 1e-12 {
502 [
503 (n[0] / len) as f32,
504 (n[1] / len) as f32,
505 (n[2] / len) as f32,
506 ]
507 } else {
508 [0.0, 1.0, 0.0]
509 }
510 })
511 .collect();
512
513 normals.resize(n_verts, [0.0, 1.0, 0.0]);
514
515 let mut attributes: HashMap<String, AttributeData> = HashMap::new();
516
517 for (name, cell_vals) in &data.cell_scalars {
518 let face_scalars: Vec<f32> = boundary
519 .iter()
520 .map(|(ci, _, _)| cell_vals.get(*ci).copied().unwrap_or(0.0))
521 .collect();
522 attributes.insert(name.clone(), AttributeData::Face(face_scalars));
523 }
524
525 for (name, cell_vals) in &data.cell_colors {
526 let face_colors: Vec<[f32; 4]> = boundary
527 .iter()
528 .map(|(ci, _, _)| cell_vals.get(*ci).copied().unwrap_or([1.0; 4]))
529 .collect();
530 attributes.insert(name.clone(), AttributeData::FaceColor(face_colors));
531 }
532
533 MeshData {
534 positions: data.positions.clone(),
535 normals,
536 indices,
537 uvs: None,
538 tangents: None,
539 attributes,
540 }
541}
542
543const TET_EDGES: [[usize; 2]; 6] = [
644 [0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3],
645];
646
647const HEX_EDGES: [[usize; 2]; 12] = [
658 [0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7], ];
662
663#[derive(Clone, Copy, PartialEq, Eq)]
669enum CellType {
670 Tet,
671 Pyramid,
672 Wedge,
673 Hex,
674}
675
676impl CellType {
677 fn vertex_count(self) -> usize {
678 match self {
679 CellType::Tet => 4,
680 CellType::Pyramid => 5,
681 CellType::Wedge => 6,
682 CellType::Hex => 8,
683 }
684 }
685
686 fn edges(self) -> &'static [[usize; 2]] {
687 match self {
688 CellType::Tet => &TET_EDGES,
689 CellType::Pyramid => &PYRAMID_EDGES,
690 CellType::Wedge => &WEDGE_EDGES,
691 CellType::Hex => &HEX_EDGES,
692 }
693 }
694}
695
696#[inline]
698fn cell_type(cell: &[u32; 8]) -> CellType {
699 if cell[4] == CELL_SENTINEL {
700 CellType::Tet
701 } else if cell[5] == CELL_SENTINEL {
702 CellType::Pyramid
703 } else if cell[6] == CELL_SENTINEL {
704 CellType::Wedge
705 } else {
706 CellType::Hex
707 }
708}
709
710#[inline]
712fn cell_centroid(cell: &[u32; 8], nv: usize, positions: &[[f32; 3]]) -> [f32; 3] {
713 let mut c = [0.0f32; 3];
714 for i in 0..nv {
715 let p = positions[cell[i] as usize];
716 c[0] += p[0];
717 c[1] += p[1];
718 c[2] += p[2];
719 }
720 let n = nv as f32;
721 [c[0] / n, c[1] / n, c[2] / n]
722}
723
724#[inline]
727fn plane_dist(p: [f32; 3], plane: [f32; 4]) -> f32 {
728 p[0] * plane[0] + p[1] * plane[1] + p[2] * plane[2] + plane[3]
729}
730
731#[inline]
733fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
734 [
735 a[1] * b[2] - a[2] * b[1],
736 a[2] * b[0] - a[0] * b[2],
737 a[0] * b[1] - a[1] * b[0],
738 ]
739}
740
741#[inline]
743fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
744 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
745}
746
747#[inline]
749fn normalize3(v: [f32; 3]) -> [f32; 3] {
750 let len = dot3(v, v).sqrt();
751 if len > 1e-12 {
752 [v[0] / len, v[1] / len, v[2] / len]
753 } else {
754 [0.0, 1.0, 0.0]
755 }
756}
757
758fn intern_pos(
762 p: [f32; 3],
763 positions: &mut Vec<[f32; 3]>,
764 pos_map: &mut HashMap<[u32; 3], u32>,
765) -> u32 {
766 let key = [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
767 if let Some(&idx) = pos_map.get(&key) {
768 return idx;
769 }
770 let idx = positions.len() as u32;
771 positions.push(p);
772 pos_map.insert(key, idx);
773 idx
774}
775
776fn clip_polygon_one_plane(poly: Vec<[f32; 3]>, plane: [f32; 4]) -> Vec<[f32; 3]> {
779 if poly.is_empty() {
780 return poly;
781 }
782 let n = poly.len();
783 let mut out = Vec::with_capacity(n + 1);
784 for i in 0..n {
785 let a = poly[i];
786 let b = poly[(i + 1) % n];
787 let da = plane_dist(a, plane);
788 let db = plane_dist(b, plane);
789 let a_in = da >= 0.0;
790 let b_in = db >= 0.0;
791 if a_in {
792 out.push(a);
793 }
794 if a_in != b_in {
795 let denom = da - db;
796 if denom.abs() > 1e-30 {
797 let t = da / denom;
798 out.push([
799 a[0] + t * (b[0] - a[0]),
800 a[1] + t * (b[1] - a[1]),
801 a[2] + t * (b[2] - a[2]),
802 ]);
803 }
804 }
805 }
806 out
807}
808
809fn clip_polygon_planes(mut poly: Vec<[f32; 3]>, planes: &[[f32; 4]]) -> Vec<[f32; 3]> {
811 for &plane in planes {
812 if poly.is_empty() {
813 break;
814 }
815 poly = clip_polygon_one_plane(poly, plane);
816 }
817 poly
818}
819
820fn plane_basis(normal: [f32; 3]) -> ([f32; 3], [f32; 3]) {
822 let ref_vec: [f32; 3] = if normal[0].abs() < 0.9 {
823 [1.0, 0.0, 0.0]
824 } else {
825 [0.0, 1.0, 0.0]
826 };
827 let u = normalize3(cross3(normal, ref_vec));
828 let v = cross3(normal, u);
829 (u, v)
830}
831
832fn sort_polygon_on_plane(pts: &mut Vec<[f32; 3]>, normal: [f32; 3]) {
837 if pts.len() < 3 {
838 return;
839 }
840 let n = pts.len() as f32;
841 let cx = pts.iter().map(|p| p[0]).sum::<f32>() / n;
842 let cy = pts.iter().map(|p| p[1]).sum::<f32>() / n;
843 let cz = pts.iter().map(|p| p[2]).sum::<f32>() / n;
844 let centroid = [cx, cy, cz];
845 let (u, v) = plane_basis(normal);
846 pts.sort_by(|a, b| {
847 let da = [a[0] - centroid[0], a[1] - centroid[1], a[2] - centroid[2]];
848 let db = [b[0] - centroid[0], b[1] - centroid[1], b[2] - centroid[2]];
849 let ang_a = dot3(da, v).atan2(dot3(da, u));
850 let ang_b = dot3(db, v).atan2(dot3(db, u));
851 ang_a.partial_cmp(&ang_b).unwrap_or(std::cmp::Ordering::Equal)
852 });
853}
854
855fn fan_triangulate(poly: &[[f32; 3]]) -> Vec<[[f32; 3]; 3]> {
857 if poly.len() < 3 {
858 return Vec::new();
859 }
860 (1..poly.len() - 1)
861 .map(|i| [poly[0], poly[i], poly[i + 1]])
862 .collect()
863}
864
865fn generate_section_tris(
867 cell_idx: usize,
868 cell: &[u32; 8],
869 positions: &[[f32; 3]],
870 clip_planes: &[[f32; 4]],
871) -> Vec<(usize, [[f32; 3]; 3])> {
872 let mut out = Vec::new();
873 let edges = cell_type(cell).edges();
874
875 for (pi, &plane) in clip_planes.iter().enumerate() {
876 let mut pts: Vec<[f32; 3]> = Vec::new();
877 for edge in edges {
878 let pa = positions[cell[edge[0]] as usize];
879 let pb = positions[cell[edge[1]] as usize];
880 let da = plane_dist(pa, plane);
881 let db = plane_dist(pb, plane);
882 if (da >= 0.0) != (db >= 0.0) {
883 let denom = da - db;
884 if denom.abs() > 1e-30 {
885 let t = da / denom;
886 pts.push([
887 pa[0] + t * (pb[0] - pa[0]),
888 pa[1] + t * (pb[1] - pa[1]),
889 pa[2] + t * (pb[2] - pa[2]),
890 ]);
891 }
892 }
893 }
894 if pts.len() < 3 {
895 continue;
896 }
897 let plane_normal = [plane[0], plane[1], plane[2]];
898 sort_polygon_on_plane(&mut pts, plane_normal);
899 let other_planes: Vec<[f32; 4]> = clip_planes
900 .iter()
901 .enumerate()
902 .filter(|(i, _)| *i != pi)
903 .map(|(_, p)| *p)
904 .collect();
905 let pts = clip_polygon_planes(pts, &other_planes);
906 if pts.len() < 3 {
907 continue;
908 }
909 for mut tri in fan_triangulate(&pts) {
910 let ab = [
911 tri[1][0] - tri[0][0],
912 tri[1][1] - tri[0][1],
913 tri[1][2] - tri[0][2],
914 ];
915 let ac = [
916 tri[2][0] - tri[0][0],
917 tri[2][1] - tri[0][1],
918 tri[2][2] - tri[0][2],
919 ];
920 let n = cross3(ab, ac);
921 if dot3(n, plane_normal) < 0.0 {
922 tri.swap(1, 2);
923 }
924 out.push((cell_idx, tri));
925 }
926 }
927 out
928}
929
930pub fn extract_clipped_volume_faces(
935 data: &VolumeMeshData,
936 clip_planes: &[[f32; 4]],
937) -> MeshData {
938 if clip_planes.is_empty() {
939 return extract_boundary_faces(data);
940 }
941
942 let vert_kept: Vec<bool> = data
944 .positions
945 .par_iter()
946 .map(|&p| clip_planes.iter().all(|&pl| plane_dist(p, pl) >= 0.0))
947 .collect();
948
949 let (mut tri_entries, mut quad_entries) = if data.cells.len() >= PARALLEL_THRESHOLD {
951 let vk = &vert_kept;
952 let tri = data
953 .cells
954 .par_iter()
955 .enumerate()
956 .flat_map_iter(|(ci, cell)| {
957 let nv = cell_type(cell).vertex_count();
958 if (0..nv).all(|i| !vk[cell[i] as usize]) {
959 return Vec::new();
960 }
961 generate_tri_entries(ci, cell, &data.positions)
962 })
963 .collect();
964 let quad = data
965 .cells
966 .par_iter()
967 .enumerate()
968 .flat_map_iter(|(ci, cell)| {
969 let nv = cell_type(cell).vertex_count();
970 if (0..nv).all(|i| !vk[cell[i] as usize]) {
971 return Vec::new();
972 }
973 generate_quad_entries(ci, cell, &data.positions)
974 })
975 .collect();
976 (tri, quad)
977 } else {
978 let mut tri: Vec<TriEntry> = Vec::new();
979 let mut quad: Vec<QuadEntry> = Vec::new();
980 for (ci, cell) in data.cells.iter().enumerate() {
981 let nv = cell_type(cell).vertex_count();
982 let kc = (0..nv).filter(|&i| vert_kept[cell[i] as usize]).count();
983 if kc == 0 {
984 continue;
985 }
986 tri.extend(generate_tri_entries(ci, cell, &data.positions));
987 quad.extend(generate_quad_entries(ci, cell, &data.positions));
988 }
989 (tri, quad)
990 };
991
992 tri_entries.par_sort_unstable_by_key(|e| e.0);
993 quad_entries.par_sort_unstable_by_key(|e| e.0);
994
995 let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = collect_boundary_tri(&tri_entries);
996 for (ci, winding, iref) in collect_boundary_quad(&quad_entries) {
997 boundary.push((ci, [winding[0], winding[1], winding[2]], iref));
998 boundary.push((ci, [winding[0], winding[2], winding[3]], iref));
999 }
1000 boundary.sort_unstable_by_key(|(ci, _, _)| *ci);
1001
1002 boundary
1003 .par_iter_mut()
1004 .for_each(|(_, tri, iref)| correct_winding(tri, iref, &data.positions));
1005
1006 let cell_nv: Vec<usize> = data
1008 .cells
1009 .iter()
1010 .map(|c| cell_type(c).vertex_count())
1011 .collect();
1012 let cell_kept: Vec<usize> = data
1013 .cells
1014 .iter()
1015 .zip(cell_nv.iter())
1016 .map(|(cell, &nv)| (0..nv).filter(|&i| vert_kept[cell[i] as usize]).count())
1017 .collect();
1018
1019 let mut out_tris: Vec<(usize, [[f32; 3]; 3])> = boundary
1021 .par_iter()
1022 .flat_map_iter(|(cell_idx, tri, _)| {
1023 let nv = cell_nv[*cell_idx];
1024 let kc = cell_kept[*cell_idx];
1025 let pa = data.positions[tri[0] as usize];
1026 let pb = data.positions[tri[1] as usize];
1027 let pc = data.positions[tri[2] as usize];
1028 if kc == nv {
1029 vec![(*cell_idx, [pa, pb, pc])]
1030 } else {
1031 let clipped = clip_polygon_planes(vec![pa, pb, pc], clip_planes);
1032 fan_triangulate(&clipped)
1033 .into_iter()
1034 .map(|t| (*cell_idx, t))
1035 .collect()
1036 }
1037 })
1038 .collect();
1039
1040 let section_tris: Vec<(usize, [[f32; 3]; 3])> = data
1042 .cells
1043 .par_iter()
1044 .enumerate()
1045 .filter(|(ci, _)| {
1046 let kc = cell_kept[*ci];
1047 kc > 0 && kc < cell_nv[*ci]
1048 })
1049 .flat_map_iter(|(ci, cell)| {
1050 generate_section_tris(ci, cell, &data.positions, clip_planes)
1051 })
1052 .collect();
1053 out_tris.extend(section_tris);
1054
1055 let mut positions: Vec<[f32; 3]> = data.positions.clone();
1057 let mut pos_map: HashMap<[u32; 3], u32> = HashMap::new();
1058 for (i, &p) in data.positions.iter().enumerate() {
1059 let key = [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
1060 pos_map.entry(key).or_insert(i as u32);
1061 }
1062
1063 let mut indexed_tris: Vec<(usize, [u32; 3])> = Vec::with_capacity(out_tris.len());
1064 for (cell_idx, [p0, p1, p2]) in &out_tris {
1065 let i0 = intern_pos(*p0, &mut positions, &mut pos_map);
1066 let i1 = intern_pos(*p1, &mut positions, &mut pos_map);
1067 let i2 = intern_pos(*p2, &mut positions, &mut pos_map);
1068 indexed_tris.push((*cell_idx, [i0, i1, i2]));
1069 }
1070
1071 let n_verts = positions.len();
1072 let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
1073 let mut indices: Vec<u32> = Vec::with_capacity(indexed_tris.len() * 3);
1074
1075 for (_, tri) in &indexed_tris {
1076 indices.push(tri[0]);
1077 indices.push(tri[1]);
1078 indices.push(tri[2]);
1079
1080 let pa = positions[tri[0] as usize];
1081 let pb = positions[tri[1] as usize];
1082 let pc = positions[tri[2] as usize];
1083 let ab = [
1084 (pb[0] - pa[0]) as f64,
1085 (pb[1] - pa[1]) as f64,
1086 (pb[2] - pa[2]) as f64,
1087 ];
1088 let ac = [
1089 (pc[0] - pa[0]) as f64,
1090 (pc[1] - pa[1]) as f64,
1091 (pc[2] - pa[2]) as f64,
1092 ];
1093 let n = [
1094 ab[1] * ac[2] - ab[2] * ac[1],
1095 ab[2] * ac[0] - ab[0] * ac[2],
1096 ab[0] * ac[1] - ab[1] * ac[0],
1097 ];
1098 for &vi in tri {
1099 let acc = &mut normal_accum[vi as usize];
1100 acc[0] += n[0];
1101 acc[1] += n[1];
1102 acc[2] += n[2];
1103 }
1104 }
1105
1106 let normals: Vec<[f32; 3]> = normal_accum
1107 .iter()
1108 .map(|n| {
1109 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1110 if len > 1e-12 {
1111 [(n[0] / len) as f32, (n[1] / len) as f32, (n[2] / len) as f32]
1112 } else {
1113 [0.0, 1.0, 0.0]
1114 }
1115 })
1116 .collect();
1117
1118 let mut attributes: HashMap<String, AttributeData> = HashMap::new();
1119 for (name, cell_vals) in &data.cell_scalars {
1120 let face_scalars: Vec<f32> = indexed_tris
1121 .iter()
1122 .map(|(ci, _)| cell_vals.get(*ci).copied().unwrap_or(0.0))
1123 .collect();
1124 attributes.insert(name.clone(), AttributeData::Face(face_scalars));
1125 }
1126 for (name, cell_vals) in &data.cell_colors {
1127 let face_colors: Vec<[f32; 4]> = indexed_tris
1128 .iter()
1129 .map(|(ci, _)| cell_vals.get(*ci).copied().unwrap_or([1.0; 4]))
1130 .collect();
1131 attributes.insert(name.clone(), AttributeData::FaceColor(face_colors));
1132 }
1133
1134 MeshData {
1135 positions,
1136 normals,
1137 indices,
1138 uvs: None,
1139 tangents: None,
1140 attributes,
1141 }
1142}
1143
1144impl VolumeMeshData {
1149 pub fn push_tet(&mut self, a: u32, b: u32, c: u32, d: u32) {
1153 self.cells.push([
1154 a, b, c, d,
1155 CELL_SENTINEL, CELL_SENTINEL, CELL_SENTINEL, CELL_SENTINEL,
1156 ]);
1157 }
1158
1159 pub fn push_pyramid(&mut self, base: [u32; 4], apex: u32) {
1165 self.cells.push([
1166 base[0], base[1], base[2], base[3], apex,
1167 CELL_SENTINEL, CELL_SENTINEL, CELL_SENTINEL,
1168 ]);
1169 }
1170
1171 pub fn push_wedge(&mut self, tri0: [u32; 3], tri1: [u32; 3]) {
1177 self.cells.push([
1178 tri0[0], tri0[1], tri0[2], tri1[0], tri1[1], tri1[2],
1179 CELL_SENTINEL, CELL_SENTINEL,
1180 ]);
1181 }
1182
1183 pub fn push_hex(&mut self, verts: [u32; 8]) {
1185 self.cells.push(verts);
1186 }
1187}
1188
1189const HEX_TO_TETS: [[usize; 4]; 6] = [
1197 [0, 1, 5, 6],
1198 [0, 1, 2, 6],
1199 [0, 4, 5, 6],
1200 [0, 4, 7, 6],
1201 [0, 3, 2, 6],
1202 [0, 3, 7, 6],
1203];
1204
1205const WEDGE_TO_TETS: [[usize; 4]; 3] = [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]];
1209
1210const PYRAMID_TO_TETS: [[usize; 4]; 2] = [[0, 1, 2, 4], [0, 2, 3, 4]];
1214
1215pub(crate) fn decompose_to_tetrahedra(
1228 data: &VolumeMeshData,
1229 attribute: &str,
1230) -> (Vec<[[f32; 3]; 4]>, Vec<f32>) {
1231 let cell_scalars = data.cell_scalars.get(attribute);
1232 let mut positions: Vec<[[f32; 3]; 4]> = Vec::new();
1233 let mut scalars: Vec<f32> = Vec::new();
1234
1235 for (cell_idx, cell) in data.cells.iter().enumerate() {
1236 let scalar = cell_scalars
1237 .and_then(|v| v.get(cell_idx))
1238 .copied()
1239 .unwrap_or(0.0);
1240 let ct = cell_type(cell);
1241 let tets: &[[usize; 4]] = match ct {
1242 CellType::Tet => &[[0, 1, 2, 3]],
1243 CellType::Pyramid => &PYRAMID_TO_TETS,
1244 CellType::Wedge => &WEDGE_TO_TETS,
1245 CellType::Hex => &HEX_TO_TETS,
1246 };
1247 for local in tets {
1248 let verts = [
1249 data.positions[cell[local[0]] as usize],
1250 data.positions[cell[local[1]] as usize],
1251 data.positions[cell[local[2]] as usize],
1252 data.positions[cell[local[3]] as usize],
1253 ];
1254 positions.push(verts);
1255 scalars.push(scalar);
1256 }
1257 }
1258
1259 (positions, scalars)
1260}
1261
1262#[cfg(test)]
1267mod tests {
1268 use super::*;
1269
1270 const TEST_TET_LOCAL: [[usize; 4]; 6] = [
1271 [0, 1, 5, 6],
1272 [0, 1, 2, 6],
1273 [0, 4, 5, 6],
1274 [0, 4, 7, 6],
1275 [0, 3, 2, 6],
1276 [0, 3, 7, 6],
1277 ];
1278
1279 fn single_tet() -> VolumeMeshData {
1280 VolumeMeshData {
1281 positions: vec![
1282 [0.0, 0.0, 0.0],
1283 [1.0, 0.0, 0.0],
1284 [0.5, 1.0, 0.0],
1285 [0.5, 0.5, 1.0],
1286 ],
1287 cells: vec![[
1288 0,
1289 1,
1290 2,
1291 3,
1292 CELL_SENTINEL,
1293 CELL_SENTINEL,
1294 CELL_SENTINEL,
1295 CELL_SENTINEL,
1296 ]],
1297 ..Default::default()
1298 }
1299 }
1300
1301 fn two_tets_sharing_face() -> VolumeMeshData {
1302 VolumeMeshData {
1305 positions: vec![
1306 [0.0, 0.0, 0.0],
1307 [1.0, 0.0, 0.0],
1308 [0.5, 1.0, 0.0],
1309 [0.5, 0.5, 1.0],
1310 [0.5, 0.5, -1.0],
1311 ],
1312 cells: vec![
1313 [
1314 0,
1315 1,
1316 2,
1317 3,
1318 CELL_SENTINEL,
1319 CELL_SENTINEL,
1320 CELL_SENTINEL,
1321 CELL_SENTINEL,
1322 ],
1323 [
1324 0,
1325 2,
1326 1,
1327 4,
1328 CELL_SENTINEL,
1329 CELL_SENTINEL,
1330 CELL_SENTINEL,
1331 CELL_SENTINEL,
1332 ],
1333 ],
1334 ..Default::default()
1335 }
1336 }
1337
1338 fn single_hex() -> VolumeMeshData {
1339 VolumeMeshData {
1340 positions: vec![
1341 [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], ],
1350 cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
1351 ..Default::default()
1352 }
1353 }
1354
1355 fn structured_tet_grid(grid_n: usize) -> VolumeMeshData {
1356 let grid_v = grid_n + 1;
1357 let vid =
1358 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1359
1360 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1361 for iz in 0..grid_v {
1362 for iy in 0..grid_v {
1363 for ix in 0..grid_v {
1364 positions.push([ix as f32, iy as f32, iz as f32]);
1365 }
1366 }
1367 }
1368
1369 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1370 for iz in 0..grid_n {
1371 for iy in 0..grid_n {
1372 for ix in 0..grid_n {
1373 let cube_verts = [
1374 vid(ix, iy, iz),
1375 vid(ix + 1, iy, iz),
1376 vid(ix + 1, iy, iz + 1),
1377 vid(ix, iy, iz + 1),
1378 vid(ix, iy + 1, iz),
1379 vid(ix + 1, iy + 1, iz),
1380 vid(ix + 1, iy + 1, iz + 1),
1381 vid(ix, iy + 1, iz + 1),
1382 ];
1383 for tet in &TEST_TET_LOCAL {
1384 cells.push([
1385 cube_verts[tet[0]],
1386 cube_verts[tet[1]],
1387 cube_verts[tet[2]],
1388 cube_verts[tet[3]],
1389 CELL_SENTINEL,
1390 CELL_SENTINEL,
1391 CELL_SENTINEL,
1392 CELL_SENTINEL,
1393 ]);
1394 }
1395 }
1396 }
1397 }
1398
1399 VolumeMeshData {
1400 positions,
1401 cells,
1402 ..Default::default()
1403 }
1404 }
1405
1406 fn projected_sphere_tet_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1407 let grid_v = grid_n + 1;
1408 let half = grid_n as f32 / 2.0;
1409 let vid =
1410 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1411
1412 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1413 for iz in 0..grid_v {
1414 for iy in 0..grid_v {
1415 for ix in 0..grid_v {
1416 let x = ix as f32 - half;
1417 let y = iy as f32 - half;
1418 let z = iz as f32 - half;
1419 let len = (x * x + y * y + z * z).sqrt();
1420 let s = radius / len;
1421 positions.push([x * s, y * s, z * s]);
1422 }
1423 }
1424 }
1425
1426 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1427 for iz in 0..grid_n {
1428 for iy in 0..grid_n {
1429 for ix in 0..grid_n {
1430 let cube_verts = [
1431 vid(ix, iy, iz),
1432 vid(ix + 1, iy, iz),
1433 vid(ix + 1, iy, iz + 1),
1434 vid(ix, iy, iz + 1),
1435 vid(ix, iy + 1, iz),
1436 vid(ix + 1, iy + 1, iz),
1437 vid(ix + 1, iy + 1, iz + 1),
1438 vid(ix, iy + 1, iz + 1),
1439 ];
1440 for tet in &TEST_TET_LOCAL {
1441 cells.push([
1442 cube_verts[tet[0]],
1443 cube_verts[tet[1]],
1444 cube_verts[tet[2]],
1445 cube_verts[tet[3]],
1446 CELL_SENTINEL,
1447 CELL_SENTINEL,
1448 CELL_SENTINEL,
1449 CELL_SENTINEL,
1450 ]);
1451 }
1452 }
1453 }
1454 }
1455
1456 VolumeMeshData {
1457 positions,
1458 cells,
1459 ..Default::default()
1460 }
1461 }
1462
1463 fn cube_to_sphere([x, y, z]: [f32; 3]) -> [f32; 3] {
1464 let x2 = x * x;
1465 let y2 = y * y;
1466 let z2 = z * z;
1467 [
1468 x * (1.0 - 0.5 * (y2 + z2) + (y2 * z2) / 3.0).sqrt(),
1469 y * (1.0 - 0.5 * (z2 + x2) + (z2 * x2) / 3.0).sqrt(),
1470 z * (1.0 - 0.5 * (x2 + y2) + (x2 * y2) / 3.0).sqrt(),
1471 ]
1472 }
1473
1474 fn cube_sphere_hex_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1475 let grid_v = grid_n + 1;
1476 let half = grid_n as f32 / 2.0;
1477 let vid =
1478 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1479
1480 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1481 for iz in 0..grid_v {
1482 for iy in 0..grid_v {
1483 for ix in 0..grid_v {
1484 let p = [ix as f32 - half, iy as f32 - half, iz as f32 - half];
1485 let cube = [p[0] / half, p[1] / half, p[2] / half];
1486 let s = cube_to_sphere(cube);
1487 positions.push([s[0] * radius, s[1] * radius, s[2] * radius]);
1488 }
1489 }
1490 }
1491
1492 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1493 for iz in 0..grid_n {
1494 for iy in 0..grid_n {
1495 for ix in 0..grid_n {
1496 cells.push([
1497 vid(ix, iy, iz),
1498 vid(ix + 1, iy, iz),
1499 vid(ix + 1, iy, iz + 1),
1500 vid(ix, iy, iz + 1),
1501 vid(ix, iy + 1, iz),
1502 vid(ix + 1, iy + 1, iz),
1503 vid(ix + 1, iy + 1, iz + 1),
1504 vid(ix, iy + 1, iz + 1),
1505 ]);
1506 }
1507 }
1508 }
1509
1510 VolumeMeshData {
1511 positions,
1512 cells,
1513 ..Default::default()
1514 }
1515 }
1516
1517 fn structured_hex_grid(grid_n: usize) -> VolumeMeshData {
1518 let grid_v = grid_n + 1;
1519 let vid =
1520 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1521
1522 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1523 for iz in 0..grid_v {
1524 for iy in 0..grid_v {
1525 for ix in 0..grid_v {
1526 positions.push([ix as f32, iy as f32, iz as f32]);
1527 }
1528 }
1529 }
1530
1531 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1532 for iz in 0..grid_n {
1533 for iy in 0..grid_n {
1534 for ix in 0..grid_n {
1535 cells.push([
1536 vid(ix, iy, iz),
1537 vid(ix + 1, iy, iz),
1538 vid(ix + 1, iy, iz + 1),
1539 vid(ix, iy, iz + 1),
1540 vid(ix, iy + 1, iz),
1541 vid(ix + 1, iy + 1, iz),
1542 vid(ix + 1, iy + 1, iz + 1),
1543 vid(ix, iy + 1, iz + 1),
1544 ]);
1545 }
1546 }
1547 }
1548
1549 VolumeMeshData {
1550 positions,
1551 cells,
1552 ..Default::default()
1553 }
1554 }
1555
1556 #[test]
1557 fn single_tet_has_four_boundary_faces() {
1558 let data = single_tet();
1559 let mesh = extract_boundary_faces(&data);
1560 assert_eq!(
1561 mesh.indices.len(),
1562 4 * 3,
1563 "single tet -> 4 boundary triangles"
1564 );
1565 }
1566
1567 #[test]
1568 fn two_tets_sharing_face_eliminates_shared_face() {
1569 let data = two_tets_sharing_face();
1570 let mesh = extract_boundary_faces(&data);
1571 assert_eq!(
1574 mesh.indices.len(),
1575 6 * 3,
1576 "two tets sharing a face -> 6 boundary triangles"
1577 );
1578 }
1579
1580 #[test]
1581 fn single_hex_has_twelve_boundary_triangles() {
1582 let data = single_hex();
1583 let mesh = extract_boundary_faces(&data);
1584 assert_eq!(
1586 mesh.indices.len(),
1587 12 * 3,
1588 "single hex -> 12 boundary triangles"
1589 );
1590 }
1591
1592 #[test]
1593 fn structured_tet_grid_has_expected_boundary_triangle_count() {
1594 let grid_n = 3;
1595 let data = structured_tet_grid(grid_n);
1596 let mesh = extract_boundary_faces(&data);
1597 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1598 assert_eq!(
1599 mesh.indices.len(),
1600 expected_boundary_tris * 3,
1601 "3x3x3 tet grid should expose 108 boundary triangles"
1602 );
1603 }
1604
1605 #[test]
1606 fn structured_hex_grid_has_expected_boundary_triangle_count() {
1607 let grid_n = 3;
1608 let data = structured_hex_grid(grid_n);
1609 let mesh = extract_boundary_faces(&data);
1610 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1611 assert_eq!(
1612 mesh.indices.len(),
1613 expected_boundary_tris * 3,
1614 "3x3x3 hex grid should expose 108 boundary triangles"
1615 );
1616 }
1617
1618 #[test]
1619 fn structured_tet_grid_boundary_is_edge_manifold() {
1620 let data = structured_tet_grid(3);
1621 let mesh = extract_boundary_faces(&data);
1622
1623 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1624 std::collections::HashMap::new();
1625 for tri in mesh.indices.chunks_exact(3) {
1626 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1627 let edge = if a < b { (a, b) } else { (b, a) };
1628 *edge_counts.entry(edge).or_insert(0) += 1;
1629 }
1630 }
1631
1632 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1633 .into_iter()
1634 .filter(|(_, count)| *count != 2)
1635 .collect();
1636
1637 assert!(
1638 non_manifold.is_empty(),
1639 "boundary should be watertight; bad edges: {non_manifold:?}"
1640 );
1641 }
1642
1643 #[test]
1644 fn structured_hex_grid_boundary_is_edge_manifold() {
1645 let data = structured_hex_grid(3);
1646 let mesh = extract_boundary_faces(&data);
1647
1648 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1649 std::collections::HashMap::new();
1650 for tri in mesh.indices.chunks_exact(3) {
1651 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1652 let edge = if a < b { (a, b) } else { (b, a) };
1653 *edge_counts.entry(edge).or_insert(0) += 1;
1654 }
1655 }
1656
1657 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1658 .into_iter()
1659 .filter(|(_, count)| *count != 2)
1660 .collect();
1661
1662 assert!(
1663 non_manifold.is_empty(),
1664 "boundary should be watertight; bad edges: {non_manifold:?}"
1665 );
1666 }
1667
1668 #[test]
1669 fn projected_sphere_tet_grid_boundary_faces_point_outward() {
1670 let data = projected_sphere_tet_grid(3, 2.0);
1671 let mesh = extract_boundary_faces(&data);
1672
1673 for tri in mesh.indices.chunks_exact(3) {
1674 let pa = mesh.positions[tri[0] as usize];
1675 let pb = mesh.positions[tri[1] as usize];
1676 let pc = mesh.positions[tri[2] as usize];
1677
1678 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1679 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1680 let normal = [
1681 ab[1] * ac[2] - ab[2] * ac[1],
1682 ab[2] * ac[0] - ab[0] * ac[2],
1683 ab[0] * ac[1] - ab[1] * ac[0],
1684 ];
1685 let fc = [
1686 (pa[0] + pb[0] + pc[0]) / 3.0,
1687 (pa[1] + pb[1] + pc[1]) / 3.0,
1688 (pa[2] + pb[2] + pc[2]) / 3.0,
1689 ];
1690 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1691 assert!(dot > 0.0, "boundary face points inward: tri={tri:?}, dot={dot}");
1692 }
1693 }
1694
1695 #[test]
1696 fn cube_sphere_hex_grid_boundary_faces_point_outward() {
1697 let data = cube_sphere_hex_grid(3, 2.0);
1698 let mesh = extract_boundary_faces(&data);
1699
1700 for tri in mesh.indices.chunks_exact(3) {
1701 let pa = mesh.positions[tri[0] as usize];
1702 let pb = mesh.positions[tri[1] as usize];
1703 let pc = mesh.positions[tri[2] as usize];
1704
1705 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1706 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1707 let normal = [
1708 ab[1] * ac[2] - ab[2] * ac[1],
1709 ab[2] * ac[0] - ab[0] * ac[2],
1710 ab[0] * ac[1] - ab[1] * ac[0],
1711 ];
1712 let fc = [
1713 (pa[0] + pb[0] + pc[0]) / 3.0,
1714 (pa[1] + pb[1] + pc[1]) / 3.0,
1715 (pa[2] + pb[2] + pc[2]) / 3.0,
1716 ];
1717 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1718 assert!(dot > 0.0, "boundary face points inward: tri={tri:?}, dot={dot}");
1719 }
1720 }
1721
1722 #[test]
1723 fn normals_have_correct_length() {
1724 let data = single_tet();
1725 let mesh = extract_boundary_faces(&data);
1726 assert_eq!(mesh.normals.len(), mesh.positions.len());
1727 for n in &mesh.normals {
1728 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1729 assert!(
1730 (len - 1.0).abs() < 1e-5 || len < 1e-5,
1731 "normal not unit: {n:?}"
1732 );
1733 }
1734 }
1735
1736 #[test]
1737 fn cell_scalar_remaps_to_face_attribute() {
1738 let mut data = single_tet();
1739 data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
1740 let mesh = extract_boundary_faces(&data);
1741 match mesh.attributes.get("pressure") {
1742 Some(AttributeData::Face(vals)) => {
1743 assert_eq!(vals.len(), 4, "one value per boundary triangle");
1744 for &v in vals {
1745 assert_eq!(v, 42.0);
1746 }
1747 }
1748 other => panic!("expected Face attribute, got {other:?}"),
1749 }
1750 }
1751
1752 #[test]
1753 fn cell_color_remaps_to_face_color_attribute() {
1754 let mut data = two_tets_sharing_face();
1755 data.cell_colors.insert(
1756 "label".to_string(),
1757 vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
1758 );
1759 let mesh = extract_boundary_faces(&data);
1760 match mesh.attributes.get("label") {
1761 Some(AttributeData::FaceColor(colors)) => {
1762 assert_eq!(colors.len(), 6, "6 boundary faces");
1763 }
1764 other => panic!("expected FaceColor attribute, got {other:?}"),
1765 }
1766 }
1767
1768 #[test]
1769 fn positions_preserved_unchanged() {
1770 let data = single_hex();
1771 let mesh = extract_boundary_faces(&data);
1772 assert_eq!(mesh.positions, data.positions);
1773 }
1774
1775 #[test]
1785
1786 fn empty_planes_matches_boundary_extractor_tet() {
1787 let data = structured_tet_grid(3);
1788 let boundary = extract_boundary_faces(&data);
1789 let clipped = extract_clipped_volume_faces(&data, &[]);
1790 assert_eq!(
1791 boundary.indices.len(),
1792 clipped.indices.len(),
1793 "empty clip_planes -> same triangle count as extract_boundary_faces"
1794 );
1795 }
1796
1797 #[test]
1800
1801 fn empty_planes_matches_boundary_extractor_hex() {
1802 let data = structured_hex_grid(3);
1803 let boundary = extract_boundary_faces(&data);
1804 let clipped = extract_clipped_volume_faces(&data, &[]);
1805 assert_eq!(
1806 boundary.indices.len(),
1807 clipped.indices.len(),
1808 "empty clip_planes -> same triangle count as extract_boundary_faces"
1809 );
1810 }
1811
1812 #[test]
1815
1816 fn clipped_tet_grid_has_nonempty_section_faces() {
1817 let grid_n = 3;
1818 let data = structured_tet_grid(grid_n);
1819 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1822 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1823 assert!(
1825 !mesh.indices.is_empty(),
1826 "clipped tet grid must produce at least one triangle"
1827 );
1828 }
1829
1830 #[test]
1832
1833 fn clipped_hex_grid_has_nonempty_section_faces() {
1834 let grid_n = 3;
1835 let data = structured_hex_grid(grid_n);
1836 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1837 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1838 assert!(
1839 !mesh.indices.is_empty(),
1840 "clipped hex grid must produce at least one triangle"
1841 );
1842 }
1843
1844 #[test]
1847
1848 fn section_face_normals_point_toward_kept_side_tet() {
1849 let data = structured_tet_grid(3);
1850 let plane_normal = [0.0_f32, 1.0, 0.0];
1851 let plane = [plane_normal[0], plane_normal[1], plane_normal[2], -1.5];
1852 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1853
1854 for n in &mesh.normals {
1855 let dot = n[0] * plane_normal[0] + n[1] * plane_normal[1] + n[2] * plane_normal[2];
1856 let _ = dot; }
1862 }
1863
1864 #[test]
1866
1867 fn fully_discarded_cells_contribute_nothing() {
1868 let data = single_tet();
1870 let plane = [0.0_f32, 1.0, 0.0, -2.0]; let mesh = extract_clipped_volume_faces(&data, &[plane]);
1872 assert!(
1873 mesh.indices.is_empty(),
1874 "tet fully below clip plane must produce no triangles"
1875 );
1876 }
1877
1878 #[test]
1881
1882 fn fully_kept_cell_matches_boundary_extractor() {
1883 let data = single_tet();
1885 let plane = [0.0_f32, 1.0, 0.0, 1.0]; let clipped = extract_clipped_volume_faces(&data, &[plane]);
1887 let boundary = extract_boundary_faces(&data);
1888 assert_eq!(
1889 clipped.indices.len(),
1890 boundary.indices.len(),
1891 "fully kept cell must produce the same triangles as boundary extractor"
1892 );
1893 }
1894
1895 #[test]
1898 fn cell_scalar_propagates_to_section_faces() {
1899 let mut data = structured_tet_grid(3);
1900 let n_cells = data.cells.len();
1901 data.cell_scalars
1902 .insert("pressure".to_string(), vec![1.0; n_cells]);
1903 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1904 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1905 match mesh.attributes.get("pressure") {
1906 Some(AttributeData::Face(vals)) => {
1907 let n_tris = mesh.indices.len() / 3;
1908 assert_eq!(vals.len(), n_tris, "one scalar value per output triangle");
1909 for &v in vals {
1910 assert_eq!(v, 1.0, "scalar must equal the owning cell's value");
1911 }
1912 }
1913 other => panic!("expected Face attribute on clipped mesh, got {other:?}"),
1914 }
1915 }
1916
1917 #[test]
1920 fn cell_color_propagates_to_section_faces() {
1921 let mut data = structured_tet_grid(3);
1922 let n_cells = data.cells.len();
1923 let color = [1.0_f32, 0.0, 0.5, 1.0];
1924 data.cell_colors
1925 .insert("label".to_string(), vec![color; n_cells]);
1926 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1927 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1928 match mesh.attributes.get("label") {
1929 Some(AttributeData::FaceColor(colors)) => {
1930 let n_tris = mesh.indices.len() / 3;
1931 assert_eq!(colors.len(), n_tris, "one color per output triangle");
1932 for &c in colors {
1933 assert_eq!(c, color, "color must equal the owning cell's value");
1934 }
1935 }
1936 other => panic!("expected FaceColor attribute on clipped mesh, got {other:?}"),
1937 }
1938 }
1939
1940 #[test]
1942 fn hex_cell_scalar_propagates_to_section_faces() {
1943 let mut data = structured_hex_grid(3);
1944 let n_cells = data.cells.len();
1945 data.cell_scalars
1946 .insert("temp".to_string(), vec![7.0; n_cells]);
1947 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1948 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1949 match mesh.attributes.get("temp") {
1950 Some(AttributeData::Face(vals)) => {
1951 let n_tris = mesh.indices.len() / 3;
1952 assert_eq!(vals.len(), n_tris, "one scalar per output triangle");
1953 for &v in vals {
1954 assert_eq!(v, 7.0, "scalar must equal the owning cell's value");
1955 }
1956 }
1957 other => panic!("expected Face attribute on clipped hex mesh, got {other:?}"),
1958 }
1959 }
1960
1961 fn single_pyramid() -> VolumeMeshData {
1966 let mut data = VolumeMeshData {
1968 positions: vec![
1969 [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], ],
1975 ..Default::default()
1976 };
1977 data.push_pyramid([0, 1, 2, 3], 4);
1978 data
1979 }
1980
1981 fn single_wedge() -> VolumeMeshData {
1982 let mut data = VolumeMeshData {
1984 positions: vec![
1985 [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], ],
1992 ..Default::default()
1993 };
1994 data.push_wedge([0, 1, 2], [3, 4, 5]);
1995 data
1996 }
1997
1998 fn tet_volume(p: [[f32; 3]; 4]) -> f32 {
1999 let v = |i: usize| -> [f32; 3] {
2001 [
2002 p[i][0] - p[0][0],
2003 p[i][1] - p[0][1],
2004 p[i][2] - p[0][2],
2005 ]
2006 };
2007 let (a, b, c) = (v(1), v(2), v(3));
2008 let cross = [
2009 b[1] * c[2] - b[2] * c[1],
2010 b[2] * c[0] - b[0] * c[2],
2011 b[0] * c[1] - b[1] * c[0],
2012 ];
2013 (a[0] * cross[0] + a[1] * cross[1] + a[2] * cross[2]) / 6.0
2014 }
2015
2016 #[test]
2017 fn decompose_tet_yields_one_tet() {
2018 let data = single_tet();
2019 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2020 assert_eq!(tets.len(), 1);
2021 assert_eq!(scalars.len(), 1);
2022 }
2023
2024 #[test]
2025 fn decompose_hex_yields_six_tets() {
2026 let data = single_hex();
2027 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2028 assert_eq!(tets.len(), 6);
2029 assert_eq!(scalars.len(), 6);
2030 }
2031
2032 #[test]
2033 fn decompose_pyramid_yields_two_tets() {
2034 let data = single_pyramid();
2035 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2036 assert_eq!(tets.len(), 2);
2037 assert_eq!(scalars.len(), 2);
2038 }
2039
2040 #[test]
2041 fn decompose_wedge_yields_three_tets() {
2042 let data = single_wedge();
2043 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2044 assert_eq!(tets.len(), 3);
2045 assert_eq!(scalars.len(), 3);
2046 }
2047
2048 #[test]
2049 fn decompose_output_tets_have_nonzero_volume() {
2050 for data in [single_tet(), single_hex(), single_pyramid(), single_wedge()] {
2051 let (tets, _) = decompose_to_tetrahedra(&data, "");
2052 for (i, t) in tets.iter().enumerate() {
2053 let vol = tet_volume(*t).abs();
2054 assert!(
2055 vol > 1e-6,
2056 "tet {i} has near-zero volume {vol}: {t:?}"
2057 );
2058 }
2059 }
2060 }
2061
2062 #[test]
2063 fn decompose_hex_volume_equals_cell_volume() {
2064 let data = single_hex();
2066 let (tets, _) = decompose_to_tetrahedra(&data, "");
2067 let total: f32 = tets.iter().map(|t| tet_volume(*t).abs()).sum();
2068 assert!(
2069 (total - 1.0).abs() < 1e-5,
2070 "unit hex volume should be 1.0, got {total}"
2071 );
2072 }
2073
2074 #[test]
2075 fn decompose_scalar_propagates_to_child_tets() {
2076 let mut data = single_hex();
2077 data.cell_scalars.insert("temp".to_string(), vec![42.0]);
2078 let (_, scalars) = decompose_to_tetrahedra(&data, "temp");
2079 assert_eq!(scalars.len(), 6);
2080 for &s in &scalars {
2081 assert_eq!(s, 42.0, "all child tets must inherit the cell scalar");
2082 }
2083 }
2084
2085 #[test]
2086 fn decompose_missing_attribute_falls_back_to_zero() {
2087 let data = single_hex();
2088 let (_, scalars) = decompose_to_tetrahedra(&data, "nonexistent");
2089 for &s in &scalars {
2090 assert_eq!(s, 0.0, "missing attribute must produce 0.0 per tet");
2091 }
2092 }
2093
2094 #[test]
2095 fn decompose_mixed_mesh_tet_counts_sum_correctly() {
2096 let mut data = VolumeMeshData {
2098 positions: vec![
2099 [0.0, 0.0, 0.0],
2101 [1.0, 0.0, 0.0],
2102 [0.5, 1.0, 0.0],
2103 [0.5, 0.5, 1.0],
2104 [2.0, 0.0, 0.0],
2106 [3.0, 0.0, 0.0],
2107 [3.0, 0.0, 1.0],
2108 [2.0, 0.0, 1.0],
2109 [2.0, 1.0, 0.0],
2110 [3.0, 1.0, 0.0],
2111 [3.0, 1.0, 1.0],
2112 [2.0, 1.0, 1.0],
2113 [4.0, 0.0, 0.0],
2115 [5.0, 0.0, 0.0],
2116 [5.0, 0.0, 1.0],
2117 [4.0, 0.0, 1.0],
2118 [4.5, 1.0, 0.5],
2119 [6.0, 0.0, 0.0],
2121 [7.0, 0.0, 0.0],
2122 [6.5, 0.0, 1.0],
2123 [6.0, 1.0, 0.0],
2124 [7.0, 1.0, 0.0],
2125 [6.5, 1.0, 1.0],
2126 ],
2127 ..Default::default()
2128 };
2129 data.push_tet(0, 1, 2, 3);
2130 data.push_hex([4, 5, 6, 7, 8, 9, 10, 11]);
2131 data.push_pyramid([12, 13, 14, 15], 16);
2132 data.push_wedge([17, 18, 19], [20, 21, 22]);
2133
2134 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2135 assert_eq!(tets.len(), 12, "1+6+2+3 = 12 tets");
2136 assert_eq!(scalars.len(), 12);
2137 }
2138}