1use super::polygon::{HalfSpace, Polygon};
4use crate::{
5 coordinates::*,
6 ensight_gold::types::{ElementsType, VolumeElementTypes},
7};
8
9fn project_points_to_plane_2d(points: &[[f64; 3]], normal: &CartesianVec3) -> Vec<[f64; 2]> {
29 let n = normal.normalized();
30 let arbitrary = if n.0[0].abs() < n.0[2].abs() {
31 CartesianVec3([1.0, 0.0, 0.0])
32 } else {
33 CartesianVec3([0.0, 0.0, 1.0])
34 };
35 let v = n.cross(&arbitrary).normalized();
38 let u = v.cross(&n).normalized();
39
40 points
41 .iter()
42 .map(|p| CartesianVec3::from_point_origin(CartesianCoordinates(*p)))
43 .map(|p| [p.dot(&u), p.dot(&v)])
44 .collect()
45}
46
47fn sort_points_ccw_3d(points: &[[f64; 3]], normal: &CartesianVec3) -> Vec<[f64; 3]> {
71 let projected = project_points_to_plane_2d(points, normal);
72 let mut indices: Vec<usize> = (0..points.len()).collect();
73 let centroid = {
74 let (mut sx, mut sy) = (0.0, 0.0);
75 for p in &projected {
76 sx += p[0];
77 sy += p[1];
78 }
79 [sx / projected.len() as f64, sy / projected.len() as f64]
80 };
81 indices.sort_by(|&a, &b| {
82 let angle_a = (projected[a][1] - centroid[1]).atan2(projected[a][0] - centroid[0]);
83 let angle_b = (projected[b][1] - centroid[1]).atan2(projected[b][0] - centroid[0]);
84 angle_a.partial_cmp(&angle_b).unwrap()
85 });
86
87 indices.iter().map(|&i| points[i]).collect()
88}
89
90const RADIAL_AXIS: usize = 0;
92
93const AXIAL_AXIS: usize = 2;
95
96pub fn is_curved_face(face: &BoundedPlane) -> bool {
98 face.axis == RADIAL_AXIS
99}
100
101pub fn tangent_plane_at(
118 patch: &BoundedPlane,
119 CartesianCoordinates(element_centroid): CartesianCoordinates,
120) -> BoundedPlane {
121 let CartesianCoordinates(patch_origin) = patch.origin;
122 let radius = patch_origin[0].hypot(patch_origin[1]);
123 let theta = element_centroid[1].atan2(element_centroid[0]);
124
125 BoundedPlane {
126 normal: CartesianVec3([theta.cos(), theta.sin(), 0.]),
127 origin: CartesianCoordinates([radius * theta.cos(), radius * theta.sin(), patch_origin[2]]),
128 extent_u: patch.extent_u,
129 extent_v: patch.extent_v,
130 axis: patch.axis,
131 }
132}
133
134fn planar_bounds(plane: &BoundedPlane) -> Vec<HalfSpace> {
138 let z_bounds = |z0: f64, z1: f64| {
139 [
140 HalfSpace {
141 normal: CartesianVec3([0., 0., 1.]),
142 offset: z0,
143 },
144 HalfSpace {
145 normal: CartesianVec3([0., 0., -1.]),
146 offset: -z1,
147 },
148 ]
149 };
150
151 let theta_bounds = |theta0: f64, theta1: f64| {
153 if theta1 - theta0 >= std::f64::consts::PI {
154 return None;
155 }
156 Some([
157 HalfSpace {
158 normal: CartesianVec3([-theta0.sin(), theta0.cos(), 0.]),
159 offset: 0.,
160 },
161 HalfSpace {
162 normal: CartesianVec3([theta1.sin(), -theta1.cos(), 0.]),
163 offset: 0.,
164 },
165 ])
166 };
167
168 let mut bounds = Vec::with_capacity(4);
169 match plane.axis {
170 0 => {
172 bounds.extend(
173 theta_bounds(plane.extent_u[0], plane.extent_u[1])
174 .into_iter()
175 .flatten(),
176 );
177 bounds.extend(z_bounds(plane.extent_v[0], plane.extent_v[1]));
178 }
179 1 => {
181 let CartesianCoordinates(origin) = plane.origin;
182 let theta = origin[1].atan2(origin[0]);
183 let radial = CartesianVec3([theta.cos(), theta.sin(), 0.]);
184
185 bounds.push(HalfSpace {
186 normal: radial,
187 offset: plane.extent_u[0],
188 });
189 bounds.push(HalfSpace {
190 normal: CartesianVec3([-radial.0[0], -radial.0[1], 0.]),
191 offset: -plane.extent_u[1],
192 });
193 bounds.extend(z_bounds(plane.extent_v[0], plane.extent_v[1]));
194 }
195 _ => bounds.extend(
197 theta_bounds(plane.extent_v[0], plane.extent_v[1])
198 .into_iter()
199 .flatten(),
200 ),
201 }
202
203 bounds
204}
205
206fn triangle_disk_area(a: [f64; 2], b: [f64; 2], radius: f64) -> f64 {
224 let cross = |u: [f64; 2], v: [f64; 2]| u[0] * v[1] - u[1] * v[0];
225 let dot = |u: [f64; 2], v: [f64; 2]| u[0] * v[0] + u[1] * v[1];
226 let sector = |u: [f64; 2], v: [f64; 2]| 0.5 * radius * radius * cross(u, v).atan2(dot(u, v));
228
229 let edge = [b[0] - a[0], b[1] - a[1]];
230 let quadratic_a = dot(edge, edge);
231 if quadratic_a < f64::EPSILON {
232 return 0.;
233 }
234 let quadratic_b = 2. * dot(a, edge);
235 let quadratic_c = dot(a, a) - radius * radius;
236 let discriminant = quadratic_b * quadratic_b - 4. * quadratic_a * quadratic_c;
237
238 if discriminant <= 0. {
239 return sector(a, b);
240 }
241
242 let root = discriminant.sqrt();
243 let entering = (-quadratic_b - root) / (2. * quadratic_a);
244 let leaving = (-quadratic_b + root) / (2. * quadratic_a);
245
246 if entering > 1. || leaving < 0. {
248 return sector(a, b);
249 }
250
251 let at = |t: f64| [a[0] + t * edge[0], a[1] + t * edge[1]];
252 let entering_point = at(entering.clamp(0., 1.));
253 let leaving_point = at(leaving.clamp(0., 1.));
254
255 sector(a, entering_point)
256 + 0.5 * cross(entering_point, leaving_point)
257 + sector(leaving_point, b)
258}
259
260fn polygon_annulus_area(polygon: &[Coords3], radii: [f64; 2]) -> f64 {
262 let radii = [radii[0].max(0.), radii[1].max(0.)];
264 let disk_area = |radius: f64| {
265 (0..polygon.len())
266 .map(|i| {
267 let current = polygon[i];
268 let next = polygon[(i + 1) % polygon.len()];
269 triangle_disk_area([current[0], current[1]], [next[0], next[1]], radius)
270 })
271 .sum::<f64>()
272 };
273
274 (disk_area(radii[1]) - disk_area(radii[0])).abs()
275}
276
277fn polygon_area_3d(points: &[Coords3], normal: &CartesianVec3) -> f64 {
278 let n = normal.normalized();
279
280 let mut area_vec = CartesianVec3([0.0, 0.0, 0.0]);
281 let n_pts = points.len();
282
283 for i in 0..n_pts {
284 let p1 = CartesianVec3(points[i]);
285 let p2 = CartesianVec3(points[(i + 1) % n_pts]);
286
287 let cross = p1.cross(&p2);
288
289 area_vec = area_vec.add(&cross);
290 }
291 0.5 * (area_vec.dot(&n)).abs()
292}
293
294fn tetra_area(vertices: [CartesianCoordinates; 4], plane: &BoundedPlane) -> f64 {
295 const REL_TOL_DISTANCE: f64 = 1e-6;
296 const EPSILON: f64 = 1e-12; let mut intersection_points = vec![];
298
299 let BoundedPlane {
300 normal,
301 origin: point,
302 ..
303 } = plane;
304
305 let d = -normal.dot(&CartesianVec3::from_point_origin(*point)); let distances: Vec<f64> = vertices
307 .iter()
308 .map(|coords| CartesianVec3::from_point_origin(*coords))
309 .map(|v| normal.dot(&v) + d)
310 .collect();
311
312 let mut points_on_plane = vec![];
313 let edge_len = (0..4)
314 .flat_map(|i| (i + 1..4).map(move |j| (i, j)))
315 .map(|(i, j)| {
316 let e = [
317 vertices[i].0[0] - vertices[j].0[0],
318 vertices[i].0[1] - vertices[j].0[1],
319 vertices[i].0[2] - vertices[j].0[2],
320 ];
321 (e[0] * e[0] + e[1] * e[1] + e[2] * e[2]).sqrt()
322 })
323 .fold(0.0_f64, f64::max);
324 if edge_len < EPSILON {
325 return 0.0;
326 }
327 let tol = REL_TOL_DISTANCE * edge_len;
328
329 for (i, dist) in distances.iter().enumerate() {
330 if dist.abs() < tol {
331 points_on_plane.push(vertices[i].0);
332 }
333 }
334
335 for i in 0..4 {
336 for j in (i + 1)..4 {
337 let d1 = distances[i];
338 let d2 = distances[j];
339 if d1.abs() < tol || d2.abs() < tol {
340 continue;
341 }
342 if d1 * d2 < 0.0 {
343 let t = d1.abs() / (d1.abs() + d2.abs());
344 let p1 = &vertices[i].0;
345 let p2 = &vertices[j].0;
346 let intersection = [
347 p1[0] + t * (p2[0] - p1[0]),
348 p1[1] + t * (p2[1] - p1[1]),
349 p1[2] + t * (p2[2] - p1[2]),
350 ];
351 intersection_points.push(intersection);
352 }
353 }
370 }
371
372 for p in &points_on_plane {
373 let already_present = intersection_points.iter().any(|q| {
374 let dx = q[0] - p[0];
375 let dy = q[1] - p[1];
376 let dz = q[2] - p[2];
377 (dx * dx + dy * dy + dz * dz).sqrt() < tol
378 });
379 if !already_present {
380 intersection_points.push(*p);
381 }
382 }
383 if intersection_points.len() < 3 {
386 return 0.0;
387 }
388
389 let sorted = sort_points_ccw_3d(&intersection_points, normal);
390
391 let clipped = planar_bounds(plane)
393 .iter()
394 .fold(Polygon::from_slice(&sorted), |polygon, half_space| {
395 polygon.clip(half_space)
396 });
397
398 if clipped.as_slice().len() < 3 {
399 return 0.0;
400 }
401
402 if plane.axis == AXIAL_AXIS {
404 return polygon_annulus_area(clipped.as_slice(), plane.extent_u);
405 }
406
407 polygon_area_3d(clipped.as_slice(), normal)
408}
409
410pub fn compute_intersection_area(
411 local_vertices: &[CartesianCoordinates],
412 elem_type: VolumeElementTypes,
413 plane: &BoundedPlane,
414) -> Option<f64> {
415 let tetra_indices = elem_type.tetra_subdivisions();
416 if local_vertices.len() != ElementsType::VolumeElementType(elem_type).node_count() as usize {
417 return None;
418 }
419 let area = tetra_indices
420 .iter()
421 .map(|&[i0, i1, i2, i3]| {
422 let a = local_vertices[i0];
423 let b = local_vertices[i1];
424 let c = local_vertices[i2];
425 let d = local_vertices[i3];
426 tetra_area([a, b, c, d], plane)
427 })
428 .sum();
429
430 Some(area)
431}
432
433#[cfg(test)]
434mod test {
435 use super::*;
436 fn make_bounded_plane(
437 normal: CartesianVec3,
438 origin: CartesianCoordinates,
439 axis: usize,
440 ) -> BoundedPlane {
441 let extent = [-10.0, 10.0];
444
445 BoundedPlane {
446 normal,
447 origin,
448 extent_u: extent,
449 extent_v: extent,
450 axis,
451 }
452 }
453
454 #[test]
455 fn test_intersection_area_r_plane() {
456 use std::f64::consts::PI;
457
458 let a = CartesianCoordinates::from(CylindricalCoordinates([1.0, 0.0, 0.0]));
459 let b = CartesianCoordinates::from(CylindricalCoordinates([1.0, PI / 2.0, 0.0]));
460 let c = CartesianCoordinates::from(CylindricalCoordinates([1.0, 0.0, 1.0]));
461 let d = CartesianCoordinates::from(CylindricalCoordinates([0., 0.0, 1.0]));
462
463 let ab = CartesianVec3::from_point(a, b);
464
465 let ac = CartesianVec3::from_point(a, c);
466
467 let cross_prod = ab.cross(&ac);
468
469 let normal = cross_prod.normalized();
470
471 let expected_area = 0.5
472 * (cross_prod.0[0].powi(2) + cross_prod.0[1].powi(2) + cross_prod.0[2].powi(2)).sqrt();
473
474 let plane = make_bounded_plane(normal, a, 0);
475
476 let area =
479 compute_intersection_area(&[a, b, c, d], VolumeElementTypes::Tetra4, &plane).unwrap();
480
481 assert!(
482 (area - expected_area).abs() < 1e-12,
483 "{} != {}",
484 area,
485 expected_area
486 );
487 }
488
489 #[test]
490 fn test_tetrahedron_intersection_area() {
491 let a = [0.0, 0.0, 0.0];
493 let b = [1.0, 0.0, 0.0];
494 let c = [0.0, 1.0, 0.0];
495 let d = [0.0, 0.0, 1.0];
496
497 let normal = CartesianVec3([0., 0., 1.]);
498 let point = CartesianCoordinates([0., 0., 0.5]);
499
500 let plane = make_bounded_plane(normal, point, 2);
501
502 let area = tetra_area(
513 [
514 CartesianCoordinates(a),
515 CartesianCoordinates(b),
516 CartesianCoordinates(c),
517 CartesianCoordinates(d),
518 ],
519 &plane,
520 );
521
522 let expected_area = 0.125;
536
537 assert!(
538 (area - expected_area).abs() < 1e-10,
539 "Expected area {}, got {}",
540 expected_area,
541 area
542 );
543 }
544
545 fn theta_face(r: [f64; 2], z: [f64; 2]) -> BoundedPlane {
547 BoundedPlane {
548 normal: CartesianVec3([0., 1., 0.]),
549 origin: CartesianCoordinates([1., 0., 0.]),
550 extent_u: r,
551 extent_v: z,
552 axis: 1,
553 }
554 }
555
556 fn tetra_cut_by_theta_face() -> [CartesianCoordinates; 4] {
565 [
566 CartesianCoordinates([1., -1., 0.]),
567 CartesianCoordinates([3., -1., 0.]),
568 CartesianCoordinates([1., -1., 2.]),
569 CartesianCoordinates([1., 1., 0.]),
570 ]
571 }
572
573 #[test]
575 fn test_annulus_area_of_a_surrounding_polygon() {
576 let square = [[-5., -5., 0.], [5., -5., 0.], [5., 5., 0.], [-5., 5., 0.]];
577
578 let area = polygon_annulus_area(&square, [1., 2.]);
579 let expected = std::f64::consts::PI * (4. - 1.);
580
581 assert!((area - expected).abs() < 1e-9, "{} != {}", area, expected);
582 }
583
584 #[test]
586 fn test_annulus_area_inside_the_hole() {
587 let square = [
588 [-0.2, -0.2, 0.],
589 [0.2, -0.2, 0.],
590 [0.2, 0.2, 0.],
591 [-0.2, 0.2, 0.],
592 ];
593
594 assert!(polygon_annulus_area(&square, [1., 2.]).abs() < 1e-12);
595 }
596
597 #[test]
599 fn test_annulus_area_of_an_inner_polygon() {
600 let patch = [
601 [1.2, -0.1, 0.],
602 [1.8, -0.1, 0.],
603 [1.8, 0.1, 0.],
604 [1.2, 0.1, 0.],
605 ];
606
607 let area = polygon_annulus_area(&patch, [1., 2.]);
608
609 assert!((area - 0.12).abs() < 1e-9, "{} != 0.12", area);
610 }
611
612 #[test]
613 fn test_area_inside_bounds_is_kept_whole() {
614 let area = tetra_area(
615 tetra_cut_by_theta_face(),
616 &theta_face([0., 10.], [-10., 10.]),
617 );
618
619 assert!((area - 0.5).abs() < 1e-10, "expected 0.5, got {}", area);
620 }
621
622 #[test]
625 fn test_area_straddling_a_bound_is_clipped() {
626 let area = tetra_area(
627 tetra_cut_by_theta_face(),
628 &theta_face([0., 10.], [-10., 0.5]),
629 );
630
631 assert!((area - 0.375).abs() < 1e-10, "expected 0.375, got {}", area);
632 }
633
634 #[test]
635 fn test_area_outside_bounds_is_dropped() {
636 let area = tetra_area(
637 tetra_cut_by_theta_face(),
638 &theta_face([5., 10.], [-10., 10.]),
639 );
640
641 assert_eq!(area, 0.);
642 }
643
644 fn radial_patch() -> BoundedPlane {
646 BoundedPlane {
647 normal: CartesianVec3([1., 0., 0.]),
648 origin: CartesianCoordinates([1., 0., 0.]),
649 extent_u: [-0.5, 0.5],
650 extent_v: [0., 1.],
651 axis: RADIAL_AXIS,
652 }
653 }
654
655 fn element_away_from_the_middle() -> [CartesianCoordinates; 4] {
657 let theta = 0.4;
658 let point = |r: f64, dtheta: f64, z: f64| {
659 CartesianCoordinates([r * (theta + dtheta).cos(), r * (theta + dtheta).sin(), z])
660 };
661 [
662 point(0.95, -0.02, 0.4),
663 point(1.05, -0.02, 0.4),
664 point(0.95, 0.02, 0.4),
665 point(0.95, -0.02, 0.5),
666 ]
667 }
668
669 #[test]
670 fn test_tangent_plane_follows_the_element() {
671 let element = element_away_from_the_middle();
672 let centroid = CartesianCoordinates([
673 element
674 .iter()
675 .map(|CartesianCoordinates(p)| p[0])
676 .sum::<f64>()
677 / 4.,
678 element
679 .iter()
680 .map(|CartesianCoordinates(p)| p[1])
681 .sum::<f64>()
682 / 4.,
683 element
684 .iter()
685 .map(|CartesianCoordinates(p)| p[2])
686 .sum::<f64>()
687 / 4.,
688 ]);
689
690 let patch = radial_patch();
691 let plane = tangent_plane_at(&patch, centroid);
692
693 let CartesianCoordinates(origin) = plane.origin;
695 assert!((origin[0].hypot(origin[1]) - 1.).abs() < 1e-12);
696 assert_eq!(plane.extent_u, patch.extent_u);
697 assert_eq!(plane.axis, patch.axis);
698
699 let with_element_plane =
701 compute_intersection_area(&element, VolumeElementTypes::Tetra4, &plane).unwrap();
702 let with_patch_plane =
703 compute_intersection_area(&element, VolumeElementTypes::Tetra4, &patch).unwrap();
704
705 assert!(
706 with_element_plane > 0.,
707 "the element must be cut by its own tangent plane"
708 );
709 assert_eq!(
710 with_patch_plane, 0.,
711 "the tangent plane of the compartment does not reach this element"
712 );
713 }
714
715 #[test]
716 fn test_sort_points_ccw_3d() {
717 let normal = CartesianVec3([0., 0., 1.]);
718 let points = vec![[0.5, 0.0, 0.5], [0.0, 0.5, 0.5], [0.0, 0.0, 0.5]];
719 let sorted = sort_points_ccw_3d(&points, &normal);
720 assert_eq!(sorted[0], [0.0, 0.0, 0.5]);
721 assert_eq!(sorted[1], [0.5, 0.0, 0.5]);
722 assert_eq!(sorted[2], [0.0, 0.5, 0.5]);
723 }
724}