brep: the geometry check reads a pcurve at its edge's parameter within the interval's rounding

Brep::validate lets a pcurve's interval differ from its edge's by PCURVE_INTERVAL (1e-9
of the magnitude: rounding room), but check_geometry then evaluated the pcurve at the
edge's own ends and samples, which may lie just past it: "the pcurve has no point at
2.0000000000082903" on four of Poppy Humanoid's solids, circle edges whose end vertex
sits on a quarter of their form, where SMLib's pcurve ends on the knot. Both of the
check's pcurve readings (the use's points and the loop's area) now take the edge
parameter on the pcurve's interval, through one reader.

Poppy: 195 → 199 bodies, 41 → 37 solids not imported; the corpus survey and the kernel
tests are as before.

Test: a pcurve whose interval ends 1e-11 short of its edge's validates and checks;
without the clamp, "the pcurve has no point at 1".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-27 16:27:16 -04:00
committed by Irakli Kourtis
parent 3a7b51f723
commit fcb162448e
2 changed files with 26 additions and 3 deletions
+11 -3
View File
@@ -12,7 +12,7 @@
//! an edge's for its pcurves, an edge's and its vertex's together where they meet, a vertex's on
//! its surface. A point that cannot be evaluated is an error, never a panic.
use super::eval::{CurveEval, SurfaceEval};
use super::{Brep, Loop};
use super::{Brep, Curve2, Loop};
use crate::Vec3;
/// Where a record disagrees with itself.
@@ -84,6 +84,14 @@ fn v3(p: [f64; 3]) -> Vec3 {
fn unevaluable(record: Record, what: impl std::fmt::Display) -> Mismatch {
Mismatch { record, problem: Problem::Unevaluable(what.to_string()) }
}
/// A use's pcurve at its edge's parameter `t`. The pcurve's interval is its edge's to within
/// rounding ([`Brep::validate`], `PCURVE_INTERVAL`), so an edge parameter just past it is read at
/// the pcurve's end.
fn on_pcurve(pc: &Curve2, t: f64) -> Option<[f64; 2]> {
let [a, b] = pc.interval()?;
pc.eval(t.clamp(a.min(b), a.max(b)))
}
/// `gap` within `tolerance` (a non-finite gap never is).
fn within(record: Record, what: &'static str, gap: f64, tolerance: f64) -> Result<()> {
if gap <= tolerance { Ok(()) } else { Err(Mismatch { record, problem: Problem::Off { what, gap, tolerance } }) }
@@ -147,7 +155,7 @@ impl Brep {
let [t0, t1] = e.interval;
let mut uv = [[0.0; 2]; 3];
for (k, t) in [t0, 0.5 * (t0 + t1), t1].into_iter().enumerate() {
let q = u.pcurve.eval(t).ok_or_else(|| unevaluable(at, format!("the pcurve has no point at {t}")))?;
let q = on_pcurve(&u.pcurve, t).ok_or_else(|| unevaluable(at, format!("the pcurve has no point at {t}")))?;
let s = surface.point(q).ok_or_else(|| unevaluable(at, format!("the surface has no point at {q:?}")))?;
let (Some(c), Some(what), Some(slot)) = (p.get(k), PCURVE_POINTS.get(k), uv.get_mut(k)) else {
return Err(unevaluable(at, "pcurve sample"));
@@ -181,7 +189,7 @@ impl Brep {
for k in 0..AREA_SAMPLES {
let f = k as f64 / AREA_SAMPLES as f64;
let t = if u.forward { t0 + (t1 - t0) * f } else { t1 - (t1 - t0) * f };
ring.push(u.pcurve.eval(t).ok_or_else(|| unevaluable(at, format!("the pcurve has no point at {t}")))?);
ring.push(on_pcurve(&u.pcurve, t).ok_or_else(|| unevaluable(at, format!("the pcurve has no point at {t}")))?);
}
}
let area = signed_area(&ring);
+15
View File
@@ -530,6 +530,21 @@ fn shift(c: &mut Curve2, by: [f64; 2]) {
}
}
/// A pcurve whose interval ends a rounding short of its edge's (as a kernel that snaps an edge's
/// end to its knot hands it back) is read at its end: the record validates and checks.
#[test]
fn a_pcurve_a_rounding_short_of_its_edge_is_read_at_its_end() {
let mut b = sheet();
let Curve2::Spline { knots, .. } = pcurve_of(&mut b, 0, 0) else { panic!("a spline pcurve") };
let end = *knots.last().unwrap();
let short = end - 1e-11 * end.abs().max(1.0);
for k in knots.iter_mut().filter(|k| **k == end) {
*k = short;
}
b.validate().unwrap();
b.check_geometry().unwrap();
}
/// One geometric defect each, structurally valid: every one is refused, naming the record.
#[test]
fn geometric_defects_are_refused_by_record() {