Files
OneCAD-Simulation/crates/mesh/src/quad/tests.rs
T
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00

571 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::BTreeMap;
use std::f64::consts::PI;
use super::*;
use crate::quality::Validity;
use crate::size::{UniformSize, divide_polyline, even_divisions, ideal_divisions, loop_divisions};
fn opts() -> QuadOptions {
QuadOptions::default()
}
/// A `w × h` rectangle at `(x0, y0)`, counterclockwise, `nx` divisions along x and `ny` along y.
fn rect(x0: f64, y0: f64, w: f64, h: f64, nx: usize, ny: usize) -> Vec<[f64; 2]> {
let mut p = Vec::new();
for i in 0..nx {
p.push([x0 + w * i as f64 / nx as f64, y0]);
}
for j in 0..ny {
p.push([x0 + w, y0 + h * j as f64 / ny as f64]);
}
for i in 0..nx {
p.push([x0 + w - w * i as f64 / nx as f64, y0 + h]);
}
for j in 0..ny {
p.push([x0, y0 + h - h * j as f64 / ny as f64]);
}
p
}
fn circle(cx: f64, cy: f64, r: f64, n: usize) -> Vec<[f64; 2]> {
(0..n).map(|k| 2.0 * PI * k as f64 / n as f64).map(|t| [cx + r * t.cos(), cy + r * t.sin()]).collect()
}
/// A closed polygon with every side divided evenly into `per_side` pieces.
fn subdivide(corners: &[[f64; 2]], per_side: usize) -> Vec<[f64; 2]> {
let mut p = Vec::new();
for (i, a) in corners.iter().enumerate() {
let b = corners[(i + 1) % corners.len()];
for k in 0..per_side {
let t = k as f64 / per_side as f64;
p.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
}
}
p
}
fn signed_area(p: &[[f64; 2]]) -> f64 {
(0..p.len()).map(|i| p[i][0] * p[(i + 1) % p.len()][1] - p[(i + 1) % p.len()][0] * p[i][1]).sum::<f64>() / 2.0
}
/// Every mesh invariant: boundary nodes unchanged and first, every element valid by the exact
/// test and counterclockwise, conformity (each interior edge used once in each direction, each
/// boundary edge once), the boundary edges exactly the input loops, and the area.
fn check(mesh: &QuadMesh, loops: &[Vec<[f64; 2]>]) {
let input: Vec<[f64; 2]> = loops.iter().flatten().copied().collect();
assert_eq!(mesh.boundary_nodes, input.len());
assert_eq!(&mesh.nodes[..input.len()], input.as_slice());
let mut area = 0.0;
let mut directed: BTreeMap<(u32, u32), usize> = BTreeMap::new();
for q in &mesh.quads {
let c = q.map(|v| mesh.nodes[v as usize]);
assert_eq!(crate::quality::planar_quad_validity(&c).unwrap(), Validity::Valid, "{c:?}");
area += signed_area(&c);
for k in 0..4 {
*directed.entry((q[k], q[(k + 1) % 4])).or_default() += 1;
}
}
assert!(directed.values().all(|&n| n == 1), "an edge is used twice in the same direction");
let boundary: Vec<(u32, u32)> = directed.keys().filter(|(a, b)| !directed.contains_key(&(*b, *a))).copied().collect();
let mut expected = Vec::new();
let mut base = 0u32;
for l in loops {
let n = l.len() as u32;
let ccw = signed_area(l) > 0.0;
let outer = base == 0;
for i in 0..n {
let (a, b) = (base + i, base + (i + 1) % n);
// The outer loop runs counterclockwise around the mesh, holes clockwise.
expected.push(if ccw == outer { (a, b) } else { (b, a) });
}
base += n;
}
expected.sort_unstable();
assert_eq!(boundary, expected, "the mesh boundary is not the input loops");
let want: f64 = loops.iter().enumerate().map(|(k, l)| if k == 0 { signed_area(l).abs() } else { -signed_area(l).abs() }).sum();
assert!((area - want).abs() <= 1e-9 * want.abs().max(1.0), "area {area} vs {want}");
}
#[test]
fn rectangle_free() {
let loops = vec![rect(0.0, 0.0, 10.0, 5.0, 20, 10)];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
assert!(m.stats.min_scaled_jacobian > 0.3, "{:?}", m.stats);
// About (10 × 5) / 0.5² = 200 quads.
assert!((100..400).contains(&m.quads.len()), "{}", m.quads.len());
}
#[test]
fn single_quad_and_a_six_point_loop() {
let quad = vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]];
let m = free_quad_mesh(&quad, &UniformSize(1.0), &opts()).unwrap();
assert_eq!(m.quads.len(), 1);
check(&m, &quad);
let six = vec![rect(0.0, 0.0, 2.0, 1.0, 2, 1)];
let m = free_quad_mesh(&six, &UniformSize(1.0), &opts()).unwrap();
check(&m, &six);
}
#[test]
fn clockwise_input_is_accepted() {
let mut l = rect(0.0, 0.0, 4.0, 2.0, 8, 4);
l.reverse();
let loops = vec![l];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
}
#[test]
fn l_shape() {
let corners: [[f64; 2]; 6] = [[0.0, 0.0], [10.0, 0.0], [10.0, 4.0], [4.0, 4.0], [4.0, 10.0], [0.0, 10.0]];
// Even divisions per side keep the corners at even positions.
let mut l = Vec::new();
for (i, a) in corners.iter().enumerate() {
let b = corners[(i + 1) % corners.len()];
let len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
let n = even_divisions(len / 0.5).unwrap();
for k in 0..n {
let t = k as f64 / n as f64;
l.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
}
}
let loops = vec![l];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
assert!(m.stats.min_scaled_jacobian > 0.2, "{:?}", m.stats);
}
#[test]
fn plate_with_a_hole() {
let loops = vec![rect(0.0, 0.0, 20.0, 10.0, 40, 20), circle(10.0, 5.0, 2.5, 32)];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
assert!(m.stats.min_scaled_jacobian > 0.2, "{:?}", m.stats);
}
#[test]
fn plate_with_many_holes() {
let mut loops = vec![rect(0.0, 0.0, 30.0, 30.0, 30, 30)];
for i in 0..3 {
for j in 0..3 {
loops.push(circle(5.0 + 10.0 * i as f64, 5.0 + 10.0 * j as f64, 2.0 + 0.5 * ((i + j) % 2) as f64, 16 + 4 * i));
}
}
// A square hole too, given clockwise.
loops[5] = rect(13.0, 13.0, 4.0, 4.0, 4, 4).into_iter().rev().collect();
let m = free_quad_mesh(&loops, &UniformSize(1.0), &opts()).unwrap();
check(&m, &loops);
}
#[test]
fn thin_sliver() {
let loops = vec![rect(0.0, 0.0, 100.0, 1.0, 200, 2)];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
assert_eq!(m.quads.len(), 400);
// A sliver one division across: the split puts a mid node on each short side.
let loops = vec![rect(0.0, 0.0, 50.0, 0.25, 100, 1)];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
}
fn star(r_out: f64, r_in: f64) -> Vec<[f64; 2]> {
(0..10).map(|k| (k as f64 * PI / 5.0 + PI / 2.0, if k % 2 == 0 { r_out } else { r_in })).map(|(t, r)| [r * t.cos(), r * t.sin()]).collect()
}
#[test]
fn concave_star() {
let loops = vec![subdivide(&star(10.0, 4.0), 4)];
let m = free_quad_mesh(&loops, &UniformSize(1.0), &opts()).unwrap();
check(&m, &loops);
// A star with a star-shaped hole.
let loops = vec![subdivide(&star(10.0, 4.0), 4), subdivide(&star(2.5, 1.0), 2)];
let m = free_quad_mesh(&loops, &UniformSize(1.0), &opts()).unwrap();
check(&m, &loops);
}
/// A polygon with every side divided evenly at about `h` (even counts, so corners stay at even
/// positions).
fn divided(corners: &[[f64; 2]], h: f64) -> Vec<[f64; 2]> {
let mut l = Vec::new();
for (i, a) in corners.iter().enumerate() {
let b = corners[(i + 1) % corners.len()];
let n = even_divisions(((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt() / h).unwrap();
l.extend((0..n).map(|k| k as f64 / n as f64).map(|t| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]));
}
l
}
#[test]
fn i_beam_end_face() {
// Flanges 10 × 1, web 1 thick, 12 deep overall: thin walls meeting at reflex corners.
let c = [
[0.0, 0.0],
[10.0, 0.0],
[10.0, 1.0],
[5.5, 1.0],
[5.5, 11.0],
[10.0, 11.0],
[10.0, 12.0],
[0.0, 12.0],
[0.0, 11.0],
[4.5, 11.0],
[4.5, 1.0],
[0.0, 1.0],
];
for h in [0.5, 0.25] {
let loops = vec![divided(&c, h)];
let m = free_quad_mesh(&loops, &UniformSize(h), &opts()).unwrap();
check(&m, &loops);
assert!(m.stats.min_scaled_jacobian > 0.2, "{h}: {:?}", m.stats);
}
}
#[test]
fn perforated_plate() {
// A 40 × 20 plate with a 7 × 3 grid of holes of radius 1 on a 5 mm pitch.
let h = 0.5;
let mut loops = vec![divided(&[[0.0, 0.0], [40.0, 0.0], [40.0, 20.0], [0.0, 20.0]], h)];
for i in 0..7 {
for j in 0..3 {
loops.push(circle(5.0 + 5.0 * i as f64, 5.0 + 5.0 * j as f64, 1.0, 2 * (PI / h).ceil() as usize));
}
}
let m = free_quad_mesh(&loops, &UniformSize(h), &opts()).unwrap();
check(&m, &loops);
assert!(m.stats.min_scaled_jacobian > 0.2, "{:?}", m.stats);
}
#[test]
fn comb_with_deep_slots() {
// Teeth of width 1 between slots of width 1, 4 deep: strongly concave.
let mut corners = vec![[0.0, 0.0], [9.0, 0.0]];
for k in (0..5).rev() {
let x = 2.0 * k as f64;
corners.extend([[x + 1.0, 6.0], [x, 6.0]]);
if k > 0 {
corners.extend([[x, 2.0], [x - 1.0, 2.0]]);
}
}
let loops = vec![subdivide(&corners, 4)];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &opts()).unwrap();
check(&m, &loops);
}
#[test]
fn graded_size_field() {
let loops = vec![rect(0.0, 0.0, 20.0, 20.0, 20, 20)];
let near = |p: [f64; 3]| 0.25 + 0.15 * (p[0] * p[0] + p[1] * p[1]).sqrt();
let graded = free_quad_mesh(&loops, &near, &opts()).unwrap();
check(&graded, &loops);
let uniform = free_quad_mesh(&loops, &UniformSize(1.0), &opts()).unwrap();
// Denser than the uniform mesh near the origin, coarser far from it.
let within = |m: &QuadMesh, r: f64| m.nodes.iter().filter(|p| p[0] * p[0] + p[1] * p[1] < r * r).count();
assert!(within(&graded, 8.0) > within(&uniform, 8.0), "{} {}", within(&graded, 8.0), within(&uniform, 8.0));
assert!(graded.nodes.len() - within(&graded, 16.0) < uniform.nodes.len() - within(&uniform, 16.0));
}
#[test]
fn boundary_from_the_size_helpers() {
// An arc-and-line boundary whose divisions come from the size field, loop parity fixed.
let field = |p: [f64; 3]| 0.3 + 0.05 * p[0].abs();
let arc: Vec<[f64; 3]> = (0..=32).map(|k| PI * k as f64 / 32.0).map(|t| [5.0 * t.cos(), 5.0 * t.sin(), 0.0]).collect();
let base = vec![[-5.0, 0.0, 0.0], [5.0, 0.0, 0.0]];
let curves = [arc, base];
let ideal: Vec<f64> = curves.iter().map(|c| ideal_divisions(&field, c).unwrap()).collect();
let counts = loop_divisions(&ideal).unwrap();
let mut l = Vec::new();
for (c, n) in curves.iter().zip(counts) {
let pts = divide_polyline(&field, c, n).unwrap();
l.extend(pts[..pts.len() - 1].iter().map(|p| [p[0], p[1]]));
}
assert_eq!(l.len() % 2, 0);
let loops = vec![l];
let m = free_quad_mesh(&loops, &field, &opts()).unwrap();
check(&m, &loops);
}
#[test]
fn deterministic() {
let loops = vec![rect(0.0, 0.0, 20.0, 10.0, 40, 20), circle(7.0, 5.0, 2.0, 24), circle(14.0, 5.0, 3.0, 30)];
let a = free_quad_mesh(&loops, &UniformSize(0.6), &opts()).unwrap();
let b = free_quad_mesh(&loops, &UniformSize(0.6), &opts()).unwrap();
assert_eq!(a, b);
}
#[test]
fn no_smoothing_is_still_valid() {
let loops = vec![rect(0.0, 0.0, 20.0, 10.0, 40, 20), circle(10.0, 5.0, 2.5, 32)];
let m = free_quad_mesh(&loops, &UniformSize(0.5), &QuadOptions { smoothing_sweeps: 0, ..opts() }).unwrap();
check(&m, &loops);
assert_eq!(m.stats.smoothing_sweeps, 0);
}
#[test]
fn hostile_free_input() {
let s = UniformSize(1.0);
let ok = rect(0.0, 0.0, 4.0, 4.0, 4, 4);
let invalid = |loops: Vec<Vec<[f64; 2]>>| matches!(free_quad_mesh(&loops, &s, &opts()), Err(Error::Invalid(_)));
assert!(invalid(vec![]));
assert!(invalid(vec![vec![[0.0, 0.0], [1.0, 0.0]]]));
// Odd count: an error, never a silent repair.
let mut odd = ok.clone();
odd.push([0.0, 0.5]);
assert!(invalid(vec![odd]));
let mut nan = ok.clone();
nan[3] = [f64::NAN, 1.0];
assert!(invalid(vec![nan]));
let mut inf = ok.clone();
inf[3] = [f64::INFINITY, 1.0];
assert!(invalid(vec![inf]));
let mut far = ok.clone();
far[3] = [1e300, 1.0];
assert!(invalid(vec![far]));
// Zero-length edge and repeated point.
let mut zero = ok.clone();
zero[2] = zero[1];
assert!(invalid(vec![zero]));
let mut repeated = ok.clone();
repeated[10] = repeated[2];
assert!(invalid(vec![repeated]));
// Self-intersecting (bow tie) loop.
assert!(invalid(vec![vec![[0.0, 0.0], [2.0, 2.0], [2.0, 1.0], [2.0, 0.0], [0.0, 2.0], [0.0, 1.0]]]));
// A point touching another edge.
assert!(invalid(vec![vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [2.0, 0.0], [0.0, 4.0], [0.0, 2.0]]]));
// Collinear fold-back.
assert!(invalid(vec![vec![[0.0, 0.0], [4.0, 0.0], [2.0, 0.0], [2.0, 2.0], [1.0, 2.0], [0.0, 2.0]]]));
// All collinear.
assert!(invalid(vec![vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0], [4.0, 0.0], [5.0, 0.0]]]));
// Holes: outside, crossing the outer loop, nested, too few points.
assert!(invalid(vec![ok.clone(), circle(10.0, 10.0, 1.0, 8)]));
assert!(invalid(vec![ok.clone(), circle(4.0, 2.0, 1.0, 8)]));
assert!(invalid(vec![rect(0.0, 0.0, 10.0, 10.0, 10, 10), circle(5.0, 5.0, 3.0, 16), circle(5.0, 5.0, 1.0, 8)]));
assert!(invalid(vec![ok.clone(), rect(1.0, 1.0, 2.0, 2.0, 1, 1)]));
// Two holes crossing each other.
assert!(invalid(vec![rect(0.0, 0.0, 10.0, 10.0, 10, 10), circle(4.0, 5.0, 2.0, 16), circle(6.0, 5.0, 2.0, 16)]));
// Bad size fields.
for h in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert!(matches!(free_quad_mesh(std::slice::from_ref(&ok), &UniformSize(h), &opts()), Err(Error::Invalid(_))), "{h}");
}
// Limits.
assert!(matches!(free_quad_mesh(std::slice::from_ref(&ok), &UniformSize(1e-4), &opts()), Err(Error::Limit(_))));
assert!(matches!(free_quad_mesh(&[ok], &UniformSize(0.1), &QuadOptions { max_elements: 10, ..opts() }), Err(Error::Limit(_))));
}
#[test]
fn random_boundaries_never_panic() {
// Pseudo-random jagged loops (a fixed linear congruential sequence), every point a corner:
// every result is a valid mesh or an input error.
let mut seed = 12345u64;
let mut next = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((seed >> 33) as f64) / (1u64 << 31) as f64
};
let (mut meshed, mut rejected) = (0, 0);
for case in 0..200 {
let n = 6 + 2 * (case % 10);
let mut loops = vec![
(0..n)
.map(|k| {
let t = 2.0 * PI * k as f64 / n as f64;
let r = 1.0 + 3.0 * next();
[r * t.cos(), r * t.sin()]
})
.collect::<Vec<_>>(),
];
if case % 3 == 0 {
loops.push(circle(0.0, 0.0, 0.5 * next(), 6));
}
if case % 7 == 0 {
loops[0] = (0..n).map(|_| [10.0 * next() - 5.0, 10.0 * next() - 5.0]).collect();
}
match free_quad_mesh(&loops, &UniformSize(0.2 + next()), &opts()) {
Ok(m) => {
check(&m, &loops);
meshed += 1;
}
Err(Error::Invalid(_)) => rejected += 1,
Err(e) => panic!("case {case}: {e}"),
}
}
// Every valid region is meshed; the rest are self-intersecting loops.
assert_eq!(meshed + rejected, 200);
assert!(meshed > 150, "{meshed}");
}
#[test]
fn mapped_rectangle_is_the_grid() {
let l = rect(0.0, 0.0, 4.0, 2.0, 4, 2);
let m = mapped_quad_mesh(&l, [0, 4, 6, 10], &opts()).unwrap();
assert_eq!((m.nu, m.nv), (4, 2));
assert_eq!(m.mesh.quads.len(), 8);
check(&m.mesh, std::slice::from_ref(&l));
for j in 0..=2 {
for i in 0..=4 {
assert_eq!(m.mesh.nodes[m.grid[j * 5 + i] as usize], [i as f64, j as f64]);
}
}
assert!((m.mesh.stats.min_scaled_jacobian - 1.0).abs() < 1e-12);
assert!(m.mesh.stats.max_skew < 1e-12);
}
#[test]
fn mapped_curved_and_graded() {
// A quarter annulus, radii 1 and 2, graded radially.
let (na, nr) = (8, 5);
let r = |j: usize| 1.0 + (j as f64 / nr as f64).powi(2);
let mut l = Vec::new();
for j in 0..nr {
l.push([r(j), 0.0]);
}
for i in 0..na {
let t = 0.5 * PI * i as f64 / na as f64;
l.push([2.0 * t.cos(), 2.0 * t.sin()]);
}
for j in 0..nr {
l.push([0.0, r(nr - j)]);
}
for i in 0..na {
let t = 0.5 * PI * (na - i) as f64 / na as f64;
l.push([t.cos(), t.sin()]);
}
let corners = [0, nr, nr + na, 2 * nr + na];
let m = mapped_quad_mesh(&l, corners, &opts()).unwrap();
check(&m.mesh, std::slice::from_ref(&l));
// The same region given clockwise.
let mut cw = l.clone();
cw.reverse();
let n = cw.len();
let m = mapped_quad_mesh(&cw, [n - 1 - corners[3], n - 1 - corners[2], n - 1 - corners[1], n - 1 - corners[0]], &opts()).unwrap();
check(&m.mesh, &[cw]);
}
#[test]
fn mapped_hostile() {
let l = rect(0.0, 0.0, 4.0, 2.0, 4, 2);
assert!(mapped_quad_mesh(&l, [0, 4, 6, 11], &opts()).is_err()); // unequal opposite sides
assert!(mapped_quad_mesh(&l, [0, 6, 4, 10], &opts()).is_err()); // out of order
assert!(mapped_quad_mesh(&l, [0, 4, 6, 99], &opts()).is_err());
assert!(mapped_quad_mesh(&l, [0, 0, 6, 10], &opts()).is_err());
let mut nan = l.clone();
nan[5] = [f64::NAN, 0.0];
assert!(mapped_quad_mesh(&nan, [0, 4, 6, 10], &opts()).is_err());
// A region whose TFI folds: the top side dips almost to the bottom between its corners.
let fold = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0], [4.0, 0.0], [4.0, 4.0], [3.0, 4.0], [2.2, 0.2], [1.8, 0.2], [1.0, 4.0]];
let r = mapped_quad_mesh(&fold, [0, 4, 5, 9], &opts());
assert!(matches!(r, Err(Error::Failed(_)) | Err(Error::Invalid(_))), "{r:?}");
}
#[test]
fn transfinite_in_space() {
let bottom = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let top = [[0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [2.0, 1.0, 1.0]];
let left = [[0.0, 0.0, 0.0], [0.0, 1.0, 1.0]];
let right = [[2.0, 0.0, 0.0], [2.0, 1.0, 1.0]];
let g = transfinite_grid(&bottom, &right, &top, &left).unwrap();
assert_eq!(g.len(), 6);
assert_eq!(g[4], [1.0, 1.0, 1.0]);
let side = |a: [f64; 2], b: [f64; 2]| [a, [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0], b];
let g =
transfinite_grid(&side([0.0, 0.0], [2.0, 0.0]), &side([2.0, 0.0], [2.0, 2.0]), &side([0.0, 2.0], [2.0, 2.0]), &side([0.0, 0.0], [0.0, 2.0]))
.unwrap();
assert_eq!(g[4], [1.0, 1.0]);
// Corners that do not match, a zero-length side, empty sides.
assert!(transfinite_grid(&bottom, &right, &top, &[[0.0, 0.0, 0.1], [0.0, 1.0, 1.0]]).is_err());
assert!(transfinite_grid(&[[0.0; 2], [0.0; 2]], &[[0.0; 2], [0.0, 1.0]], &[[0.0, 1.0], [0.0, 1.0]], &[[0.0; 2], [0.0, 1.0]]).is_err());
assert!(transfinite_grid::<2>(&[], &[], &[], &[]).is_err());
}
#[test]
fn planar_quad_metrics() {
use crate::quality::{Metric, planar_quad_quality, planar_quad_validity};
let rect = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]];
let q = planar_quad_quality(&rect).unwrap();
assert_eq!(q.validity(), Some(Validity::Valid));
assert_eq!(q.size, 2.0);
assert_eq!(q.get(Metric::AspectRatio), Some(2.0));
assert_eq!(q.get(Metric::Skew), Some(0.0));
assert_eq!(q.get(Metric::ScaledJacobian), Some(1.0));
assert_eq!(q.get(Metric::JacobianRatio), Some(1.0));
// A rhombus with 60° corners: its principal axes are its edge directions, so skew cos 60°
// and aspect 1; the scaled Jacobian is sin 60°.
let h = 3.0_f64.sqrt() / 2.0;
let rh = [[0.0, 0.0], [1.0, 0.0], [1.5, h], [0.5, h]];
let q = planar_quad_quality(&rh).unwrap();
assert!((q.get(Metric::ScaledJacobian).unwrap() - h).abs() < 1e-12);
assert!((q.get(Metric::Skew).unwrap() - 0.5).abs() < 1e-12);
assert!((q.get(Metric::AspectRatio).unwrap() - 1.0).abs() < 1e-12);
// Clockwise, bow tie, concave (dart), degenerate (a straight corner): invalid by the exact test.
let cw = [rect[0], rect[3], rect[2], rect[1]];
assert_eq!(planar_quad_validity(&cw).unwrap(), Validity::Invalid);
assert_eq!(planar_quad_validity(&[[0.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]).unwrap(), Validity::Invalid);
assert_eq!(planar_quad_validity(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.7, 0.3]]).unwrap(), Validity::Invalid);
assert_eq!(planar_quad_validity(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [0.0, 1.0]]).unwrap(), Validity::Invalid);
// A corner off by one ulp from straight is decided exactly.
let x = 1.0 + f64::EPSILON;
assert_eq!(planar_quad_validity(&[[0.0, 0.0], [1.0, 0.0], [2.0, x - 1.0], [0.0, 1.0]]).unwrap(), Validity::Valid);
assert!(planar_quad_validity(&[[0.0, 0.0], [1.0, f64::NAN], [1.0, 1.0], [0.0, 1.0]]).is_err());
let q = planar_quad_quality(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [0.0, 1.0]]).unwrap();
assert_eq!(q.get(Metric::JacobianRatio), Some(0.0));
}
#[test]
#[ignore = "timing and quality report: cargo test --release -p fieldcraft-mesh quad_report -- --ignored --nocapture"]
fn quad_report() {
let cases: Vec<(&str, Vec<Vec<[f64; 2]>>, f64)> = vec![
("rectangle", vec![rect(0.0, 0.0, 10.0, 5.0, 20, 10)], 0.5),
("plate with hole", vec![rect(0.0, 0.0, 20.0, 10.0, 40, 20), circle(10.0, 5.0, 2.5, 32)], 0.5),
("star", vec![subdivide(&star(10.0, 4.0), 4)], 1.0),
("large plate", vec![rect(0.0, 0.0, 200.0, 100.0, 1000, 500), circle(50.0, 50.0, 20.0, 400), circle(150.0, 50.0, 20.0, 400)], 0.2),
];
for (name, loops, h) in cases {
let t = std::time::Instant::now();
let m = free_quad_mesh(&loops, &UniformSize(h), &opts()).unwrap();
println!("{name}: {} quads in {:?}, {:?}", m.quads.len(), t.elapsed(), m.stats);
let t = std::time::Instant::now();
let _ = free_quad_mesh(&loops, &UniformSize(h), &QuadOptions { smoothing_sweeps: 0, ..opts() }).unwrap();
println!(" without smoothing {:?}", t.elapsed());
}
}
#[test]
fn unsmoothed_meshes_are_valid() {
// Regression: recombination once accepted nearly straight corners that the split turned
// degenerate after rounding the mid nodes (this plate had one before the quality floor).
let loops = vec![rect(0.0, 0.0, 60.0, 30.0, 300, 150), circle(15.0, 15.0, 6.0, 240)];
let m = free_quad_mesh(&loops, &UniformSize(0.2), &QuadOptions { smoothing_sweeps: 0, ..opts() }).unwrap();
check(&m, &loops);
}
#[test]
fn free_triangle_mesh_of_a_plate_with_a_hole() {
// Odd point counts are fine for triangles.
let outer = rect(0.0, 0.0, 10.0, 6.0, 9, 5);
let hole: Vec<[f64; 2]> = circle(4.0, 3.0, 1.5, 17).into_iter().rev().collect();
let loops = vec![outer.clone(), hole.clone()];
let m = free_tri_mesh(&loops, &UniformSize(0.7), &opts()).unwrap();
assert_eq!(m.boundary_nodes, outer.len() + hole.len());
let boundary: Vec<[f64; 2]> = outer.iter().chain(&hole).copied().collect();
assert_eq!(&m.nodes[..m.boundary_nodes], boundary.as_slice(), "boundary nodes are the input points");
let area: f64 = m
.triangles
.iter()
.map(|t| {
let [a, b, c] = t.map(|v| m.nodes[v as usize]);
let s = 0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]));
assert!(s > 0.0, "counterclockwise");
s
})
.sum();
let want = signed_area(&outer).abs() - signed_area(&hole).abs();
assert!((area - want).abs() < 1e-9 * want, "{area} vs {want}");
assert!(m.triangles.len() > 100, "refined to the size: {}", m.triangles.len());
// Deterministic.
assert_eq!(free_tri_mesh(&loops, &UniformSize(0.7), &opts()).unwrap(), m);
// Hostile.
assert!(free_tri_mesh(&[vec![[0.0, 0.0], [1.0, 0.0]]], &UniformSize(0.5), &opts()).is_err());
assert!(free_tri_mesh(std::slice::from_ref(&outer), &UniformSize(0.001), &QuadOptions { max_elements: 100, ..opts() }).is_err());
assert!(free_tri_mesh(&[vec![[0.0, 0.0], [f64::NAN, 0.0], [0.0, 1.0]]], &UniformSize(0.5), &opts()).is_err());
}