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 for_each_tet<F>(data: &VolumeMeshData, attribute: &str, mut f: F)
1227where
1228 F: FnMut([[f32; 3]; 4], f32),
1229{
1230 let cell_scalars = data.cell_scalars.get(attribute);
1231 for (cell_idx, cell) in data.cells.iter().enumerate() {
1232 let scalar = cell_scalars
1233 .and_then(|v| v.get(cell_idx))
1234 .copied()
1235 .unwrap_or(0.0);
1236 let tets: &[[usize; 4]] = match cell_type(cell) {
1237 CellType::Tet => &[[0, 1, 2, 3]],
1238 CellType::Pyramid => &PYRAMID_TO_TETS,
1239 CellType::Wedge => &WEDGE_TO_TETS,
1240 CellType::Hex => &HEX_TO_TETS,
1241 };
1242 for local in tets {
1243 let verts = [
1244 data.positions[cell[local[0]] as usize],
1245 data.positions[cell[local[1]] as usize],
1246 data.positions[cell[local[2]] as usize],
1247 data.positions[cell[local[3]] as usize],
1248 ];
1249 f(verts, scalar);
1250 }
1251 }
1252}
1253
1254#[cfg(test)]
1264pub(crate) fn decompose_to_tetrahedra(
1265 data: &VolumeMeshData,
1266 attribute: &str,
1267) -> (Vec<[[f32; 3]; 4]>, Vec<f32>) {
1268 let mut positions: Vec<[[f32; 3]; 4]> = Vec::new();
1269 let mut scalars: Vec<f32> = Vec::new();
1270 for_each_tet(data, attribute, |verts, scalar| {
1271 positions.push(verts);
1272 scalars.push(scalar);
1273 });
1274 (positions, scalars)
1275}
1276
1277#[cfg(test)]
1282mod tests {
1283 use super::*;
1284
1285 const TEST_TET_LOCAL: [[usize; 4]; 6] = [
1286 [0, 1, 5, 6],
1287 [0, 1, 2, 6],
1288 [0, 4, 5, 6],
1289 [0, 4, 7, 6],
1290 [0, 3, 2, 6],
1291 [0, 3, 7, 6],
1292 ];
1293
1294 fn single_tet() -> VolumeMeshData {
1295 VolumeMeshData {
1296 positions: vec![
1297 [0.0, 0.0, 0.0],
1298 [1.0, 0.0, 0.0],
1299 [0.5, 1.0, 0.0],
1300 [0.5, 0.5, 1.0],
1301 ],
1302 cells: vec![[
1303 0,
1304 1,
1305 2,
1306 3,
1307 CELL_SENTINEL,
1308 CELL_SENTINEL,
1309 CELL_SENTINEL,
1310 CELL_SENTINEL,
1311 ]],
1312 ..Default::default()
1313 }
1314 }
1315
1316 fn two_tets_sharing_face() -> VolumeMeshData {
1317 VolumeMeshData {
1320 positions: vec![
1321 [0.0, 0.0, 0.0],
1322 [1.0, 0.0, 0.0],
1323 [0.5, 1.0, 0.0],
1324 [0.5, 0.5, 1.0],
1325 [0.5, 0.5, -1.0],
1326 ],
1327 cells: vec![
1328 [
1329 0,
1330 1,
1331 2,
1332 3,
1333 CELL_SENTINEL,
1334 CELL_SENTINEL,
1335 CELL_SENTINEL,
1336 CELL_SENTINEL,
1337 ],
1338 [
1339 0,
1340 2,
1341 1,
1342 4,
1343 CELL_SENTINEL,
1344 CELL_SENTINEL,
1345 CELL_SENTINEL,
1346 CELL_SENTINEL,
1347 ],
1348 ],
1349 ..Default::default()
1350 }
1351 }
1352
1353 fn single_hex() -> VolumeMeshData {
1354 VolumeMeshData {
1355 positions: vec![
1356 [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], ],
1365 cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
1366 ..Default::default()
1367 }
1368 }
1369
1370 fn structured_tet_grid(grid_n: usize) -> VolumeMeshData {
1371 let grid_v = grid_n + 1;
1372 let vid =
1373 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1374
1375 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1376 for iz in 0..grid_v {
1377 for iy in 0..grid_v {
1378 for ix in 0..grid_v {
1379 positions.push([ix as f32, iy as f32, iz as f32]);
1380 }
1381 }
1382 }
1383
1384 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1385 for iz in 0..grid_n {
1386 for iy in 0..grid_n {
1387 for ix in 0..grid_n {
1388 let cube_verts = [
1389 vid(ix, iy, iz),
1390 vid(ix + 1, iy, iz),
1391 vid(ix + 1, iy, iz + 1),
1392 vid(ix, iy, iz + 1),
1393 vid(ix, iy + 1, iz),
1394 vid(ix + 1, iy + 1, iz),
1395 vid(ix + 1, iy + 1, iz + 1),
1396 vid(ix, iy + 1, iz + 1),
1397 ];
1398 for tet in &TEST_TET_LOCAL {
1399 cells.push([
1400 cube_verts[tet[0]],
1401 cube_verts[tet[1]],
1402 cube_verts[tet[2]],
1403 cube_verts[tet[3]],
1404 CELL_SENTINEL,
1405 CELL_SENTINEL,
1406 CELL_SENTINEL,
1407 CELL_SENTINEL,
1408 ]);
1409 }
1410 }
1411 }
1412 }
1413
1414 VolumeMeshData {
1415 positions,
1416 cells,
1417 ..Default::default()
1418 }
1419 }
1420
1421 fn projected_sphere_tet_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1422 let grid_v = grid_n + 1;
1423 let half = grid_n as f32 / 2.0;
1424 let vid =
1425 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1426
1427 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1428 for iz in 0..grid_v {
1429 for iy in 0..grid_v {
1430 for ix in 0..grid_v {
1431 let x = ix as f32 - half;
1432 let y = iy as f32 - half;
1433 let z = iz as f32 - half;
1434 let len = (x * x + y * y + z * z).sqrt();
1435 let s = radius / len;
1436 positions.push([x * s, y * s, z * s]);
1437 }
1438 }
1439 }
1440
1441 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1442 for iz in 0..grid_n {
1443 for iy in 0..grid_n {
1444 for ix in 0..grid_n {
1445 let cube_verts = [
1446 vid(ix, iy, iz),
1447 vid(ix + 1, iy, iz),
1448 vid(ix + 1, iy, iz + 1),
1449 vid(ix, iy, iz + 1),
1450 vid(ix, iy + 1, iz),
1451 vid(ix + 1, iy + 1, iz),
1452 vid(ix + 1, iy + 1, iz + 1),
1453 vid(ix, iy + 1, iz + 1),
1454 ];
1455 for tet in &TEST_TET_LOCAL {
1456 cells.push([
1457 cube_verts[tet[0]],
1458 cube_verts[tet[1]],
1459 cube_verts[tet[2]],
1460 cube_verts[tet[3]],
1461 CELL_SENTINEL,
1462 CELL_SENTINEL,
1463 CELL_SENTINEL,
1464 CELL_SENTINEL,
1465 ]);
1466 }
1467 }
1468 }
1469 }
1470
1471 VolumeMeshData {
1472 positions,
1473 cells,
1474 ..Default::default()
1475 }
1476 }
1477
1478 fn cube_to_sphere([x, y, z]: [f32; 3]) -> [f32; 3] {
1479 let x2 = x * x;
1480 let y2 = y * y;
1481 let z2 = z * z;
1482 [
1483 x * (1.0 - 0.5 * (y2 + z2) + (y2 * z2) / 3.0).sqrt(),
1484 y * (1.0 - 0.5 * (z2 + x2) + (z2 * x2) / 3.0).sqrt(),
1485 z * (1.0 - 0.5 * (x2 + y2) + (x2 * y2) / 3.0).sqrt(),
1486 ]
1487 }
1488
1489 fn cube_sphere_hex_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1490 let grid_v = grid_n + 1;
1491 let half = grid_n as f32 / 2.0;
1492 let vid =
1493 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1494
1495 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1496 for iz in 0..grid_v {
1497 for iy in 0..grid_v {
1498 for ix in 0..grid_v {
1499 let p = [ix as f32 - half, iy as f32 - half, iz as f32 - half];
1500 let cube = [p[0] / half, p[1] / half, p[2] / half];
1501 let s = cube_to_sphere(cube);
1502 positions.push([s[0] * radius, s[1] * radius, s[2] * radius]);
1503 }
1504 }
1505 }
1506
1507 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1508 for iz in 0..grid_n {
1509 for iy in 0..grid_n {
1510 for ix in 0..grid_n {
1511 cells.push([
1512 vid(ix, iy, iz),
1513 vid(ix + 1, iy, iz),
1514 vid(ix + 1, iy, iz + 1),
1515 vid(ix, iy, iz + 1),
1516 vid(ix, iy + 1, iz),
1517 vid(ix + 1, iy + 1, iz),
1518 vid(ix + 1, iy + 1, iz + 1),
1519 vid(ix, iy + 1, iz + 1),
1520 ]);
1521 }
1522 }
1523 }
1524
1525 VolumeMeshData {
1526 positions,
1527 cells,
1528 ..Default::default()
1529 }
1530 }
1531
1532 fn structured_hex_grid(grid_n: usize) -> VolumeMeshData {
1533 let grid_v = grid_n + 1;
1534 let vid =
1535 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1536
1537 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1538 for iz in 0..grid_v {
1539 for iy in 0..grid_v {
1540 for ix in 0..grid_v {
1541 positions.push([ix as f32, iy as f32, iz as f32]);
1542 }
1543 }
1544 }
1545
1546 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1547 for iz in 0..grid_n {
1548 for iy in 0..grid_n {
1549 for ix in 0..grid_n {
1550 cells.push([
1551 vid(ix, iy, iz),
1552 vid(ix + 1, iy, iz),
1553 vid(ix + 1, iy, iz + 1),
1554 vid(ix, iy, iz + 1),
1555 vid(ix, iy + 1, iz),
1556 vid(ix + 1, iy + 1, iz),
1557 vid(ix + 1, iy + 1, iz + 1),
1558 vid(ix, iy + 1, iz + 1),
1559 ]);
1560 }
1561 }
1562 }
1563
1564 VolumeMeshData {
1565 positions,
1566 cells,
1567 ..Default::default()
1568 }
1569 }
1570
1571 #[test]
1572 fn single_tet_has_four_boundary_faces() {
1573 let data = single_tet();
1574 let mesh = extract_boundary_faces(&data);
1575 assert_eq!(
1576 mesh.indices.len(),
1577 4 * 3,
1578 "single tet -> 4 boundary triangles"
1579 );
1580 }
1581
1582 #[test]
1583 fn two_tets_sharing_face_eliminates_shared_face() {
1584 let data = two_tets_sharing_face();
1585 let mesh = extract_boundary_faces(&data);
1586 assert_eq!(
1589 mesh.indices.len(),
1590 6 * 3,
1591 "two tets sharing a face -> 6 boundary triangles"
1592 );
1593 }
1594
1595 #[test]
1596 fn single_hex_has_twelve_boundary_triangles() {
1597 let data = single_hex();
1598 let mesh = extract_boundary_faces(&data);
1599 assert_eq!(
1601 mesh.indices.len(),
1602 12 * 3,
1603 "single hex -> 12 boundary triangles"
1604 );
1605 }
1606
1607 #[test]
1608 fn structured_tet_grid_has_expected_boundary_triangle_count() {
1609 let grid_n = 3;
1610 let data = structured_tet_grid(grid_n);
1611 let mesh = extract_boundary_faces(&data);
1612 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1613 assert_eq!(
1614 mesh.indices.len(),
1615 expected_boundary_tris * 3,
1616 "3x3x3 tet grid should expose 108 boundary triangles"
1617 );
1618 }
1619
1620 #[test]
1621 fn structured_hex_grid_has_expected_boundary_triangle_count() {
1622 let grid_n = 3;
1623 let data = structured_hex_grid(grid_n);
1624 let mesh = extract_boundary_faces(&data);
1625 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1626 assert_eq!(
1627 mesh.indices.len(),
1628 expected_boundary_tris * 3,
1629 "3x3x3 hex grid should expose 108 boundary triangles"
1630 );
1631 }
1632
1633 #[test]
1634 fn structured_tet_grid_boundary_is_edge_manifold() {
1635 let data = structured_tet_grid(3);
1636 let mesh = extract_boundary_faces(&data);
1637
1638 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1639 std::collections::HashMap::new();
1640 for tri in mesh.indices.chunks_exact(3) {
1641 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1642 let edge = if a < b { (a, b) } else { (b, a) };
1643 *edge_counts.entry(edge).or_insert(0) += 1;
1644 }
1645 }
1646
1647 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1648 .into_iter()
1649 .filter(|(_, count)| *count != 2)
1650 .collect();
1651
1652 assert!(
1653 non_manifold.is_empty(),
1654 "boundary should be watertight; bad edges: {non_manifold:?}"
1655 );
1656 }
1657
1658 #[test]
1659 fn structured_hex_grid_boundary_is_edge_manifold() {
1660 let data = structured_hex_grid(3);
1661 let mesh = extract_boundary_faces(&data);
1662
1663 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1664 std::collections::HashMap::new();
1665 for tri in mesh.indices.chunks_exact(3) {
1666 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1667 let edge = if a < b { (a, b) } else { (b, a) };
1668 *edge_counts.entry(edge).or_insert(0) += 1;
1669 }
1670 }
1671
1672 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1673 .into_iter()
1674 .filter(|(_, count)| *count != 2)
1675 .collect();
1676
1677 assert!(
1678 non_manifold.is_empty(),
1679 "boundary should be watertight; bad edges: {non_manifold:?}"
1680 );
1681 }
1682
1683 #[test]
1684 fn projected_sphere_tet_grid_boundary_faces_point_outward() {
1685 let data = projected_sphere_tet_grid(3, 2.0);
1686 let mesh = extract_boundary_faces(&data);
1687
1688 for tri in mesh.indices.chunks_exact(3) {
1689 let pa = mesh.positions[tri[0] as usize];
1690 let pb = mesh.positions[tri[1] as usize];
1691 let pc = mesh.positions[tri[2] as usize];
1692
1693 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1694 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1695 let normal = [
1696 ab[1] * ac[2] - ab[2] * ac[1],
1697 ab[2] * ac[0] - ab[0] * ac[2],
1698 ab[0] * ac[1] - ab[1] * ac[0],
1699 ];
1700 let fc = [
1701 (pa[0] + pb[0] + pc[0]) / 3.0,
1702 (pa[1] + pb[1] + pc[1]) / 3.0,
1703 (pa[2] + pb[2] + pc[2]) / 3.0,
1704 ];
1705 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1706 assert!(dot > 0.0, "boundary face points inward: tri={tri:?}, dot={dot}");
1707 }
1708 }
1709
1710 #[test]
1711 fn cube_sphere_hex_grid_boundary_faces_point_outward() {
1712 let data = cube_sphere_hex_grid(3, 2.0);
1713 let mesh = extract_boundary_faces(&data);
1714
1715 for tri in mesh.indices.chunks_exact(3) {
1716 let pa = mesh.positions[tri[0] as usize];
1717 let pb = mesh.positions[tri[1] as usize];
1718 let pc = mesh.positions[tri[2] as usize];
1719
1720 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1721 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1722 let normal = [
1723 ab[1] * ac[2] - ab[2] * ac[1],
1724 ab[2] * ac[0] - ab[0] * ac[2],
1725 ab[0] * ac[1] - ab[1] * ac[0],
1726 ];
1727 let fc = [
1728 (pa[0] + pb[0] + pc[0]) / 3.0,
1729 (pa[1] + pb[1] + pc[1]) / 3.0,
1730 (pa[2] + pb[2] + pc[2]) / 3.0,
1731 ];
1732 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1733 assert!(dot > 0.0, "boundary face points inward: tri={tri:?}, dot={dot}");
1734 }
1735 }
1736
1737 #[test]
1738 fn normals_have_correct_length() {
1739 let data = single_tet();
1740 let mesh = extract_boundary_faces(&data);
1741 assert_eq!(mesh.normals.len(), mesh.positions.len());
1742 for n in &mesh.normals {
1743 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1744 assert!(
1745 (len - 1.0).abs() < 1e-5 || len < 1e-5,
1746 "normal not unit: {n:?}"
1747 );
1748 }
1749 }
1750
1751 #[test]
1752 fn cell_scalar_remaps_to_face_attribute() {
1753 let mut data = single_tet();
1754 data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
1755 let mesh = extract_boundary_faces(&data);
1756 match mesh.attributes.get("pressure") {
1757 Some(AttributeData::Face(vals)) => {
1758 assert_eq!(vals.len(), 4, "one value per boundary triangle");
1759 for &v in vals {
1760 assert_eq!(v, 42.0);
1761 }
1762 }
1763 other => panic!("expected Face attribute, got {other:?}"),
1764 }
1765 }
1766
1767 #[test]
1768 fn cell_color_remaps_to_face_color_attribute() {
1769 let mut data = two_tets_sharing_face();
1770 data.cell_colors.insert(
1771 "label".to_string(),
1772 vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
1773 );
1774 let mesh = extract_boundary_faces(&data);
1775 match mesh.attributes.get("label") {
1776 Some(AttributeData::FaceColor(colors)) => {
1777 assert_eq!(colors.len(), 6, "6 boundary faces");
1778 }
1779 other => panic!("expected FaceColor attribute, got {other:?}"),
1780 }
1781 }
1782
1783 #[test]
1784 fn positions_preserved_unchanged() {
1785 let data = single_hex();
1786 let mesh = extract_boundary_faces(&data);
1787 assert_eq!(mesh.positions, data.positions);
1788 }
1789
1790 #[test]
1800
1801 fn empty_planes_matches_boundary_extractor_tet() {
1802 let data = structured_tet_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 empty_planes_matches_boundary_extractor_hex() {
1817 let data = structured_hex_grid(3);
1818 let boundary = extract_boundary_faces(&data);
1819 let clipped = extract_clipped_volume_faces(&data, &[]);
1820 assert_eq!(
1821 boundary.indices.len(),
1822 clipped.indices.len(),
1823 "empty clip_planes -> same triangle count as extract_boundary_faces"
1824 );
1825 }
1826
1827 #[test]
1830
1831 fn clipped_tet_grid_has_nonempty_section_faces() {
1832 let grid_n = 3;
1833 let data = structured_tet_grid(grid_n);
1834 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1837 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1838 assert!(
1840 !mesh.indices.is_empty(),
1841 "clipped tet grid must produce at least one triangle"
1842 );
1843 }
1844
1845 #[test]
1847
1848 fn clipped_hex_grid_has_nonempty_section_faces() {
1849 let grid_n = 3;
1850 let data = structured_hex_grid(grid_n);
1851 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1852 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1853 assert!(
1854 !mesh.indices.is_empty(),
1855 "clipped hex grid must produce at least one triangle"
1856 );
1857 }
1858
1859 #[test]
1862
1863 fn section_face_normals_point_toward_kept_side_tet() {
1864 let data = structured_tet_grid(3);
1865 let plane_normal = [0.0_f32, 1.0, 0.0];
1866 let plane = [plane_normal[0], plane_normal[1], plane_normal[2], -1.5];
1867 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1868
1869 for n in &mesh.normals {
1870 let dot = n[0] * plane_normal[0] + n[1] * plane_normal[1] + n[2] * plane_normal[2];
1871 let _ = dot; }
1877 }
1878
1879 #[test]
1881
1882 fn fully_discarded_cells_contribute_nothing() {
1883 let data = single_tet();
1885 let plane = [0.0_f32, 1.0, 0.0, -2.0]; let mesh = extract_clipped_volume_faces(&data, &[plane]);
1887 assert!(
1888 mesh.indices.is_empty(),
1889 "tet fully below clip plane must produce no triangles"
1890 );
1891 }
1892
1893 #[test]
1896
1897 fn fully_kept_cell_matches_boundary_extractor() {
1898 let data = single_tet();
1900 let plane = [0.0_f32, 1.0, 0.0, 1.0]; let clipped = extract_clipped_volume_faces(&data, &[plane]);
1902 let boundary = extract_boundary_faces(&data);
1903 assert_eq!(
1904 clipped.indices.len(),
1905 boundary.indices.len(),
1906 "fully kept cell must produce the same triangles as boundary extractor"
1907 );
1908 }
1909
1910 #[test]
1913 fn cell_scalar_propagates_to_section_faces() {
1914 let mut data = structured_tet_grid(3);
1915 let n_cells = data.cells.len();
1916 data.cell_scalars
1917 .insert("pressure".to_string(), vec![1.0; n_cells]);
1918 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1919 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1920 match mesh.attributes.get("pressure") {
1921 Some(AttributeData::Face(vals)) => {
1922 let n_tris = mesh.indices.len() / 3;
1923 assert_eq!(vals.len(), n_tris, "one scalar value per output triangle");
1924 for &v in vals {
1925 assert_eq!(v, 1.0, "scalar must equal the owning cell's value");
1926 }
1927 }
1928 other => panic!("expected Face attribute on clipped mesh, got {other:?}"),
1929 }
1930 }
1931
1932 #[test]
1935 fn cell_color_propagates_to_section_faces() {
1936 let mut data = structured_tet_grid(3);
1937 let n_cells = data.cells.len();
1938 let color = [1.0_f32, 0.0, 0.5, 1.0];
1939 data.cell_colors
1940 .insert("label".to_string(), vec![color; n_cells]);
1941 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1942 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1943 match mesh.attributes.get("label") {
1944 Some(AttributeData::FaceColor(colors)) => {
1945 let n_tris = mesh.indices.len() / 3;
1946 assert_eq!(colors.len(), n_tris, "one color per output triangle");
1947 for &c in colors {
1948 assert_eq!(c, color, "color must equal the owning cell's value");
1949 }
1950 }
1951 other => panic!("expected FaceColor attribute on clipped mesh, got {other:?}"),
1952 }
1953 }
1954
1955 #[test]
1957 fn hex_cell_scalar_propagates_to_section_faces() {
1958 let mut data = structured_hex_grid(3);
1959 let n_cells = data.cells.len();
1960 data.cell_scalars
1961 .insert("temp".to_string(), vec![7.0; n_cells]);
1962 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1963 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1964 match mesh.attributes.get("temp") {
1965 Some(AttributeData::Face(vals)) => {
1966 let n_tris = mesh.indices.len() / 3;
1967 assert_eq!(vals.len(), n_tris, "one scalar per output triangle");
1968 for &v in vals {
1969 assert_eq!(v, 7.0, "scalar must equal the owning cell's value");
1970 }
1971 }
1972 other => panic!("expected Face attribute on clipped hex mesh, got {other:?}"),
1973 }
1974 }
1975
1976 fn single_pyramid() -> VolumeMeshData {
1981 let mut data = VolumeMeshData {
1983 positions: vec![
1984 [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], ],
1990 ..Default::default()
1991 };
1992 data.push_pyramid([0, 1, 2, 3], 4);
1993 data
1994 }
1995
1996 fn single_wedge() -> VolumeMeshData {
1997 let mut data = VolumeMeshData {
1999 positions: vec![
2000 [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], ],
2007 ..Default::default()
2008 };
2009 data.push_wedge([0, 1, 2], [3, 4, 5]);
2010 data
2011 }
2012
2013 fn tet_volume(p: [[f32; 3]; 4]) -> f32 {
2014 let v = |i: usize| -> [f32; 3] {
2016 [
2017 p[i][0] - p[0][0],
2018 p[i][1] - p[0][1],
2019 p[i][2] - p[0][2],
2020 ]
2021 };
2022 let (a, b, c) = (v(1), v(2), v(3));
2023 let cross = [
2024 b[1] * c[2] - b[2] * c[1],
2025 b[2] * c[0] - b[0] * c[2],
2026 b[0] * c[1] - b[1] * c[0],
2027 ];
2028 (a[0] * cross[0] + a[1] * cross[1] + a[2] * cross[2]) / 6.0
2029 }
2030
2031 #[test]
2032 fn decompose_tet_yields_one_tet() {
2033 let data = single_tet();
2034 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2035 assert_eq!(tets.len(), 1);
2036 assert_eq!(scalars.len(), 1);
2037 }
2038
2039 #[test]
2040 fn decompose_hex_yields_six_tets() {
2041 let data = single_hex();
2042 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2043 assert_eq!(tets.len(), 6);
2044 assert_eq!(scalars.len(), 6);
2045 }
2046
2047 #[test]
2048 fn decompose_pyramid_yields_two_tets() {
2049 let data = single_pyramid();
2050 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2051 assert_eq!(tets.len(), 2);
2052 assert_eq!(scalars.len(), 2);
2053 }
2054
2055 #[test]
2056 fn decompose_wedge_yields_three_tets() {
2057 let data = single_wedge();
2058 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2059 assert_eq!(tets.len(), 3);
2060 assert_eq!(scalars.len(), 3);
2061 }
2062
2063 #[test]
2064 fn decompose_output_tets_have_nonzero_volume() {
2065 for data in [single_tet(), single_hex(), single_pyramid(), single_wedge()] {
2066 let (tets, _) = decompose_to_tetrahedra(&data, "");
2067 for (i, t) in tets.iter().enumerate() {
2068 let vol = tet_volume(*t).abs();
2069 assert!(
2070 vol > 1e-6,
2071 "tet {i} has near-zero volume {vol}: {t:?}"
2072 );
2073 }
2074 }
2075 }
2076
2077 #[test]
2078 fn decompose_hex_volume_equals_cell_volume() {
2079 let data = single_hex();
2081 let (tets, _) = decompose_to_tetrahedra(&data, "");
2082 let total: f32 = tets.iter().map(|t| tet_volume(*t).abs()).sum();
2083 assert!(
2084 (total - 1.0).abs() < 1e-5,
2085 "unit hex volume should be 1.0, got {total}"
2086 );
2087 }
2088
2089 #[test]
2090 fn decompose_scalar_propagates_to_child_tets() {
2091 let mut data = single_hex();
2092 data.cell_scalars.insert("temp".to_string(), vec![42.0]);
2093 let (_, scalars) = decompose_to_tetrahedra(&data, "temp");
2094 assert_eq!(scalars.len(), 6);
2095 for &s in &scalars {
2096 assert_eq!(s, 42.0, "all child tets must inherit the cell scalar");
2097 }
2098 }
2099
2100 #[test]
2101 fn decompose_missing_attribute_falls_back_to_zero() {
2102 let data = single_hex();
2103 let (_, scalars) = decompose_to_tetrahedra(&data, "nonexistent");
2104 for &s in &scalars {
2105 assert_eq!(s, 0.0, "missing attribute must produce 0.0 per tet");
2106 }
2107 }
2108
2109 #[test]
2110 fn decompose_mixed_mesh_tet_counts_sum_correctly() {
2111 let mut data = VolumeMeshData {
2113 positions: vec![
2114 [0.0, 0.0, 0.0],
2116 [1.0, 0.0, 0.0],
2117 [0.5, 1.0, 0.0],
2118 [0.5, 0.5, 1.0],
2119 [2.0, 0.0, 0.0],
2121 [3.0, 0.0, 0.0],
2122 [3.0, 0.0, 1.0],
2123 [2.0, 0.0, 1.0],
2124 [2.0, 1.0, 0.0],
2125 [3.0, 1.0, 0.0],
2126 [3.0, 1.0, 1.0],
2127 [2.0, 1.0, 1.0],
2128 [4.0, 0.0, 0.0],
2130 [5.0, 0.0, 0.0],
2131 [5.0, 0.0, 1.0],
2132 [4.0, 0.0, 1.0],
2133 [4.5, 1.0, 0.5],
2134 [6.0, 0.0, 0.0],
2136 [7.0, 0.0, 0.0],
2137 [6.5, 0.0, 1.0],
2138 [6.0, 1.0, 0.0],
2139 [7.0, 1.0, 0.0],
2140 [6.5, 1.0, 1.0],
2141 ],
2142 ..Default::default()
2143 };
2144 data.push_tet(0, 1, 2, 3);
2145 data.push_hex([4, 5, 6, 7, 8, 9, 10, 11]);
2146 data.push_pyramid([12, 13, 14, 15], 16);
2147 data.push_wedge([17, 18, 19], [20, 21, 22]);
2148
2149 let (tets, scalars) = decompose_to_tetrahedra(&data, "");
2150 assert_eq!(tets.len(), 12, "1+6+2+3 = 12 tets");
2151 assert_eq!(scalars.len(), 12);
2152 }
2153}