diff --git a/crates/engine/src/cmd/cut.rs b/crates/engine/src/cmd/cut.rs index 04fa0c7f0..48f2c31e6 100644 --- a/crates/engine/src/cmd/cut.rs +++ b/crates/engine/src/cmd/cut.rs @@ -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); diff --git a/crates/pathops/src/boolean.rs b/crates/pathops/src/boolean.rs index 27294956e..f61ac5e48 100644 --- a/crates/pathops/src/boolean.rs +++ b/crates/pathops/src/boolean.rs @@ -144,7 +144,7 @@ pub(crate) fn eps_for<'a>(paths: impl IntoIterator) -> 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) -> 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 { 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 { 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 { let c = normalize_bez(&fill_bezpath(path), rule)?; - let area = c.contours().map(|ct| ct.path.area()).sum::().abs(); - if area.is_finite() { Ok(area) } else { Err(PathOpsError::NonFinite) } + Ok(c.contours().map(|ct| ct.path.area()).sum::().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());