conspire/geometry/mesh/tessellation/sdf/
mod.rs1#[cfg(test)]
2mod test;
3
4use std::{
5 f64::consts::TAU,
6 thread::{available_parallelism, scope},
7};
8
9use crate::{
10 geometry::{Direction, DirectionsRef, mesh::tessellation::Tessellation},
11 math::{Quantity, QuantityVector, Scalar, Tensor},
12 units::Length,
13};
14
15impl Tessellation {
16 pub fn shape_diameter_function(
20 &self,
21 half_angle: Scalar,
22 rings: usize,
23 azimuthal: usize,
24 ) -> QuantityVector<Length> {
25 let mesh = self.mesh();
26 let bvh = self.bvh();
27 let elements: Vec<&[usize]> = mesh.connectivities().iter().flatten().collect();
28 let coordinates = mesh.coordinates();
29 let centroids = mesh.centroids();
30 let normals: DirectionsRef<'_, 3> = self.normals.iter().flatten().collect();
31 let number_of_faces = normals.len();
32 let mut face_diameters = vec![Quantity::<Length>::default(); number_of_faces];
33 let threads = available_parallelism().map_or(1, |threads| threads.get());
34 let chunk_size = number_of_faces.div_ceil(threads).max(1);
35 scope(|scope| {
36 let (bvh, elements, centroids, normals) = (bvh, &elements, ¢roids, &normals);
37 face_diameters
38 .chunks_mut(chunk_size)
39 .enumerate()
40 .for_each(|(chunk, diameters)| {
41 scope.spawn(move || {
42 let offset = chunk * chunk_size;
43 diameters
44 .iter_mut()
45 .enumerate()
46 .for_each(|(local, diameter)| {
47 let face = offset + local;
48 if !normals[face].norm().value().is_finite() {
49 return;
50 }
51 let samples =
52 cone_directions(&-&normals[face], half_angle, rings, azimuthal)
53 .into_iter()
54 .filter_map(|(direction, weight)| {
55 let ray = (centroids[face].clone(), direction).into();
56 bvh.intersect(&ray, coordinates, elements)
57 .filter(|hit| hit.index() != face)
58 .map(|hit| (hit.distance(), weight))
59 })
60 .collect();
61 *diameter = weighted_diameter(samples);
62 });
63 });
64 });
65 });
66 interpolate_to_nodes(face_diameters.into(), elements, coordinates.len())
67 }
68}
69
70fn interpolate_to_nodes(
71 face_diameters: QuantityVector<Length>,
72 elements: Vec<&[usize]>,
73 number_of_nodes: usize,
74) -> QuantityVector<Length> {
75 let mut nodal = QuantityVector::zero(number_of_nodes);
76 let mut counts = vec![0; number_of_nodes];
77 elements
78 .into_iter()
79 .zip(face_diameters)
80 .for_each(|(element, diameter)| {
81 element.iter().for_each(|&node| {
82 nodal[node] += diameter;
83 counts[node] += 1;
84 })
85 });
86 nodal.iter_mut().zip(counts).for_each(|(value, count)| {
87 if count > 0 {
88 *value /= count as Scalar
89 }
90 });
91 nodal
92}
93
94fn cone_directions(
95 axis: &Direction<3>,
96 half_angle: Scalar,
97 rings: usize,
98 azimuthal: usize,
99) -> Vec<(Direction<3>, Scalar)> {
100 let basis = axis.orthonormal_basis();
101 let (axis, tangent_1, tangent_2) = (&basis[0], &basis[1], &basis[2]);
102 let sigma = half_angle / 3.0;
103 let mut directions = Vec::with_capacity(1 + rings * azimuthal);
104 directions.push((axis.clone(), 1.0));
105 for ring in 1..=rings {
106 let polar = half_angle * ring as Scalar / rings as Scalar;
107 let (sin_polar, cos_polar) = polar.sin_cos();
108 let weight = (-0.5 * (polar / sigma).powi(2)).exp();
109 for sample in 0..azimuthal {
110 let (sin_azimuth, cos_azimuth) =
111 (TAU * sample as Scalar / azimuthal as Scalar).sin_cos();
112 let direction = axis * cos_polar
113 + tangent_1 * (sin_polar * cos_azimuth)
114 + tangent_2 * (sin_polar * sin_azimuth);
115 directions.push((direction, weight));
116 }
117 }
118 directions
119}
120
121fn weighted_diameter(samples: Vec<(Quantity<Length>, Scalar)>) -> Quantity<Length> {
122 if samples.is_empty() {
123 return Quantity::default();
124 }
125 let mut distances: Vec<Quantity<Length>> =
126 samples.iter().map(|&(distance, _)| distance).collect();
127 distances.sort_by(|a, b| a.partial_cmp(b).unwrap());
128 let median = distances[distances.len() / 2];
129 let mean = distances.iter().copied().sum::<Quantity<Length>>() / distances.len() as Scalar;
130 let standard_deviation = Quantity::new(
131 (distances
132 .iter()
133 .map(|&distance| (distance - mean).value().powi(2))
134 .sum::<Scalar>()
135 / distances.len() as Scalar)
136 .sqrt(),
137 );
138 let (numerator, denominator) = samples
139 .into_iter()
140 .filter(|&(distance, _)| (distance - median).abs() <= standard_deviation)
141 .fold(
142 (Quantity::default(), 0.0),
143 |(numerator, denominator), (distance, weight)| {
144 (numerator + distance * weight, denominator + weight)
145 },
146 );
147 if denominator > 0.0 {
148 numerator / denominator
149 } else {
150 median
151 }
152}