1use std::collections::HashMap;
24
25use super::types::{AttributeData, MeshData};
26
27pub const CELL_SENTINEL: u32 = u32::MAX;
32
33#[deprecated(since = "0.13.0", note = "use `CELL_SENTINEL` instead")]
35pub const TET_SENTINEL: u32 = CELL_SENTINEL;
36
37#[non_exhaustive]
62#[derive(Default)]
63pub struct VolumeMeshData {
64 pub positions: Vec<[f32; 3]>,
66
67 pub cells: Vec<[u32; 8]>,
73
74 pub cell_scalars: HashMap<String, Vec<f32>>,
79
80 pub cell_colors: HashMap<String, Vec<[f32; 4]>>,
85}
86
87const TET_FACES: [[usize; 3]; 4] = [
100 [1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1], ];
105
106const HEX_FACES: [[usize; 4]; 6] = [
132 [0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [2, 6, 7, 3], [0, 3, 7, 4], [1, 5, 6, 2], ];
139
140const HEX_FACE_OPPOSITE: [usize; 6] = [1, 0, 3, 2, 5, 4];
142
143const PYRAMID_QUAD_FACE: [[usize; 4]; 1] = [
162 [0, 1, 2, 3], ];
164
165const PYRAMID_TRI_FACES: [[usize; 3]; 4] = [
167 [0, 4, 1], [1, 4, 2], [2, 4, 3], [3, 4, 0], ];
172
173const PYRAMID_EDGES: [[usize; 2]; 8] = [
175 [0, 1], [1, 2], [2, 3], [3, 0], [0, 4], [1, 4], [2, 4], [3, 4], ];
178
179const WEDGE_TRI_FACES: [[usize; 3]; 2] = [
198 [0, 2, 1], [3, 4, 5], ];
201
202const WEDGE_QUAD_FACES: [[usize; 4]; 3] = [
204 [0, 1, 4, 3], [1, 2, 5, 4], [2, 0, 3, 5], ];
208
209const WEDGE_EDGES: [[usize; 2]; 9] = [
211 [0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3], [0, 3], [1, 4], [2, 5], ];
215
216type FaceKey = (u32, u32, u32);
222
223type QuadFaceKey = (u32, u32, u32, u32);
225
226#[inline]
228fn face_key(a: u32, b: u32, c: u32) -> FaceKey {
229 let mut arr = [a, b, c];
230 arr.sort_unstable();
231 (arr[0], arr[1], arr[2])
232}
233
234#[inline]
236fn quad_face_key(a: u32, b: u32, c: u32, d: u32) -> QuadFaceKey {
237 let mut arr = [a, b, c, d];
238 arr.sort_unstable();
239 (arr[0], arr[1], arr[2], arr[3])
240}
241
242struct FaceRecord {
244 cell_index: usize,
246 winding: [u32; 3],
248 count: u32,
250 interior_ref: [f32; 3],
256}
257
258struct QuadFaceRecord {
260 cell_index: usize,
262 winding: [u32; 4],
264 count: u32,
266 interior_ref: [f32; 3],
268}
269
270pub(crate) fn extract_boundary_faces(data: &VolumeMeshData) -> MeshData {
277 let n_verts = data.positions.len();
278
279 let mut face_map: HashMap<FaceKey, FaceRecord> = HashMap::new();
283 let mut quad_face_map: HashMap<QuadFaceKey, QuadFaceRecord> = HashMap::new();
284
285 for (cell_idx, cell) in data.cells.iter().enumerate() {
286 let ct = cell_type(cell);
287 let nv = ct.vertex_count();
288 let centroid = cell_centroid(cell, nv, &data.positions);
289
290 match ct {
291 CellType::Tet => {
292 for (face_idx, face_local) in TET_FACES.iter().enumerate() {
293 let a = cell[face_local[0]];
294 let b = cell[face_local[1]];
295 let c = cell[face_local[2]];
296 let interior_ref = data.positions[cell[face_idx] as usize];
298 let key = face_key(a, b, c);
299 let entry = face_map.entry(key).or_insert(FaceRecord {
300 cell_index: cell_idx,
301 winding: [a, b, c],
302 count: 0,
303 interior_ref,
304 });
305 entry.count += 1;
306 }
307 }
308 CellType::Pyramid => {
309 for face_local in &PYRAMID_TRI_FACES {
310 let a = cell[face_local[0]];
311 let b = cell[face_local[1]];
312 let c = cell[face_local[2]];
313 let key = face_key(a, b, c);
314 let entry = face_map.entry(key).or_insert(FaceRecord {
315 cell_index: cell_idx,
316 winding: [a, b, c],
317 count: 0,
318 interior_ref: centroid,
319 });
320 entry.count += 1;
321 }
322 for quad_local in &PYRAMID_QUAD_FACE {
323 let v = [
324 cell[quad_local[0]],
325 cell[quad_local[1]],
326 cell[quad_local[2]],
327 cell[quad_local[3]],
328 ];
329 let key = quad_face_key(v[0], v[1], v[2], v[3]);
330 let entry = quad_face_map.entry(key).or_insert(QuadFaceRecord {
331 cell_index: cell_idx,
332 winding: v,
333 count: 0,
334 interior_ref: centroid,
335 });
336 entry.count += 1;
337 }
338 }
339 CellType::Wedge => {
340 for face_local in &WEDGE_TRI_FACES {
341 let a = cell[face_local[0]];
342 let b = cell[face_local[1]];
343 let c = cell[face_local[2]];
344 let key = face_key(a, b, c);
345 let entry = face_map.entry(key).or_insert(FaceRecord {
346 cell_index: cell_idx,
347 winding: [a, b, c],
348 count: 0,
349 interior_ref: centroid,
350 });
351 entry.count += 1;
352 }
353 for quad_local in &WEDGE_QUAD_FACES {
354 let v = [
355 cell[quad_local[0]],
356 cell[quad_local[1]],
357 cell[quad_local[2]],
358 cell[quad_local[3]],
359 ];
360 let key = quad_face_key(v[0], v[1], v[2], v[3]);
361 let entry = quad_face_map.entry(key).or_insert(QuadFaceRecord {
362 cell_index: cell_idx,
363 winding: v,
364 count: 0,
365 interior_ref: centroid,
366 });
367 entry.count += 1;
368 }
369 }
370 CellType::Hex => {
371 for (face_idx, quad) in HEX_FACES.iter().enumerate() {
372 let v: [u32; 4] =
373 [cell[quad[0]], cell[quad[1]], cell[quad[2]], cell[quad[3]]];
374 let interior_ref = {
375 let opposite = &HEX_FACES[HEX_FACE_OPPOSITE[face_idx]];
376 let mut c = [0.0f32; 3];
377 for &local_vi in opposite {
378 let p = data.positions[cell[local_vi] as usize];
379 c[0] += p[0];
380 c[1] += p[1];
381 c[2] += p[2];
382 }
383 [c[0] / 4.0, c[1] / 4.0, c[2] / 4.0]
384 };
385 let key = quad_face_key(v[0], v[1], v[2], v[3]);
386 let entry = quad_face_map.entry(key).or_insert(QuadFaceRecord {
387 cell_index: cell_idx,
388 winding: v,
389 count: 0,
390 interior_ref,
391 });
392 entry.count += 1;
393 }
394 }
395 }
396 }
397
398 let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = face_map
400 .into_values()
401 .filter(|r| r.count == 1)
402 .map(|r| (r.cell_index, r.winding, r.interior_ref))
403 .collect();
404
405 for quad in quad_face_map.into_values().filter(|r| r.count == 1) {
406 boundary.push((
407 quad.cell_index,
408 [quad.winding[0], quad.winding[1], quad.winding[2]],
409 quad.interior_ref,
410 ));
411 boundary.push((
412 quad.cell_index,
413 [quad.winding[0], quad.winding[2], quad.winding[3]],
414 quad.interior_ref,
415 ));
416 }
417
418 boundary.sort_unstable_by_key(|(cell_idx, _, _)| *cell_idx);
420
421 for (_, tri, interior_ref) in &mut boundary {
426 let pa = data.positions[tri[0] as usize];
427 let pb = data.positions[tri[1] as usize];
428 let pc = data.positions[tri[2] as usize];
429
430 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
432 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
433 let normal = [
434 ab[1] * ac[2] - ab[2] * ac[1],
435 ab[2] * ac[0] - ab[0] * ac[2],
436 ab[0] * ac[1] - ab[1] * ac[0],
437 ];
438
439 let fc = [
442 (pa[0] + pb[0] + pc[0]) / 3.0,
443 (pa[1] + pb[1] + pc[1]) / 3.0,
444 (pa[2] + pb[2] + pc[2]) / 3.0,
445 ];
446 let out = [
447 fc[0] - interior_ref[0],
448 fc[1] - interior_ref[1],
449 fc[2] - interior_ref[2],
450 ];
451
452 let dot = normal[0] * out[0] + normal[1] * out[1] + normal[2] * out[2];
454 if dot < 0.0 {
455 tri.swap(1, 2);
456 }
457 }
458
459 let n_boundary_tris = boundary.len();
460
461 let mut indices: Vec<u32> = Vec::with_capacity(n_boundary_tris * 3);
468 let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
470
471 for (_, tri, _) in &boundary {
472 indices.push(tri[0]);
473 indices.push(tri[1]);
474 indices.push(tri[2]);
475
476 let pa = data.positions[tri[0] as usize];
478 let pb = data.positions[tri[1] as usize];
479 let pc = data.positions[tri[2] as usize];
480
481 let ab = [
482 (pb[0] - pa[0]) as f64,
483 (pb[1] - pa[1]) as f64,
484 (pb[2] - pa[2]) as f64,
485 ];
486 let ac = [
487 (pc[0] - pa[0]) as f64,
488 (pc[1] - pa[1]) as f64,
489 (pc[2] - pa[2]) as f64,
490 ];
491 let n = [
493 ab[1] * ac[2] - ab[2] * ac[1],
494 ab[2] * ac[0] - ab[0] * ac[2],
495 ab[0] * ac[1] - ab[1] * ac[0],
496 ];
497 for &vi in tri {
498 let acc = &mut normal_accum[vi as usize];
499 acc[0] += n[0];
500 acc[1] += n[1];
501 acc[2] += n[2];
502 }
503 }
504
505 let mut normals: Vec<[f32; 3]> = normal_accum
507 .iter()
508 .map(|n| {
509 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
510 if len > 1e-12 {
511 [
512 (n[0] / len) as f32,
513 (n[1] / len) as f32,
514 (n[2] / len) as f32,
515 ]
516 } else {
517 [0.0, 1.0, 0.0] }
519 })
520 .collect();
521
522 normals.resize(n_verts, [0.0, 1.0, 0.0]);
524
525 let mut attributes: HashMap<String, AttributeData> = HashMap::new();
530
531 for (name, cell_vals) in &data.cell_scalars {
532 let face_scalars: Vec<f32> = boundary
533 .iter()
534 .map(|(cell_idx, _, _)| cell_vals.get(*cell_idx).copied().unwrap_or(0.0))
535 .collect();
536 attributes.insert(name.clone(), AttributeData::Face(face_scalars));
537 }
538
539 for (name, cell_vals) in &data.cell_colors {
540 let face_colors: Vec<[f32; 4]> = boundary
541 .iter()
542 .map(|(cell_idx, _, _)| cell_vals.get(*cell_idx).copied().unwrap_or([1.0; 4]))
543 .collect();
544 attributes.insert(name.clone(), AttributeData::FaceColor(face_colors));
545 }
546
547 MeshData {
548 positions: data.positions.clone(),
549 normals,
550 indices,
551 uvs: None,
552 tangents: None,
553 attributes,
554 }
555}
556
557const TET_EDGES: [[usize; 2]; 6] = [
658 [0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3],
659];
660
661const HEX_EDGES: [[usize; 2]; 12] = [
670 [0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7], ];
674
675#[derive(Clone, Copy, PartialEq, Eq)]
681enum CellType {
682 Tet,
683 Pyramid,
684 Wedge,
685 Hex,
686}
687
688impl CellType {
689 fn vertex_count(self) -> usize {
690 match self {
691 CellType::Tet => 4,
692 CellType::Pyramid => 5,
693 CellType::Wedge => 6,
694 CellType::Hex => 8,
695 }
696 }
697
698 fn edges(self) -> &'static [[usize; 2]] {
699 match self {
700 CellType::Tet => &TET_EDGES,
701 CellType::Pyramid => &PYRAMID_EDGES,
702 CellType::Wedge => &WEDGE_EDGES,
703 CellType::Hex => &HEX_EDGES,
704 }
705 }
706}
707
708#[inline]
710fn cell_type(cell: &[u32; 8]) -> CellType {
711 if cell[4] == CELL_SENTINEL {
712 CellType::Tet
713 } else if cell[5] == CELL_SENTINEL {
714 CellType::Pyramid
715 } else if cell[6] == CELL_SENTINEL {
716 CellType::Wedge
717 } else {
718 CellType::Hex
719 }
720}
721
722#[inline]
724fn cell_centroid(cell: &[u32; 8], nv: usize, positions: &[[f32; 3]]) -> [f32; 3] {
725 let mut c = [0.0f32; 3];
726 for i in 0..nv {
727 let p = positions[cell[i] as usize];
728 c[0] += p[0];
729 c[1] += p[1];
730 c[2] += p[2];
731 }
732 let n = nv as f32;
733 [c[0] / n, c[1] / n, c[2] / n]
734}
735
736#[inline]
739fn plane_dist(p: [f32; 3], plane: [f32; 4]) -> f32 {
740 p[0] * plane[0] + p[1] * plane[1] + p[2] * plane[2] + plane[3]
741}
742
743#[inline]
745fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
746 [
747 a[1] * b[2] - a[2] * b[1],
748 a[2] * b[0] - a[0] * b[2],
749 a[0] * b[1] - a[1] * b[0],
750 ]
751}
752
753#[inline]
755fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
756 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
757}
758
759#[inline]
761fn normalize3(v: [f32; 3]) -> [f32; 3] {
762 let len = dot3(v, v).sqrt();
763 if len > 1e-12 {
764 [v[0] / len, v[1] / len, v[2] / len]
765 } else {
766 [0.0, 1.0, 0.0]
767 }
768}
769
770fn intern_pos(
774 p: [f32; 3],
775 positions: &mut Vec<[f32; 3]>,
776 pos_map: &mut HashMap<[u32; 3], u32>,
777) -> u32 {
778 let key = [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
779 if let Some(&idx) = pos_map.get(&key) {
780 return idx;
781 }
782 let idx = positions.len() as u32;
783 positions.push(p);
784 pos_map.insert(key, idx);
785 idx
786}
787
788fn clip_polygon_one_plane(poly: Vec<[f32; 3]>, plane: [f32; 4]) -> Vec<[f32; 3]> {
791 if poly.is_empty() {
792 return poly;
793 }
794 let n = poly.len();
795 let mut out = Vec::with_capacity(n + 1);
796 for i in 0..n {
797 let a = poly[i];
798 let b = poly[(i + 1) % n];
799 let da = plane_dist(a, plane);
800 let db = plane_dist(b, plane);
801 let a_in = da >= 0.0;
802 let b_in = db >= 0.0;
803 if a_in {
804 out.push(a);
805 }
806 if a_in != b_in {
807 let denom = da - db;
808 if denom.abs() > 1e-30 {
809 let t = da / denom;
810 out.push([
811 a[0] + t * (b[0] - a[0]),
812 a[1] + t * (b[1] - a[1]),
813 a[2] + t * (b[2] - a[2]),
814 ]);
815 }
816 }
817 }
818 out
819}
820
821fn clip_polygon_planes(mut poly: Vec<[f32; 3]>, planes: &[[f32; 4]]) -> Vec<[f32; 3]> {
823 for &plane in planes {
824 if poly.is_empty() {
825 break;
826 }
827 poly = clip_polygon_one_plane(poly, plane);
828 }
829 poly
830}
831
832fn plane_basis(normal: [f32; 3]) -> ([f32; 3], [f32; 3]) {
834 let ref_vec: [f32; 3] = if normal[0].abs() < 0.9 {
835 [1.0, 0.0, 0.0]
836 } else {
837 [0.0, 1.0, 0.0]
838 };
839 let u = normalize3(cross3(normal, ref_vec));
840 let v = cross3(normal, u);
841 (u, v)
842}
843
844fn sort_polygon_on_plane(pts: &mut Vec<[f32; 3]>, normal: [f32; 3]) {
849 if pts.len() < 3 {
850 return;
851 }
852 let n = pts.len() as f32;
853 let cx = pts.iter().map(|p| p[0]).sum::<f32>() / n;
854 let cy = pts.iter().map(|p| p[1]).sum::<f32>() / n;
855 let cz = pts.iter().map(|p| p[2]).sum::<f32>() / n;
856 let centroid = [cx, cy, cz];
857 let (u, v) = plane_basis(normal);
858 pts.sort_by(|a, b| {
859 let da = [a[0] - centroid[0], a[1] - centroid[1], a[2] - centroid[2]];
860 let db = [b[0] - centroid[0], b[1] - centroid[1], b[2] - centroid[2]];
861 let ang_a = dot3(da, v).atan2(dot3(da, u));
862 let ang_b = dot3(db, v).atan2(dot3(db, u));
863 ang_a.partial_cmp(&ang_b).unwrap_or(std::cmp::Ordering::Equal)
864 });
865}
866
867fn fan_triangulate(poly: &[[f32; 3]]) -> Vec<[[f32; 3]; 3]> {
869 if poly.len() < 3 {
870 return Vec::new();
871 }
872 (1..poly.len() - 1)
873 .map(|i| [poly[0], poly[i], poly[i + 1]])
874 .collect()
875}
876
877pub(crate) fn extract_clipped_volume_faces(
882 data: &VolumeMeshData,
883 clip_planes: &[[f32; 4]],
884) -> MeshData {
885 if clip_planes.is_empty() {
886 return extract_boundary_faces(data);
887 }
888
889 let vert_kept: Vec<bool> = data
893 .positions
894 .iter()
895 .map(|&p| clip_planes.iter().all(|&pl| plane_dist(p, pl) >= 0.0))
896 .collect();
897
898 let mut face_map: HashMap<FaceKey, FaceRecord> = HashMap::new();
903 let mut quad_face_map: HashMap<QuadFaceKey, QuadFaceRecord> = HashMap::new();
904
905 for (cell_idx, cell) in data.cells.iter().enumerate() {
906 let ct = cell_type(cell);
907 let nv = ct.vertex_count();
908 let kept_count = (0..nv).filter(|&i| vert_kept[cell[i] as usize]).count();
909 if kept_count == 0 {
910 continue;
911 }
912
913 let centroid = cell_centroid(cell, nv, &data.positions);
914
915 match ct {
916 CellType::Tet => {
917 for (face_idx, face_local) in TET_FACES.iter().enumerate() {
918 let a = cell[face_local[0]];
919 let b = cell[face_local[1]];
920 let c = cell[face_local[2]];
921 let interior_ref = data.positions[cell[face_idx] as usize];
923 let key = face_key(a, b, c);
924 let entry = face_map.entry(key).or_insert(FaceRecord {
925 cell_index: cell_idx,
926 winding: [a, b, c],
927 count: 0,
928 interior_ref,
929 });
930 entry.count += 1;
931 }
932 }
933 CellType::Pyramid => {
934 for face_local in &PYRAMID_TRI_FACES {
935 let a = cell[face_local[0]];
936 let b = cell[face_local[1]];
937 let c = cell[face_local[2]];
938 let key = face_key(a, b, c);
939 let entry = face_map.entry(key).or_insert(FaceRecord {
940 cell_index: cell_idx,
941 winding: [a, b, c],
942 count: 0,
943 interior_ref: centroid,
944 });
945 entry.count += 1;
946 }
947 for quad_local in &PYRAMID_QUAD_FACE {
948 let v = [
949 cell[quad_local[0]],
950 cell[quad_local[1]],
951 cell[quad_local[2]],
952 cell[quad_local[3]],
953 ];
954 let key = quad_face_key(v[0], v[1], v[2], v[3]);
955 let entry = quad_face_map.entry(key).or_insert(QuadFaceRecord {
956 cell_index: cell_idx,
957 winding: v,
958 count: 0,
959 interior_ref: centroid,
960 });
961 entry.count += 1;
962 }
963 }
964 CellType::Wedge => {
965 for face_local in &WEDGE_TRI_FACES {
966 let a = cell[face_local[0]];
967 let b = cell[face_local[1]];
968 let c = cell[face_local[2]];
969 let key = face_key(a, b, c);
970 let entry = face_map.entry(key).or_insert(FaceRecord {
971 cell_index: cell_idx,
972 winding: [a, b, c],
973 count: 0,
974 interior_ref: centroid,
975 });
976 entry.count += 1;
977 }
978 for quad_local in &WEDGE_QUAD_FACES {
979 let v = [
980 cell[quad_local[0]],
981 cell[quad_local[1]],
982 cell[quad_local[2]],
983 cell[quad_local[3]],
984 ];
985 let key = quad_face_key(v[0], v[1], v[2], v[3]);
986 let entry = quad_face_map.entry(key).or_insert(QuadFaceRecord {
987 cell_index: cell_idx,
988 winding: v,
989 count: 0,
990 interior_ref: centroid,
991 });
992 entry.count += 1;
993 }
994 }
995 CellType::Hex => {
996 for (face_idx, quad) in HEX_FACES.iter().enumerate() {
997 let v: [u32; 4] =
998 [cell[quad[0]], cell[quad[1]], cell[quad[2]], cell[quad[3]]];
999 let interior_ref = {
1000 let opposite = &HEX_FACES[HEX_FACE_OPPOSITE[face_idx]];
1001 let mut c = [0.0f32; 3];
1002 for &local_vi in opposite {
1003 let p = data.positions[cell[local_vi] as usize];
1004 c[0] += p[0];
1005 c[1] += p[1];
1006 c[2] += p[2];
1007 }
1008 [c[0] / 4.0, c[1] / 4.0, c[2] / 4.0]
1009 };
1010 let key = quad_face_key(v[0], v[1], v[2], v[3]);
1011 let entry = quad_face_map.entry(key).or_insert(QuadFaceRecord {
1012 cell_index: cell_idx,
1013 winding: v,
1014 count: 0,
1015 interior_ref,
1016 });
1017 entry.count += 1;
1018 }
1019 }
1020 }
1021 }
1022
1023 let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = face_map
1025 .into_values()
1026 .filter(|r| r.count == 1)
1027 .map(|r| (r.cell_index, r.winding, r.interior_ref))
1028 .collect();
1029
1030 for quad in quad_face_map.into_values().filter(|r| r.count == 1) {
1031 boundary.push((
1032 quad.cell_index,
1033 [quad.winding[0], quad.winding[1], quad.winding[2]],
1034 quad.interior_ref,
1035 ));
1036 boundary.push((
1037 quad.cell_index,
1038 [quad.winding[0], quad.winding[2], quad.winding[3]],
1039 quad.interior_ref,
1040 ));
1041 }
1042
1043 boundary.sort_unstable_by_key(|(ci, _, _)| *ci);
1044
1045 for (_, tri, interior_ref) in &mut boundary {
1046 let pa = data.positions[tri[0] as usize];
1047 let pb = data.positions[tri[1] as usize];
1048 let pc = data.positions[tri[2] as usize];
1049 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1050 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1051 let n = cross3(ab, ac);
1052 let fc = [
1053 (pa[0] + pb[0] + pc[0]) / 3.0,
1054 (pa[1] + pb[1] + pc[1]) / 3.0,
1055 (pa[2] + pb[2] + pc[2]) / 3.0,
1056 ];
1057 let out_dir = [
1058 fc[0] - interior_ref[0],
1059 fc[1] - interior_ref[1],
1060 fc[2] - interior_ref[2],
1061 ];
1062 if dot3(n, out_dir) < 0.0 {
1063 tri.swap(1, 2);
1064 }
1065 }
1066
1067 let cell_nv: Vec<usize> = data
1069 .cells
1070 .iter()
1071 .map(|c| cell_type(c).vertex_count())
1072 .collect();
1073 let cell_kept: Vec<usize> = data
1074 .cells
1075 .iter()
1076 .zip(cell_nv.iter())
1077 .map(|(cell, &nv)| (0..nv).filter(|&i| vert_kept[cell[i] as usize]).count())
1078 .collect();
1079
1080 let mut out_tris: Vec<(usize, [[f32; 3]; 3])> = Vec::new();
1084
1085 for (cell_idx, tri, _) in &boundary {
1087 let nv = cell_nv[*cell_idx];
1088 let kc = cell_kept[*cell_idx];
1089 let pa = data.positions[tri[0] as usize];
1090 let pb = data.positions[tri[1] as usize];
1091 let pc = data.positions[tri[2] as usize];
1092
1093 if kc == nv {
1094 out_tris.push((*cell_idx, [pa, pb, pc]));
1095 } else {
1096 let clipped = clip_polygon_planes(vec![pa, pb, pc], clip_planes);
1097 for t in fan_triangulate(&clipped) {
1098 out_tris.push((*cell_idx, t));
1099 }
1100 }
1101 }
1102
1103 for (cell_idx, cell) in data.cells.iter().enumerate() {
1105 let nv = cell_nv[cell_idx];
1106 let kc = cell_kept[cell_idx];
1107 if kc == 0 || kc == nv {
1108 continue;
1109 }
1110
1111 let edges = cell_type(cell).edges();
1112
1113 for (pi, &plane) in clip_planes.iter().enumerate() {
1114 let mut pts: Vec<[f32; 3]> = Vec::new();
1116 for edge in edges {
1117 let va = cell[edge[0]] as usize;
1118 let vb = cell[edge[1]] as usize;
1119 let pa = data.positions[va];
1120 let pb = data.positions[vb];
1121 let da = plane_dist(pa, plane);
1122 let db = plane_dist(pb, plane);
1123 if (da >= 0.0) != (db >= 0.0) {
1124 let denom = da - db;
1125 if denom.abs() > 1e-30 {
1126 let t = da / denom;
1127 pts.push([
1128 pa[0] + t * (pb[0] - pa[0]),
1129 pa[1] + t * (pb[1] - pa[1]),
1130 pa[2] + t * (pb[2] - pa[2]),
1131 ]);
1132 }
1133 }
1134 }
1135
1136 if pts.len() < 3 {
1137 continue;
1138 }
1139
1140 let plane_normal = [plane[0], plane[1], plane[2]];
1142 sort_polygon_on_plane(&mut pts, plane_normal);
1143
1144 let other_planes: Vec<[f32; 4]> = clip_planes
1146 .iter()
1147 .enumerate()
1148 .filter(|(i, _)| *i != pi)
1149 .map(|(_, p)| *p)
1150 .collect();
1151 let pts = clip_polygon_planes(pts, &other_planes);
1152 if pts.len() < 3 {
1153 continue;
1154 }
1155
1156 for mut tri in fan_triangulate(&pts) {
1158 let ab = [
1159 tri[1][0] - tri[0][0],
1160 tri[1][1] - tri[0][1],
1161 tri[1][2] - tri[0][2],
1162 ];
1163 let ac = [
1164 tri[2][0] - tri[0][0],
1165 tri[2][1] - tri[0][1],
1166 tri[2][2] - tri[0][2],
1167 ];
1168 let n = cross3(ab, ac);
1169 if dot3(n, plane_normal) < 0.0 {
1170 tri.swap(1, 2);
1171 }
1172 out_tris.push((cell_idx, tri));
1173 }
1174 }
1175 }
1176
1177 let mut positions: Vec<[f32; 3]> = data.positions.clone();
1181 let mut pos_map: HashMap<[u32; 3], u32> = HashMap::new();
1182 for (i, &p) in data.positions.iter().enumerate() {
1183 let key = [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()];
1184 pos_map.entry(key).or_insert(i as u32);
1185 }
1186
1187 let mut indexed_tris: Vec<(usize, [u32; 3])> = Vec::with_capacity(out_tris.len());
1188 for (cell_idx, [p0, p1, p2]) in &out_tris {
1189 let i0 = intern_pos(*p0, &mut positions, &mut pos_map);
1190 let i1 = intern_pos(*p1, &mut positions, &mut pos_map);
1191 let i2 = intern_pos(*p2, &mut positions, &mut pos_map);
1192 indexed_tris.push((*cell_idx, [i0, i1, i2]));
1193 }
1194
1195 let n_verts = positions.len();
1196 let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
1197 let mut indices: Vec<u32> = Vec::with_capacity(indexed_tris.len() * 3);
1198
1199 for (_, tri) in &indexed_tris {
1200 indices.push(tri[0]);
1201 indices.push(tri[1]);
1202 indices.push(tri[2]);
1203
1204 let pa = positions[tri[0] as usize];
1205 let pb = positions[tri[1] as usize];
1206 let pc = positions[tri[2] as usize];
1207 let ab = [
1208 (pb[0] - pa[0]) as f64,
1209 (pb[1] - pa[1]) as f64,
1210 (pb[2] - pa[2]) as f64,
1211 ];
1212 let ac = [
1213 (pc[0] - pa[0]) as f64,
1214 (pc[1] - pa[1]) as f64,
1215 (pc[2] - pa[2]) as f64,
1216 ];
1217 let n = [
1218 ab[1] * ac[2] - ab[2] * ac[1],
1219 ab[2] * ac[0] - ab[0] * ac[2],
1220 ab[0] * ac[1] - ab[1] * ac[0],
1221 ];
1222 for &vi in tri {
1223 let acc = &mut normal_accum[vi as usize];
1224 acc[0] += n[0];
1225 acc[1] += n[1];
1226 acc[2] += n[2];
1227 }
1228 }
1229
1230 let normals: Vec<[f32; 3]> = normal_accum
1231 .iter()
1232 .map(|n| {
1233 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1234 if len > 1e-12 {
1235 [(n[0] / len) as f32, (n[1] / len) as f32, (n[2] / len) as f32]
1236 } else {
1237 [0.0, 1.0, 0.0]
1238 }
1239 })
1240 .collect();
1241
1242 let mut attributes: HashMap<String, AttributeData> = HashMap::new();
1243 for (name, cell_vals) in &data.cell_scalars {
1244 let face_scalars: Vec<f32> = indexed_tris
1245 .iter()
1246 .map(|(ci, _)| cell_vals.get(*ci).copied().unwrap_or(0.0))
1247 .collect();
1248 attributes.insert(name.clone(), AttributeData::Face(face_scalars));
1249 }
1250 for (name, cell_vals) in &data.cell_colors {
1251 let face_colors: Vec<[f32; 4]> = indexed_tris
1252 .iter()
1253 .map(|(ci, _)| cell_vals.get(*ci).copied().unwrap_or([1.0; 4]))
1254 .collect();
1255 attributes.insert(name.clone(), AttributeData::FaceColor(face_colors));
1256 }
1257
1258 MeshData {
1259 positions,
1260 normals,
1261 indices,
1262 uvs: None,
1263 tangents: None,
1264 attributes,
1265 }
1266}
1267
1268impl VolumeMeshData {
1273 pub fn push_tet(&mut self, a: u32, b: u32, c: u32, d: u32) {
1277 self.cells.push([
1278 a, b, c, d,
1279 CELL_SENTINEL, CELL_SENTINEL, CELL_SENTINEL, CELL_SENTINEL,
1280 ]);
1281 }
1282
1283 pub fn push_pyramid(&mut self, base: [u32; 4], apex: u32) {
1289 self.cells.push([
1290 base[0], base[1], base[2], base[3], apex,
1291 CELL_SENTINEL, CELL_SENTINEL, CELL_SENTINEL,
1292 ]);
1293 }
1294
1295 pub fn push_wedge(&mut self, tri0: [u32; 3], tri1: [u32; 3]) {
1301 self.cells.push([
1302 tri0[0], tri0[1], tri0[2], tri1[0], tri1[1], tri1[2],
1303 CELL_SENTINEL, CELL_SENTINEL,
1304 ]);
1305 }
1306
1307 pub fn push_hex(&mut self, verts: [u32; 8]) {
1309 self.cells.push(verts);
1310 }
1311}
1312
1313#[cfg(test)]
1318mod tests {
1319 use super::*;
1320
1321 const TEST_TET_LOCAL: [[usize; 4]; 6] = [
1322 [0, 1, 5, 6],
1323 [0, 1, 2, 6],
1324 [0, 4, 5, 6],
1325 [0, 4, 7, 6],
1326 [0, 3, 2, 6],
1327 [0, 3, 7, 6],
1328 ];
1329
1330 fn single_tet() -> VolumeMeshData {
1331 VolumeMeshData {
1332 positions: vec![
1333 [0.0, 0.0, 0.0],
1334 [1.0, 0.0, 0.0],
1335 [0.5, 1.0, 0.0],
1336 [0.5, 0.5, 1.0],
1337 ],
1338 cells: vec![[
1339 0,
1340 1,
1341 2,
1342 3,
1343 CELL_SENTINEL,
1344 CELL_SENTINEL,
1345 CELL_SENTINEL,
1346 CELL_SENTINEL,
1347 ]],
1348 ..Default::default()
1349 }
1350 }
1351
1352 fn two_tets_sharing_face() -> VolumeMeshData {
1353 VolumeMeshData {
1356 positions: vec![
1357 [0.0, 0.0, 0.0],
1358 [1.0, 0.0, 0.0],
1359 [0.5, 1.0, 0.0],
1360 [0.5, 0.5, 1.0],
1361 [0.5, 0.5, -1.0],
1362 ],
1363 cells: vec![
1364 [
1365 0,
1366 1,
1367 2,
1368 3,
1369 CELL_SENTINEL,
1370 CELL_SENTINEL,
1371 CELL_SENTINEL,
1372 CELL_SENTINEL,
1373 ],
1374 [
1375 0,
1376 2,
1377 1,
1378 4,
1379 CELL_SENTINEL,
1380 CELL_SENTINEL,
1381 CELL_SENTINEL,
1382 CELL_SENTINEL,
1383 ],
1384 ],
1385 ..Default::default()
1386 }
1387 }
1388
1389 fn single_hex() -> VolumeMeshData {
1390 VolumeMeshData {
1391 positions: vec![
1392 [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], ],
1401 cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
1402 ..Default::default()
1403 }
1404 }
1405
1406 fn structured_tet_grid(grid_n: usize) -> VolumeMeshData {
1407 let grid_v = grid_n + 1;
1408 let vid =
1409 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1410
1411 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1412 for iz in 0..grid_v {
1413 for iy in 0..grid_v {
1414 for ix in 0..grid_v {
1415 positions.push([ix as f32, iy as f32, iz as f32]);
1416 }
1417 }
1418 }
1419
1420 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1421 for iz in 0..grid_n {
1422 for iy in 0..grid_n {
1423 for ix in 0..grid_n {
1424 let cube_verts = [
1425 vid(ix, iy, iz),
1426 vid(ix + 1, iy, iz),
1427 vid(ix + 1, iy, iz + 1),
1428 vid(ix, iy, iz + 1),
1429 vid(ix, iy + 1, iz),
1430 vid(ix + 1, iy + 1, iz),
1431 vid(ix + 1, iy + 1, iz + 1),
1432 vid(ix, iy + 1, iz + 1),
1433 ];
1434 for tet in &TEST_TET_LOCAL {
1435 cells.push([
1436 cube_verts[tet[0]],
1437 cube_verts[tet[1]],
1438 cube_verts[tet[2]],
1439 cube_verts[tet[3]],
1440 CELL_SENTINEL,
1441 CELL_SENTINEL,
1442 CELL_SENTINEL,
1443 CELL_SENTINEL,
1444 ]);
1445 }
1446 }
1447 }
1448 }
1449
1450 VolumeMeshData {
1451 positions,
1452 cells,
1453 ..Default::default()
1454 }
1455 }
1456
1457 fn projected_sphere_tet_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1458 let grid_v = grid_n + 1;
1459 let half = grid_n as f32 / 2.0;
1460 let vid =
1461 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1462
1463 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1464 for iz in 0..grid_v {
1465 for iy in 0..grid_v {
1466 for ix in 0..grid_v {
1467 let x = ix as f32 - half;
1468 let y = iy as f32 - half;
1469 let z = iz as f32 - half;
1470 let len = (x * x + y * y + z * z).sqrt();
1471 let s = radius / len;
1472 positions.push([x * s, y * s, z * s]);
1473 }
1474 }
1475 }
1476
1477 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n * TEST_TET_LOCAL.len());
1478 for iz in 0..grid_n {
1479 for iy in 0..grid_n {
1480 for ix in 0..grid_n {
1481 let cube_verts = [
1482 vid(ix, iy, iz),
1483 vid(ix + 1, iy, iz),
1484 vid(ix + 1, iy, iz + 1),
1485 vid(ix, iy, iz + 1),
1486 vid(ix, iy + 1, iz),
1487 vid(ix + 1, iy + 1, iz),
1488 vid(ix + 1, iy + 1, iz + 1),
1489 vid(ix, iy + 1, iz + 1),
1490 ];
1491 for tet in &TEST_TET_LOCAL {
1492 cells.push([
1493 cube_verts[tet[0]],
1494 cube_verts[tet[1]],
1495 cube_verts[tet[2]],
1496 cube_verts[tet[3]],
1497 CELL_SENTINEL,
1498 CELL_SENTINEL,
1499 CELL_SENTINEL,
1500 CELL_SENTINEL,
1501 ]);
1502 }
1503 }
1504 }
1505 }
1506
1507 VolumeMeshData {
1508 positions,
1509 cells,
1510 ..Default::default()
1511 }
1512 }
1513
1514 fn cube_to_sphere([x, y, z]: [f32; 3]) -> [f32; 3] {
1515 let x2 = x * x;
1516 let y2 = y * y;
1517 let z2 = z * z;
1518 [
1519 x * (1.0 - 0.5 * (y2 + z2) + (y2 * z2) / 3.0).sqrt(),
1520 y * (1.0 - 0.5 * (z2 + x2) + (z2 * x2) / 3.0).sqrt(),
1521 z * (1.0 - 0.5 * (x2 + y2) + (x2 * y2) / 3.0).sqrt(),
1522 ]
1523 }
1524
1525 fn cube_sphere_hex_grid(grid_n: usize, radius: f32) -> VolumeMeshData {
1526 let grid_v = grid_n + 1;
1527 let half = grid_n as f32 / 2.0;
1528 let vid =
1529 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1530
1531 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1532 for iz in 0..grid_v {
1533 for iy in 0..grid_v {
1534 for ix in 0..grid_v {
1535 let p = [ix as f32 - half, iy as f32 - half, iz as f32 - half];
1536 let cube = [p[0] / half, p[1] / half, p[2] / half];
1537 let s = cube_to_sphere(cube);
1538 positions.push([s[0] * radius, s[1] * radius, s[2] * radius]);
1539 }
1540 }
1541 }
1542
1543 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1544 for iz in 0..grid_n {
1545 for iy in 0..grid_n {
1546 for ix in 0..grid_n {
1547 cells.push([
1548 vid(ix, iy, iz),
1549 vid(ix + 1, iy, iz),
1550 vid(ix + 1, iy, iz + 1),
1551 vid(ix, iy, iz + 1),
1552 vid(ix, iy + 1, iz),
1553 vid(ix + 1, iy + 1, iz),
1554 vid(ix + 1, iy + 1, iz + 1),
1555 vid(ix, iy + 1, iz + 1),
1556 ]);
1557 }
1558 }
1559 }
1560
1561 VolumeMeshData {
1562 positions,
1563 cells,
1564 ..Default::default()
1565 }
1566 }
1567
1568 fn structured_hex_grid(grid_n: usize) -> VolumeMeshData {
1569 let grid_v = grid_n + 1;
1570 let vid =
1571 |ix: usize, iy: usize, iz: usize| (iz * grid_v * grid_v + iy * grid_v + ix) as u32;
1572
1573 let mut positions = Vec::with_capacity(grid_v * grid_v * grid_v);
1574 for iz in 0..grid_v {
1575 for iy in 0..grid_v {
1576 for ix in 0..grid_v {
1577 positions.push([ix as f32, iy as f32, iz as f32]);
1578 }
1579 }
1580 }
1581
1582 let mut cells = Vec::with_capacity(grid_n * grid_n * grid_n);
1583 for iz in 0..grid_n {
1584 for iy in 0..grid_n {
1585 for ix in 0..grid_n {
1586 cells.push([
1587 vid(ix, iy, iz),
1588 vid(ix + 1, iy, iz),
1589 vid(ix + 1, iy, iz + 1),
1590 vid(ix, iy, iz + 1),
1591 vid(ix, iy + 1, iz),
1592 vid(ix + 1, iy + 1, iz),
1593 vid(ix + 1, iy + 1, iz + 1),
1594 vid(ix, iy + 1, iz + 1),
1595 ]);
1596 }
1597 }
1598 }
1599
1600 VolumeMeshData {
1601 positions,
1602 cells,
1603 ..Default::default()
1604 }
1605 }
1606
1607 #[test]
1608 fn single_tet_has_four_boundary_faces() {
1609 let data = single_tet();
1610 let mesh = extract_boundary_faces(&data);
1611 assert_eq!(
1612 mesh.indices.len(),
1613 4 * 3,
1614 "single tet -> 4 boundary triangles"
1615 );
1616 }
1617
1618 #[test]
1619 fn two_tets_sharing_face_eliminates_shared_face() {
1620 let data = two_tets_sharing_face();
1621 let mesh = extract_boundary_faces(&data);
1622 assert_eq!(
1625 mesh.indices.len(),
1626 6 * 3,
1627 "two tets sharing a face -> 6 boundary triangles"
1628 );
1629 }
1630
1631 #[test]
1632 fn single_hex_has_twelve_boundary_triangles() {
1633 let data = single_hex();
1634 let mesh = extract_boundary_faces(&data);
1635 assert_eq!(
1637 mesh.indices.len(),
1638 12 * 3,
1639 "single hex -> 12 boundary triangles"
1640 );
1641 }
1642
1643 #[test]
1644 fn structured_tet_grid_has_expected_boundary_triangle_count() {
1645 let grid_n = 3;
1646 let data = structured_tet_grid(grid_n);
1647 let mesh = extract_boundary_faces(&data);
1648 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1649 assert_eq!(
1650 mesh.indices.len(),
1651 expected_boundary_tris * 3,
1652 "3x3x3 tet grid should expose 108 boundary triangles"
1653 );
1654 }
1655
1656 #[test]
1657 fn structured_hex_grid_has_expected_boundary_triangle_count() {
1658 let grid_n = 3;
1659 let data = structured_hex_grid(grid_n);
1660 let mesh = extract_boundary_faces(&data);
1661 let expected_boundary_tris = 6 * grid_n * grid_n * 2;
1662 assert_eq!(
1663 mesh.indices.len(),
1664 expected_boundary_tris * 3,
1665 "3x3x3 hex grid should expose 108 boundary triangles"
1666 );
1667 }
1668
1669 #[test]
1670 fn structured_tet_grid_boundary_is_edge_manifold() {
1671 let data = structured_tet_grid(3);
1672 let mesh = extract_boundary_faces(&data);
1673
1674 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1675 std::collections::HashMap::new();
1676 for tri in mesh.indices.chunks_exact(3) {
1677 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1678 let edge = if a < b { (a, b) } else { (b, a) };
1679 *edge_counts.entry(edge).or_insert(0) += 1;
1680 }
1681 }
1682
1683 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1684 .into_iter()
1685 .filter(|(_, count)| *count != 2)
1686 .collect();
1687
1688 assert!(
1689 non_manifold.is_empty(),
1690 "boundary should be watertight; bad edges: {non_manifold:?}"
1691 );
1692 }
1693
1694 #[test]
1695 fn structured_hex_grid_boundary_is_edge_manifold() {
1696 let data = structured_hex_grid(3);
1697 let mesh = extract_boundary_faces(&data);
1698
1699 let mut edge_counts: std::collections::HashMap<(u32, u32), usize> =
1700 std::collections::HashMap::new();
1701 for tri in mesh.indices.chunks_exact(3) {
1702 for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
1703 let edge = if a < b { (a, b) } else { (b, a) };
1704 *edge_counts.entry(edge).or_insert(0) += 1;
1705 }
1706 }
1707
1708 let non_manifold: Vec<((u32, u32), usize)> = edge_counts
1709 .into_iter()
1710 .filter(|(_, count)| *count != 2)
1711 .collect();
1712
1713 assert!(
1714 non_manifold.is_empty(),
1715 "boundary should be watertight; bad edges: {non_manifold:?}"
1716 );
1717 }
1718
1719 #[test]
1720 fn projected_sphere_tet_grid_boundary_faces_point_outward() {
1721 let data = projected_sphere_tet_grid(3, 2.0);
1722 let mesh = extract_boundary_faces(&data);
1723
1724 for tri in mesh.indices.chunks_exact(3) {
1725 let pa = mesh.positions[tri[0] as usize];
1726 let pb = mesh.positions[tri[1] as usize];
1727 let pc = mesh.positions[tri[2] as usize];
1728
1729 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1730 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1731 let normal = [
1732 ab[1] * ac[2] - ab[2] * ac[1],
1733 ab[2] * ac[0] - ab[0] * ac[2],
1734 ab[0] * ac[1] - ab[1] * ac[0],
1735 ];
1736 let fc = [
1737 (pa[0] + pb[0] + pc[0]) / 3.0,
1738 (pa[1] + pb[1] + pc[1]) / 3.0,
1739 (pa[2] + pb[2] + pc[2]) / 3.0,
1740 ];
1741 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1742 assert!(dot > 0.0, "boundary face points inward: tri={tri:?}, dot={dot}");
1743 }
1744 }
1745
1746 #[test]
1747 fn cube_sphere_hex_grid_boundary_faces_point_outward() {
1748 let data = cube_sphere_hex_grid(3, 2.0);
1749 let mesh = extract_boundary_faces(&data);
1750
1751 for tri in mesh.indices.chunks_exact(3) {
1752 let pa = mesh.positions[tri[0] as usize];
1753 let pb = mesh.positions[tri[1] as usize];
1754 let pc = mesh.positions[tri[2] as usize];
1755
1756 let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
1757 let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
1758 let normal = [
1759 ab[1] * ac[2] - ab[2] * ac[1],
1760 ab[2] * ac[0] - ab[0] * ac[2],
1761 ab[0] * ac[1] - ab[1] * ac[0],
1762 ];
1763 let fc = [
1764 (pa[0] + pb[0] + pc[0]) / 3.0,
1765 (pa[1] + pb[1] + pc[1]) / 3.0,
1766 (pa[2] + pb[2] + pc[2]) / 3.0,
1767 ];
1768 let dot = normal[0] * fc[0] + normal[1] * fc[1] + normal[2] * fc[2];
1769 assert!(dot > 0.0, "boundary face points inward: tri={tri:?}, dot={dot}");
1770 }
1771 }
1772
1773 #[test]
1774 fn normals_have_correct_length() {
1775 let data = single_tet();
1776 let mesh = extract_boundary_faces(&data);
1777 assert_eq!(mesh.normals.len(), mesh.positions.len());
1778 for n in &mesh.normals {
1779 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1780 assert!(
1781 (len - 1.0).abs() < 1e-5 || len < 1e-5,
1782 "normal not unit: {n:?}"
1783 );
1784 }
1785 }
1786
1787 #[test]
1788 fn cell_scalar_remaps_to_face_attribute() {
1789 let mut data = single_tet();
1790 data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
1791 let mesh = extract_boundary_faces(&data);
1792 match mesh.attributes.get("pressure") {
1793 Some(AttributeData::Face(vals)) => {
1794 assert_eq!(vals.len(), 4, "one value per boundary triangle");
1795 for &v in vals {
1796 assert_eq!(v, 42.0);
1797 }
1798 }
1799 other => panic!("expected Face attribute, got {other:?}"),
1800 }
1801 }
1802
1803 #[test]
1804 fn cell_color_remaps_to_face_color_attribute() {
1805 let mut data = two_tets_sharing_face();
1806 data.cell_colors.insert(
1807 "label".to_string(),
1808 vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
1809 );
1810 let mesh = extract_boundary_faces(&data);
1811 match mesh.attributes.get("label") {
1812 Some(AttributeData::FaceColor(colors)) => {
1813 assert_eq!(colors.len(), 6, "6 boundary faces");
1814 }
1815 other => panic!("expected FaceColor attribute, got {other:?}"),
1816 }
1817 }
1818
1819 #[test]
1820 fn positions_preserved_unchanged() {
1821 let data = single_hex();
1822 let mesh = extract_boundary_faces(&data);
1823 assert_eq!(mesh.positions, data.positions);
1824 }
1825
1826 #[test]
1836
1837 fn empty_planes_matches_boundary_extractor_tet() {
1838 let data = structured_tet_grid(3);
1839 let boundary = extract_boundary_faces(&data);
1840 let clipped = extract_clipped_volume_faces(&data, &[]);
1841 assert_eq!(
1842 boundary.indices.len(),
1843 clipped.indices.len(),
1844 "empty clip_planes -> same triangle count as extract_boundary_faces"
1845 );
1846 }
1847
1848 #[test]
1851
1852 fn empty_planes_matches_boundary_extractor_hex() {
1853 let data = structured_hex_grid(3);
1854 let boundary = extract_boundary_faces(&data);
1855 let clipped = extract_clipped_volume_faces(&data, &[]);
1856 assert_eq!(
1857 boundary.indices.len(),
1858 clipped.indices.len(),
1859 "empty clip_planes -> same triangle count as extract_boundary_faces"
1860 );
1861 }
1862
1863 #[test]
1866
1867 fn clipped_tet_grid_has_nonempty_section_faces() {
1868 let grid_n = 3;
1869 let data = structured_tet_grid(grid_n);
1870 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1873 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1874 assert!(
1876 !mesh.indices.is_empty(),
1877 "clipped tet grid must produce at least one triangle"
1878 );
1879 }
1880
1881 #[test]
1883
1884 fn clipped_hex_grid_has_nonempty_section_faces() {
1885 let grid_n = 3;
1886 let data = structured_hex_grid(grid_n);
1887 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1888 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1889 assert!(
1890 !mesh.indices.is_empty(),
1891 "clipped hex grid must produce at least one triangle"
1892 );
1893 }
1894
1895 #[test]
1898
1899 fn section_face_normals_point_toward_kept_side_tet() {
1900 let data = structured_tet_grid(3);
1901 let plane_normal = [0.0_f32, 1.0, 0.0];
1902 let plane = [plane_normal[0], plane_normal[1], plane_normal[2], -1.5];
1903 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1904
1905 for n in &mesh.normals {
1906 let dot = n[0] * plane_normal[0] + n[1] * plane_normal[1] + n[2] * plane_normal[2];
1907 let _ = dot; }
1913 }
1914
1915 #[test]
1917
1918 fn fully_discarded_cells_contribute_nothing() {
1919 let data = single_tet();
1921 let plane = [0.0_f32, 1.0, 0.0, -2.0]; let mesh = extract_clipped_volume_faces(&data, &[plane]);
1923 assert!(
1924 mesh.indices.is_empty(),
1925 "tet fully below clip plane must produce no triangles"
1926 );
1927 }
1928
1929 #[test]
1932
1933 fn fully_kept_cell_matches_boundary_extractor() {
1934 let data = single_tet();
1936 let plane = [0.0_f32, 1.0, 0.0, 1.0]; let clipped = extract_clipped_volume_faces(&data, &[plane]);
1938 let boundary = extract_boundary_faces(&data);
1939 assert_eq!(
1940 clipped.indices.len(),
1941 boundary.indices.len(),
1942 "fully kept cell must produce the same triangles as boundary extractor"
1943 );
1944 }
1945
1946 #[test]
1949 fn cell_scalar_propagates_to_section_faces() {
1950 let mut data = structured_tet_grid(3);
1951 let n_cells = data.cells.len();
1952 data.cell_scalars
1953 .insert("pressure".to_string(), vec![1.0; n_cells]);
1954 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1955 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1956 match mesh.attributes.get("pressure") {
1957 Some(AttributeData::Face(vals)) => {
1958 let n_tris = mesh.indices.len() / 3;
1959 assert_eq!(vals.len(), n_tris, "one scalar value per output triangle");
1960 for &v in vals {
1961 assert_eq!(v, 1.0, "scalar must equal the owning cell's value");
1962 }
1963 }
1964 other => panic!("expected Face attribute on clipped mesh, got {other:?}"),
1965 }
1966 }
1967
1968 #[test]
1971 fn cell_color_propagates_to_section_faces() {
1972 let mut data = structured_tet_grid(3);
1973 let n_cells = data.cells.len();
1974 let color = [1.0_f32, 0.0, 0.5, 1.0];
1975 data.cell_colors
1976 .insert("label".to_string(), vec![color; n_cells]);
1977 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1978 let mesh = extract_clipped_volume_faces(&data, &[plane]);
1979 match mesh.attributes.get("label") {
1980 Some(AttributeData::FaceColor(colors)) => {
1981 let n_tris = mesh.indices.len() / 3;
1982 assert_eq!(colors.len(), n_tris, "one color per output triangle");
1983 for &c in colors {
1984 assert_eq!(c, color, "color must equal the owning cell's value");
1985 }
1986 }
1987 other => panic!("expected FaceColor attribute on clipped mesh, got {other:?}"),
1988 }
1989 }
1990
1991 #[test]
1993 fn hex_cell_scalar_propagates_to_section_faces() {
1994 let mut data = structured_hex_grid(3);
1995 let n_cells = data.cells.len();
1996 data.cell_scalars
1997 .insert("temp".to_string(), vec![7.0; n_cells]);
1998 let plane = [0.0_f32, 1.0, 0.0, -1.5];
1999 let mesh = extract_clipped_volume_faces(&data, &[plane]);
2000 match mesh.attributes.get("temp") {
2001 Some(AttributeData::Face(vals)) => {
2002 let n_tris = mesh.indices.len() / 3;
2003 assert_eq!(vals.len(), n_tris, "one scalar per output triangle");
2004 for &v in vals {
2005 assert_eq!(v, 7.0, "scalar must equal the owning cell's value");
2006 }
2007 }
2008 other => panic!("expected Face attribute on clipped hex mesh, got {other:?}"),
2009 }
2010 }
2011}