CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
Documentation / Build API Documentation (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 6s
Documentation / Build User Guide (push) Successful in 6s
CI / Format Check (push) Failing after 14s
CI / Clippy Check (push) Failing after 48s
CI / Build CPU-Only (Explicit) (push) Failing after 2m21s
Performance Benchmarks / Run Benchmarks (push) Successful in 4m4s
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
1225 lines
44 KiB
Rust
1225 lines
44 KiB
Rust
//! Patch generators: transfinite (linear-blend) interpolation between two
|
||
//! curves with geometric stretching across, and the test shapes the
|
||
//! curvilinear solver is verified on (`docs/overset_metal_campaign.md`
|
||
//! §2.2 P0 and §5.3).
|
||
|
||
use super::patch_mesh::PatchMesh;
|
||
use crate::error::CfdResult;
|
||
use std::f64::consts::PI;
|
||
|
||
/// Fractions `η_k ∈ [0, 1]`, `k = 0..=nn`, with spacing growing
|
||
/// geometrically by the total factor `stretch` (last / first); `1` is
|
||
/// uniform.
|
||
pub fn stretched_fractions(nn: usize, stretch: f64) -> Vec<f64> {
|
||
if nn == 0 {
|
||
return vec![0.0];
|
||
}
|
||
if (stretch - 1.0).abs() < 1e-12 || nn == 1 {
|
||
return (0..=nn).map(|k| k as f64 / nn as f64).collect();
|
||
}
|
||
let g = stretch.powf(1.0 / (nn as f64 - 1.0));
|
||
let total = (1.0 - g.powi(nn as i32)) / (1.0 - g);
|
||
let mut eta = Vec::with_capacity(nn + 1);
|
||
let mut acc = 0.0;
|
||
eta.push(0.0);
|
||
for k in 0..nn {
|
||
acc += g.powi(k as i32) / total;
|
||
eta.push(acc);
|
||
}
|
||
eta[nn] = 1.0;
|
||
eta
|
||
}
|
||
|
||
/// Linear-blend transfinite interpolation between `inner` (row 0) and
|
||
/// `outer` (row nn), both with `ns + 1` points (the last repeating the
|
||
/// first plus `shift` when periodic). Straight rays between corresponding
|
||
/// points; `stretch` is the across-patch spacing ratio.
|
||
pub fn transfinite(
|
||
inner: &[[f64; 2]],
|
||
outer: &[[f64; 2]],
|
||
nn: usize,
|
||
stretch: f64,
|
||
periodic: Option<[f64; 2]>,
|
||
) -> CfdResult<PatchMesh> {
|
||
assert_eq!(inner.len(), outer.len(), "curves need equal point counts");
|
||
let ns = inner.len() - 1;
|
||
let eta = stretched_fractions(nn, stretch);
|
||
let mut x = Vec::with_capacity((nn + 1) * (ns + 1));
|
||
let mut y = Vec::with_capacity((nn + 1) * (ns + 1));
|
||
for &e in &eta {
|
||
for i in 0..=ns {
|
||
x.push((1.0 - e) * inner[i][0] + e * outer[i][0]);
|
||
y.push((1.0 - e) * inner[i][1] + e * outer[i][1]);
|
||
}
|
||
}
|
||
PatchMesh::from_nodes(ns, nn, x, y, periodic)
|
||
}
|
||
|
||
/// A uniform Cartesian patch: `s = x`, `n = y`. Non-periodic unless
|
||
/// `periodic_x`, in which case the seam carries the shift `(lx, 0)`.
|
||
pub fn cartesian(nx: usize, ny: usize, lx: f64, ly: f64, periodic_x: bool) -> CfdResult<PatchMesh> {
|
||
let (dx, dy) = (lx / nx as f64, ly / ny as f64);
|
||
let mut x = Vec::with_capacity((ny + 1) * (nx + 1));
|
||
let mut y = Vec::with_capacity(x.capacity());
|
||
for k in 0..=ny {
|
||
for i in 0..=nx {
|
||
x.push(i as f64 * dx);
|
||
y.push(k as f64 * dy);
|
||
}
|
||
}
|
||
PatchMesh::from_nodes(nx, ny, x, y, periodic_x.then_some([lx, 0.0]))
|
||
}
|
||
|
||
/// An O-grid annulus around the circle of radius `r0` centred at `centre`
|
||
/// out to a wobbly outer ring of mean radius `r1`: the outer points are
|
||
/// rotated by `skew * sin(θ)` and their radius modulated by
|
||
/// `1 + 0.1 · skew · sin(2θ)`, so the rays are non-orthogonal to the
|
||
/// rings and the cells are skewed smoothly. `skew = 0` gives the polar
|
||
/// grid. Periodic in s. `s` runs CLOCKWISE (the body on the right, `n`
|
||
/// outward): that is the right-handed `(s, n)` frame `PatchMesh` needs.
|
||
pub fn annulus_skewed(
|
||
centre: [f64; 2],
|
||
r0: f64,
|
||
r1: f64,
|
||
ns: usize,
|
||
nn: usize,
|
||
skew: f64,
|
||
stretch: f64,
|
||
) -> CfdResult<PatchMesh> {
|
||
let mut inner = Vec::with_capacity(ns + 1);
|
||
let mut outer = Vec::with_capacity(ns + 1);
|
||
for i in 0..=ns {
|
||
let th = -2.0 * PI * (i % ns) as f64 / ns as f64;
|
||
inner.push([centre[0] + r0 * th.cos(), centre[1] + r0 * th.sin()]);
|
||
let th_o = th + skew * th.sin();
|
||
let r_o = r1 * (1.0 + 0.1 * skew * (2.0 * th).sin());
|
||
outer.push([centre[0] + r_o * th_o.cos(), centre[1] + r_o * th_o.sin()]);
|
||
}
|
||
transfinite(&inner, &outer, nn, stretch, Some([0.0, 0.0]))
|
||
}
|
||
|
||
/// A channel `[0, lx] × [0, ly]` sheared affinely: `x' = x + alpha · y`.
|
||
/// All cells are congruent parallelograms; the n-faces stay horizontal.
|
||
pub fn channel_sheared(
|
||
lx: f64,
|
||
ly: f64,
|
||
nx: usize,
|
||
ny: usize,
|
||
alpha: f64,
|
||
periodic_x: bool,
|
||
) -> CfdResult<PatchMesh> {
|
||
let (dx, dy) = (lx / nx as f64, ly / ny as f64);
|
||
let mut x = Vec::with_capacity((ny + 1) * (nx + 1));
|
||
let mut y = Vec::with_capacity(x.capacity());
|
||
for k in 0..=ny {
|
||
let yy = k as f64 * dy;
|
||
for i in 0..=nx {
|
||
x.push(i as f64 * dx + alpha * yy);
|
||
y.push(yy);
|
||
}
|
||
}
|
||
PatchMesh::from_nodes(nx, ny, x, y, periodic_x.then_some([lx, 0.0]))
|
||
}
|
||
|
||
/// A channel whose skew varies smoothly along x: `x' = x + alpha · y ·
|
||
/// sin(2π x / lx)` (zero at both ends, so it can be periodic), with the
|
||
/// across-channel spacing stretched by `stretch` toward the top wall.
|
||
/// Folds when `2π alpha ly / lx > 1`; keep `alpha` around 0.1.
|
||
pub fn channel_varying_skew(
|
||
lx: f64,
|
||
ly: f64,
|
||
nx: usize,
|
||
ny: usize,
|
||
alpha: f64,
|
||
stretch: f64,
|
||
periodic_x: bool,
|
||
) -> CfdResult<PatchMesh> {
|
||
let dx = lx / nx as f64;
|
||
let eta = stretched_fractions(ny, stretch);
|
||
let mut x = Vec::with_capacity((ny + 1) * (nx + 1));
|
||
let mut y = Vec::with_capacity(x.capacity());
|
||
for &e in &eta {
|
||
let yy = e * ly;
|
||
for i in 0..=nx {
|
||
let xx = i as f64 * dx;
|
||
x.push(xx + alpha * yy * (2.0 * PI * xx / lx).sin());
|
||
y.push(yy);
|
||
}
|
||
}
|
||
PatchMesh::from_nodes(nx, ny, x, y, periodic_x.then_some([lx, 0.0]))
|
||
}
|
||
|
||
/// Spacings along a straight of `length` that grow geometrically from `d0`
|
||
/// at both ends (ratio `ratio`) to at most `d1` in the middle, symmetric;
|
||
/// returns the cumulative fractions `0 ..= 1`. The count adapts to the
|
||
/// length.
|
||
pub fn graded_fractions(length: f64, d0: f64, d1: f64, ratio: f64) -> Vec<f64> {
|
||
// One end's graded run, until the spacing reaches d1 or half the length.
|
||
let mut run = Vec::new();
|
||
let mut d = d0;
|
||
let mut acc = 0.0;
|
||
while d < d1 && acc + d < 0.5 * length {
|
||
run.push(d);
|
||
acc += d;
|
||
d *= ratio;
|
||
}
|
||
let middle = length - 2.0 * acc;
|
||
let n_mid = ((middle / d1).round() as usize).max(1);
|
||
let d_mid = middle / n_mid as f64;
|
||
let mut spacings = run.clone();
|
||
spacings.extend(std::iter::repeat_n(d_mid, n_mid));
|
||
spacings.extend(run.iter().rev());
|
||
let total: f64 = spacings.iter().sum();
|
||
let mut fr = Vec::with_capacity(spacings.len() + 1);
|
||
let mut s = 0.0;
|
||
fr.push(0.0);
|
||
for w in &spacings {
|
||
s += w;
|
||
fr.push(s / total);
|
||
}
|
||
let last = fr.len() - 1;
|
||
fr[last] = 1.0;
|
||
fr
|
||
}
|
||
|
||
/// An O-grid around a STADIUM (a rectangle of half-length `hx` and
|
||
/// half-thickness `r` with semicircular ends of radius `r`, the plate of
|
||
/// the fresh-cell falsifier rounded at its ends) centred at `centre`: the
|
||
/// inner ring is the stadium, the outer ring its normal offset by
|
||
/// `offset`, so every ray is a normal (orthogonal cells). Along the body
|
||
/// the straights are graded from the arc spacing `d0 = π r / k_arc` at the
|
||
/// tangent points to `d_straight` in the middle (ratio `grade`); each end
|
||
/// arc carries `k_arc` cells. Across, `nn` cells with the geometric
|
||
/// `stretch` (wall spacing = `offset · (g − 1)/(g^nn − 1)`). `s` runs
|
||
/// CLOCKWISE (the right-handed frame `PatchMesh` needs), starting at the
|
||
/// top-left tangent point. Periodic.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn stadium(
|
||
centre: [f64; 2],
|
||
hx: f64,
|
||
r: f64,
|
||
offset: f64,
|
||
k_arc: usize,
|
||
d_straight: f64,
|
||
grade: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
) -> CfdResult<PatchMesh> {
|
||
let a = hx - r; // half-length of the straights
|
||
let d0 = PI * r / k_arc as f64;
|
||
let straight = graded_fractions(2.0 * a, d0, d_straight, grade);
|
||
let mut inner = Vec::new();
|
||
let mut outer = Vec::new();
|
||
let mut push = |p: [f64; 2], n: [f64; 2]| {
|
||
inner.push([centre[0] + p[0], centre[1] + p[1]]);
|
||
outer.push([
|
||
centre[0] + p[0] + offset * n[0],
|
||
centre[1] + p[1] + offset * n[1],
|
||
]);
|
||
};
|
||
// Top straight, left → right (clockwise around the body).
|
||
for &f in &straight[..straight.len() - 1] {
|
||
push([-a + f * 2.0 * a, r], [0.0, 1.0]);
|
||
}
|
||
// Right arc, from +90° down to −90° (exclusive of both ends' duplicates
|
||
// handled by the straights: include angles strictly between).
|
||
for k in 0..k_arc {
|
||
let th = PI / 2.0 - PI * k as f64 / k_arc as f64;
|
||
let (s, c) = th.sin_cos();
|
||
push([a + r * c, r * s], [c, s]);
|
||
}
|
||
// Bottom straight, right → left.
|
||
for &f in &straight[..straight.len() - 1] {
|
||
push([a - f * 2.0 * a, -r], [0.0, -1.0]);
|
||
}
|
||
// Left arc, from −90° down to −270°.
|
||
for k in 0..k_arc {
|
||
let th = -PI / 2.0 - PI * k as f64 / k_arc as f64;
|
||
let (s, c) = th.sin_cos();
|
||
push([-a + r * c, r * s], [c, s]);
|
||
}
|
||
// Close the ring: the last point repeats the first.
|
||
inner.push(inner[0]);
|
||
outer.push(outer[0]);
|
||
transfinite(&inner, &outer, nn, stretch, Some([0.0, 0.0]))
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
/// Cumulative arclength fractions of a closed polyline (`pts[0]` repeated
|
||
/// as the implicit last point): `frac[i]` for `i = 0..=n`, `frac[n] = 1`.
|
||
fn arclength_fractions(pts: &[[f64; 2]]) -> Vec<f64> {
|
||
let n = pts.len();
|
||
let mut cum = Vec::with_capacity(n + 1);
|
||
let mut s = 0.0;
|
||
cum.push(0.0);
|
||
for i in 0..n {
|
||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||
s += ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
|
||
cum.push(s);
|
||
}
|
||
cum.iter().map(|c| c / s).collect()
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
/// Point at arclength fraction `f` of the closed polyline `pts`.
|
||
fn point_at_fraction(pts: &[[f64; 2]], cum: &[f64], f: f64) -> [f64; 2] {
|
||
let n = pts.len();
|
||
let f = f.clamp(0.0, 1.0);
|
||
// cum has n+1 entries, cum[i] .. cum[i+1] is segment i.
|
||
let mut i = match cum.binary_search_by(|c| c.partial_cmp(&f).unwrap()) {
|
||
Ok(k) => k.min(n - 1),
|
||
Err(k) => k.saturating_sub(1).min(n - 1),
|
||
};
|
||
while i + 1 < cum.len() - 1 && cum[i + 1] < f {
|
||
i += 1;
|
||
}
|
||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||
let seg = cum[i + 1] - cum[i];
|
||
let t = if seg > 0.0 { (f - cum[i]) / seg } else { 0.0 };
|
||
[a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])]
|
||
}
|
||
|
||
/// Nearest point on the closed polyline `pts` to `q` (segment projection).
|
||
fn nearest_on_polyline(pts: &[[f64; 2]], q: [f64; 2]) -> [f64; 2] {
|
||
let n = pts.len();
|
||
let (mut best, mut best_d) = (pts[0], f64::INFINITY);
|
||
for i in 0..n {
|
||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||
let l2 = dx * dx + dy * dy;
|
||
let t = if l2 > 0.0 {
|
||
(((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / l2).clamp(0.0, 1.0)
|
||
} else {
|
||
0.0
|
||
};
|
||
let p = [a[0] + t * dx, a[1] + t * dy];
|
||
let d = (p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2);
|
||
if d < best_d {
|
||
best_d = d;
|
||
best = p;
|
||
}
|
||
}
|
||
best
|
||
}
|
||
|
||
/// Points on the circular arc of centre `c`, radius `r`, from angle `a0` to
|
||
/// `a1` (radians, signed sweep), `k` segments, EXCLUDING the end point.
|
||
fn arc_points(c: [f64; 2], r: f64, a0: f64, a1: f64, k: usize) -> Vec<[f64; 2]> {
|
||
(0..k)
|
||
.map(|m| {
|
||
let th = a0 + (a1 - a0) * m as f64 / k as f64;
|
||
[c[0] + r * th.cos(), c[1] + r * th.sin()]
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// The Turek–Hron rigid body — the cylinder of radius `r` at `centre` with
|
||
/// the flag of half-thickness `t` reaching to `x_tip` — as a closed
|
||
/// COUNTER-CLOCKWISE outline: the flag tip a semicircle of radius `t`, the
|
||
/// two concave junctions filleted with radius `fillet` (`k_fillet` cells
|
||
/// each), straights graded from the tip/fillet spacing to `d_straight`
|
||
/// (ratio `grade`), the cylinder arc at ≈ `d_straight`, the tip arc with
|
||
/// `k_tip` cells. Starts at the tip's topmost point.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_outline(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
x_tip: f64,
|
||
fillet: f64,
|
||
k_fillet: usize,
|
||
k_tip: usize,
|
||
d_straight: f64,
|
||
grade: f64,
|
||
) -> Vec<[f64; 2]> {
|
||
let (cx, cy) = (centre[0], centre[1]);
|
||
let (y_top, y_bot) = (cy + t, cy - t);
|
||
let tip_c = [x_tip - t, cy];
|
||
// Junction x on the circle, and the fillet centre F (in the fluid,
|
||
// tangent to the flag line and externally to the cylinder).
|
||
let x_f = cx + ((r + fillet).powi(2) - (t + fillet).powi(2)).sqrt();
|
||
let f_top = [x_f, y_top + fillet];
|
||
let f_bot = [x_f, y_bot - fillet];
|
||
// Tangent points: on the flag line straight below/above F; on the circle
|
||
// along C → F.
|
||
let t1_top = [x_f, y_top];
|
||
let dir_top = [
|
||
(f_top[0] - cx) / (r + fillet),
|
||
(f_top[1] - cy) / (r + fillet),
|
||
];
|
||
let t2_top = [cx + r * dir_top[0], cy + r * dir_top[1]];
|
||
let t1_bot = [x_f, y_bot];
|
||
let dir_bot = [
|
||
(f_bot[0] - cx) / (r + fillet),
|
||
(f_bot[1] - cy) / (r + fillet),
|
||
];
|
||
let t2_bot = [cx + r * dir_bot[0], cy + r * dir_bot[1]];
|
||
let d0 = (PI * t / k_tip as f64).min(fillet * PI / 2.0 / k_fillet as f64);
|
||
|
||
let mut pts: Vec<[f64; 2]> = Vec::new();
|
||
// 1. Top edge, from the tip top (x_tip − t, y_top) to the fillet tangent
|
||
// point (x_f, y_top), moving −x (CCW: body on the left).
|
||
let len_top = (x_tip - t) - x_f;
|
||
let fr = graded_fractions(len_top, d0, d_straight, grade);
|
||
for &f in &fr[..fr.len() - 1] {
|
||
pts.push([(x_tip - t) - f * len_top, y_top]);
|
||
}
|
||
// 2. Top fillet, from T1 (angle −90° about F) to T2 (angle of C − F),
|
||
// the short way.
|
||
let a_t1 = -PI / 2.0;
|
||
let mut a_t2 = (t2_top[1] - f_top[1]).atan2(t2_top[0] - f_top[0]);
|
||
while a_t2 - a_t1 > PI {
|
||
a_t2 -= 2.0 * PI;
|
||
}
|
||
while a_t2 - a_t1 < -PI {
|
||
a_t2 += 2.0 * PI;
|
||
}
|
||
pts.extend(arc_points(f_top, fillet, a_t1, a_t2, k_fillet));
|
||
// 3. Cylinder arc, CCW from angle(T2_top) to angle(T2_bot) + 2π.
|
||
let th_top = (t2_top[1] - cy).atan2(t2_top[0] - cx);
|
||
let th_bot = (t2_bot[1] - cy).atan2(t2_bot[0] - cx) + 2.0 * PI;
|
||
let arc_len = (th_bot - th_top) * r;
|
||
let k_arc = ((arc_len / d_straight).round() as usize).max(8);
|
||
pts.extend(arc_points(centre, r, th_top, th_bot, k_arc));
|
||
// 4. Bottom fillet, from T2_bot to T1_bot (angle +90° about F_bot).
|
||
let a_b2 = (t2_bot[1] - f_bot[1]).atan2(t2_bot[0] - f_bot[0]);
|
||
let mut a_b1 = PI / 2.0;
|
||
while a_b1 - a_b2 > PI {
|
||
a_b1 -= 2.0 * PI;
|
||
}
|
||
while a_b1 - a_b2 < -PI {
|
||
a_b1 += 2.0 * PI;
|
||
}
|
||
pts.extend(arc_points(f_bot, fillet, a_b2, a_b1, k_fillet));
|
||
// 5. Bottom edge, from (x_f, y_bot) to the tip bottom, moving +x.
|
||
for &f in &fr[..fr.len() - 1] {
|
||
pts.push([x_f + f * len_top, y_bot]);
|
||
}
|
||
// 6. Tip semicircle, from −90° to +90° about the tip centre (CCW).
|
||
pts.extend(arc_points(tip_c, t, -PI / 2.0, PI / 2.0, k_tip));
|
||
let _ = (t1_top, t1_bot);
|
||
pts
|
||
}
|
||
|
||
/// The 2-D convex hull (Andrew's monotone chain), counter-clockwise.
|
||
fn convex_hull(mut pts: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
|
||
pts.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
pts.dedup();
|
||
if pts.len() < 3 {
|
||
return pts;
|
||
}
|
||
let cross = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| {
|
||
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||
};
|
||
let mut lower: Vec<[f64; 2]> = Vec::new();
|
||
for &p in &pts {
|
||
while lower.len() >= 2 && cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0 {
|
||
lower.pop();
|
||
}
|
||
lower.push(p);
|
||
}
|
||
let mut upper: Vec<[f64; 2]> = Vec::new();
|
||
for &p in pts.iter().rev() {
|
||
while upper.len() >= 2 && cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0 {
|
||
upper.pop();
|
||
}
|
||
upper.push(p);
|
||
}
|
||
lower.pop();
|
||
upper.pop();
|
||
lower.extend(upper);
|
||
lower
|
||
}
|
||
|
||
/// The normal offset by `d` of a convex counter-clockwise polygon, as a
|
||
/// fine polyline (`per_vertex` points on each rounded corner).
|
||
fn offset_convex_polygon(hull: &[[f64; 2]], d: f64, per_vertex: usize) -> Vec<[f64; 2]> {
|
||
let n = hull.len();
|
||
let mut out = Vec::new();
|
||
for i in 0..n {
|
||
let prev = hull[(i + n - 1) % n];
|
||
let cur = hull[i];
|
||
let next = hull[(i + 1) % n];
|
||
// Outward normals of the incoming and outgoing edges (CCW polygon:
|
||
// outward = right-hand normal (dy, −dx)).
|
||
let n_in = {
|
||
let (dx, dy) = (cur[0] - prev[0], cur[1] - prev[1]);
|
||
let l = (dx * dx + dy * dy).sqrt().max(1e-300);
|
||
[dy / l, -dx / l]
|
||
};
|
||
let n_out = {
|
||
let (dx, dy) = (next[0] - cur[0], next[1] - cur[1]);
|
||
let l = (dx * dx + dy * dy).sqrt().max(1e-300);
|
||
[dy / l, -dx / l]
|
||
};
|
||
let a0 = n_in[1].atan2(n_in[0]);
|
||
let mut a1 = n_out[1].atan2(n_out[0]);
|
||
while a1 < a0 {
|
||
a1 += 2.0 * PI;
|
||
}
|
||
// Rounded corner around `cur` from n_in to n_out (CCW sweep ≤ π).
|
||
let k = if a1 - a0 > 1e-9 { per_vertex } else { 1 };
|
||
for m in 0..k {
|
||
let th = a0 + (a1 - a0) * m as f64 / k as f64;
|
||
out.push([cur[0] + d * th.cos(), cur[1] + d * th.sin()]);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Winslow (TTM) smoothing of the interior nodes of a periodic structured
|
||
/// grid `x[k][i]`, `k = 0..=nn` (row 0 fixed; row nn fixed, or SLIDING on
|
||
/// the closed curve `outer_curve` when given: after each interior sweep
|
||
/// every outer node is re-placed at the nearest point of the curve to the
|
||
/// extrapolated ray from rows nn−2, nn−1 — the distribution on the outer
|
||
/// ring follows the interior instead of dictating it), `i = 0..ns`
|
||
/// (column `ns` mirrors column 0). Gauss–Seidel until the largest node
|
||
/// move is below `tol` or `max_sweeps`. Returns `(sweeps, last move)`.
|
||
pub fn winslow_smooth(
|
||
x: &mut [Vec<[f64; 2]>],
|
||
ns: usize,
|
||
tol: f64,
|
||
max_sweeps: usize,
|
||
outer_curve: Option<&[[f64; 2]]>,
|
||
) -> (usize, f64) {
|
||
let nn = x.len() - 1;
|
||
let mut sweeps = 0;
|
||
let mut moved = f64::INFINITY;
|
||
while sweeps < max_sweeps && moved > tol {
|
||
sweeps += 1;
|
||
moved = 0.0;
|
||
if let Some(curve) = outer_curve {
|
||
for i in 0..ns {
|
||
let (a, b) = (x[nn - 2][i], x[nn - 1][i]);
|
||
let cand = [2.0 * b[0] - a[0], 2.0 * b[1] - a[1]];
|
||
let p = nearest_on_polyline(curve, cand);
|
||
let d = ((p[0] - x[nn][i][0]).powi(2) + (p[1] - x[nn][i][1]).powi(2)).sqrt();
|
||
moved = moved.max(d);
|
||
x[nn][i] = p;
|
||
}
|
||
x[nn][ns] = x[nn][0];
|
||
}
|
||
for k in 1..nn {
|
||
for i in 0..ns {
|
||
let ip = (i + 1) % ns;
|
||
let im = (i + ns - 1) % ns;
|
||
let (xe, xw, xn, xs) = (x[k][ip], x[k][im], x[k + 1][i], x[k - 1][i]);
|
||
let (xne, xnw, xse, xsw) = (x[k + 1][ip], x[k + 1][im], x[k - 1][ip], x[k - 1][im]);
|
||
let xi = [0.5 * (xe[0] - xw[0]), 0.5 * (xe[1] - xw[1])];
|
||
let eta = [0.5 * (xn[0] - xs[0]), 0.5 * (xn[1] - xs[1])];
|
||
let g11 = xi[0] * xi[0] + xi[1] * xi[1];
|
||
let g22 = eta[0] * eta[0] + eta[1] * eta[1];
|
||
let g12 = xi[0] * eta[0] + xi[1] * eta[1];
|
||
let denom = 2.0 * (g11 + g22);
|
||
if denom <= 1e-300 {
|
||
continue;
|
||
}
|
||
let mut new = [0.0; 2];
|
||
for c in 0..2 {
|
||
let cross = 0.25 * (xne[c] - xnw[c] - xse[c] + xsw[c]);
|
||
new[c] =
|
||
(g22 * (xe[c] + xw[c]) + g11 * (xn[c] + xs[c]) - 2.0 * g12 * cross) / denom;
|
||
}
|
||
let d = ((new[0] - x[k][i][0]).powi(2) + (new[1] - x[k][i][1]).powi(2)).sqrt();
|
||
moved = moved.max(d);
|
||
x[k][i] = new;
|
||
}
|
||
x[k][ns] = x[k][0];
|
||
}
|
||
}
|
||
(sweeps, moved)
|
||
}
|
||
|
||
/// Re-space the nodes of every column (ray) of `x` along its polyline to the
|
||
/// fractions `eta` (keeps the smoothed ray SHAPES, restores the across-body
|
||
/// stretch Winslow equidistributes away).
|
||
pub fn respace_rays(x: &mut [Vec<[f64; 2]>], ns: usize, eta: &[f64]) {
|
||
let nn = x.len() - 1;
|
||
for i in 0..=ns {
|
||
let ray: Vec<[f64; 2]> = (0..=nn).map(|k| x[k][i]).collect();
|
||
let mut cum = vec![0.0; nn + 1];
|
||
for k in 1..=nn {
|
||
cum[k] = cum[k - 1]
|
||
+ ((ray[k][0] - ray[k - 1][0]).powi(2) + (ray[k][1] - ray[k - 1][1]).powi(2))
|
||
.sqrt();
|
||
}
|
||
let total = cum[nn];
|
||
for (k, &e) in eta.iter().enumerate().skip(1).take(nn - 1) {
|
||
let target = e * total;
|
||
let mut seg = 0;
|
||
while seg + 1 < nn && cum[seg + 1] < target {
|
||
seg += 1;
|
||
}
|
||
let l = cum[seg + 1] - cum[seg];
|
||
let tt = if l > 0.0 {
|
||
(target - cum[seg]) / l
|
||
} else {
|
||
0.0
|
||
};
|
||
x[k][i] = [
|
||
ray[seg][0] + tt * (ray[seg + 1][0] - ray[seg][0]),
|
||
ray[seg][1] + tt * (ray[seg + 1][1] - ray[seg][1]),
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The O-grid around the Turek–Hron rigid body (`docs/overset_metal_campaign.md`
|
||
/// §5.11): inner ring = `cylinder_flag_outline` (reversed to CLOCKWISE, the
|
||
/// right-handed frame), outer ring = the `offset` normal offset of the
|
||
/// body's convex hull sampled at the inner ring's arclength fractions,
|
||
/// transfinite start with the geometric `stretch` across, Winslow smoothing
|
||
/// of the interior, then re-spacing along each ray to the stretch. Returns
|
||
/// the mesh and `(winslow sweeps, last move)`.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_patch(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
x_tip: f64,
|
||
h: f64,
|
||
fillet: f64,
|
||
offset: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
winslow_sweeps: usize,
|
||
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||
// The fillet radius is a GEOMETRY parameter, fixed across a refinement
|
||
// ladder (a radius ∝ h is an O(h) boundary error: measured Stokes orders
|
||
// 1.74 → 1.31 with fillet = h/2 on the ny = 41/62/82 ladder).
|
||
let inner = cylinder_flag_outline(centre, r, t, x_tip, fillet, 3, 16, h, 1.15);
|
||
let hull_src = hull_source(centre, r, [x_tip - t, centre[1]], t, [1.0, 0.0]);
|
||
o_grid_from_outline(
|
||
inner,
|
||
hull_src,
|
||
h,
|
||
offset,
|
||
nn,
|
||
stretch,
|
||
winslow_sweeps,
|
||
None,
|
||
)
|
||
}
|
||
|
||
/// The FEA flag's wetted edges, deformed: `bottom` from the root to the
|
||
/// tip, `tip` from the bottom corner to the top corner, `top` from the
|
||
/// tip back to the root (the `Interface` walk's order).
|
||
#[derive(Debug, Clone)]
|
||
pub struct FlagEdges {
|
||
/// Root → tip.
|
||
pub bottom: Vec<[f64; 2]>,
|
||
/// Bottom corner → top corner.
|
||
pub tip: Vec<[f64; 2]>,
|
||
/// Tip → root.
|
||
pub top: Vec<[f64; 2]>,
|
||
}
|
||
|
||
/// The O-grid around the cylinder and a DEFORMED flag (P5-0, §5.12): the
|
||
/// inner ring from [`cylinder_flag_outline_deformed`], everything else as
|
||
/// [`cylinder_flag_patch`]; with undeformed edges the two agree to
|
||
/// rounding.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_patch_deformed(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
edges: &FlagEdges,
|
||
x_tip_ref: f64,
|
||
h: f64,
|
||
fillet: f64,
|
||
offset: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
winslow_sweeps: usize,
|
||
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||
cylinder_flag_patch_deformed_from(
|
||
None,
|
||
centre,
|
||
r,
|
||
t,
|
||
edges,
|
||
x_tip_ref,
|
||
h,
|
||
fillet,
|
||
offset,
|
||
nn,
|
||
stretch,
|
||
winslow_sweeps,
|
||
)
|
||
}
|
||
|
||
/// [`cylinder_flag_patch_deformed`] warm-started from `prev` (the same
|
||
/// topology: its interior rows are the Winslow start instead of the
|
||
/// transfinite one, so a few sweeps suffice when the outline moved a
|
||
/// little — P5's per-pass regeneration). `None` is the cold build.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_patch_deformed_from(
|
||
prev: Option<&PatchMesh>,
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
edges: &FlagEdges,
|
||
x_tip_ref: f64,
|
||
h: f64,
|
||
fillet: f64,
|
||
offset: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
winslow_sweeps: usize,
|
||
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||
cylinder_flag_patch_deformed_from_tip(
|
||
prev,
|
||
centre,
|
||
r,
|
||
t,
|
||
edges,
|
||
x_tip_ref,
|
||
h,
|
||
fillet,
|
||
offset,
|
||
nn,
|
||
stretch,
|
||
winslow_sweeps,
|
||
t,
|
||
)
|
||
}
|
||
|
||
/// [`cylinder_flag_patch_deformed`] with the flat tip of
|
||
/// [`cylinder_flag_outline_deformed_tip`] (`tip_corner = t` is the recorded
|
||
/// patch).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_patch_deformed_tip(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
edges: &FlagEdges,
|
||
x_tip_ref: f64,
|
||
h: f64,
|
||
fillet: f64,
|
||
offset: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
winslow_sweeps: usize,
|
||
tip_corner: f64,
|
||
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||
cylinder_flag_patch_deformed_from_tip(
|
||
None,
|
||
centre,
|
||
r,
|
||
t,
|
||
edges,
|
||
x_tip_ref,
|
||
h,
|
||
fillet,
|
||
offset,
|
||
nn,
|
||
stretch,
|
||
winslow_sweeps,
|
||
tip_corner,
|
||
)
|
||
}
|
||
|
||
/// [`cylinder_flag_patch_deformed_from`] with the flat tip.
|
||
#[allow(clippy::too_many_arguments)]
|
||
thread_local! {
|
||
/// PERF-2 P0 (`docs/perf2_campaign.md`): wall time [ns] of the
|
||
/// regeneration's stages on this thread — outline, hull + offset, ring
|
||
/// projection, Winslow sweeps, respace, warm bookkeeping, mesh
|
||
/// finalisation — and the number of builds (slot 7).
|
||
static REGEN_NS: std::cell::RefCell<[u64; 8]> = const { std::cell::RefCell::new([0; 8]) };
|
||
}
|
||
|
||
fn regen_charge(slot: usize, start: std::time::Instant) {
|
||
REGEN_NS.with(|r| r.borrow_mut()[slot] += start.elapsed().as_nanos() as u64);
|
||
}
|
||
|
||
/// The regeneration's stage times so far on this thread (see `REGEN_NS`).
|
||
pub fn regen_profile() -> [u64; 8] {
|
||
REGEN_NS.with(|r| *r.borrow())
|
||
}
|
||
|
||
pub fn cylinder_flag_patch_deformed_from_tip(
|
||
prev: Option<&PatchMesh>,
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
edges: &FlagEdges,
|
||
x_tip_ref: f64,
|
||
h: f64,
|
||
fillet: f64,
|
||
offset: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
winslow_sweeps: usize,
|
||
tip_corner: f64,
|
||
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||
let t0 = std::time::Instant::now();
|
||
let (inner, tip) = cylinder_flag_outline_deformed_tip(
|
||
centre, r, t, edges, x_tip_ref, fillet, 3, 16, h, 1.15, tip_corner,
|
||
);
|
||
regen_charge(0, t0);
|
||
// With a flat tip the outline's corners would put 90° corners on the
|
||
// hull, whose normal offset folds the rays at the tip; the tip disc
|
||
// (already a hull source) covers the tip, so the inner points ahead of
|
||
// the disc's centre plane are left out of the hull source.
|
||
let flat = tip_corner < tip.radius - 1e-12;
|
||
// The hull source carries the deformed outline itself: for a straight
|
||
// flag the cylinder and the tip disc alone give the flag's surfaces as
|
||
// the hull's tangents, for a BENT flag they give the chord under the
|
||
// convex side — measured on FSI2 at tip −35 mm as a patch 4.4 h thick
|
||
// where 6 h was asked, the acceptors' donors reaching into the fringe
|
||
// (P5-2's first death).
|
||
let mut hull_src = hull_source(centre, r, tip.centre, tip.radius, tip.axis);
|
||
hull_src.extend(inner.iter().copied().filter(|p| {
|
||
!flat || (p[0] - tip.centre[0]) * tip.axis[0] + (p[1] - tip.centre[1]) * tip.axis[1] < 0.0
|
||
}));
|
||
let start = prev.and_then(|m| {
|
||
(m.ns() == inner.len() && m.nn() == nn).then(|| {
|
||
(0..=nn)
|
||
.map(|k| (0..=m.ns()).map(|i| m.node_xy(m.node(k, i))).collect())
|
||
.collect::<Vec<Vec<[f64; 2]>>>()
|
||
})
|
||
});
|
||
o_grid_from_outline(
|
||
inner,
|
||
hull_src,
|
||
h,
|
||
offset,
|
||
nn,
|
||
stretch,
|
||
winslow_sweeps,
|
||
start,
|
||
)
|
||
}
|
||
|
||
/// The deformed flag's rounded tip: the semicircle's centre, radius and
|
||
/// outward axis.
|
||
#[derive(Debug, Clone, Copy)]
|
||
pub struct TipArc {
|
||
/// Centre of the semicircle.
|
||
pub centre: [f64; 2],
|
||
/// Radius (half the tip edge's length, ≈ the half-thickness).
|
||
pub radius: f64,
|
||
/// Unit vector from the centre through the arc's apex (the flag's
|
||
/// tangent at the tip).
|
||
pub axis: [f64; 2],
|
||
}
|
||
|
||
/// Cumulative arclength of an OPEN polyline (`cum[i]` at `pts[i]`).
|
||
fn open_cum(pts: &[[f64; 2]]) -> Vec<f64> {
|
||
let mut cum = Vec::with_capacity(pts.len());
|
||
let mut s = 0.0;
|
||
cum.push(0.0);
|
||
for w in pts.windows(2) {
|
||
s += ((w[1][0] - w[0][0]).powi(2) + (w[1][1] - w[0][1]).powi(2)).sqrt();
|
||
cum.push(s);
|
||
}
|
||
cum
|
||
}
|
||
|
||
/// Point at arclength `s` along an open polyline.
|
||
fn open_point_at(pts: &[[f64; 2]], cum: &[f64], s: f64) -> [f64; 2] {
|
||
let n = pts.len();
|
||
let s = s.clamp(0.0, cum[n - 1]);
|
||
let mut i = 0;
|
||
while i + 2 < n && cum[i + 1] < s {
|
||
i += 1;
|
||
}
|
||
let seg = cum[i + 1] - cum[i];
|
||
let t = if seg > 0.0 { (s - cum[i]) / seg } else { 0.0 };
|
||
[
|
||
pts[i][0] + t * (pts[i + 1][0] - pts[i][0]),
|
||
pts[i][1] + t * (pts[i + 1][1] - pts[i][1]),
|
||
]
|
||
}
|
||
|
||
/// The sub-polyline of an open polyline between arclengths `s0 < s1`,
|
||
/// with interpolated end points.
|
||
fn open_slice(pts: &[[f64; 2]], cum: &[f64], s0: f64, s1: f64) -> Vec<[f64; 2]> {
|
||
let mut out = vec![open_point_at(pts, cum, s0)];
|
||
for (i, c) in cum.iter().enumerate() {
|
||
if *c > s0 && *c < s1 {
|
||
out.push(pts[i]);
|
||
}
|
||
}
|
||
out.push(open_point_at(pts, cum, s1));
|
||
out
|
||
}
|
||
|
||
/// Arclength at which an open polyline first crosses `x = x0` (searched
|
||
/// from the end `from_end` — the root end of a flag edge), linear on the
|
||
/// crossing segment.
|
||
fn arclength_at_x(pts: &[[f64; 2]], cum: &[f64], x0: f64, from_end: bool) -> f64 {
|
||
let n = pts.len();
|
||
let order: Vec<usize> = if from_end {
|
||
(0..n - 1).rev().collect()
|
||
} else {
|
||
(0..n - 1).collect()
|
||
};
|
||
for i in order {
|
||
let (a, b) = (pts[i], pts[i + 1]);
|
||
if (a[0] - x0) * (b[0] - x0) <= 0.0 && a[0] != b[0] {
|
||
let f = (x0 - a[0]) / (b[0] - a[0]);
|
||
return cum[i] + f * (cum[i + 1] - cum[i]);
|
||
}
|
||
}
|
||
if from_end { cum[n - 1] } else { 0.0 }
|
||
}
|
||
|
||
/// Points at the arclength fractions `fr` (`0 ..= 1`) along an open
|
||
/// polyline from its start, EXCLUDING the end point (the rigid outline's
|
||
/// convention). The fractions are the UNDEFORMED edge's graded ones, so
|
||
/// the point count — the patch topology — is fixed across a march.
|
||
fn along_fractions(pts: &[[f64; 2]], fr: &[f64]) -> Vec<[f64; 2]> {
|
||
let cum = open_cum(pts);
|
||
let len = cum[cum.len() - 1];
|
||
fr[..fr.len() - 1]
|
||
.iter()
|
||
.map(|&f| open_point_at(pts, &cum, f * len))
|
||
.collect()
|
||
}
|
||
|
||
/// The deformed Turek–Hron body outline (counter-clockwise, starting at
|
||
/// the tip arc's top end, the order of [`cylinder_flag_outline`]): the top
|
||
/// edge along the deformed top polyline from the tip arc to the root
|
||
/// fillet's tangent point (resampled at the graded arclength spacing), the
|
||
/// root fillets and the cylinder arc from the RIGID construction (the
|
||
/// clamp keeps the root straight to a micron), the bottom edge, and the
|
||
/// tip semicircle whose centre and axis come from the deformed tip. The
|
||
/// edges' graded spacing is that of the UNDEFORMED straight edge (tip at
|
||
/// `x_tip_ref`), so the outline's point count — the patch topology the
|
||
/// composite's `set_mesh` requires fixed — does not change with the
|
||
/// deformation. Returns the outline and the tip arc.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_outline_deformed(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
edges: &FlagEdges,
|
||
x_tip_ref: f64,
|
||
fillet: f64,
|
||
k_fillet: usize,
|
||
k_tip: usize,
|
||
d_straight: f64,
|
||
grade: f64,
|
||
) -> (Vec<[f64; 2]>, TipArc) {
|
||
cylinder_flag_outline_deformed_tip(
|
||
centre, r, t, edges, x_tip_ref, fillet, k_fillet, k_tip, d_straight, grade, t,
|
||
)
|
||
}
|
||
|
||
/// [`cylinder_flag_outline_deformed`] with the tip as the benchmark's FLAT
|
||
/// face between two corner arcs of radius `tip_corner` (P5-3 option B):
|
||
/// `tip_corner = t` is the recorded semicircle, bit for bit; smaller
|
||
/// corners keep the along-wall spacing `d0` (the corner arcs and the flat
|
||
/// face are sampled at it, so the fluid step does not collapse) and a
|
||
/// point count fixed across the deformation (the flat face's count comes
|
||
/// from the UNDEFORMED thickness). The returned [`TipArc`] is still the
|
||
/// tip disc of radius half the tip edge — the hull source's shape.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cylinder_flag_outline_deformed_tip(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
t: f64,
|
||
edges: &FlagEdges,
|
||
x_tip_ref: f64,
|
||
fillet: f64,
|
||
k_fillet: usize,
|
||
k_tip: usize,
|
||
d_straight: f64,
|
||
grade: f64,
|
||
tip_corner: f64,
|
||
) -> (Vec<[f64; 2]>, TipArc) {
|
||
let (cx, cy) = (centre[0], centre[1]);
|
||
// Root junction, as the rigid outline.
|
||
let x_f = cx + ((r + fillet).powi(2) - (t + fillet).powi(2)).sqrt();
|
||
let f_top = [x_f, cy + t + fillet];
|
||
let f_bot = [x_f, cy - t - fillet];
|
||
let dir_top = [
|
||
(f_top[0] - cx) / (r + fillet),
|
||
(f_top[1] - cy) / (r + fillet),
|
||
];
|
||
let t2_top = [cx + r * dir_top[0], cy + r * dir_top[1]];
|
||
let dir_bot = [
|
||
(f_bot[0] - cx) / (r + fillet),
|
||
(f_bot[1] - cy) / (r + fillet),
|
||
];
|
||
let t2_bot = [cx + r * dir_bot[0], cy + r * dir_bot[1]];
|
||
let d0 = (PI * t / k_tip as f64).min(fillet * PI / 2.0 / k_fillet as f64);
|
||
// The edge fractions of the undeformed straight edge (as the rigid
|
||
// outline's `len_top`), applied to each deformed edge's arclength.
|
||
let fr = graded_fractions((x_tip_ref - t) - x_f, d0, d_straight, grade);
|
||
// The tip: corners, the tangent axis from the last segments, the
|
||
// semicircle of radius half the tip edge, centred `radius` back.
|
||
let (bottom, top) = (&edges.bottom, &edges.top);
|
||
let (nb, nt) = (bottom.len(), top.len());
|
||
let (b, tc) = (bottom[nb - 1], top[0]);
|
||
let (bp, tp) = (bottom[nb - 2], top[1]);
|
||
let ax = [
|
||
(b[0] - bp[0]) + (tc[0] - tp[0]),
|
||
(b[1] - bp[1]) + (tc[1] - tp[1]),
|
||
];
|
||
let al = (ax[0] * ax[0] + ax[1] * ax[1]).sqrt().max(1e-300);
|
||
let axis = [ax[0] / al, ax[1] / al];
|
||
let normal = [-axis[1], axis[0]]; // left of the axis = the top side
|
||
let radius = 0.5 * ((tc[0] - b[0]).powi(2) + (tc[1] - b[1]).powi(2)).sqrt();
|
||
let mid = [0.5 * (b[0] + tc[0]), 0.5 * (b[1] + tc[1])];
|
||
let tip_c = [mid[0] - radius * axis[0], mid[1] - radius * axis[1]];
|
||
let start_top = [tip_c[0] + radius * normal[0], tip_c[1] + radius * normal[1]];
|
||
let start_bot = [tip_c[0] - radius * normal[0], tip_c[1] - radius * normal[1]];
|
||
|
||
let mut pts: Vec<[f64; 2]> = Vec::new();
|
||
// 1. Top edge: from the arc's top end toward the root, along the top
|
||
// polyline (given tip → root) from arclength `radius` to x = x_f.
|
||
// The corner radius: `radius` (the semicircle) or the flat tip's. The
|
||
// branch is chosen against the UNDEFORMED half-thickness `t`, never the
|
||
// deformed tip's `radius` (which breathes by nanometres under the FEA's
|
||
// deformation and flipped the topology at the semicircle setting).
|
||
let flat = tip_corner < t - 1e-9;
|
||
let rc = if flat {
|
||
tip_corner.min(radius).max(0.0)
|
||
} else {
|
||
radius
|
||
};
|
||
// Corner arc centres and the tangent points on the faces.
|
||
let c_top = [
|
||
tc[0] - rc * axis[0] - rc * normal[0],
|
||
tc[1] - rc * axis[1] - rc * normal[1],
|
||
];
|
||
let c_bot = [
|
||
b[0] - rc * axis[0] + rc * normal[0],
|
||
b[1] - rc * axis[1] + rc * normal[1],
|
||
];
|
||
let (start_top, start_bot, s_top0, s_bot1) = if flat {
|
||
(
|
||
[c_top[0] + rc * normal[0], c_top[1] + rc * normal[1]],
|
||
[c_bot[0] - rc * normal[0], c_bot[1] - rc * normal[1]],
|
||
rc,
|
||
rc,
|
||
)
|
||
} else {
|
||
(start_top, start_bot, radius, radius)
|
||
};
|
||
let cum_t = open_cum(top);
|
||
let s_root_t = arclength_at_x(top, &cum_t, x_f, true);
|
||
let mut top_run = open_slice(top, &cum_t, s_top0, s_root_t);
|
||
top_run[0] = start_top;
|
||
pts.extend(along_fractions(&top_run, &fr));
|
||
// 2. Top fillet, 3. cylinder arc, 4. bottom fillet — the rigid construction.
|
||
let a_t1 = -PI / 2.0;
|
||
let mut a_t2 = (t2_top[1] - f_top[1]).atan2(t2_top[0] - f_top[0]);
|
||
while a_t2 - a_t1 > PI {
|
||
a_t2 -= 2.0 * PI;
|
||
}
|
||
while a_t2 - a_t1 < -PI {
|
||
a_t2 += 2.0 * PI;
|
||
}
|
||
pts.extend(arc_points(f_top, fillet, a_t1, a_t2, k_fillet));
|
||
let th_top = (t2_top[1] - cy).atan2(t2_top[0] - cx);
|
||
let th_bot = (t2_bot[1] - cy).atan2(t2_bot[0] - cx) + 2.0 * PI;
|
||
let arc_len = (th_bot - th_top) * r;
|
||
let k_arc = ((arc_len / d_straight).round() as usize).max(8);
|
||
pts.extend(arc_points(centre, r, th_top, th_bot, k_arc));
|
||
let a_b2 = (t2_bot[1] - f_bot[1]).atan2(t2_bot[0] - f_bot[0]);
|
||
let mut a_b1 = PI / 2.0;
|
||
while a_b1 - a_b2 > PI {
|
||
a_b1 -= 2.0 * PI;
|
||
}
|
||
while a_b1 - a_b2 < -PI {
|
||
a_b1 += 2.0 * PI;
|
||
}
|
||
pts.extend(arc_points(f_bot, fillet, a_b2, a_b1, k_fillet));
|
||
// 5. Bottom edge: from x = x_f along the bottom polyline (root → tip)
|
||
// to `radius` short of the corner, ending at the arc's bottom end.
|
||
let cum_b = open_cum(bottom);
|
||
let s_root_b = arclength_at_x(bottom, &cum_b, x_f, false);
|
||
let mut bot_run = open_slice(bottom, &cum_b, s_root_b, cum_b[nb - 1] - s_bot1);
|
||
let last = bot_run.len() - 1;
|
||
bot_run[last] = start_bot;
|
||
pts.extend(along_fractions(&bot_run, &fr));
|
||
if flat {
|
||
// 6'. Bottom corner arc (from the bottom face to the tip face), the
|
||
// flat tip face, the top corner arc (to the top face, whose start
|
||
// point opens the outline — excluded here). Counts from the
|
||
// UNDEFORMED thickness so the topology is fixed.
|
||
let k_c = ((PI * rc / 2.0) / d0).round().max(2.0) as usize;
|
||
let k_f = ((2.0 * (t - rc)) / d0).round().max(1.0) as usize;
|
||
let a_b0 = (-normal[1]).atan2(-normal[0]);
|
||
pts.extend(arc_points(c_bot, rc, a_b0, a_b0 + PI / 2.0, k_c));
|
||
let (p0, p1) = (
|
||
[c_bot[0] + rc * axis[0], c_bot[1] + rc * axis[1]],
|
||
[c_top[0] + rc * axis[0], c_top[1] + rc * axis[1]],
|
||
);
|
||
for m in 0..k_f {
|
||
let f = m as f64 / k_f as f64;
|
||
pts.push([p0[0] + f * (p1[0] - p0[0]), p0[1] + f * (p1[1] - p0[1])]);
|
||
}
|
||
let a_t0 = axis[1].atan2(axis[0]);
|
||
pts.extend(arc_points(c_top, rc, a_t0, a_t0 + PI / 2.0, k_c));
|
||
} else {
|
||
// 6. Tip semicircle from the bottom end through the apex to the top end.
|
||
let a0 = (start_bot[1] - tip_c[1]).atan2(start_bot[0] - tip_c[0]);
|
||
pts.extend(arc_points(tip_c, radius, a0, a0 + PI, k_tip));
|
||
}
|
||
(
|
||
pts,
|
||
TipArc {
|
||
centre: tip_c,
|
||
radius,
|
||
axis,
|
||
},
|
||
)
|
||
}
|
||
|
||
/// The hull source points: the cylinder sampled finely, the tip
|
||
/// semicircle (centre, radius, outward axis) sampled finely, and its two
|
||
/// end points.
|
||
fn hull_source(
|
||
centre: [f64; 2],
|
||
r: f64,
|
||
tip_c: [f64; 2],
|
||
rt: f64,
|
||
axis: [f64; 2],
|
||
) -> Vec<[f64; 2]> {
|
||
let mut hull_src: Vec<[f64; 2]> = arc_points(centre, r, 0.0, 2.0 * PI, 256);
|
||
let a = axis[1].atan2(axis[0]);
|
||
hull_src.extend(arc_points(tip_c, rt, a - PI / 2.0, a + PI / 2.0, 64));
|
||
let normal = [-axis[1], axis[0]];
|
||
hull_src.push([tip_c[0] + rt * normal[0], tip_c[1] + rt * normal[1]]);
|
||
hull_src.push([tip_c[0] - rt * normal[0], tip_c[1] - rt * normal[1]]);
|
||
hull_src
|
||
}
|
||
|
||
/// The O-grid body shared by the rigid and the deformed generators: the
|
||
/// inner ring (counter-clockwise in, reversed to clockwise), the outer
|
||
/// ring on the hull offset, transfinite start, Winslow, re-spacing.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn o_grid_from_outline(
|
||
mut inner: Vec<[f64; 2]>,
|
||
hull_src: Vec<[f64; 2]>,
|
||
h: f64,
|
||
offset: f64,
|
||
nn: usize,
|
||
stretch: f64,
|
||
winslow_sweeps: usize,
|
||
start: Option<Vec<Vec<[f64; 2]>>>,
|
||
) -> CfdResult<(PatchMesh, (usize, f64))> {
|
||
inner.reverse(); // clockwise
|
||
let ns = inner.len();
|
||
let t0 = std::time::Instant::now();
|
||
let hull = convex_hull(hull_src);
|
||
let outer_poly = offset_convex_polygon(&hull, offset, 24);
|
||
regen_charge(1, t0);
|
||
let t0 = std::time::Instant::now();
|
||
// Initial outer ring: the inner point pushed along its outward normal
|
||
// (the ring is clockwise, so the outward normal is the LEFT-hand normal
|
||
// of the direction of travel) and projected onto the hull offset —
|
||
// exact on the convex parts, bunched across the concave junctions (the
|
||
// swallowtail), which the sliding Winslow rows then spread out.
|
||
let outer: Vec<[f64; 2]> = (0..=ns)
|
||
.map(|i| {
|
||
let i0 = i % ns;
|
||
let prev = inner[(i0 + ns - 1) % ns];
|
||
let cur = inner[i0];
|
||
let next = inner[(i0 + 1) % ns];
|
||
let (dx, dy) = (next[0] - prev[0], next[1] - prev[1]);
|
||
let l = (dx * dx + dy * dy).sqrt().max(1e-300);
|
||
let normal = [-dy / l, dx / l];
|
||
nearest_on_polyline(
|
||
&outer_poly,
|
||
[cur[0] + offset * normal[0], cur[1] + offset * normal[1]],
|
||
)
|
||
})
|
||
.collect();
|
||
regen_charge(2, t0);
|
||
let mut inner_closed = inner.clone();
|
||
inner_closed.push(inner[0]);
|
||
// Transfinite start, or the previous mesh's interior with the new
|
||
// inner and outer rings (the warm start).
|
||
let eta = stretched_fractions(nn, stretch);
|
||
let warm = start.is_some();
|
||
let mut x: Vec<Vec<[f64; 2]>> = match start {
|
||
Some(mut rows) => {
|
||
rows[0] = inner_closed.clone();
|
||
// The outer ring warm-starts too: the previous ring's nodes
|
||
// projected onto the new offset polygon keep the sliding
|
||
// rows' converged distribution (the crude normal push would
|
||
// undo it every build — measured as the interior moving
|
||
// 0.05 h with the wall at rest).
|
||
rows[nn] = rows[nn]
|
||
.iter()
|
||
.map(|&p| nearest_on_polyline(&outer_poly, p))
|
||
.collect();
|
||
rows
|
||
}
|
||
None => eta
|
||
.iter()
|
||
.map(|&e| {
|
||
(0..=ns)
|
||
.map(|i| {
|
||
[
|
||
(1.0 - e) * inner_closed[i][0] + e * outer[i][0],
|
||
(1.0 - e) * inner_closed[i][1] + e * outer[i][1],
|
||
]
|
||
})
|
||
.collect()
|
||
})
|
||
.collect(),
|
||
};
|
||
// The cold build: Winslow, then the ray re-spacing (the P4-0 one-shot).
|
||
// The warm build must be IDEMPOTENT at rest, and the one-shot is not
|
||
// (the re-spaced mesh is not the smoother's fixed point: measured as
|
||
// the interior moving 0.05 h per build with the wall at rest), so it
|
||
// alternates one sweep with a re-spacing — the base converged as a
|
||
// fixed point of that map stays put, and a moving outline is tracked.
|
||
let report = if warm {
|
||
let mut last = f64::INFINITY;
|
||
let mut done = 0;
|
||
for k in 0..winslow_sweeps {
|
||
let t0 = std::time::Instant::now();
|
||
let before = x.clone();
|
||
regen_charge(5, t0);
|
||
let t0 = std::time::Instant::now();
|
||
winslow_smooth(&mut x, ns, 0.0, 1, Some(&outer_poly));
|
||
regen_charge(3, t0);
|
||
let t0 = std::time::Instant::now();
|
||
respace_rays(&mut x, ns, &eta);
|
||
regen_charge(4, t0);
|
||
let t0 = std::time::Instant::now();
|
||
last = x
|
||
.iter()
|
||
.zip(&before)
|
||
.flat_map(|(r, b)| r.iter().zip(b))
|
||
.map(|(p, q)| ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2)).sqrt())
|
||
.fold(0.0, f64::max);
|
||
regen_charge(5, t0);
|
||
done = k + 1;
|
||
if last < 1e-10 * h {
|
||
break;
|
||
}
|
||
}
|
||
(done, last)
|
||
} else {
|
||
let t0 = std::time::Instant::now();
|
||
let report = winslow_smooth(&mut x, ns, 1e-10 * h, winslow_sweeps, Some(&outer_poly));
|
||
regen_charge(3, t0);
|
||
let t0 = std::time::Instant::now();
|
||
respace_rays(&mut x, ns, &eta);
|
||
regen_charge(4, t0);
|
||
report
|
||
};
|
||
let t0 = std::time::Instant::now();
|
||
let mut xs = Vec::with_capacity((nn + 1) * (ns + 1));
|
||
let mut ys = Vec::with_capacity(xs.capacity());
|
||
for row in &x {
|
||
for p in row {
|
||
xs.push(p[0]);
|
||
ys.push(p[1]);
|
||
}
|
||
}
|
||
let mesh = PatchMesh::from_nodes(ns, nn, xs, ys, Some([0.0, 0.0]))?;
|
||
regen_charge(6, t0);
|
||
REGEN_NS.with(|r| r.borrow_mut()[7] += 1);
|
||
Ok((mesh, report))
|
||
}
|