mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
tess: no edge segment spans more than a quarter turn; loops touching at a vertex close
Every edge segment now spans at most a quarter turn of its curve, whatever the chord tolerance: a hole far below the tolerance (a 1 mm hole through a 990 mm plate at a 2 mm chord) stays a polygon with area, where each half arc had been one chord and the two chords lay on one line, a hole of no area. The boundary check takes boundary nodes at the very same (u, v) as one vertex, as the triangulation does: two loops touching at a vertex they share (a ring standing on a perforated plate, tangent to the holes beside it) bound a face the triangles follow. A seam's two uses, one point in space a period apart in (u, v), stay two. Fixes rig booleans/3665 and sheet-metal/1040. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm (cherry picked from commit 53f4f9a2a991fcd6d1374ca2d647e3a1be36554f)
This commit is contained in:
@@ -12,6 +12,10 @@ use crate::{KernelError, Result};
|
||||
pub(crate) const MAX_EDGE_NODES: usize = 20_000;
|
||||
/// Deepest bisection of one edge segment.
|
||||
const MAX_DEPTH: u32 = 24;
|
||||
/// The most of its curve's turn one segment may span (a quarter turn, with rounding room): the
|
||||
/// boundary of every face stays a polygon with the face's area, as a closed curve's first four
|
||||
/// segments keep it.
|
||||
const QUARTER_TURN: f64 = std::f64::consts::FRAC_PI_2 * (1.0 + 1e-9);
|
||||
|
||||
/// An edge's nodes, from its start vertex (interval min) to its end vertex (interval max).
|
||||
pub(crate) struct Nodes {
|
||||
@@ -23,9 +27,10 @@ pub(crate) struct Nodes {
|
||||
|
||||
/// Nodes of edge `ei` of `b` from its start to its end: every chord within the tolerance of
|
||||
/// the curve (checked at the quarter points, so an S-bend in one segment is not missed), at
|
||||
/// most `h` long, and within the rules' facet angle. The end nodes are the vertices' own
|
||||
/// points, so edges meeting at a vertex meet exactly. A curve that does not evaluate somewhere
|
||||
/// on its interval is an error naming the edge.
|
||||
/// most `h` long, within the rules' facet angle and never spanning more than a quarter turn of
|
||||
/// the curve. The end nodes are the vertices' own points, so edges meeting at a vertex meet
|
||||
/// exactly. A curve that does not evaluate somewhere on its interval is an error naming the
|
||||
/// edge.
|
||||
pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> Result<Nodes> {
|
||||
let fail = |why: String| KernelError::Failed(format!("tessellation: edge {ei}: {why}"));
|
||||
let edge = b.edges.get(ei).ok_or_else(|| fail("no such edge".into()))?;
|
||||
@@ -92,8 +97,13 @@ pub(crate) fn sample_edge(b: &brep::Brep, ei: usize, rules: &Rules, h: f64) -> R
|
||||
let sag = [q1, pm, q3].iter().map(|q| q.dist_to_segment(pa, pb)).fold(0.0, f64::max);
|
||||
let short = !(pa.dist(pb) > h * (1.0 + 1e-9));
|
||||
// The two half chords of an arc turning θ differ by θ/2.
|
||||
let straight = rules.turn.is_none_or(|t| !t.exceeds(2.0 * turn(pm - pa, pb - pm), sag));
|
||||
short && straight && sag <= tol
|
||||
let bend = 2.0 * turn(pm - pa, pb - pm);
|
||||
let straight = rules.turn.is_none_or(|t| !t.exceeds(bend, sag));
|
||||
// Whatever the tolerance, no segment spans more than a quarter turn of its curve: a
|
||||
// small circle split into two half arcs would otherwise be two chords on one line,
|
||||
// a hole of no area (the 1 mm hole of a 990 mm plate at its 2 mm chord).
|
||||
let quarter = bend <= QUARTER_TURN;
|
||||
short && straight && quarter && sag <= tol
|
||||
};
|
||||
if !done && !fine() {
|
||||
stack.push(Seg { a: m, b, pa: pm, pb, mid: Some(q3), depth: sg.depth + 1 });
|
||||
|
||||
@@ -644,7 +644,7 @@ fn plan_face(
|
||||
let pole_pair = |i: u32, j: u32| i != j && fixed.get(i as usize).zip(fixed.get(j as usize)).is_some_and(|(p, q)| p == q);
|
||||
let mut triangles = triangles;
|
||||
triangles.retain(|t| !(pole_pair(t[0], t[1]) || pole_pair(t[1], t[2]) || pole_pair(t[2], t[0])));
|
||||
if !closed_boundary(&ranges, &fixed, &triangles) {
|
||||
if !closed_boundary(&ranges, &fixed, &uvs, &triangles) {
|
||||
return Err(fail("triangulation does not follow the boundary"));
|
||||
}
|
||||
let mut all = uvs;
|
||||
@@ -862,9 +862,13 @@ fn graded(a: [f64; 2], b: [f64; 2], sa: f64, sb: f64, h: f64, speed: [f64; 2], f
|
||||
|
||||
/// Every boundary segment (consecutive nodes of a loop) is the side of exactly one triangle:
|
||||
/// the face covers its boundary with nothing missing or doubled, so it meets its neighbours
|
||||
/// exactly. Pole copies of one node (consecutive in a loop, at one point) are one vertex: the
|
||||
/// segment between them has no length, and a side ending on either copy is the same side.
|
||||
fn closed_boundary(ranges: &[std::ops::Range<usize>], fixed: &[Vec3], triangles: &[[u32; 3]]) -> bool {
|
||||
/// exactly. Some boundary nodes are one vertex, a side ending on any of them the same side: pole
|
||||
/// copies of one node (consecutive in a loop, at one point, the segment between them of no
|
||||
/// length), and nodes at the very same (u, v), which the triangulation takes as one point: two
|
||||
/// loops touching at a vertex they share (a ring standing on a perforated plate, tangent to the
|
||||
/// holes beside it). A seam's two uses are at one point in space but a period apart in (u, v),
|
||||
/// and stay two.
|
||||
fn closed_boundary(ranges: &[std::ops::Range<usize>], fixed: &[Vec3], uvs: &[[f64; 2]], triangles: &[[u32; 3]]) -> bool {
|
||||
let mut one: Vec<u32> = (0..fixed.len() as u32).collect();
|
||||
let root = |one: &[u32], mut i: u32| {
|
||||
while let Some(&j) = one.get(i as usize).filter(|j| **j != i) {
|
||||
@@ -873,17 +877,30 @@ fn closed_boundary(ranges: &[std::ops::Range<usize>], fixed: &[Vec3], triangles:
|
||||
i
|
||||
};
|
||||
let segment = |r: &std::ops::Range<usize>, k: usize| ((r.start + k) as u32, (r.start + (k + 1) % r.len()) as u32);
|
||||
let join = |one: &mut [u32], a: u32, b: u32| {
|
||||
let (ra, rb) = (root(one, a), root(one, b));
|
||||
if let Some(x) = one.get_mut(rb.max(ra) as usize) {
|
||||
*x = ra.min(rb);
|
||||
}
|
||||
};
|
||||
for r in ranges {
|
||||
for k in 0..r.len() {
|
||||
let (a, b) = segment(r, k);
|
||||
if fixed.get(a as usize).zip(fixed.get(b as usize)).is_some_and(|(p, q)| p == q) {
|
||||
let (ra, rb) = (root(&one, a), root(&one, b));
|
||||
if let Some(x) = one.get_mut(rb.max(ra) as usize) {
|
||||
*x = ra.min(rb);
|
||||
}
|
||||
join(&mut one, a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
// By value: −0.0 and 0.0 are one point.
|
||||
let key = |q: &[f64; 2]| [(q[0] + 0.0).to_bits(), (q[1] + 0.0).to_bits()];
|
||||
let mut first: HashMap<[u64; 2], u32> = HashMap::with_capacity(uvs.len());
|
||||
for (i, q) in uvs.iter().enumerate().take(fixed.len()) {
|
||||
let i = i as u32;
|
||||
let f = *first.entry(key(q)).or_insert(i);
|
||||
if f != i {
|
||||
join(&mut one, f, i);
|
||||
}
|
||||
}
|
||||
let side = |a: u32, b: u32| {
|
||||
let (a, b) = (root(&one, a), root(&one, b));
|
||||
(a != b).then_some((a.min(b), a.max(b)))
|
||||
|
||||
@@ -588,3 +588,51 @@ fn thinned_grid_lines_stay_within_the_cap() {
|
||||
assert!(us.first() == Some(&0.0) && us.last() == Some(&1.0) && vs.first() == Some(&0.0) && vs.last() == Some(&1.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Whatever the chord tolerance, no edge segment spans more than a quarter turn of its curve:
|
||||
/// a 1 mm hole through a 990 mm plate, at a 2 mm chord and no facet angle, keeps a polygon with
|
||||
/// area. Each half arc of the hole was one chord, two chords on one line: a hole of no area.
|
||||
#[test]
|
||||
fn a_hole_far_below_the_tolerance_keeps_its_area() {
|
||||
let plate = ok(crate::box_solid(Vec3::ZERO, Vec3::new(990.0, 990.0, 10.0)));
|
||||
let (axis, r) = (Vec3::new(495.0, 495.0, 0.0), 0.5);
|
||||
let body = cut(&plate, &ok(crate::cylinder(axis - Vec3::Z, Vec3::Z, r, 12.0)));
|
||||
let rec = record(&body);
|
||||
let (p, _) = ok(plan(&rec, 2.0, None));
|
||||
let mut rims = 0;
|
||||
for (ei, e) in rec.edges.iter().enumerate() {
|
||||
if matches!(e.curve, Curve::Line { .. }) {
|
||||
continue;
|
||||
}
|
||||
let pts = &p.edges[ei];
|
||||
// On the hole's rim: every chord at most a quarter turn's, r·√2.
|
||||
let off = |q: &Vec3| (Vec3::new(q.x, q.y, 0.0).dist(axis) - r).abs();
|
||||
assert!(pts.iter().all(|q| off(q) < 1e-6), "edge {ei} is not on the hole");
|
||||
for w in pts.windows(2) {
|
||||
assert!(w[0].dist(w[1]) <= r * 2f64.sqrt() * (1.0 + 1e-6), "edge {ei}: a chord of {} spans more than a quarter turn", w[0].dist(w[1]));
|
||||
}
|
||||
rims += 1;
|
||||
}
|
||||
assert!(rims >= 2, "{rims} rim edges");
|
||||
let m = p.evaluate();
|
||||
assert_watertight(&m, "plate with a small hole");
|
||||
// The hole's wall: π · 1 · 10 less its chords' shortfall (a square: 2√2 · 0.5 · 10 at least).
|
||||
let wall = m.measure().area - 2.0 * (990.0 * 990.0 + 2.0 * 990.0 * 10.0) + 2.0 * PI * r * r;
|
||||
assert!(wall > 4.0 * 2f64.sqrt() * r * 10.0 * 0.99, "the hole's wall measures {wall}");
|
||||
}
|
||||
|
||||
/// Boundary nodes at the very same (u, v) are one vertex to the triangulation: two holes that
|
||||
/// touch at a vertex they share (a ring standing on a perforated plate, tangent to the holes
|
||||
/// beside it) bound a face the triangles follow, though each hole has its own copy of the node.
|
||||
#[test]
|
||||
fn loops_touching_at_a_shared_vertex_bound_the_face() {
|
||||
let outer = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
|
||||
let left = [[1.0, 1.0], [1.0, 3.0], [2.0, 2.0]];
|
||||
let right = [[2.0, 2.0], [3.0, 3.0], [3.0, 1.0]];
|
||||
let uvs: Vec<[f64; 2]> = outer.iter().chain(&left).chain(&right).copied().collect();
|
||||
let fixed: Vec<Vec3> = uvs.iter().map(|q| Vec3::new(q[0], q[1], 0.0)).collect();
|
||||
let ranges = vec![0..4, 4..7, 7..10];
|
||||
let fill = cdt::Fill { seeds: &[], refine: None, shape: false };
|
||||
let Some((_, triangles)) = cdt::triangulate(&uvs, &ranges, fill) else { panic!("triangulates") };
|
||||
assert!(closed_boundary(&ranges, &fixed, &uvs, &triangles), "the triangles follow both holes through their shared vertex");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user