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.
349 lines
14 KiB
Rust
349 lines
14 KiB
Rust
//! A trimmed face cut into four-sided patches. The cuts are segments in (u, v): the squares
|
|
//! round the face's holes (each joined to its hole at four points, so the ring between them is
|
|
//! four collar patches) and the motorcycle traces that extend the squares' sides. Each cut is
|
|
//! sampled once and both patches beside it use those nodes (as faces share their edges' nodes),
|
|
//! so the patches meet exactly. A cut never adds a node to the face's boundary: it ends on a
|
|
//! boundary node, at a point inside the face (a square's corner, or where two traces met
|
|
//! head-on), or on another cut (a T-node, which that cut then passes through).
|
|
|
|
use std::collections::HashMap;
|
|
|
|
/// Where a cut segment ends.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub(crate) enum End {
|
|
/// A boundary node.
|
|
Node(u32),
|
|
/// A point inside the face; points at the same place are one node.
|
|
Point([f64; 2]),
|
|
/// A point on cut segment `.0` (a T-node: that segment passes through it).
|
|
On(usize, [f64; 2]),
|
|
}
|
|
|
|
/// A cut from `a` to `b`, straight in (u, v).
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub(crate) struct Segment {
|
|
pub a: End,
|
|
pub b: End,
|
|
/// Segments to sample it in (spread over its pieces between T-nodes by length), instead of
|
|
/// the seed rule: a butterfly's square side takes the count of the hole arc facing it.
|
|
pub count: Option<usize>,
|
|
}
|
|
|
|
/// A patch: its outer ring and any holes inside it, as indices into the boundary nodes followed
|
|
/// by the cut nodes, counter-clockwise (holes clockwise) in (u, v).
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct Piece {
|
|
pub ring: Vec<u32>,
|
|
pub holes: Vec<Vec<u32>>,
|
|
}
|
|
|
|
/// Cut the face bounded by `loops` (ranges of `boundary`) along `segments`, measured in the
|
|
/// metric `scale`. `seeds(a, b, sa, sb)`: where to put nodes between two nodes of a cut (in
|
|
/// (u, v)), as fractions strictly between 0 and 1, given the spacing wanted at each (in the
|
|
/// metric): the boundary's own spacing at a boundary node, unbounded inside the face (the seed
|
|
/// rule then sets it). Returns the cut nodes (numbered after the boundary) and the patches;
|
|
/// `None` when the cuts do not make a planar subdivision (the face is then meshed whole).
|
|
pub(crate) fn cut(
|
|
boundary: &[[f64; 2]],
|
|
loops: &[std::ops::Range<usize>],
|
|
segments: &[Segment],
|
|
scale: [f64; 2],
|
|
seeds: &dyn Fn([f64; 2], [f64; 2], f64, f64) -> Vec<f64>,
|
|
) -> Option<(Vec<[f64; 2]>, Vec<Piece>)> {
|
|
let n = boundary.len();
|
|
let metric = |q: [f64; 2]| [q[0] * scale[0], q[1] * scale[1]];
|
|
let dist = |a: [f64; 2], b: [f64; 2]| {
|
|
let (a, b) = (metric(a), metric(b));
|
|
(a[0] - b[0]).hypot(a[1] - b[1])
|
|
};
|
|
let mut cut_uv: Vec<[f64; 2]> = Vec::new();
|
|
let mut points: HashMap<[u64; 2], u32> = HashMap::new();
|
|
// Each segment's end nodes, and the T-nodes other segments put on it.
|
|
let mut ends: Vec<(u32, u32)> = Vec::with_capacity(segments.len());
|
|
let mut along: Vec<Vec<u32>> = vec![Vec::new(); segments.len()];
|
|
for s in segments {
|
|
let mut key = |e: End| -> Option<u32> {
|
|
let mut point = |q: [f64; 2]| -> Option<u32> {
|
|
let bits = [q[0].to_bits(), q[1].to_bits()];
|
|
if let Some(&k) = points.get(&bits) {
|
|
return Some(k);
|
|
}
|
|
let k = u32::try_from(n + cut_uv.len()).ok()?;
|
|
cut_uv.push(q);
|
|
points.insert(bits, k);
|
|
Some(k)
|
|
};
|
|
match e {
|
|
End::Node(k) => ((k as usize) < n).then_some(k),
|
|
End::Point(q) => point(q),
|
|
End::On(on, q) => {
|
|
let k = point(q)?;
|
|
along.get_mut(on)?.push(k);
|
|
Some(k)
|
|
}
|
|
}
|
|
};
|
|
let (a, b) = (key(s.a)?, key(s.b)?);
|
|
ends.push((a, b));
|
|
}
|
|
let at_in = |cut_uv: &[[f64; 2]], k: u32| -> Option<[f64; 2]> {
|
|
let k = k as usize;
|
|
if k < n { boundary.get(k).copied() } else { cut_uv.get(k - n).copied() }
|
|
};
|
|
// The spacing wanted at a node: at a boundary node the mean of its two boundary segments.
|
|
let spacing = |k: u32| -> f64 {
|
|
let k = k as usize;
|
|
let Some(r) = loops.iter().find(|r| r.contains(&k)) else { return f64::INFINITY };
|
|
let m = r.len();
|
|
let i = k - r.start;
|
|
let (prev, next) = (r.start + (i + m - 1) % m, r.start + (i + 1) % m);
|
|
match (boundary.get(prev), boundary.get(k), boundary.get(next)) {
|
|
(Some(&p), Some(&q), Some(&x)) => 0.5 * (dist(p, q) + dist(q, x)),
|
|
_ => f64::INFINITY,
|
|
}
|
|
};
|
|
// Boundary sides, so a cut that is one of them is left out.
|
|
let mut sides: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
|
|
for r in loops {
|
|
let m = r.len();
|
|
for k in 0..m {
|
|
let (a, b) = (u32::try_from(r.start + k).ok()?, u32::try_from(r.start + (k + 1) % m).ok()?);
|
|
sides.insert((a.min(b), a.max(b)));
|
|
}
|
|
}
|
|
let mut cuts: Vec<(u32, u32)> = Vec::new();
|
|
let mut taken: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
|
|
for (i, &(s0, s1)) in ends.iter().enumerate() {
|
|
if s0 == s1 {
|
|
continue;
|
|
}
|
|
let p0 = at_in(&cut_uv, s0)?;
|
|
let mut nodes: Vec<(f64, u32)> = vec![(0.0, s0)];
|
|
for &k in along.get(i)? {
|
|
nodes.push((dist(p0, at_in(&cut_uv, k)?), k));
|
|
}
|
|
let total = dist(p0, at_in(&cut_uv, s1)?);
|
|
nodes.push((total, s1));
|
|
nodes.sort_by(|a, b| a.0.total_cmp(&b.0));
|
|
nodes.dedup_by_key(|x| x.1);
|
|
let count = segments.get(i).and_then(|s| s.count);
|
|
for w in nodes.windows(2) {
|
|
let (Some(&(_, a)), Some(&(_, b))) = (w.first(), w.get(1)) else { continue };
|
|
if a == b {
|
|
continue;
|
|
}
|
|
// The same cut twice (two segments between the same nodes) is one cut.
|
|
let key = (a.min(b), a.max(b));
|
|
if sides.contains(&key) || !taken.insert(key) {
|
|
continue;
|
|
}
|
|
let (pa, pb) = (at_in(&cut_uv, a)?, at_in(&cut_uv, b)?);
|
|
let fractions: Vec<f64> = match count {
|
|
Some(k) if total > 0.0 => {
|
|
let share = dist(pa, pb) / total;
|
|
let k = ((k as f64 * share).round() as usize).clamp(1, 4096);
|
|
(1..k).map(|j| j as f64 / k as f64).collect()
|
|
}
|
|
_ => seeds(pa, pb, spacing(a), spacing(b)),
|
|
};
|
|
let mut prev = a;
|
|
for f in fractions.into_iter().take(4096) {
|
|
let k = u32::try_from(n + cut_uv.len()).ok()?;
|
|
cut_uv.push([pa[0] + (pb[0] - pa[0]) * f, pa[1] + (pb[1] - pa[1]) * f]);
|
|
cuts.push((prev, k));
|
|
prev = k;
|
|
}
|
|
cuts.push((prev, b));
|
|
}
|
|
}
|
|
if cuts.is_empty() {
|
|
return None;
|
|
}
|
|
let at = |k: u32| at_in(&cut_uv, k);
|
|
let pos = |k: u32| at(k).map(metric);
|
|
// Half-edges: boundary sides with the face on their left, cut sides both ways.
|
|
let inside = |q: [f64; 2]| {
|
|
let mut odd = false;
|
|
for r in loops {
|
|
let m = r.len();
|
|
for k in 0..m {
|
|
let (Some(a), Some(b)) = (boundary.get(r.start + k).map(|p| metric(*p)), boundary.get(r.start + (k + 1) % m).map(|p| metric(*p)))
|
|
else {
|
|
continue;
|
|
};
|
|
if (a[1] > q[1]) != (b[1] > q[1]) && q[0] < a[0] + (q[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0]) {
|
|
odd = !odd;
|
|
}
|
|
}
|
|
}
|
|
odd
|
|
};
|
|
let mut half: Vec<(u32, u32)> = Vec::new();
|
|
for r in loops {
|
|
let m = r.len();
|
|
let side = |k: usize| -> Option<(u32, u32)> { Some((u32::try_from(r.start + k).ok()?, u32::try_from(r.start + (k + 1) % m).ok()?)) };
|
|
// Which way round the face lies to the left: test beside the loop's longest side.
|
|
let longest = (0..m).max_by(|&x, &y| {
|
|
let l = |k: usize| side(k).and_then(|(a, b)| Some(dist(at(a)?, at(b)?))).unwrap_or(0.0);
|
|
l(x).total_cmp(&l(y))
|
|
})?;
|
|
let (a, b) = side(longest)?;
|
|
let (pa, pb) = (pos(a)?, pos(b)?);
|
|
let len = (pb[0] - pa[0]).hypot(pb[1] - pa[1]);
|
|
if !(len > 0.0) {
|
|
return None;
|
|
}
|
|
let left = [0.5 * (pa[0] + pb[0]) - (pb[1] - pa[1]) * 1e-4, 0.5 * (pa[1] + pb[1]) + (pb[0] - pa[0]) * 1e-4];
|
|
let forward = inside(left);
|
|
for k in 0..m {
|
|
let (a, b) = side(k)?;
|
|
if a == b {
|
|
continue;
|
|
}
|
|
half.push(if forward { (a, b) } else { (b, a) });
|
|
}
|
|
}
|
|
for &(a, b) in &cuts {
|
|
half.push((a, b));
|
|
half.push((b, a));
|
|
}
|
|
// Outgoing half-edges per node, by angle.
|
|
let angle = |a: u32, b: u32| -> Option<f64> {
|
|
let (p, q) = (pos(a)?, pos(b)?);
|
|
Some((q[1] - p[1]).atan2(q[0] - p[0]))
|
|
};
|
|
let mut out: HashMap<u32, Vec<(f64, usize)>> = HashMap::new();
|
|
for (h, &(a, b)) in half.iter().enumerate() {
|
|
out.entry(a).or_default().push((angle(a, b)?, h));
|
|
}
|
|
for v in out.values_mut() {
|
|
v.sort_by(|x, y| x.0.total_cmp(&y.0));
|
|
}
|
|
// The face on the left of u→v continues at v with the first outgoing side clockwise from
|
|
// v→u.
|
|
let next = |h: usize| -> Option<usize> {
|
|
let &(u, v) = half.get(h)?;
|
|
let back = angle(v, u)?;
|
|
let list = out.get(&v)?;
|
|
let before = list
|
|
.iter()
|
|
.rev()
|
|
.find(|(a, e)| *a < back - 1e-12 && half.get(*e).is_some_and(|x| x.1 != u))
|
|
.or_else(|| list.iter().rev().find(|(_, e)| half.get(*e).is_some_and(|x| x.1 != u)));
|
|
before.map(|(_, e)| *e).or_else(|| list.iter().find(|(_, e)| half.get(*e).is_some_and(|x| x.1 == u)).map(|(_, e)| *e))
|
|
};
|
|
let mut seen = vec![false; half.len()];
|
|
let mut rings: Vec<Vec<u32>> = Vec::new();
|
|
for h0 in 0..half.len() {
|
|
if *seen.get(h0)? {
|
|
continue;
|
|
}
|
|
let mut ring = Vec::new();
|
|
let mut h = h0;
|
|
loop {
|
|
*seen.get_mut(h)? = true;
|
|
ring.push(half.get(h)?.0);
|
|
h = next(h)?;
|
|
if h == h0 {
|
|
break;
|
|
}
|
|
if ring.len() > half.len() || *seen.get(h)? {
|
|
return None;
|
|
}
|
|
}
|
|
rings.push(ring);
|
|
}
|
|
let area = |ring: &[u32]| -> Option<f64> {
|
|
let mut s = 0.0;
|
|
for k in 0..ring.len() {
|
|
let (a, b) = (pos(*ring.get(k)?)?, pos(*ring.get((k + 1) % ring.len())?)?);
|
|
s += a[0] * b[1] - b[0] * a[1];
|
|
}
|
|
Some(0.5 * s)
|
|
};
|
|
let mut pieces: Vec<Piece> = Vec::new();
|
|
let mut holes: Vec<Vec<u32>> = Vec::new();
|
|
for r in rings {
|
|
if area(&r)? > 0.0 {
|
|
pieces.push(Piece { ring: r, holes: Vec::new() });
|
|
} else {
|
|
holes.push(r);
|
|
}
|
|
}
|
|
// A loop no cut reached is a hole of the patch around it.
|
|
for h in holes {
|
|
let p = pos(*h.first()?)?;
|
|
let contains = |ring: &[u32]| {
|
|
let mut odd = false;
|
|
for k in 0..ring.len() {
|
|
let (Some(a), Some(b)) = (ring.get(k).and_then(|x| pos(*x)), ring.get((k + 1) % ring.len()).and_then(|x| pos(*x))) else { continue };
|
|
if (a[1] > p[1]) != (b[1] > p[1]) && p[0] < a[0] + (p[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0]) {
|
|
odd = !odd;
|
|
}
|
|
}
|
|
odd
|
|
};
|
|
let host = pieces.iter_mut().filter(|pc| contains(&pc.ring)).min_by(|a, b| {
|
|
let s = |pc: &Piece| area(&pc.ring).unwrap_or(f64::MAX);
|
|
s(a).total_cmp(&s(b))
|
|
})?;
|
|
host.holes.push(h);
|
|
}
|
|
Some((cut_uv, pieces))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn area(all: &[[f64; 2]], ring: &[u32]) -> f64 {
|
|
(0..ring.len())
|
|
.map(|k| {
|
|
let (a, b) = (all[ring[k] as usize], all[ring[(k + 1) % ring.len()] as usize]);
|
|
0.5 * (a[0] * b[1] - b[0] * a[1])
|
|
})
|
|
.sum()
|
|
}
|
|
|
|
/// A square cut by four segments meeting at its centre (one point inside the face, shared)
|
|
/// gives four quarters covering it.
|
|
#[test]
|
|
fn a_cross_cuts_a_square_into_four() {
|
|
let b = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [0.0, 2.0], [0.0, 1.0]];
|
|
let loops = [std::ops::Range { start: 0, end: 8 }];
|
|
let c = End::Point([1.0, 1.0]);
|
|
let segs = [
|
|
Segment { a: End::Node(1), b: c, count: None },
|
|
Segment { a: End::Node(3), b: c, count: None },
|
|
Segment { a: End::Node(5), b: c, count: None },
|
|
Segment { a: End::Node(7), b: c, count: None },
|
|
];
|
|
let Some((cut_uv, pieces)) = cut(&b, &loops, &segs, [1.0, 1.0], &|_, _, _, _| vec![0.5]) else { panic!("cuts") };
|
|
assert_eq!(pieces.len(), 4);
|
|
let mut all = b.clone();
|
|
all.extend(cut_uv);
|
|
let total: f64 = pieces.iter().map(|p| area(&all, &p.ring)).sum();
|
|
assert!((total - 4.0).abs() < 1e-12, "{total}");
|
|
}
|
|
|
|
/// A segment ending on another (a T-node) splits it there: three patches.
|
|
#[test]
|
|
fn a_t_node_splits_the_segment_it_lands_on() {
|
|
let b = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [2.0, 2.0], [0.0, 2.0], [0.0, 1.0]];
|
|
let loops = [std::ops::Range { start: 0, end: 7 }];
|
|
let segs = [Segment { a: End::Node(6), b: End::Node(3), count: None }, Segment { a: End::Node(1), b: End::On(0, [1.0, 1.0]), count: None }];
|
|
let Some((cut_uv, pieces)) = cut(&b, &loops, &segs, [1.0, 1.0], &|_, _, _, _| Vec::new()) else { panic!("cuts") };
|
|
assert_eq!(pieces.len(), 3, "{pieces:?}");
|
|
let mut all = b.clone();
|
|
all.extend(cut_uv);
|
|
let total: f64 = pieces.iter().map(|p| area(&all, &p.ring)).sum();
|
|
assert!((total - 4.0).abs() < 1e-12, "{total}");
|
|
}
|
|
|
|
#[test]
|
|
fn no_segments_no_cut() {
|
|
let b = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
|
assert!(cut(&b, &[std::ops::Range { start: 0, end: 4 }], &[], [1.0, 1.0], &|_, _, _, _| Vec::new()).is_none());
|
|
}
|
|
}
|