Booleans: limit of one unit, crop outside shortcut, tests (#5465)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-11-04 08:13:12 +07:00
committed by bflatastic
co-authored by Claude Sonnet 5.5
parent e951095617
commit 0a2935cf8f
2 changed files with 31 additions and 14 deletions
+16 -8
View File
@@ -155,7 +155,7 @@ fn run_cut(s: &mut Session, cmd: &str, label: &str, cut: &dyn Fn(&PathData, Fill
if pd.subpaths.iter().flat_map(|sp| &sp.anchors).any(|a| !(a.p.x.is_finite() && a.p.y.is_finite())) {
return Err(bad(cmd, "paths contain invalid coordinates"));
}
let pieces = cut(&pd, rule).map_err(|e| bad(cmd, format!("a shape cannot be cut: {e}")))?;
let pieces = cut(&pd, rule).map_err(|e| bad(cmd, format!("the cut cannot be made on this art: {e}")))?;
if let Some(pieces) = pieces {
plan.push((id, pieces));
}
@@ -251,6 +251,10 @@ fn crop(pd: &PathData, rule: FillRule, rect: Rect) -> Cut {
return Ok(None);
}
if pd.is_closed() {
// Wholly outside: nothing is left, whatever the booleans would make of the rectangle's coordinates.
if rect.intersect(b).is_zero_area() {
return Ok(Some(vec![]));
}
let r = intersect(
pd,
rule,
@@ -417,15 +421,19 @@ mod tests {
#[test]
fn a_shape_the_booleans_refuse_is_an_error_not_no_pieces() {
let far = polygon(&[
Point::new(1e13, 1e13),
Point::new(1e13 + 100.0, 1e13),
Point::new(1e13 + 100.0, 1e13 + 100.0),
Point::new(1e13, 1e13 + 100.0),
Point::new(1e15, 1e15),
Point::new(1e15 + 100.0, 1e15),
Point::new(1e15 + 100.0, 1e15 + 100.0),
Point::new(1e15, 1e15 + 100.0),
]);
let axis = Axis { p: Point::ZERO, d: Vec2::new(0.0, 1.0) };
assert!(mirror(&far, FillRule::NonZero, &axis, 1.0).is_err());
assert!(crop(&far, FillRule::NonZero, Rect::new(0.0, 0.0, 100.0, 100.0)).is_err());
assert!(split_by_line(&far, FillRule::NonZero, &axis).is_err());
let refused = Some(po::PathOpsError::Degenerate);
assert_eq!(mirror(&far, FillRule::NonZero, &axis, 1.0).err(), refused);
assert_eq!(split_by_line(&far, FillRule::NonZero, &axis).err(), refused);
// A rectangle that far out, against art near the origin, still crops it to nothing.
let far_rect = Rect::new(1e15, 1e15, 1e15 + 100.0, 1e15 + 100.0);
let near_art = polygon(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0), Point::new(0.0, 10.0)]);
assert_eq!(crop(&near_art, FillRule::NonZero, far_rect).unwrap(), Some(vec![]));
// Near the origin the same cuts work.
let near = polygon(&[Point::new(-50.0, 0.0), Point::new(50.0, 0.0), Point::new(50.0, 100.0), Point::new(-50.0, 100.0)]);
assert_eq!(mirror(&near, FillRule::NonZero, &axis, 1.0).unwrap().len(), 1);
+15 -6
View File
@@ -144,7 +144,7 @@ pub(crate) fn eps_for<'a>(paths: impl IntoIterator<Item = &'a BezPath>) -> Resul
}
}
let eps = (m * f64::EPSILON * 64.0).max(1e-6);
// Past this the sweep tolerance is wider than the refit precision: features thinner than it collapse and the call
// Past this the sweep tolerance is wider than a document unit: features thinner than it collapse and the call
// would answer `Ok` with part of the art (or all of it) gone. Refuse instead, so callers keep the original.
if eps > MAX_SWEEP_EPS {
return Err(PathOpsError::Degenerate);
@@ -152,8 +152,10 @@ pub(crate) fn eps_for<'a>(paths: impl IntoIterator<Item = &'a BezPath>) -> Resul
Ok(eps)
}
/// The widest sweep tolerance [`eps_for`] accepts: [`DEFAULT_PRECISION`], reached by coordinates of about 7e11.
const MAX_SWEEP_EPS: f64 = DEFAULT_PRECISION;
/// The widest sweep tolerance [`eps_for`] accepts: one document unit, reached by coordinates of about 7e13. (The engine
/// refuses coordinates far below that, so this only guards the library's own callers; it does not follow the `precision`
/// a caller asks for.)
const MAX_SWEEP_EPS: f64 = 1.0;
fn el_points(el: &kurbo::PathEl) -> impl Iterator<Item = Point> {
use kurbo::PathEl::*;
@@ -339,7 +341,7 @@ impl Arrangement {
}
/// [`Self::contours`] with a panic in the sweep's contour pass turned into an error, as `new` does for the
/// sweep itself. Used by the calls that can return an error; the rest have none to return and let the engine's
/// sweep itself (native builds only: on the web a panic aborts, whatever is caught here). Used by the calls that can return an error; the rest have none to return and let the engine's
/// guard report it.
pub(crate) fn try_contours(&self, pred: impl Fn(&[bool]) -> bool) -> Result<Contours, PathOpsError> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.contours(pred))).map_err(|_| PathOpsError::Degenerate)
@@ -767,8 +769,7 @@ pub fn unite_all_with(paths: &[(&PathData, FillRule)], precision: f64) -> PathDa
/// [`area`] that reports a refused input instead of an area of zero.
pub fn try_area(path: &PathData, rule: FillRule) -> Result<f64, PathOpsError> {
let c = normalize_bez(&fill_bezpath(path), rule)?;
let area = c.contours().map(|ct| ct.path.area()).sum::<f64>().abs();
if area.is_finite() { Ok(area) } else { Err(PathOpsError::NonFinite) }
Ok(c.contours().map(|ct| ct.path.area()).sum::<f64>().abs())
}
/// Unsigned filled area of a path under a fill rule (normalises first, so it is exact for
@@ -891,6 +892,14 @@ mod tests {
}
#[test]
fn the_coordinate_limit_sits_where_the_tolerance_passes_a_unit() {
// 100 unit squares: tolerance 0.7 at 5e13 (kept), 1.4 at 1e14 (refused).
assert!(try_area(&square(5e13, 5e13, 100.0), FillRule::NonZero).is_ok());
assert_eq!(try_area(&square(1e14, 1e14, 100.0), FillRule::NonZero), Err(PathOpsError::Degenerate));
}
#[test]
#[cfg(panic = "unwind")]
fn a_panic_in_the_contour_pass_becomes_an_error() {
let arr = Arrangement::new(vec![(fill_bezpath(&square(0.0, 0.0, 10.0)), FillRule::NonZero)]).unwrap();
assert!(arr.try_contours(|m| m[0]).is_ok());