mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
Tessellation: a curved face's grid reads the quarter turn from its surface, not its parameter
uv::divide started every curved face from ceil(range / (pi/2)) lines, as if the parameter were an angle. A spline's is not: SMLib holds a 90 degree bend as one rational quadratic span with v in [0, pi/2], and the far pcurve of its boundary gives v = -6.7e-13, so the range is a rounding over a quarter, the count rounds up to 2 and the bend gets a line at 45 degrees whatever the tolerance (display_mesh_tests::a_small_bend_on_a_large_sheet_is_meshed_round: 3 angles across the bend at the chord tolerance instead of 2). The division now starts from the whole range, and the limits refuse an interval that spans more than a quarter turn of its iso-lines, read from the points as an edge segment's is (edges::QUARTER_TURN: the half chords of an arc turning theta differ by theta/2, a closed iso-line's by pi). On a revolved surface the parameter is the angle, so the lines are the same as before; an iso-line within a millionth of the tolerance of one point (a pole) has no turn to read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ 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);
|
||||
pub(crate) 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 {
|
||||
@@ -163,7 +163,7 @@ pub(crate) fn split_segments(b: &brep::Brep, ei: usize, nodes: &mut Nodes, segs:
|
||||
}
|
||||
|
||||
/// Angle (radians) between two directions; zero when either is empty.
|
||||
fn turn(a: Vec3, b: Vec3) -> f64 {
|
||||
pub(crate) fn turn(a: Vec3, b: Vec3) -> f64 {
|
||||
let (la, lb) = (a.len(), b.len());
|
||||
if !(la > 0.0 && lb > 0.0) {
|
||||
return 0.0;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use solidcraft_geom::tess::EvalSurface;
|
||||
pub(crate) use solidcraft_geom::tess::uv::{Domain, degenerate_within, domain, unwrap};
|
||||
|
||||
use super::edges::{QUARTER_TURN, turn};
|
||||
use super::size::Rules;
|
||||
|
||||
/// Division of `range` along direction `d` so that between neighbouring lines the surface sags
|
||||
@@ -16,19 +17,10 @@ pub(crate) fn divide(s: &EvalSurface, d: usize, range: (f64, f64), across: (f64,
|
||||
if !(a1 > a0) {
|
||||
return vec![a0, a1];
|
||||
}
|
||||
let flat = limits(s, d, across, rules, h);
|
||||
// Start from quarter turns at most, so a closed iso-line is not taken for a flat one (a
|
||||
// plane's parameters are lengths and its lines are never closed).
|
||||
let initial = if matches!(s, EvalSurface::Plane { .. }) { 1 } else { ((a1 - a0) / std::f64::consts::FRAC_PI_2).ceil().clamp(1.0, 64.0) as u32 };
|
||||
// Both ends exactly (a0 + (a1 − a0) · 1 may round away from a1): the end lines are the
|
||||
// face's own parameter box, which the boundary nodes sit on.
|
||||
let starts: Vec<f64> = (0..=initial)
|
||||
.map(|i| {
|
||||
let t = f64::from(i) / f64::from(initial);
|
||||
a0 * (1.0 - t) + a1 * t
|
||||
})
|
||||
.collect();
|
||||
split(&starts, &flat, max_lines)
|
||||
// The whole range first: the limits split it, and no interval spans more than a quarter
|
||||
// turn of its iso-lines, so a closed iso-line is never taken for a flat one. Both ends
|
||||
// exactly: the end lines are the face's own parameter box, which the boundary nodes sit on.
|
||||
split(&[a0, a1], &limits(s, d, across, rules, h), max_lines)
|
||||
}
|
||||
|
||||
/// `lines` (increasing, ends included) with every interval that fails the same limits as
|
||||
@@ -80,8 +72,14 @@ pub(crate) fn fine_between(s: &EvalSurface, rules: &Rules, h: f64, a: [f64; 2],
|
||||
}
|
||||
}
|
||||
|
||||
/// An iso-line's extent below which it is a point, as a fraction of the chord tolerance (which
|
||||
/// is never finer than a millionth of the body, so this is far above rounding).
|
||||
const POINT: f64 = 1e-6;
|
||||
|
||||
/// Whether the surface between the iso-lines at `a` and `b` (direction `d`, sampled along
|
||||
/// iso-lines across `across`) is within the tolerance, the side length `h` and the facet angle.
|
||||
/// iso-lines across `across`) is within the tolerance, the side length `h` and the facet angle,
|
||||
/// and spans at most a quarter turn of each iso-line (as an edge segment does, see
|
||||
/// [`QUARTER_TURN`]): a turn read from the points, since a spline's parameter is no angle.
|
||||
fn limits<'a>(s: &'a EvalSurface, d: usize, across: (f64, f64), rules: &'a Rules, h: f64) -> impl Fn(f64, f64) -> bool + 'a {
|
||||
let tol = rules.tol;
|
||||
let uv = move |x: f64, y: f64| if d == 0 { (x, y) } else { (y, x) };
|
||||
@@ -105,6 +103,15 @@ fn limits<'a>(s: &'a EvalSurface, d: usize, across: (f64, f64), rules: &'a Rules
|
||||
if sag > tol {
|
||||
return false;
|
||||
}
|
||||
// The two half chords of an arc turning θ differ by θ/2; a closed iso-line's by π.
|
||||
// An iso-line within a millionth of the tolerance of one point (a pole) turns no
|
||||
// amount: its directions are rounding.
|
||||
if let Some(pm) = at(0.5 * (a + b), y)
|
||||
&& pa.dist(pm).max(pm.dist(pb)) > POINT * tol
|
||||
&& 2.0 * turn(pm - pa, pb - pm) > QUARTER_TURN
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// At a pole the normal is undefined: take it from just inside the segment.
|
||||
let na = normal(a, y).or_else(|| normal(a + 1e-4 * (b - a), y));
|
||||
let nb = normal(b, y).or_else(|| normal(b - 1e-4 * (b - a), y));
|
||||
@@ -147,6 +154,26 @@ mod tests {
|
||||
assert_eq!(along.len(), 9);
|
||||
}
|
||||
|
||||
/// A quarter of a cylinder as a rational quadratic spline (as SMLib holds a bend), its range a
|
||||
/// rounding over π/2 as a boundary's pcurves give it: the quarter turn is read from the
|
||||
/// surface, not the parameter, so at a tolerance above its sag it is one interval; and a whole
|
||||
/// turn is still divided into quarters however coarse the tolerance.
|
||||
#[test]
|
||||
fn a_quarter_turn_is_read_from_the_surface_not_the_parameter() {
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let q = std::f64::consts::FRAC_PI_2;
|
||||
let s = EvalSurface::nurbs(
|
||||
[1, 2],
|
||||
[vec![0.0, 0.0, 40.0, 40.0], vec![0.0, 0.0, 0.0, q, q, q]],
|
||||
[2, 3],
|
||||
vec![[1.0, 0.0, 0.0, 1.0], [1.0, 0.0, 40.0, 1.0], [w, w, 0.0, w], [w, w, 40.0 * w, w], [0.0, 1.0, 0.0, 1.0], [0.0, 1.0, 40.0, 1.0]],
|
||||
);
|
||||
let coarse = Rules { tol: 3.0, max_len: f64::INFINITY, turn: None };
|
||||
assert_eq!(divide(&s, 1, (-6.7e-13, q), (0.0, 40.0), &coarse, f64::INFINITY, 10_000), vec![-6.7e-13, q]);
|
||||
let whole = divide(&cylinder(), 1, (0.0, std::f64::consts::TAU), (0.0, 1.0), &Rules { tol: 100.0, ..coarse }, f64::INFINITY, 10_000);
|
||||
assert_eq!(whole.len(), 5, "{whole:?}");
|
||||
}
|
||||
|
||||
/// The first and last lines are the range's ends to the last bit: the boundary nodes sit on
|
||||
/// them, and a line a rounding inside would be taken for an interior one (a drill point's
|
||||
/// cone, where a0 + (a1 − a0) is not a1).
|
||||
|
||||
Reference in New Issue
Block a user