Trimmed faces are cut into four-sided patches (squares around holes, butterflies for round faces, motorcycle traces from concave corners) and each is meshed as a Coons grid, with a cut grid as the fallback; cells are at most 2:1 along straight directions. Folded blends are refused rather than overlapped. The README lists display meshes and the gpu crate.
519 lines
22 KiB
Rust
519 lines
22 KiB
Rust
//! A trimmed face as an iso-line T-mesh: full-length grid lines from the size rules plus the
|
||
//! face's motorcycle traces (which run only as far as they reach), whose rectangles that lie
|
||
//! well inside the face are meshed directly. The band between those rectangles (a staircase)
|
||
//! and the face's boundary nodes is triangulated with both kept as constraints and no point
|
||
//! added. The boundary stays exactly the shared edge nodes, so the face meets its neighbours
|
||
//! exactly, and only the band next to the boundary is irregular (cut-cell meshing).
|
||
|
||
use super::cdt;
|
||
use super::motorcycle::Trace;
|
||
|
||
/// A cell is kept when the boundary stays at least this fraction of its smaller side away from
|
||
/// it (in the metric): closer, the band triangle between them would be a sliver.
|
||
const CLEARANCE: f64 = 0.35;
|
||
/// Two lines closer than this fraction of a grid cell would leave a strip of slivers between
|
||
/// them: a grid line that close to a trace moves onto the trace (the trace runs through its
|
||
/// peak, which is the point of it), and a trace that close to an earlier one joins it.
|
||
const SNAP: f64 = 0.25;
|
||
|
||
/// One direction's lines: positions (increasing) and whether each runs the whole face.
|
||
struct Lines {
|
||
at: Vec<f64>,
|
||
full: Vec<bool>,
|
||
}
|
||
|
||
/// The full lines `grid` plus the traces' lines in this direction (see [`SNAP`]; the first and
|
||
/// last grid lines, the face's parameter box, never move). Returns the lines and, per trace
|
||
/// (in order), its line's index.
|
||
fn lines(grid: &[f64], traces: &[f64]) -> Option<(Lines, Vec<usize>)> {
|
||
let mut at: Vec<f64> = grid.to_vec();
|
||
let mut full = vec![true; at.len()];
|
||
let mut place = Vec::with_capacity(traces.len());
|
||
for &c in traces {
|
||
// The full-grid cell around c, and every line inside it.
|
||
let k = grid.partition_point(|g| *g < c);
|
||
let (lo, hi) = (*grid.get(k.saturating_sub(1))?, *grid.get(k.min(grid.len().saturating_sub(1)))?);
|
||
let w = (hi - lo).max(f64::MIN_POSITIVE);
|
||
let last = grid.len().saturating_sub(1);
|
||
let near = at.iter().enumerate().filter(|(_, x)| (**x - c).abs() < SNAP * w).min_by(|a, b| (a.1 - c).abs().total_cmp(&(b.1 - c).abs()));
|
||
match near {
|
||
// An earlier trace, or a box edge: join it.
|
||
Some((i, _)) if !full.get(i).copied().unwrap_or(false) || i == 0 || i == last => place.push(i),
|
||
// An inner grid line: it moves onto the trace.
|
||
Some((i, _)) => {
|
||
*at.get_mut(i)? = c;
|
||
place.push(i);
|
||
}
|
||
None => {
|
||
at.push(c);
|
||
full.push(false);
|
||
place.push(at.len() - 1);
|
||
}
|
||
}
|
||
}
|
||
// Sort, carrying the trace indices along.
|
||
let mut order: Vec<usize> = (0..at.len()).collect();
|
||
order.sort_by(|a, b| at.get(*a).copied().unwrap_or(0.0).total_cmp(&at.get(*b).copied().unwrap_or(0.0)));
|
||
let mut rank = vec![0; at.len()];
|
||
for (r, &i) in order.iter().enumerate() {
|
||
*rank.get_mut(i)? = r;
|
||
}
|
||
let lines =
|
||
Lines { at: order.iter().filter_map(|&i| at.get(i).copied()).collect(), full: order.iter().filter_map(|&i| full.get(i).copied()).collect() };
|
||
let place = place.iter().filter_map(|&i| rank.get(i).copied()).collect::<Vec<_>>();
|
||
(place.len() == traces.len()).then_some((lines, place))
|
||
}
|
||
|
||
/// Mesh the region bounded by `loops` (ranges of `boundary`, each closed; even–odd, so inner
|
||
/// loops are holes) with the full grid lines `us` × `vs` (increasing, spanning the region's
|
||
/// parameter box) and the motorcycle `traces`, measured in the metric `scale`. Boundary
|
||
/// vertices keep their indices; grid points follow. Triangles counter-clockwise in (u, v).
|
||
/// `None` when no cell is kept or the band cannot be triangulated (the face then goes to
|
||
/// Delaunay).
|
||
pub(crate) fn cut_grid(
|
||
boundary: &[[f64; 2]],
|
||
loops: &[std::ops::Range<usize>],
|
||
us: &[f64],
|
||
vs: &[f64],
|
||
traces: &[Trace],
|
||
scale: [f64; 2],
|
||
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
|
||
if us.len() < 2 || vs.len() < 2 || !scale.iter().all(|s| s.is_finite() && *s > 0.0) {
|
||
return None;
|
||
}
|
||
// Traces on u = c (axis 0) are lines in u; on v = c, lines in v.
|
||
let tu: Vec<&Trace> = traces.iter().filter(|t| t.axis == 0).collect();
|
||
let tv: Vec<&Trace> = traces.iter().filter(|t| t.axis == 1).collect();
|
||
let (lu, pu) = lines(us, &tu.iter().map(|t| t.at).collect::<Vec<_>>())?;
|
||
let (lv, pv) = lines(vs, &tv.iter().map(|t| t.at).collect::<Vec<_>>())?;
|
||
let (nu, nv) = (lu.at.len(), lv.at.len());
|
||
let (cu, cv) = (nu - 1, nv - 1);
|
||
let cells = cu.checked_mul(cv)?;
|
||
if cells > super::grid::MAX_FACE_GRID {
|
||
return None;
|
||
}
|
||
// Which sides of the fine cells are lines: the vertical side at column i in row j, the
|
||
// horizontal side at row j in column i. A trace covers a side it runs at least half along.
|
||
let covers = |t: &Trace, a: f64, b: f64| (t.to.min(b) - t.from.max(a)) >= 0.5 * (b - a);
|
||
let mut vert = vec![false; nu * cv];
|
||
for i in 0..nu {
|
||
let full = lu.full.get(i).copied().unwrap_or(false);
|
||
for j in 0..cv {
|
||
let (a, b) = (*lv.at.get(j)?, *lv.at.get(j + 1)?);
|
||
*vert.get_mut(j * nu + i)? = full || tu.iter().zip(&pu).any(|(t, &k)| k == i && covers(t, a, b));
|
||
}
|
||
}
|
||
let mut horiz = vec![false; nv * cu];
|
||
for j in 0..nv {
|
||
let full = lv.full.get(j).copied().unwrap_or(false);
|
||
for i in 0..cu {
|
||
let (a, b) = (*lu.at.get(i)?, *lu.at.get(i + 1)?);
|
||
*horiz.get_mut(j * cu + i)? = full || tv.iter().zip(&pv).any(|(t, &k)| k == j && covers(t, a, b));
|
||
}
|
||
}
|
||
// Rectangles: fine cells joined across sides that are no line. A T-mesh's faces are
|
||
// rectangles; anything else (a trace ending in the open after snapping) falls back to
|
||
// every line running the whole face.
|
||
let rects = rectangles(cu, cv, &vert, &horiz).or_else(|| {
|
||
vert.iter_mut().for_each(|s| *s = true);
|
||
horiz.iter_mut().for_each(|s| *s = true);
|
||
rectangles(cu, cv, &vert, &horiz)
|
||
})?;
|
||
// In the metric, so clearances are lengths on the surface.
|
||
let gx: Vec<f64> = lu.at.iter().map(|u| u * scale[0]).collect();
|
||
let gy: Vec<f64> = lv.at.iter().map(|v| v * scale[1]).collect();
|
||
let segs: Vec<([f64; 2], [f64; 2])> = loops
|
||
.iter()
|
||
.flat_map(|r| {
|
||
let n = r.len();
|
||
(0..n).filter_map(move |k| Some((*boundary.get(r.start + k)?, *boundary.get(r.start + (k + 1) % n)?)))
|
||
})
|
||
.map(|(a, b)| ([a[0] * scale[0], a[1] * scale[1]], [b[0] * scale[0], b[1] * scale[1]]))
|
||
.collect();
|
||
let keep = clear_cells(&gx, &gy, &segs, &rects)?;
|
||
if !keep.iter().any(|k| *k) {
|
||
return None;
|
||
}
|
||
let kept = |i: isize, j: isize| -> bool {
|
||
i >= 0
|
||
&& j >= 0
|
||
&& (i as usize) < cu
|
||
&& (j as usize) < cv
|
||
&& rects.of.get(j as usize * cu + i as usize).and_then(|r| keep.get(*r)).copied().unwrap_or(false)
|
||
};
|
||
// Grid points on the kept rectangles' corners, numbered after the boundary.
|
||
let mut used = vec![false; nu * nv];
|
||
for (r, b) in rects.boxes.iter().enumerate() {
|
||
if keep.get(r).copied().unwrap_or(false) {
|
||
for (i, j) in [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])] {
|
||
*used.get_mut(j * nu + i)? = true;
|
||
}
|
||
}
|
||
}
|
||
let n = boundary.len();
|
||
let mut id: Vec<Option<u32>> = vec![None; nu * nv];
|
||
let mut extra: Vec<[f64; 2]> = Vec::new();
|
||
let mut node = |i: usize, j: usize, extra: &mut Vec<[f64; 2]>| -> Option<u32> {
|
||
let slot = id.get_mut(j * nu + i)?;
|
||
if let Some(k) = *slot {
|
||
return Some(k);
|
||
}
|
||
let k = u32::try_from(n + extra.len()).ok()?;
|
||
extra.push([*lu.at.get(i)?, *lv.at.get(j)?]);
|
||
*slot = Some(k);
|
||
Some(k)
|
||
};
|
||
let mut tris: Vec<[u32; 3]> = Vec::new();
|
||
// The staircase: pieces of kept rectangles' sides with no kept rectangle across,
|
||
// counter-clockwise around the kept region (so the band lies to their right).
|
||
let mut stairs: Vec<(u32, u32)> = Vec::new();
|
||
for (r, b) in rects.boxes.iter().enumerate() {
|
||
if !keep.get(r).copied().unwrap_or(false) {
|
||
continue;
|
||
}
|
||
let [i0, j0, i1, j1] = *b;
|
||
// The perimeter counter-clockwise as grid points, each with the fine cell across the
|
||
// side that leaves it.
|
||
let mut ring: Vec<((usize, usize), (isize, isize))> = Vec::new();
|
||
for i in i0..i1 {
|
||
ring.push(((i, j0), (i as isize, j0 as isize - 1)));
|
||
}
|
||
for j in j0..j1 {
|
||
ring.push(((i1, j), (i1 as isize, j as isize)));
|
||
}
|
||
for i in (i0 + 1..=i1).rev() {
|
||
ring.push(((i, j1), (i as isize - 1, j1 as isize)));
|
||
}
|
||
for j in (j0 + 1..=j1).rev() {
|
||
ring.push(((i0, j), (i0 as isize - 1, j as isize - 1)));
|
||
}
|
||
let corners = ring.iter().filter(|((i, j), _)| used.get(j * nu + i).copied().unwrap_or(false)).count();
|
||
let mut poly: Vec<u32> = Vec::with_capacity(corners);
|
||
let mut across: Vec<(isize, isize)> = Vec::with_capacity(corners);
|
||
for &((i, j), c) in &ring {
|
||
if used.get(j * nu + i).copied().unwrap_or(false) {
|
||
poly.push(node(i, j, &mut extra)?);
|
||
across.push(c);
|
||
}
|
||
}
|
||
if poly.len() == 4 {
|
||
let [a, b, c, d] = [*poly.first()?, *poly.get(1)?, *poly.get(2)?, *poly.get(3)?];
|
||
tris.push([a, b, c]);
|
||
tris.push([a, c, d]);
|
||
} else {
|
||
// Hanging nodes from neighbours: a fan from the rectangle's centre.
|
||
let centre = [0.5 * (lu.at.get(i0)? + lu.at.get(i1)?), 0.5 * (lv.at.get(j0)? + lv.at.get(j1)?)];
|
||
let m = u32::try_from(n + extra.len()).ok()?;
|
||
extra.push(centre);
|
||
for k in 0..poly.len() {
|
||
tris.push([m, *poly.get(k)?, *poly.get((k + 1) % poly.len())?]);
|
||
}
|
||
}
|
||
for k in 0..poly.len() {
|
||
let (ci, cj) = *across.get(k)?;
|
||
if !kept(ci, cj) {
|
||
stairs.push((*poly.get(k)?, *poly.get((k + 1) % poly.len())?));
|
||
}
|
||
}
|
||
}
|
||
let mut all = boundary.to_vec();
|
||
all.extend_from_slice(&extra);
|
||
let band = cdt::band(&all, n, loops, &stairs, scale)?;
|
||
tris.extend(band);
|
||
// Every triangle counter-clockwise with some area: one that is not has folded.
|
||
for t in &tris {
|
||
let (a, b, c) = (all.get(t[0] as usize)?, all.get(t[1] as usize)?, all.get(t[2] as usize)?);
|
||
let area = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
||
if !(area > 0.0) {
|
||
return None;
|
||
}
|
||
}
|
||
Some((extra, tris))
|
||
}
|
||
|
||
/// The rectangles of a T-mesh over `cu` × `cv` fine cells: per cell its rectangle, and per
|
||
/// rectangle its corners as grid indices `[i0, j0, i1, j1]`. `None` if a face is not a
|
||
/// rectangle.
|
||
struct Rects {
|
||
of: Vec<usize>,
|
||
boxes: Vec<[usize; 4]>,
|
||
}
|
||
|
||
fn rectangles(cu: usize, cv: usize, vert: &[bool], horiz: &[bool]) -> Option<Rects> {
|
||
let nu = cu + 1;
|
||
let mut of = vec![usize::MAX; cu * cv];
|
||
let mut boxes = Vec::new();
|
||
for j0 in 0..cv {
|
||
for i0 in 0..cu {
|
||
if *of.get(j0 * cu + i0)? != usize::MAX {
|
||
continue;
|
||
}
|
||
// Grow right while no line separates, then up while the whole row is open.
|
||
let mut i1 = i0 + 1;
|
||
while i1 < cu && !*vert.get(j0 * nu + i1)? {
|
||
i1 += 1;
|
||
}
|
||
let mut j1 = j0 + 1;
|
||
while j1 < cv && (i0..i1).all(|i| horiz.get(j1 * cu + i).is_some_and(|h| !*h)) {
|
||
j1 += 1;
|
||
}
|
||
let r = boxes.len();
|
||
for j in j0..j1 {
|
||
for i in i0..i1 {
|
||
let slot = of.get_mut(j * cu + i)?;
|
||
if *slot != usize::MAX {
|
||
return None;
|
||
}
|
||
*slot = r;
|
||
}
|
||
}
|
||
boxes.push([i0, j0, i1, j1]);
|
||
}
|
||
}
|
||
// A rectangle: no line inside it, and lines all round it.
|
||
for b in &boxes {
|
||
let [i0, j0, i1, j1] = *b;
|
||
for j in j0..j1 {
|
||
for i in i0 + 1..i1 {
|
||
if *vert.get(j * nu + i)? {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
for i in i0..i1 {
|
||
for j in j0 + 1..j1 {
|
||
if *horiz.get(j * cu + i)? {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
for j in j0..j1 {
|
||
if (i0 > 0 && !*vert.get(j * nu + i0)?) || (i1 < cu && !*vert.get(j * nu + i1)?) {
|
||
return None;
|
||
}
|
||
}
|
||
for i in i0..i1 {
|
||
if (j0 > 0 && !*horiz.get(j0 * cu + i)?) || (j1 < cv && !*horiz.get(j1 * cu + i)?) {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
Some(Rects { of, boxes })
|
||
}
|
||
|
||
/// Per rectangle: kept when every fine cell in it is inside the region and clear of the
|
||
/// boundary (see [`CLEARANCE`]). `gx`, `gy`: the lines in the metric; `segs`: the boundary.
|
||
fn clear_cells(gx: &[f64], gy: &[f64], segs: &[([f64; 2], [f64; 2])], rects: &Rects) -> Option<Vec<bool>> {
|
||
let (cu, cv) = (gx.len().saturating_sub(1), gy.len().saturating_sub(1));
|
||
let cells = cu * cv;
|
||
let cell = |i: usize, j: usize| -> Option<([f64; 2], [f64; 2])> {
|
||
let (x0, x1, y0, y1) = (*gx.get(i)?, *gx.get(i + 1)?, *gy.get(j)?, *gy.get(j + 1)?);
|
||
// The clearance of the rectangle the cell belongs to, not of the (possibly thin) cell.
|
||
let b = rects.boxes.get(*rects.of.get(j * cu + i)?)?;
|
||
let (w, h) = (gx.get(b[2])? - gx.get(b[0])?, gy.get(b[3])? - gy.get(b[1])?);
|
||
let pad = CLEARANCE * w.min(h);
|
||
Some(([x0 - pad, y0 - pad], [x1 + pad, y1 + pad]))
|
||
};
|
||
// Cells the boundary comes near: each segment marks the cells whose padded box it touches.
|
||
let mut near = vec![false; cells];
|
||
let maxpad = rects
|
||
.boxes
|
||
.iter()
|
||
.filter_map(|b| Some(CLEARANCE * (gx.get(b[2])? - gx.get(b[0])?).min(gy.get(b[3])? - gy.get(b[1])?)))
|
||
.fold(0.0f64, f64::max);
|
||
let span = |g: &[f64], lo: f64, hi: f64| {
|
||
let a = g.partition_point(|t| *t < lo - maxpad).saturating_sub(1);
|
||
let b = (g.partition_point(|t| *t <= hi + maxpad) + 1).min(g.len().saturating_sub(1));
|
||
(a, b)
|
||
};
|
||
for (a, b) in segs {
|
||
let (i0, i1) = span(gx, a[0].min(b[0]), a[0].max(b[0]));
|
||
let (j0, j1) = span(gy, a[1].min(b[1]), a[1].max(b[1]));
|
||
for j in j0..j1 {
|
||
for i in i0..i1 {
|
||
let Some((lo, hi)) = cell(i, j) else { continue };
|
||
if segment_meets_box(*a, *b, lo, hi)
|
||
&& let Some(c) = near.get_mut(j * cu + i)
|
||
{
|
||
*c = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// The other cells are wholly inside or wholly outside: even–odd along each row's middle.
|
||
let mut keep = vec![true; rects.boxes.len()];
|
||
for j in 0..cv {
|
||
let (Some(y0), Some(y1)) = (gy.get(j), gy.get(j + 1)) else { return None };
|
||
let ym = 0.5 * (y0 + y1);
|
||
let mut cross: Vec<f64> =
|
||
segs.iter().filter(|(a, b)| (a[1] > ym) != (b[1] > ym)).map(|(a, b)| a[0] + (ym - a[1]) / (b[1] - a[1]) * (b[0] - a[0])).collect();
|
||
cross.sort_by(f64::total_cmp);
|
||
for i in 0..cu {
|
||
let k = j * cu + i;
|
||
let (Some(x0), Some(x1)) = (gx.get(i), gx.get(i + 1)) else { return None };
|
||
let xm = 0.5 * (x0 + x1);
|
||
let inside = cross.partition_point(|c| *c < xm) % 2 == 1;
|
||
if near.get(k).copied().unwrap_or(true) || !inside {
|
||
*keep.get_mut(*rects.of.get(k)?)? = false;
|
||
}
|
||
}
|
||
}
|
||
Some(keep)
|
||
}
|
||
|
||
/// Whether the segment `a`–`b` meets the closed box `lo`–`hi` (Liang–Barsky clipping).
|
||
fn segment_meets_box(a: [f64; 2], b: [f64; 2], lo: [f64; 2], hi: [f64; 2]) -> bool {
|
||
let (mut t0, mut t1) = (0.0f64, 1.0f64);
|
||
for d in 0..2 {
|
||
let delta = b[d] - a[d];
|
||
for (p, q) in [(-delta, a[d] - lo[d]), (delta, hi[d] - a[d])] {
|
||
if p == 0.0 {
|
||
if q < 0.0 {
|
||
return false;
|
||
}
|
||
} else {
|
||
let r = q / p;
|
||
if p < 0.0 {
|
||
t0 = t0.max(r);
|
||
} else {
|
||
t1 = t1.min(r);
|
||
}
|
||
if t0 > t1 {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn circle(c: [f64; 2], r: f64, n: usize, ccw: bool) -> Vec<[f64; 2]> {
|
||
(0..n)
|
||
.map(|k| {
|
||
let a = std::f64::consts::TAU * k as f64 / n as f64 * if ccw { 1.0 } else { -1.0 };
|
||
[c[0] + r * a.cos(), c[1] + r * a.sin()]
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// A single loop over all `n` boundary points.
|
||
fn one(n: usize) -> [std::ops::Range<usize>; 1] {
|
||
[std::ops::Range { start: 0, end: n }]
|
||
}
|
||
|
||
fn lines(a: f64, b: f64, n: usize) -> Vec<f64> {
|
||
(0..=n).map(|k| a + (b - a) * k as f64 / n as f64).collect()
|
||
}
|
||
|
||
/// Every boundary segment is the side of exactly one triangle, every inner side of two,
|
||
/// and the triangles cover the region's area.
|
||
fn check(boundary: &[[f64; 2]], loops: &[std::ops::Range<usize>], area: f64, out: &(Vec<[f64; 2]>, Vec<[u32; 3]>)) {
|
||
let (extra, tris) = out;
|
||
let mut all = boundary.to_vec();
|
||
all.extend_from_slice(extra);
|
||
let mut sides: std::collections::HashMap<(u32, u32), i32> = std::collections::HashMap::new();
|
||
let mut sum = 0.0;
|
||
for t in tris {
|
||
let [a, b, c] = t.map(|i| all[i as usize]);
|
||
let ar = 0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]));
|
||
assert!(ar > 0.0, "counter-clockwise");
|
||
sum += ar;
|
||
for e in 0..3 {
|
||
*sides.entry((t[e], t[(e + 1) % 3])).or_default() += 1;
|
||
}
|
||
}
|
||
for r in loops {
|
||
let n = r.len();
|
||
for k in 0..n {
|
||
let (a, b) = ((r.start + k) as u32, (r.start + (k + 1) % n) as u32);
|
||
assert_eq!(sides.get(&(a, b)).copied().unwrap_or(0), 1, "boundary side {a}-{b} used once, forwards");
|
||
sides.remove(&(a, b));
|
||
}
|
||
}
|
||
for (&(a, b), &k) in &sides {
|
||
assert_eq!(k, 1);
|
||
assert_eq!(sides.get(&(b, a)).copied(), Some(1), "inner side {a}-{b} shared");
|
||
}
|
||
assert!((sum - area).abs() < 1e-9 * area.max(1.0), "area {sum} vs {area}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_disc_is_cells_and_a_band() {
|
||
let b = circle([0.0, 0.0], 1.0, 64, true);
|
||
let loops = one(64);
|
||
let poly_area = 0.5 * 64.0 * (std::f64::consts::TAU / 64.0).sin();
|
||
let Some(out) = cut_grid(&b, &loops, &lines(-1.0, 1.0, 10), &lines(-1.0, 1.0, 10), &[], [1.0, 1.0]) else { panic!("meshes") };
|
||
check(&b, &loops, poly_area, &out);
|
||
// Most of the disc is grid cells (two triangles each).
|
||
assert!(out.1.len() > 100, "{}", out.1.len());
|
||
}
|
||
|
||
#[test]
|
||
fn a_square_with_a_hole() {
|
||
let mut b = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
|
||
b.extend(circle([2.0, 2.0], 1.0, 32, false));
|
||
let loops = [0..4, 4..36];
|
||
let hole = 0.5 * 32.0 * (std::f64::consts::TAU / 32.0).sin();
|
||
let Some(out) = cut_grid(&b, &loops, &lines(0.0, 4.0, 16), &lines(0.0, 4.0, 16), &[], [1.0, 1.0]) else { panic!("meshes") };
|
||
check(&b, &loops, 16.0 - hole, &out);
|
||
}
|
||
|
||
#[test]
|
||
fn a_curved_triangle_in_a_stretched_metric() {
|
||
// A three-cornered patch (as a vertex blend in its parameters), u twice as long as v.
|
||
let mut b: Vec<[f64; 2]> = (0..=8).map(|k| [k as f64 / 8.0, 0.0]).collect();
|
||
b.extend((1..8).map(|k| {
|
||
let a = std::f64::consts::FRAC_PI_2 * k as f64 / 8.0;
|
||
[a.cos(), a.sin()]
|
||
}));
|
||
b.extend((1..8).rev().map(|k| [0.0, k as f64 / 8.0]));
|
||
let n = b.len();
|
||
let loops = one(n);
|
||
let area: f64 = (0..n)
|
||
.map(|k| {
|
||
let (p, q) = (b[k], b[(k + 1) % n]);
|
||
0.5 * (p[0] * q[1] - q[0] * p[1])
|
||
})
|
||
.sum();
|
||
let Some(out) = cut_grid(&b, &loops, &lines(0.0, 1.0, 12), &lines(0.0, 1.0, 6), &[], [2.0, 1.0]) else { panic!("meshes") };
|
||
check(&b, &loops, area, &out);
|
||
}
|
||
|
||
/// The hole's motorcycle traces (lines through its four extremes) become T-mesh lines that
|
||
/// stop where the traces stop; the mesh stays exact on the boundary and covers the region.
|
||
#[test]
|
||
fn a_square_with_a_hole_and_its_traces() {
|
||
let mut b = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
|
||
b.extend(circle([1.7, 2.3], 1.0, 32, false));
|
||
let loops = [0..4, 4..36];
|
||
let hole = 0.5 * 32.0 * (std::f64::consts::TAU / 32.0).sin();
|
||
let traces = super::super::motorcycle::traces(&b, &loops, [1.0, 1.0], 0.0);
|
||
assert_eq!(traces.len(), 4, "{traces:?}");
|
||
let Some(out) = cut_grid(&b, &loops, &lines(0.0, 4.0, 16), &lines(0.0, 4.0, 16), &traces, [1.0, 1.0]) else { panic!("meshes") };
|
||
check(&b, &loops, 16.0 - hole, &out);
|
||
// The traces run through the hole's extremes: along v = 2.3 from its u-extremes and
|
||
// along u = 1.7 from its v-extremes; their lines are in the grid.
|
||
for (d, x) in [(0, 1.7), (1, 2.3)] {
|
||
assert!(out.0.iter().any(|p| (p[d] - x).abs() < 1e-9), "a grid point on line {d} = {x}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn nothing_kept_is_refused() {
|
||
let b = circle([0.0, 0.0], 0.1, 16, true);
|
||
assert!(cut_grid(&b, &one(16), &lines(-1.0, 1.0, 1), &lines(-1.0, 1.0, 1), &[], [1.0, 1.0]).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn segments_meet_boxes() {
|
||
assert!(segment_meets_box([-1.0, 0.5], [2.0, 0.5], [0.0, 0.0], [1.0, 1.0]));
|
||
assert!(!segment_meets_box([-1.0, 2.0], [2.0, 2.0], [0.0, 0.0], [1.0, 1.0]));
|
||
assert!(segment_meets_box([0.5, 0.5], [0.6, 0.6], [0.0, 0.0], [1.0, 1.0]));
|
||
assert!(!segment_meets_box([2.0, 0.0], [3.0, 1.0], [0.0, 0.0], [1.0, 1.0]));
|
||
}
|
||
}
|