1use axiolid_core::{Point3, Scalar, Vec3};
46use axiolid_mesh::halfedge::{FaceId, HalfedgeMesh, VertexId};
47use axiolid_numeric::{conjugate_gradient, ConjugateGradientOptions, NumericError, SparseMatrix};
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51#[non_exhaustive]
52pub enum FairingWeights {
53 #[default]
57 Cotangent,
58 Uniform,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64#[non_exhaustive]
65pub enum FairingOrder {
66 Harmonic,
68 #[default]
71 Biharmonic,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct FairOptions {
77 pub weights: FairingWeights,
79 pub order: FairingOrder,
81 pub relative_tolerance: Scalar,
84 pub max_iterations: Option<usize>,
87}
88
89impl Default for FairOptions {
90 fn default() -> Self {
91 Self {
92 weights: FairingWeights::Cotangent,
93 order: FairingOrder::Biharmonic,
94 relative_tolerance: 1e-12,
95 max_iterations: None,
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq)]
102pub struct FairReport {
103 pub vertices: usize,
105 pub iterations: [usize; 3],
107 pub relative_residual: Scalar,
110}
111
112#[derive(Debug, Clone, PartialEq, thiserror::Error)]
114#[non_exhaustive]
115pub enum FairError {
116 #[error("vertex {vertex} is not a live vertex")]
118 RemovedVertex {
119 vertex: VertexId,
121 },
122 #[error("vertex {vertex} is not connected to any fixed vertex")]
125 Unanchored {
126 vertex: VertexId,
128 },
129 #[error("face {face} has {degree} sides; cotangent weights need triangles")]
131 NotATriangle {
132 face: FaceId,
134 degree: usize,
136 },
137 #[error("face {face} is degenerate; its cotangent weights are undefined")]
140 DegenerateTriangle {
141 face: FaceId,
143 },
144 #[error("relative tolerance {0} is not a finite non-negative number")]
146 InvalidTolerance(Scalar),
147 #[error("fairing solve for coordinate {coordinate} stopped at relative residual {relative_residual}")]
149 NotConverged {
150 coordinate: usize,
152 relative_residual: Scalar,
154 },
155 #[error("fairing solve refused: {0}")]
159 Numeric(NumericError),
160}
161
162const MIN_SINE: Scalar = 1e-12;
164
165pub fn fair(
177 mesh: &mut HalfedgeMesh,
178 vertices: &[VertexId],
179 options: &FairOptions,
180) -> Result<FairReport, FairError> {
181 if !options.relative_tolerance.is_finite() || options.relative_tolerance < 0.0 {
182 return Err(FairError::InvalidTolerance(options.relative_tolerance));
183 }
184 let mut free: Vec<VertexId> = vertices.to_vec();
185 free.sort_unstable();
186 free.dedup();
187 for &v in &free {
188 if !mesh.contains_vertex(v) {
189 return Err(FairError::RemovedVertex { vertex: v });
190 }
191 }
192 if free.is_empty() {
193 return Ok(FairReport {
194 vertices: 0,
195 iterations: [0; 3],
196 relative_residual: 0.0,
197 });
198 }
199 let slot_count = mesh.positions().len();
200 let mut unknown = vec![usize::MAX; slot_count];
201 for (k, &v) in free.iter().enumerate() {
202 unknown[v.index()] = k;
203 }
204 require_anchored(mesh, &free, &unknown)?;
205
206 let mut in_rows = vec![false; slot_count];
209 for &v in &free {
210 in_rows[v.index()] = true;
211 for u in mesh.vertex_vertices(v) {
212 in_rows[u.index()] = true;
213 }
214 }
215 let rows: Vec<VertexId> = (0..slot_count)
216 .filter(|&i| in_rows[i])
217 .map(|i| VertexId::new(i as u32))
218 .collect();
219
220 let anchors: Vec<VertexId> = rows
225 .iter()
226 .copied()
227 .filter(|v| unknown[v.index()] == usize::MAX)
228 .collect();
229 let origin = anchors
230 .iter()
231 .fold(Vec3::ZERO, |s, &v| s + mesh.position(v))
232 / anchors.len() as Scalar;
233 let local = |v: VertexId| mesh.position(v) - origin;
234
235 let n = free.len();
236 let mut triplets: Vec<(usize, usize, Scalar)> = Vec::new();
237 let mut rhs = [vec![0.0; n], vec![0.0; n], vec![0.0; n]];
238 match options.order {
239 FairingOrder::Harmonic => {
240 for (a, &v) in free.iter().enumerate() {
241 let (diagonal, neighbours) = laplacian_row(mesh, v, options.weights)?;
242 triplets.push((a, a, -diagonal));
243 for (u, w) in neighbours {
244 let b = unknown[u.index()];
245 if b == usize::MAX {
246 let p = local(u);
247 for (axis, r) in rhs.iter_mut().enumerate() {
248 r[a] += w * p[axis];
249 }
250 } else {
251 triplets.push((a, b, -w));
252 }
253 }
254 }
255 }
256 FairingOrder::Biharmonic => {
257 for &k in &rows {
258 let (diagonal, neighbours) = laplacian_row(mesh, k, options.weights)?;
259 let mass = vertex_mass(mesh, k, options.weights)?;
260 let mut free_part: Vec<(usize, Scalar)> = Vec::new();
261 let mut fixed_part = Vec3::ZERO;
262 for (u, w) in core::iter::once((k, diagonal)).chain(neighbours) {
263 let b = unknown[u.index()];
264 if b == usize::MAX {
265 fixed_part += w * local(u);
266 } else {
267 free_part.push((b, w));
268 }
269 }
270 for &(a, wa) in &free_part {
271 for &(b, wb) in &free_part {
272 triplets.push((a, b, wa * wb / mass));
273 }
274 for (axis, r) in rhs.iter_mut().enumerate() {
275 r[a] -= wa * fixed_part[axis] / mass;
276 }
277 }
278 }
279 }
280 }
281 let matrix = SparseMatrix::from_triplets(n, n, &triplets).map_err(FairError::Numeric)?;
282 let cg = ConjugateGradientOptions {
283 relative_tolerance: options.relative_tolerance,
284 max_iterations: options.max_iterations,
285 };
286 let mut solved = [vec![], vec![], vec![]];
287 let mut iterations = [0usize; 3];
288 let mut relative_residual: Scalar = 0.0;
289 for axis in 0..3 {
290 let initial: Vec<Scalar> = free.iter().map(|&v| local(v)[axis]).collect();
291 let solution = conjugate_gradient(&matrix, &rhs[axis], Some(&initial), cg)
292 .map_err(FairError::Numeric)?;
293 if solution.status != axiolid_numeric::Status::Converged {
294 return Err(FairError::NotConverged {
295 coordinate: axis,
296 relative_residual: solution.relative_residual,
297 });
298 }
299 iterations[axis] = solution.iterations;
300 relative_residual = relative_residual.max(solution.relative_residual);
301 solved[axis] = solution.x;
302 }
303 for (k, &v) in free.iter().enumerate() {
304 let p = Point3::new(solved[0][k], solved[1][k], solved[2][k]) + origin;
305 mesh.set_position(v, p);
306 }
307 Ok(FairReport {
308 vertices: n,
309 iterations,
310 relative_residual,
311 })
312}
313
314fn require_anchored(
316 mesh: &HalfedgeMesh,
317 free: &[VertexId],
318 unknown: &[usize],
319) -> Result<(), FairError> {
320 let mut seen = vec![false; free.len()];
321 for start in 0..free.len() {
322 if seen[start] {
323 continue;
324 }
325 seen[start] = true;
326 let mut stack = vec![free[start]];
327 let mut anchored = false;
328 while let Some(v) = stack.pop() {
329 for u in mesh.vertex_vertices(v) {
330 let k = unknown[u.index()];
331 if k == usize::MAX {
332 anchored = true;
333 } else if !seen[k] {
334 seen[k] = true;
335 stack.push(u);
336 }
337 }
338 }
339 if !anchored {
340 return Err(FairError::Unanchored {
341 vertex: free[start],
342 });
343 }
344 }
345 Ok(())
346}
347
348fn laplacian_row(
351 mesh: &HalfedgeMesh,
352 v: VertexId,
353 weights: FairingWeights,
354) -> Result<(Scalar, Vec<(VertexId, Scalar)>), FairError> {
355 let mut row = Vec::new();
356 let mut sum = 0.0;
357 for h in mesh.outgoing_halfedges(v) {
358 let w = match weights {
359 FairingWeights::Uniform => 1.0,
360 FairingWeights::Cotangent => {
361 let mut w = 0.0;
362 for g in [h, mesh.opposite(h)] {
363 if let Some(f) = mesh.face(g) {
364 let opposite = triangle_apex(mesh, g, f)?;
365 w += 0.5 * cotangent(mesh, f, opposite, mesh.source(g), mesh.target(g))?;
366 }
367 }
368 w
369 }
370 };
371 sum += w;
372 row.push((mesh.target(h), w));
373 }
374 Ok((-sum, row))
375}
376
377fn triangle_apex(
379 mesh: &HalfedgeMesh,
380 g: axiolid_mesh::halfedge::HalfedgeId,
381 f: FaceId,
382) -> Result<VertexId, FairError> {
383 let degree = mesh.face_degree(f);
384 if degree != 3 {
385 return Err(FairError::NotATriangle { face: f, degree });
386 }
387 Ok(mesh.target(mesh.next(g)))
388}
389
390fn cotangent(
392 mesh: &HalfedgeMesh,
393 f: FaceId,
394 apex: VertexId,
395 a: VertexId,
396 b: VertexId,
397) -> Result<Scalar, FairError> {
398 let p = mesh.position(apex);
399 let u = mesh.position(a) - p;
400 let v = mesh.position(b) - p;
401 let cross = u.cross(v).length();
402 if cross.is_nan() || cross <= MIN_SINE * u.length() * v.length() {
403 return Err(FairError::DegenerateTriangle { face: f });
404 }
405 Ok(u.dot(v) / cross)
406}
407
408fn vertex_mass(
411 mesh: &HalfedgeMesh,
412 v: VertexId,
413 weights: FairingWeights,
414) -> Result<Scalar, FairError> {
415 match weights {
416 FairingWeights::Uniform => Ok(mesh.degree(v) as Scalar),
417 FairingWeights::Cotangent => {
418 let mut area = 0.0;
419 for f in mesh.vertex_faces(v) {
420 let corners: Vec<Point3> =
421 mesh.face_vertices(f).map(|c| mesh.position(c)).collect();
422 if corners.len() != 3 {
423 return Err(FairError::NotATriangle {
424 face: f,
425 degree: corners.len(),
426 });
427 }
428 area += 0.5
429 * (corners[1] - corners[0])
430 .cross(corners[2] - corners[0])
431 .length();
432 }
433 if area > 0.0 {
436 Ok(area / 3.0)
437 } else {
438 Err(FairError::Unanchored { vertex: v })
439 }
440 }
441 }
442}