Files
OneCAD-Simulation/crates/mesh/src/quad/guard.rs
T
6c36f5b2ad 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>
2026-10-13 16:15:54 -04:00

122 lines
5.6 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.
//! Where the half-resolution triangulation may place Steiner points.
//!
//! The half-resolution region replaces each pair of boundary edges `a–m–b` by the chord `a–b`;
//! the split later puts `m` back as the chord's mid node. The final mesh therefore covers the true
//! region, not the coarse one, and a Steiner point must respect the true boundary: it may not lie
//! in a *cap* (the triangle `a, b, m` when `m` lies inside the coarse region, which the true region
//! excludes), and it keeps a clearance from the true edges `a–m`, `m–b` of at least half the chord
//! (so the elements along the boundary keep the boundary's spacing and can absorb `m`'s offset
//! from the chord). Lookups go through a uniform grid; the cap test is exact (`orient2d`).
use crate::predicates::{Point2, Sign, orient2d};
use crate::{Error, Result};
/// Above this many grid cells a clearance query scans every segment instead.
const MAX_QUERY_CELLS: usize = 1024;
pub(crate) struct Guard {
lo: [f64; 2],
step: [f64; 2],
g: usize,
/// True boundary edges with their own clearance radius.
segs: Vec<([f64; 2], [f64; 2], f64)>,
/// Counterclockwise caps `(a, b, m)`.
caps: Vec<[Point2; 3]>,
seg_cells: Vec<Vec<u32>>,
cap_cells: Vec<Vec<u32>>,
}
fn distance(a: [f64; 2], b: [f64; 2], p: [f64; 2]) -> f64 {
let (d, w) = ([b[0] - a[0], b[1] - a[1]], [p[0] - a[0], p[1] - a[1]]);
let len2 = d[0] * d[0] + d[1] * d[1];
let t = if len2 > 0.0 { ((w[0] * d[0] + w[1] * d[1]) / len2).clamp(0.0, 1.0) } else { 0.0 };
((w[0] - t * d[0]).powi(2) + (w[1] - t * d[1]).powi(2)).sqrt()
}
impl Guard {
/// The guard of chords `(a, b, m)`: coarse boundary edge `a → b` (region on its left) and the
/// skipped boundary point `m`.
pub fn new(chords: &[(Point2, Point2, Point2)]) -> Result<Guard> {
let (lo, hi) = super::domain::bbox(chords.iter().flat_map(|(a, b, m)| [a.coords(), b.coords(), m.coords()]));
if chords.is_empty() {
return Err(Error::Failed("guard without a boundary".into()));
}
let g = ((2.0 * chords.len() as f64).sqrt().ceil() as usize).clamp(1, 1024);
let step = [((hi[0] - lo[0]) / g as f64).max(f64::MIN_POSITIVE), ((hi[1] - lo[1]) / g as f64).max(f64::MIN_POSITIVE)];
let mut guard =
Guard { lo, step, g, segs: Vec::new(), caps: Vec::new(), seg_cells: vec![Vec::new(); g * g], cap_cells: vec![Vec::new(); g * g] };
for (a, b, m) in chords {
let (pa, pb, pm) = (a.coords(), b.coords(), m.coords());
let radius = 0.5 * ((pb[0] - pa[0]).powi(2) + (pb[1] - pa[1]).powi(2)).sqrt();
for (s, t) in [(pa, pm), (pm, pb)] {
let id = guard.segs.len() as u32;
guard.segs.push((s, t, radius));
let (x0, y0) = guard.cell([s[0].min(t[0]) - radius, s[1].min(t[1]) - radius]);
let (x1, y1) = guard.cell([s[0].max(t[0]) + radius, s[1].max(t[1]) + radius]);
guard.register(true, id, (x0, y0), (x1, y1));
}
if orient2d(a, b, m) == Sign::Positive {
let id = guard.caps.len() as u32;
guard.caps.push([*a, *b, *m]);
let (x0, y0) = guard.cell([pa[0].min(pb[0]).min(pm[0]), pa[1].min(pb[1]).min(pm[1])]);
let (x1, y1) = guard.cell([pa[0].max(pb[0]).max(pm[0]), pa[1].max(pb[1]).max(pm[1])]);
guard.register(false, id, (x0, y0), (x1, y1));
}
}
Ok(guard)
}
fn cell(&self, p: [f64; 2]) -> (usize, usize) {
let f = |x: f64, lo: f64, step: f64| {
let c = ((x - lo) / step).floor();
if c.is_nan() || c < 0.0 { 0 } else { (c as usize).min(self.g - 1) }
};
(f(p[0], self.lo[0], self.step[0]), f(p[1], self.lo[1], self.step[1]))
}
fn register(&mut self, seg: bool, id: u32, from: (usize, usize), to: (usize, usize)) {
let cells = if seg { &mut self.seg_cells } else { &mut self.cap_cells };
for y in from.1..=to.1 {
for x in from.0..=to.0 {
if let Some(c) = cells.get_mut(y * self.g + x) {
c.push(id);
}
}
}
}
/// Whether a Steiner point may go at `p`: outside every cap, and farther than both the
/// segment's own radius and `clearance` from every true boundary edge.
pub fn allows(&self, p: &Point2, clearance: f64) -> bool {
let c = p.coords();
let (cx, cy) = self.cell(c);
if let Some(caps) = self.cap_cells.get(cy * self.g + cx) {
for &k in caps {
if let Some([a, b, m]) = self.caps.get(k as usize)
&& orient2d(a, b, p) != Sign::Negative
&& orient2d(b, m, p) != Sign::Negative
&& orient2d(m, a, p) != Sign::Negative
{
return false;
}
}
}
let too_close = |&(a, b, r): &([f64; 2], [f64; 2], f64)| distance(a, b, c) < r.max(clearance);
let (x0, y0) = self.cell([c[0] - clearance, c[1] - clearance]);
let (x1, y1) = self.cell([c[0] + clearance, c[1] + clearance]);
if (x1 - x0 + 1) * (y1 - y0 + 1) > MAX_QUERY_CELLS {
return !self.segs.iter().any(too_close);
}
for y in y0..=y1 {
for x in x0..=x1 {
for &k in self.seg_cells.get(y * self.g + x).map(Vec::as_slice).unwrap_or(&[]) {
if self.segs.get(k as usize).is_some_and(too_close) {
return false;
}
}
}
}
true
}
}