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.
This commit is contained in:
1 parent
e519fbd14d
commit
e3459edea8
11 files changed
+2068
-38
No files matched your search
+285
-21
@@ -14,11 +14,20 @@ const CORNER_TURN: f64 = 0.6;
|
||||
struct Side {
|
||||
nodes: Vec<u32>,
|
||||
at: Vec<f64>,
|
||||
/// Its length in 3D (mm).
|
||||
len: f64,
|
||||
}
|
||||
|
||||
/// The four corners of a single boundary loop, by how sharply the loop turns there in (u, v)
|
||||
/// scaled by `scale` (one unit per tolerance cell); `None` unless there are exactly four.
|
||||
fn corners(uv: &[[f64; 2]], scale: [f64; 2]) -> Option<[usize; 4]> {
|
||||
let [a, b, c, d] = candidates(uv, scale)?[..] else { return None };
|
||||
Some([a, b, c, d])
|
||||
}
|
||||
|
||||
/// Every node where the loop turns more than [`CORNER_TURN`] (or meets a pole copy), in loop
|
||||
/// order.
|
||||
fn candidates(uv: &[[f64; 2]], scale: [f64; 2]) -> Option<Vec<usize>> {
|
||||
let n = uv.len();
|
||||
if n < 4 {
|
||||
return None;
|
||||
@@ -39,8 +48,60 @@ fn corners(uv: &[[f64; 2]], scale: [f64; 2]) -> Option<[usize; 4]> {
|
||||
found.push(i);
|
||||
}
|
||||
}
|
||||
let [a, b, c, d] = found[..] else { return None };
|
||||
Some([a, b, c, d])
|
||||
Some(found)
|
||||
}
|
||||
|
||||
/// Most segments a patch's own level gives one direction.
|
||||
const MAX_LEVEL: usize = 4096;
|
||||
|
||||
/// Most corner candidates [`coons_best`] chooses four from (8 choose 4 = 70 blends at most).
|
||||
const MAX_CANDIDATES: usize = 8;
|
||||
|
||||
/// Like [`coons`] for a loop with more than four corners (a patch of a trimmed face with a
|
||||
/// corner cut off, the wing of a butterfly around a hole): every choice of four of its corners
|
||||
/// is blended, and the one whose smallest triangle angle (in the metric `scale`) is largest
|
||||
/// wins. Its other corners lie on sides. `None` if no choice gives a blend that does not fold.
|
||||
pub(crate) fn coons_best(uv: &[[f64; 2]], fixed: &[Vec3], scale: [f64; 2], max_points: usize, h: f64) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
|
||||
let c = candidates(uv, scale)?;
|
||||
if c.len() < 4 || c.len() > MAX_CANDIDATES {
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<(f64, (Vec<[f64; 2]>, Vec<[u32; 3]>))> = None;
|
||||
let k = c.len();
|
||||
for a in 0..k {
|
||||
for b in a + 1..k {
|
||||
for d in b + 1..k {
|
||||
for e in d + 1..k {
|
||||
let pick = [*c.get(a)?, *c.get(b)?, *c.get(d)?, *c.get(e)?];
|
||||
let Some(r) = coons_at(uv, fixed, pick, max_points, h, None) else { continue };
|
||||
let q = min_angle(uv, &r, scale);
|
||||
if best.as_ref().is_none_or(|(bq, _)| q > *bq) {
|
||||
best = Some((q, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(_, r)| r)
|
||||
}
|
||||
|
||||
/// Smallest corner angle (radians) of the triangles, in the metric.
|
||||
fn min_angle(uv: &[[f64; 2]], r: &(Vec<[f64; 2]>, Vec<[u32; 3]>), scale: [f64; 2]) -> f64 {
|
||||
let at = |i: u32| {
|
||||
let i = i as usize;
|
||||
let q = if i < uv.len() { uv.get(i) } else { r.0.get(i - uv.len()) };
|
||||
q.map(|q| [q[0] * scale[0], q[1] * scale[1]])
|
||||
};
|
||||
let mut worst = std::f64::consts::PI;
|
||||
for t in &r.1 {
|
||||
let (Some(a), Some(b), Some(c)) = (at(t[0]), at(t[1]), at(t[2])) else { continue };
|
||||
for (p, q, s) in [(a, b, c), (b, c, a), (c, a, b)] {
|
||||
let (u, v) = ([q[0] - p[0], q[1] - p[1]], [s[0] - p[0], s[1] - p[1]]);
|
||||
let ang = (u[0] * v[1] - u[1] * v[0]).abs().atan2(u[0] * v[0] + u[1] * v[1]);
|
||||
worst = worst.min(ang);
|
||||
}
|
||||
}
|
||||
worst
|
||||
}
|
||||
|
||||
fn side(fixed: &[Vec3], n: usize, from: usize, to: usize) -> Option<Side> {
|
||||
@@ -58,9 +119,9 @@ fn side(fixed: &[Vec3], n: usize, from: usize, to: usize) -> Option<Side> {
|
||||
if !(total > 0.0) {
|
||||
// A pole side: evenly along it.
|
||||
let m = (nodes.len() - 1).max(1) as f64;
|
||||
return Some(Side { at: (0..nodes.len()).map(|k| k as f64 / m).collect(), nodes });
|
||||
return Some(Side { at: (0..nodes.len()).map(|k| k as f64 / m).collect(), nodes, len: 0.0 });
|
||||
}
|
||||
Some(Side { nodes, at: at.into_iter().map(|x| x / total).collect() })
|
||||
Some(Side { nodes, at: at.into_iter().map(|x| x / total).collect(), len: total })
|
||||
}
|
||||
|
||||
/// The point of a side at position `s` (0 to 1), linear between its nodes.
|
||||
@@ -72,9 +133,12 @@ fn along(side: &Side, uv: &[[f64; 2]], s: f64) -> Option<[f64; 2]> {
|
||||
Some([p[0] + (q[0] - p[0]) * f, p[1] + (q[1] - p[1]) * f])
|
||||
}
|
||||
|
||||
/// Interior line positions for a pair of opposite sides: the sides' own when they have the
|
||||
/// same nodes (so the grid meets both node for node), else evenly as many as the denser side.
|
||||
fn lines(a: &Side, b: &Side) -> Vec<f64> {
|
||||
/// Interior line positions for a pair of opposite sides: the sides' own when they have as many
|
||||
/// nodes as each other (so the grid meets both node for node); else evenly at the inner level a
|
||||
/// GPU tessellator takes, the larger of the two sides' (outer) levels, and never coarser than the
|
||||
/// size rules ask (the sides' mean length over the target side `h`). The denser side then meets
|
||||
/// the grid node for node and the sparser one steps down to it through its ring.
|
||||
fn lines(a: &Side, b: &Side, h: f64) -> Vec<f64> {
|
||||
let inner = |s: &Side| s.at.get(1..s.at.len().saturating_sub(1)).map(<[f64]>::to_vec).unwrap_or_default();
|
||||
if a.at.len() == b.at.len() {
|
||||
let l: Vec<f64> = inner(a).iter().zip(inner(b)).map(|(x, y)| 0.5 * (x + y)).collect();
|
||||
@@ -82,23 +146,43 @@ fn lines(a: &Side, b: &Side) -> Vec<f64> {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
let n = a.at.len().max(b.at.len()).saturating_sub(1).max(2);
|
||||
let size = if h > 0.0 { (0.5 * (a.len + b.len) / h).ceil() } else { 1.0 };
|
||||
let size = if size.is_finite() { size as usize } else { 1 };
|
||||
let outer = a.at.len().max(b.at.len()).saturating_sub(1);
|
||||
let n = outer.max(size).clamp(2, MAX_LEVEL);
|
||||
(1..n).map(|i| i as f64 / n as f64).collect()
|
||||
}
|
||||
|
||||
/// Triangulate the loop `uv` (with its fixed 3D nodes) as a Coons patch: boundary vertices
|
||||
/// keep their indices, the interior grid follows. Triangles counter-clockwise in (u, v).
|
||||
/// `fine(a, b)`: whether the surface between two parameter points needs no point between them
|
||||
/// (the grid lines, taken from the sides, are split inside the face where it does not).
|
||||
///
|
||||
/// `fine(a, b)`: whether the surface between two parameter points needs no point between them.
|
||||
/// Given for a whole face, whose sides are its edges' nodes: where its grid converges (towards a
|
||||
/// pole) the interior can need more lines than the sides carry, and the lines are split there.
|
||||
/// A motorcycle patch passes `None`: its sides are cuts seeded at the face's global seed.
|
||||
pub(crate) fn coons(
|
||||
uv: &[[f64; 2]],
|
||||
fixed: &[Vec3],
|
||||
scale: [f64; 2],
|
||||
max_points: usize,
|
||||
fine: &dyn Fn([f64; 2], [f64; 2]) -> bool,
|
||||
h: f64,
|
||||
fine: Option<&dyn Fn([f64; 2], [f64; 2]) -> bool>,
|
||||
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
|
||||
let pick = corners(uv, scale)?;
|
||||
coons_at(uv, fixed, pick, max_points, h, fine)
|
||||
}
|
||||
|
||||
/// [`coons`] with the four corners given (loop indices, in loop order).
|
||||
pub(crate) fn coons_at(
|
||||
uv: &[[f64; 2]],
|
||||
fixed: &[Vec3],
|
||||
pick: [usize; 4],
|
||||
max_points: usize,
|
||||
h: f64,
|
||||
fine: Option<&dyn Fn([f64; 2], [f64; 2]) -> bool>,
|
||||
) -> Option<(Vec<[f64; 2]>, Vec<[u32; 3]>)> {
|
||||
let n = uv.len();
|
||||
let [c0, c1, c2, c3] = corners(uv, scale)?;
|
||||
let [c0, c1, c2, c3] = pick;
|
||||
// Sides in loop order; the bottom (0) and right (1) run with the loop, the top (2) and
|
||||
// left (3) against it, so s and t both run from corner c0.
|
||||
let bottom = side(fixed, n, c0, c1)?;
|
||||
@@ -116,8 +200,16 @@ pub(crate) fn coons(
|
||||
};
|
||||
Some([blend(0), blend(1)])
|
||||
};
|
||||
let ss = refine(lines(&bottom, &top), max_points, &|a, b, x| Some(fine(at(a, x)?, at(b, x)?)));
|
||||
let ts = refine(lines(&left, &right), max_points, &|a, b, x| Some(fine(at(x, a)?, at(x, b)?)));
|
||||
// The lines come from the sides' seeds (see [`lines`]): every side is seeded at the face's
|
||||
// global seed, which the size rules set for the whole surface, so the interior needs no finer
|
||||
// spacing than its sides.
|
||||
let (ss, ts) = (lines(&bottom, &top, h), lines(&left, &right, h));
|
||||
let (ss, ts) = match fine {
|
||||
Some(fine) => {
|
||||
(refine(ss, max_points, &|a, b, x| Some(fine(at(a, x)?, at(b, x)?))), refine(ts, max_points, &|a, b, x| Some(fine(at(x, a)?, at(x, b)?))))
|
||||
}
|
||||
None => (ss, ts),
|
||||
};
|
||||
if ss.len().checked_mul(ts.len())? > max_points {
|
||||
return None;
|
||||
}
|
||||
@@ -141,10 +233,46 @@ pub(crate) fn coons(
|
||||
// the bottom and right strips come out counter-clockwise, the top and left clockwise.
|
||||
let row = |j: usize| (0..ns).map(|i| g(i, j)).collect::<Vec<u32>>();
|
||||
let col = |i: usize| (0..nt).map(|j| g(i, j)).collect::<Vec<u32>>();
|
||||
zip(&bottom, &row(0), &ss, false, &mut tris)?;
|
||||
zip(&right, &col(ns - 1), &ts, false, &mut tris)?;
|
||||
zip(&top, &row(nt - 1), &ss, true, &mut tris)?;
|
||||
zip(&left, &col(0), &ts, true, &mut tris)?;
|
||||
// Where a side's segments and its grid row's differ, a transition row steps between them
|
||||
// halfway to the first grid line (its segments the two counts' geometric mean), each zipped
|
||||
// to it, with one triangle at each end closing it against the corner.
|
||||
let (s_first, s_last) = (*ss.first()?, *ss.last()?);
|
||||
let (t_first, t_last) = (*ts.first()?, *ts.last()?);
|
||||
// Each side: its grid row, the row's positions, the orientation, where the transition row
|
||||
// runs, and the point at a position along it. The row lies halfway between the side and the
|
||||
// grid row; its ends lie halfway between the diagonal from the corner to the grid row's end
|
||||
// and that end, so they are strictly inside the side's own ring.
|
||||
#[allow(clippy::type_complexity)]
|
||||
let sides: [(&Side, Vec<u32>, &[f64], bool, (f64, f64), &dyn Fn(f64) -> [f64; 2]); 4] = [
|
||||
(&bottom, row(0), &ss, false, (0.75 * s_first, 1.0 - 0.75 * (1.0 - s_last)), &|x| [x, 0.5 * t_first]),
|
||||
(&right, col(ns - 1), &ts, false, (0.75 * t_first, 1.0 - 0.75 * (1.0 - t_last)), &|x| [0.5 * (1.0 + s_last), x]),
|
||||
(&top, row(nt - 1), &ss, true, (0.75 * s_first, 1.0 - 0.75 * (1.0 - s_last)), &|x| [x, 0.5 * (1.0 + t_last)]),
|
||||
(&left, col(0), &ts, true, (0.75 * t_first, 1.0 - 0.75 * (1.0 - t_last)), &|x| [0.5 * s_first, x]),
|
||||
];
|
||||
for (side, inner, inner_at, flip, (x0, x1), place) in sides {
|
||||
let (outer_segs, inner_segs) = (side.nodes.len().saturating_sub(1), inner.len() + 1);
|
||||
let mean = ((outer_segs * inner_segs) as f64).sqrt().round() as usize;
|
||||
if mean == outer_segs || mean == inner_segs || mean < 2 {
|
||||
zip(side, &inner, inner_at, flip, &mut tris)?;
|
||||
continue;
|
||||
}
|
||||
let k = mean - 1;
|
||||
let at_t: Vec<f64> = (0..k).map(|j| if k > 1 { x0 + (x1 - x0) * j as f64 / (k - 1) as f64 } else { 0.5 * (x0 + x1) }).collect();
|
||||
let mut mid: Vec<u32> = Vec::with_capacity(k);
|
||||
for &x in &at_t {
|
||||
let [sx, tx] = place(x);
|
||||
mid.push(u32::try_from(out.len()).ok()?);
|
||||
out.push(at(sx, tx)?);
|
||||
}
|
||||
zip(side, &mid, &at_t, flip, &mut tris)?;
|
||||
zip(&Side { nodes: mid.clone(), at: at_t.clone(), len: 0.0 }, &inner, inner_at, flip, &mut tris)?;
|
||||
// The two end triangles: corner, transition end, grid end, turned like the zips' (in
|
||||
// (s, t) the first runs with them and the last against). Never turned to fit the blend:
|
||||
// a blend that folds there leaves one inverted, and the patch is refused below.
|
||||
for t in [[*side.nodes.first()?, *mid.first()?, *inner.first()?], [*side.nodes.last()?, *inner.last()?, *mid.last()?]] {
|
||||
tris.push(if flip { [t[0], t[2], t[1]] } else { t });
|
||||
}
|
||||
}
|
||||
// The (s, t) square maps to (u, v) keeping or reversing orientation, with the loop: all
|
||||
// triangles must share one sign, which is then made counter-clockwise.
|
||||
let area = |t: &[u32; 3]| -> Option<f64> {
|
||||
@@ -188,7 +316,7 @@ fn refine(lines: Vec<f64>, max: usize, fine: &dyn Fn(f64, f64, f64) -> Option<bo
|
||||
}
|
||||
|
||||
fn reversed(s: Side) -> Side {
|
||||
Side { nodes: s.nodes.into_iter().rev().collect(), at: s.at.into_iter().rev().map(|x| 1.0 - x).collect() }
|
||||
Side { nodes: s.nodes.into_iter().rev().collect(), at: s.at.into_iter().rev().map(|x| 1.0 - x).collect(), len: s.len }
|
||||
}
|
||||
|
||||
/// Join a side (its nodes at positions `side.at`) to a grid row at positions `inner_at`.
|
||||
@@ -251,7 +379,7 @@ mod tests {
|
||||
b.reverse();
|
||||
}
|
||||
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
|
||||
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 10_000, &|_, _| true) else { panic!("meshes (k {k})") };
|
||||
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 10_000, 1.0, None) else { panic!("meshes (k {k})") };
|
||||
let mut uv = b.clone();
|
||||
uv.extend(extra);
|
||||
assert!((area(&uv, &tris) - 16.0).abs() < 1e-9, "k {k} reverse {reverse}: {}", area(&uv, &tris));
|
||||
@@ -259,10 +387,146 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opposite sides with different node counts (20 against 4): the interior grid takes the
|
||||
/// larger side's level (a tessellator's inner level), so the dense side meets it node for node
|
||||
/// and the sparse side steps down to it through a transition row. Covered exactly, nothing
|
||||
/// missing or doubled.
|
||||
#[test]
|
||||
fn the_interior_level_is_the_larger_sides_and_the_sparser_side_steps_down() {
|
||||
// A 4 × 4 square: bottom in 20 segments, the other sides in 4.
|
||||
let mut b: Vec<[f64; 2]> = (0..20).map(|k| [k as f64 * 0.2, 0.0]).collect();
|
||||
b.extend((0..4).map(|k| [4.0, k as f64]));
|
||||
b.extend((0..4).map(|k| [4.0 - k as f64, 4.0]));
|
||||
b.extend((0..4).map(|k| [0.0, 4.0 - k as f64]));
|
||||
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
|
||||
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 10_000, 1.0, None) else { panic!("meshes") };
|
||||
let mut uv = b.clone();
|
||||
uv.extend(extra.iter().copied());
|
||||
assert!((area(&uv, &tris) - 16.0).abs() < 1e-9);
|
||||
// Level 20 along s (the bottom's), 4 along t: 19 × 3 interior points; plus the transition
|
||||
// row by the 4-segment top (√(4 · 20) ≈ 9 segments: 8 points).
|
||||
assert_eq!(extra.len(), 19 * 3 + 8, "{}", extra.len());
|
||||
// Every inner side shared by two triangles, every boundary side used once: nothing
|
||||
// missing or doubled at the transition's ends.
|
||||
let mut sides: std::collections::HashMap<(u32, u32), i32> = std::collections::HashMap::new();
|
||||
for t in &tris {
|
||||
for e in 0..3 {
|
||||
*sides.entry((t[e], t[(e + 1) % 3])).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let n = b.len() as u32;
|
||||
for (&(a, c), &k) in &sides {
|
||||
assert_eq!(k, 1, "side {a}-{c} once each way");
|
||||
let boundary = a < n && c < n && (c == (a + 1) % n);
|
||||
assert!(boundary || sides.contains_key(&(c, a)), "inner side {a}-{c} shared");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_corners_is_not_a_coons_patch() {
|
||||
let b = [[0.0, 0.0], [1.0, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 1.0]];
|
||||
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
|
||||
assert!(coons(&b, &fixed, [1.0, 1.0], 100, &|_, _| true).is_none());
|
||||
assert!(coons(&b, &fixed, [1.0, 1.0], 100, 1.0, None).is_none());
|
||||
}
|
||||
|
||||
/// A planar strip with a fillet at one end and a chamfer at the other, in mm (a face of
|
||||
/// robustness seed 33): its corners make the one-segment chamfer a side facing the long one,
|
||||
/// and at some target sides the blend folds beside the chamfer. The transition row's end
|
||||
/// triangles are turned like its zips, never to fit the blend, so a fold leaves a triangle
|
||||
/// inverted and the blend is refused; any blend given covers the loop exactly once.
|
||||
#[test]
|
||||
#[allow(clippy::approx_constant)] // measured coordinates, one of them near ln 10
|
||||
fn a_blend_folding_beside_a_short_side_is_refused_not_overlapped() {
|
||||
let b: Vec<[f64; 2]> = vec![
|
||||
[24.0000, 3.0150],
|
||||
[23.0000, 4.4292],
|
||||
[16.6667, 4.4292],
|
||||
[10.3333, 4.4292],
|
||||
[4.0000, 4.4292],
|
||||
[4.0000, 3.0150],
|
||||
[4.0002, 2.9836],
|
||||
[4.0010, 2.9520],
|
||||
[4.0023, 2.9201],
|
||||
[4.0040, 2.8880],
|
||||
[4.0064, 2.8556],
|
||||
[4.0092, 2.8231],
|
||||
[4.0127, 2.7905],
|
||||
[4.0167, 2.7577],
|
||||
[4.0213, 2.7248],
|
||||
[4.0265, 2.6919],
|
||||
[4.0322, 2.6589],
|
||||
[4.0386, 2.6259],
|
||||
[4.0456, 2.5929],
|
||||
[4.0532, 2.5600],
|
||||
[4.0614, 2.5272],
|
||||
[4.0702, 2.4945],
|
||||
[4.0797, 2.4619],
|
||||
[4.0897, 2.4295],
|
||||
[4.1004, 2.3974],
|
||||
[4.1117, 2.3655],
|
||||
[4.1236, 2.3339],
|
||||
[4.1362, 2.3026],
|
||||
[4.1493, 2.2717],
|
||||
[4.1630, 2.2412],
|
||||
[4.1773, 2.2111],
|
||||
[4.1922, 2.1814],
|
||||
[4.2076, 2.1523],
|
||||
[4.2236, 2.1237],
|
||||
[4.2402, 2.0956],
|
||||
[4.2573, 2.0681],
|
||||
[4.2748, 2.0412],
|
||||
[4.2929, 2.0150],
|
||||
[4.3114, 1.9895],
|
||||
[4.3304, 1.9646],
|
||||
[4.3499, 1.9405],
|
||||
[4.3697, 1.9171],
|
||||
[4.3899, 1.8945],
|
||||
[4.4105, 1.8726],
|
||||
[4.4315, 1.8516],
|
||||
[4.4528, 1.8313],
|
||||
[4.4744, 1.8119],
|
||||
[4.4962, 1.7934],
|
||||
[4.5184, 1.7757],
|
||||
[4.5407, 1.7588],
|
||||
[4.5633, 1.7428],
|
||||
[4.5860, 1.7277],
|
||||
[4.6089, 1.7135],
|
||||
[4.6319, 1.7001],
|
||||
[4.6550, 1.6876],
|
||||
[4.6783, 1.6760],
|
||||
[4.7015, 1.6653],
|
||||
[4.7248, 1.6554],
|
||||
[4.7482, 1.6464],
|
||||
[4.7715, 1.6382],
|
||||
[4.7948, 1.6309],
|
||||
[4.8180, 1.6244],
|
||||
[4.8412, 1.6188],
|
||||
[4.8643, 1.6139],
|
||||
[4.8873, 1.6098],
|
||||
[4.9101, 1.6065],
|
||||
[4.9329, 1.6040],
|
||||
[4.9554, 1.6022],
|
||||
[4.9778, 1.6012],
|
||||
[5.0000, 1.6008],
|
||||
[11.3333, 1.6008],
|
||||
[17.6667, 1.6008],
|
||||
[24.0000, 1.6008],
|
||||
];
|
||||
let fixed: Vec<Vec3> = b.iter().map(|p| Vec3::new(p[0], p[1], 0.0)).collect();
|
||||
let n = b.len();
|
||||
let loop_area: f64 = (0..n).map(|k| 0.5 * (b[k][0] * b[(k + 1) % n][1] - b[(k + 1) % n][0] * b[k][1])).sum();
|
||||
for h in [0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0] {
|
||||
let Some((extra, tris)) = coons(&b, &fixed, [1.0, 1.0], 100_000, h, None) else { continue };
|
||||
let mut uv = b.clone();
|
||||
uv.extend(extra);
|
||||
assert!((area(&uv, &tris) - loop_area).abs() < 1e-9 * loop_area, "h {h}: {} for {loop_area}", area(&uv, &tris));
|
||||
let mut sides: std::collections::HashMap<(u32, u32), usize> = std::collections::HashMap::new();
|
||||
for t in &tris {
|
||||
for k in 0..3 {
|
||||
*sides.entry((t[k], t[(k + 1) % 3])).or_default() += 1;
|
||||
}
|
||||
}
|
||||
assert!(sides.values().all(|&k| k == 1), "h {h}: a side used twice the same way");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user