forked from 1b-agent/fieldcraft-upstream
Planar all-quad meshing: size fields, TFI mapped meshes, free quad mesher (M5 base) (#18)
* Planar all-quad meshing: size fields, TFI mapped meshes, free quad mesher, planar quad validity - modelcraft_mesh::size: SizeField trait, division counts, graded polyline division, per-loop parity - modelcraft_mesh::quad: transfinite (Gordon-Hall) mapped meshes of four-sided regions; free all-quad meshes of polygons with holes on prescribed boundary nodes (half-resolution CDT with Ruppert refinement, Lee-Lo recombination, uniform split, smart Laplacian plus Freitag-Plassmann untangling), exact validity gate on every element - modelcraft_mesh::quality: Robinson aspect ratio and skew for quads, exact planar quad validity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * quad mesher: true-boundary guard for Steiner points, untangling by corner area, parity retry, recombination quality floor; I-beam and perforated plate tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: quad meshing papers (B7), roadmap M5 and M2, architecture pipeline step Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * quad mesher: simplify (shared corner and validity helpers, vertex rotation helper, guard decoupled from the CDT, verify inside the CDT, non-allocating final gate) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
16 files changed
+3185
-6
No files matched your search
@@ -27,8 +27,9 @@ use modelcraft_model::{ElementKind, check_coordinates};
|
||||
|
||||
pub use jacobian::{JacobianCheck, Validity};
|
||||
pub use report::{GateOptions, GateReport, MetricStats, QualityReport, enclosed_volume, quality_report, validity_gate, validity_gate_elements};
|
||||
pub(crate) use shape::{planar_corner, quad_axes};
|
||||
|
||||
use crate::predicates::{Point3, Sign, tet_volume_sign};
|
||||
use crate::predicates::{Point2, Point3, Sign, orient2d, snap_to_domain, tet_volume_sign};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A quality metric. Angles are in degrees.
|
||||
@@ -60,6 +61,9 @@ pub enum Metric {
|
||||
MinAngle,
|
||||
/// Largest corner angle of a triangle or quadrilateral.
|
||||
MaxAngle,
|
||||
/// Skew of a quadrilateral: `|cos θ|` of the angle between its principal axes, in `[0, 1]`;
|
||||
/// 0 for a rectangle, 1 when degenerate.
|
||||
Skew,
|
||||
}
|
||||
|
||||
impl Metric {
|
||||
@@ -76,19 +80,20 @@ impl Metric {
|
||||
Metric::EdgeRatio => "edge_ratio",
|
||||
Metric::MinAngle => "min_angle",
|
||||
Metric::MaxAngle => "max_angle",
|
||||
Metric::Skew => "skew",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a larger value is a better element.
|
||||
pub fn higher_is_better(self) -> bool {
|
||||
!matches!(self, Metric::MaxDihedral | Metric::AspectRatio | Metric::EdgeRatio | Metric::MaxAngle)
|
||||
!matches!(self, Metric::MaxDihedral | Metric::AspectRatio | Metric::EdgeRatio | Metric::MaxAngle | Metric::Skew)
|
||||
}
|
||||
|
||||
/// The fixed histogram range; values outside it fall in the first or last bin.
|
||||
pub fn range(self) -> (f64, f64) {
|
||||
match self {
|
||||
Metric::ScaledJacobian | Metric::JacobianRatio | Metric::RadiusRatio | Metric::VolumeLength => (-1.0, 1.0),
|
||||
Metric::Shape => (0.0, 1.0),
|
||||
Metric::Shape | Metric::Skew => (0.0, 1.0),
|
||||
Metric::MinDihedral | Metric::MaxDihedral | Metric::MinAngle | Metric::MaxAngle => (0.0, 180.0),
|
||||
Metric::AspectRatio | Metric::EdgeRatio => (1.0, 11.0),
|
||||
}
|
||||
@@ -218,5 +223,52 @@ pub fn element_quality(kind: ElementKind, xyz: &[[f64; 3]]) -> Result<ElementQua
|
||||
Ok(ElementQuality { kind, size: check.volume, jacobian: Some(check), metrics })
|
||||
}
|
||||
|
||||
/// The exact validity of a planar bilinear quadrilateral with corners `p` in counterclockwise
|
||||
/// order. Its Jacobian determinant is affine in each reference coordinate (the `ξη` term of a
|
||||
/// planar bilinear map cancels), so it is positive on the whole element exactly when it is
|
||||
/// positive at the four corners, i.e. when every corner turns left: `orient2d(p[i-1], p[i],
|
||||
/// p[i+1]) > 0`, decided by the exact predicate. A zero corner (degenerate) or a negative one
|
||||
/// (concave, bow-tie or clockwise) is [`Validity::Invalid`]. Coordinates below the predicates'
|
||||
/// smallest magnitude are rounded to zero first.
|
||||
pub fn planar_quad_validity(p: &[[f64; 2]; 4]) -> Result<Validity> {
|
||||
check_coordinates(p.as_flattened())?;
|
||||
let [a, b, c, d] = p.map(|c| Point2::new(snap_to_domain(c[0]), snap_to_domain(c[1])));
|
||||
Ok(if turns_left([&a?, &b?, &c?, &d?]) { Validity::Valid } else { Validity::Invalid })
|
||||
}
|
||||
|
||||
/// The exact corner test of [`planar_quad_validity`] on admissible points: every corner of the
|
||||
/// quadrilateral turns left.
|
||||
pub(crate) fn turns_left(p: [&Point2; 4]) -> bool {
|
||||
(0..4).all(|i| orient2d(p[(i + 3) % 4], p[i], p[(i + 1) % 4]) == Sign::Positive)
|
||||
}
|
||||
|
||||
/// Every metric of a planar quadrilateral (counterclockwise corners in the xy plane) with its
|
||||
/// exact validity ([`planar_quad_validity`]): the Jacobian ratio is the smallest corner
|
||||
/// determinant over the largest magnitude (exact bounds, see there) and the size is the signed
|
||||
/// area. The shape metrics are those of [`element_quality`] for a `Quad4`.
|
||||
pub fn planar_quad_quality(p: &[[f64; 2]; 4]) -> Result<ElementQuality> {
|
||||
let validity = planar_quad_validity(p)?;
|
||||
let o = p[0];
|
||||
let l = p.map(|c| [c[0] - o[0], c[1] - o[1]]);
|
||||
let corners = [0, 1, 2, 3].map(|i| shape::planar_corner(&l, i));
|
||||
let dets = corners.map(|c| c.0);
|
||||
let lo = dets.iter().fold(f64::INFINITY, |m, &d| m.min(d));
|
||||
let hi = dets.iter().fold(0.0_f64, |m, &d| m.max(d.abs()));
|
||||
let ratio = if hi > 0.0 { (lo / hi).clamp(-1.0, 1.0) } else { 0.0 };
|
||||
let area = 0.5 * ((l[2][0] - l[0][0]) * (l[3][1] - l[1][1]) - (l[2][1] - l[0][1]) * (l[3][0] - l[1][0]));
|
||||
let lifted = l.map(|c| [c[0], c[1], 0.0]);
|
||||
let (_, mut metrics) = shape::quad(lifted);
|
||||
// Corner scaled Jacobians measured against +z (the 3D form measures against the diagonals'
|
||||
// normal, which flips with a clockwise quad).
|
||||
let scaled = corners.iter().fold(f64::INFINITY, |m, c| m.min(c.1.unwrap_or(0.0)));
|
||||
for (m, v) in metrics.iter_mut() {
|
||||
if *m == Metric::ScaledJacobian {
|
||||
*v = scaled;
|
||||
}
|
||||
}
|
||||
metrics.push((Metric::JacobianRatio, ratio));
|
||||
Ok(ElementQuality { kind: ElementKind::Quad4, size: area, jacobian: Some(JacobianCheck { validity, ratio, volume: area }), metrics })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -6,6 +6,10 @@
|
||||
//! - Hexahedra, wedges and quadrilaterals: Knupp's algebraic metrics built on the corner Jacobian
|
||||
//! matrices (Knupp, *Algebraic mesh quality metrics*, SIAM J. Sci. Comput. 23(1), 2001): the
|
||||
//! scaled Jacobian `det(A) / (|a1| |a2| |a3|)` and the shape metric `3 det(A)^{2/3} / |A|²_F`.
|
||||
//! - Quadrilateral aspect ratio and skew from the principal axes `X1 = (p1 − p0) + (p2 − p3)`,
|
||||
//! `X2 = (p2 − p1) + (p3 − p0)` (Robinson, *CRE method of element testing and the Jacobian
|
||||
//! shape parameters*, Eng. Comput. 4(2):113–118, 1987): aspect `max(|X1|, |X2|) / min(…)`, skew
|
||||
//! `|X1 · X2| / (|X1| |X2|)`.
|
||||
//!
|
||||
//! Every metric is defined for degenerate input (no division by zero, no NaN): a degenerate
|
||||
//! element scores 0 on bounded metrics and `+∞` on aspect and edge ratios.
|
||||
@@ -132,6 +136,7 @@ pub(crate) fn quad(p: [P; 4]) -> (f64, Vec<(Metric, f64)>) {
|
||||
angles = (angles.0.min(a), angles.1.max(a));
|
||||
}
|
||||
let edges = [(0, 1), (1, 2), (2, 3), (3, 0)];
|
||||
let (aspect, skew) = quad_axes(p);
|
||||
(
|
||||
area,
|
||||
vec![
|
||||
@@ -139,10 +144,36 @@ pub(crate) fn quad(p: [P; 4]) -> (f64, Vec<(Metric, f64)>) {
|
||||
(Metric::MinAngle, angles.0),
|
||||
(Metric::MaxAngle, angles.1),
|
||||
(Metric::EdgeRatio, edge_ratio(&p, &edges)),
|
||||
(Metric::AspectRatio, aspect),
|
||||
(Metric::Skew, skew),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Corner `i` of a planar quadrilateral: the cross product `(p[i+1] − p[i]) × (p[i−1] − p[i])`
|
||||
/// (twice the corner triangle's signed area, positive for a left turn of a counterclockwise quad)
|
||||
/// and that cross over the two edge lengths (the corner's scaled Jacobian, `None` when an edge
|
||||
/// has zero length).
|
||||
pub(crate) fn planar_corner(p: &[[f64; 2]; 4], i: usize) -> (f64, Option<f64>) {
|
||||
let (a, b, c) = (p[(i + 3) % 4], p[i % 4], p[(i + 1) % 4]);
|
||||
let (u, w) = ([c[0] - b[0], c[1] - b[1]], [a[0] - b[0], a[1] - b[1]]);
|
||||
let cross = u[0] * w[1] - u[1] * w[0];
|
||||
let den = (u[0] * u[0] + u[1] * u[1]).sqrt() * (w[0] * w[0] + w[1] * w[1]).sqrt();
|
||||
let scaled = cross / den;
|
||||
(cross, (den > 0.0 && scaled.is_finite()).then_some(scaled))
|
||||
}
|
||||
|
||||
/// Robinson's aspect ratio and skew of a quadrilateral from its principal axes; `(+∞, 1)` when
|
||||
/// an axis vanishes.
|
||||
pub(crate) fn quad_axes(p: [P; 4]) -> (f64, f64) {
|
||||
let x1 = [p[1][0] - p[0][0] + p[2][0] - p[3][0], p[1][1] - p[0][1] + p[2][1] - p[3][1], p[1][2] - p[0][2] + p[2][2] - p[3][2]];
|
||||
let x2 = [p[2][0] - p[1][0] + p[3][0] - p[0][0], p[2][1] - p[1][1] + p[3][1] - p[0][1], p[2][2] - p[1][2] + p[3][2] - p[0][2]];
|
||||
let (l1, l2) = (norm(x1), norm(x2));
|
||||
let aspect = ratio(l1.max(l2), l1.min(l2), f64::INFINITY).max(1.0);
|
||||
let skew = ratio(dot(x1, x2).abs(), l1 * l2, 1.0).min(1.0);
|
||||
(aspect, skew)
|
||||
}
|
||||
|
||||
/// Wedge scaled Jacobian: the smallest determinant of the three unit edge vectors at a corner,
|
||||
/// taken in right-handed order (1 for a right prism on a right-angled corner, sin 60° ≈ 0.866 on
|
||||
/// an equilateral triangle).
|
||||
|
||||
Reference in new issue
Block a user