mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
Tessellation: the band beside a slit and the sag refinement keep the face on both sides of it
A face that meets itself along a line (the Pinch case's boss side, turned 30 degrees: the hole's wall touches it along a vertical line from the hole's bottom to the top rim) has its loop run up that line and back down. Its motorcycle trace runs from the slit's tip to the bottom and the iso-line grid meshes it, but the band between the grid cells and the boundary was filled by a parity flood that flipped at every constraint, the slit's too: the band on one side of the slit was taken for outside, the face did not follow its boundary, and the lenient fill took over. The band now crosses a side the loops run along both ways without counting it, as the triangulation itself already did. Delaunay's sag refinement had the same flood and left the far side of a slit unrefined (the lenient fill's 0.52 mm^3 shortfall); it takes the slits too. The Pinch case now meshes strictly: the boss side is 628.3177 mm^2 against 200 pi = 628.3185, and hole_tangent_inside_a_stepped_boss_cuts_exactly passes. New tess test: a square with a slit meshed by the cut grid follows the slit both ways and covers the square's area exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,18 +79,16 @@ pub(crate) fn triangulate(boundary: &[[f64; 2]], loops: &[std::ops::Range<usize>
|
||||
index.entry(h).or_insert(u32::try_from(i).ok()?);
|
||||
handles.push(h);
|
||||
}
|
||||
// How often the loops run along each side: a side run both ways (a slit, where a face's
|
||||
// boundary goes out along a line its two sides share and back) bounds the face on neither.
|
||||
let mut runs: HashMap<(FixedVertexHandle, FixedVertexHandle), usize> = HashMap::new();
|
||||
// A side the loops run along both ways (a slit, where a face's boundary goes out along a
|
||||
// line its two sides share and back) bounds the face on neither.
|
||||
let mut sides: Vec<(FixedVertexHandle, FixedVertexHandle)> = Vec::new();
|
||||
for r in loops {
|
||||
let n = r.len();
|
||||
for k in 0..n {
|
||||
let (a, b) = (*handles.get(r.start + k)?, *handles.get(r.start + (k + 1) % n)?);
|
||||
if a != b {
|
||||
*runs.entry(if a.index() < b.index() { (a, b) } else { (b, a) }).or_insert(0) += 1;
|
||||
}
|
||||
sides.push((*handles.get(r.start + k)?, *handles.get(r.start + (k + 1) % n)?));
|
||||
}
|
||||
}
|
||||
let slits = slits(&sides);
|
||||
for r in loops {
|
||||
let n = r.len();
|
||||
for k in 0..n {
|
||||
@@ -114,7 +112,7 @@ pub(crate) fn triangulate(boundary: &[[f64; 2]], loops: &[std::ops::Range<usize>
|
||||
}
|
||||
if let Some((sag, budget)) = &fill.refine {
|
||||
let constraints = cdt.num_constraints();
|
||||
let added = refine_sag(&mut cdt, sag, *budget, &from);
|
||||
let added = refine_sag(&mut cdt, sag, *budget, &from, &slits);
|
||||
if fill.shape {
|
||||
let params = RefinementParameters::<f64>::new()
|
||||
.keep_constraint_edges()
|
||||
@@ -127,7 +125,7 @@ pub(crate) fn triangulate(boundary: &[[f64; 2]], loops: &[std::ops::Range<usize>
|
||||
// Shaping flips sides and adds points of its own; the triangles it leaves may
|
||||
// sag again (a long strip round a cylinder), so the sag is checked once more.
|
||||
if shaped > 0 {
|
||||
refine_sag(&mut cdt, sag, budget.saturating_sub(added + shaped), &from);
|
||||
refine_sag(&mut cdt, sag, budget.saturating_sub(added + shaped), &from, &slits);
|
||||
}
|
||||
}
|
||||
// A split constraint would put a point on the boundary the neighbour does not have.
|
||||
@@ -137,9 +135,7 @@ pub(crate) fn triangulate(boundary: &[[f64; 2]], loops: &[std::ops::Range<usize>
|
||||
}
|
||||
let mut uv: Vec<[f64; 2]> = boundary.to_vec();
|
||||
let mut tris = Vec::new();
|
||||
let slits: std::collections::HashSet<(FixedVertexHandle, FixedVertexHandle)> =
|
||||
runs.into_iter().filter(|(_, n)| n % 2 == 0).map(|(e, _)| e).collect();
|
||||
let region = region_without(&cdt, &slits);
|
||||
let region = region(&cdt, &slits);
|
||||
for face in cdt.inner_faces() {
|
||||
if !region.get(face.index()).copied().unwrap_or(false) {
|
||||
continue;
|
||||
@@ -199,6 +195,8 @@ pub(crate) fn band(points: &[[f64; 2]], n: usize, loops: &[std::ops::Range<usize
|
||||
sides.push((add(&mut cdt, a)?, add(&mut cdt, b)?));
|
||||
}
|
||||
}
|
||||
// The band runs along both sides of a slit (see `triangulate`): its sides bound it on neither.
|
||||
let slits = slits(&sides);
|
||||
for &(a, b) in stairs {
|
||||
sides.push((add(&mut cdt, a)?, add(&mut cdt, b)?));
|
||||
}
|
||||
@@ -211,7 +209,7 @@ pub(crate) fn band(points: &[[f64; 2]], n: usize, loops: &[std::ops::Range<usize
|
||||
}
|
||||
cdt.add_constraint(a, b);
|
||||
}
|
||||
let region = region(&cdt);
|
||||
let region = region(&cdt, &slits);
|
||||
let mut tris = Vec::new();
|
||||
for face in cdt.inner_faces() {
|
||||
if !region.get(face.index()).copied().unwrap_or(false) {
|
||||
@@ -230,7 +228,14 @@ pub(crate) fn band(points: &[[f64; 2]], n: usize, loops: &[std::ops::Range<usize
|
||||
/// the tolerance from the triangle or whose corner normals turn more than the facet angle, and
|
||||
/// under the middle of every non-constraint side that sags more than the tolerance or is
|
||||
/// longer than [`LONG`] times `h`, until none does or `budget` points were added. Returns the points added.
|
||||
fn refine_sag(cdt: &mut Cdt, sag: &Sag, budget: usize, from: &dyn Fn(Point2<f64>) -> [f64; 2]) -> usize {
|
||||
/// `slits`: constraint sides the loops run along both ways, inside the region on both sides.
|
||||
fn refine_sag(
|
||||
cdt: &mut Cdt,
|
||||
sag: &Sag,
|
||||
budget: usize,
|
||||
from: &dyn Fn(Point2<f64>) -> [f64; 2],
|
||||
slits: &std::collections::HashSet<(FixedVertexHandle, FixedVertexHandle)>,
|
||||
) -> usize {
|
||||
let mut added = 0;
|
||||
let mut cache: HashMap<FixedVertexHandle, Option<(Vec3, Option<Vec3>)>> = HashMap::new();
|
||||
// The widest turn between two corner normals.
|
||||
@@ -246,7 +251,7 @@ fn refine_sag(cdt: &mut Cdt, sag: &Sag, budget: usize, from: &dyn Fn(Point2<f64>
|
||||
(sag.eval)(q[0], q[1])
|
||||
})
|
||||
};
|
||||
let region = region(cdt);
|
||||
let region = region(cdt, slits);
|
||||
for face in cdt.inner_faces() {
|
||||
if !region.get(face.index()).copied().unwrap_or(false) {
|
||||
continue;
|
||||
@@ -318,15 +323,23 @@ fn refine_sag(cdt: &mut Cdt, sag: &Sag, budget: usize, from: &dyn Fn(Point2<f64>
|
||||
added
|
||||
}
|
||||
|
||||
/// Per face (by index): inside the region the constraint loops bound. A flood fill from the
|
||||
/// outer face that flips parity each time it crosses a constraint edge (holes are out), linear
|
||||
/// in the number of triangles.
|
||||
fn region(cdt: &Cdt) -> Vec<bool> {
|
||||
region_without(cdt, &std::collections::HashSet::new())
|
||||
/// The sides a boundary runs along an even number of times (a slit: out along a line and back),
|
||||
/// each as its vertices in index order: the region lies on both sides of them.
|
||||
fn slits(sides: &[(FixedVertexHandle, FixedVertexHandle)]) -> std::collections::HashSet<(FixedVertexHandle, FixedVertexHandle)> {
|
||||
let mut runs: HashMap<(FixedVertexHandle, FixedVertexHandle), usize> = HashMap::new();
|
||||
for &(a, b) in sides {
|
||||
if a != b {
|
||||
*runs.entry(if a.index() < b.index() { (a, b) } else { (b, a) }).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
runs.into_iter().filter(|(_, n)| n % 2 == 0).map(|(e, _)| e).collect()
|
||||
}
|
||||
|
||||
/// [`region`], the constraint sides in `slits` (run along both ways) crossed without counting.
|
||||
fn region_without(cdt: &Cdt, slits: &std::collections::HashSet<(FixedVertexHandle, FixedVertexHandle)>) -> Vec<bool> {
|
||||
/// Per face (by index): inside the region the constraint loops bound. A flood fill from the
|
||||
/// outer face that flips parity each time it crosses a constraint edge (holes are out), but for
|
||||
/// the sides in `slits` (run along both ways, the region on both sides), linear in the number of
|
||||
/// triangles.
|
||||
fn region(cdt: &Cdt, slits: &std::collections::HashSet<(FixedVertexHandle, FixedVertexHandle)>) -> Vec<bool> {
|
||||
use spade::handles::{FixedFaceHandle, InnerTag};
|
||||
let counts = |e: spade::handles::DirectedEdgeHandle<'_, _, _, _, _>| {
|
||||
let [a, b] = e.vertices().map(|v| v.fix());
|
||||
|
||||
@@ -669,6 +669,39 @@ fn loops_touching_at_a_shared_vertex_bound_the_face() {
|
||||
assert!(closed_boundary(&ranges, &fixed, &uvs, &triangles), "the triangles follow both holes through their shared vertex");
|
||||
}
|
||||
|
||||
/// A face meeting itself along a line (a hole's wall tangent inside the wall round it, cut from
|
||||
/// the boss side's top rim down to the hole's bottom): its loop runs up the slit and back down
|
||||
/// it, with the face on both sides. The iso-line grid's band beside the slit is the face on
|
||||
/// either side of it, and the triangles follow the slit both ways and cover the whole square.
|
||||
#[test]
|
||||
fn a_slit_in_a_trimmed_face_has_the_face_on_both_sides() {
|
||||
// The square [0, 8] × [0, 4], its loop starting at the slit's tip (3, 1) up to the top, round
|
||||
// the square and back down the slit.
|
||||
let mut uvs: Vec<[f64; 2]> = (1..=4).map(|k| [3.0, k as f64]).collect();
|
||||
uvs.extend((0..3).rev().map(|k| [k as f64, 4.0]));
|
||||
uvs.extend((0..4).rev().map(|k| [0.0, k as f64]));
|
||||
uvs.extend((1..=8).map(|k| [k as f64, 0.0]));
|
||||
uvs.extend((1..=4).map(|k| [8.0, k as f64]));
|
||||
uvs.extend((3..8).rev().map(|k| [k as f64, 4.0]));
|
||||
uvs.extend((2..4).rev().map(|k| [3.0, k as f64]));
|
||||
let fixed: Vec<Vec3> = uvs.iter().map(|q| Vec3::new(q[0], q[1], 0.0)).collect();
|
||||
let ranges = [std::ops::Range { start: 0, end: uvs.len() }];
|
||||
let traces = motorcycle::traces(&uvs, &ranges, [1.0, 1.0], 0.0);
|
||||
let lines = |n: usize, top: f64| (0..=n).map(|k| top * k as f64 / n as f64).collect::<Vec<f64>>();
|
||||
let Some((extra, triangles)) = cutgrid::cut_grid(&uvs, &ranges, &lines(16, 8.0), &lines(8, 4.0), &traces, [1.0, 1.0]) else { panic!("meshes") };
|
||||
assert!(closed_boundary(&ranges, &fixed, &uvs, &triangles), "the triangles follow the slit both ways");
|
||||
let mut all = uvs.clone();
|
||||
all.extend(extra);
|
||||
let area: f64 = triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let [a, b, c] = t.map(|i| all[i as usize]);
|
||||
0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]))
|
||||
})
|
||||
.sum();
|
||||
assert!((area - 32.0).abs() < 1e-12, "area {area}");
|
||||
}
|
||||
|
||||
/// A face whose loop is a thin lune meshes: a half disc of radius 2.5 less a circle through the
|
||||
/// same ends 0.03 mm inside it at the middle, its inner side split at 11.25°. Each edge is
|
||||
/// sampled to the 0.1 mm chord on its own: the outer arc's chords (every 22.5°) sag 0.048 mm,
|
||||
|
||||
Reference in New Issue
Block a user