From e9510956172c5e48f555db7a93e2d139d757830f Mon Sep 17 00:00:00 2001 From: vangel76 Date: Wed, 4 Nov 2026 01:44:24 +0100 Subject: [PATCH 1/2] Path booleans: refuse out-of-range input, guard the contour pass, cuts report refusals (#5465) Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/cut.rs | 94 +++++++++++++++++++++++------------ crates/pathops/src/boolean.rs | 88 ++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 35 deletions(-) diff --git a/crates/engine/src/cmd/cut.rs b/crates/engine/src/cmd/cut.rs index 90bd276c9..04fa0c7f0 100644 --- a/crates/engine/src/cmd/cut.rs +++ b/crates/engine/src/cmd/cut.rs @@ -138,8 +138,12 @@ fn cut_targets(s: &Session) -> Result> { /// What a cut does to one object: its new pieces (none deletes it). type Plan = Vec<(NodeId, Vec)>; -/// Run `cut` on every target's geometry (`None`: unchanged) and apply the result as one undo step. -fn run_cut(s: &mut Session, cmd: &str, label: &str, cut: &dyn Fn(&PathData, FillRule) -> Option>) -> Result { +/// What a cut makes of one object: its pieces (`None`: unchanged), or the pathops refusal. +type Cut = std::result::Result>, po::PathOpsError>; + +/// Run `cut` on every target's geometry (`None`: unchanged) and apply the result as one undo step. A shape the +/// booleans refuse (too complex, too far out) fails the command with the art untouched: it is not "nothing on that side". +fn run_cut(s: &mut Session, cmd: &str, label: &str, cut: &dyn Fn(&PathData, FillRule) -> Cut) -> Result { let targets = cut_targets(s)?; if targets.is_empty() { return Err(bad(cmd, "select paths or compound paths")); @@ -151,7 +155,8 @@ 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")); } - if let Some(pieces) = cut(&pd, rule) { + let pieces = cut(&pd, rule).map_err(|e| bad(cmd, format!("a shape cannot be cut: {e}")))?; + if let Some(pieces) = pieces { plan.push((id, pieces)); } } @@ -184,9 +189,9 @@ fn reach(pd: &PathData) -> Rect { } /// `pd ∩ region`, dropping slivers. -fn intersect(pd: &PathData, rule: FillRule, region: &PathData) -> PathData { - let r = po::boolean(pd, rule, region, FillRule::NonZero, BoolOp::Intersect); - if po::area(&r, FillRule::NonZero) <= 1e-6 { PathData::default() } else { r } +fn intersect(pd: &PathData, rule: FillRule, region: &PathData) -> std::result::Result { + let r = po::try_boolean(pd, rule, region, FillRule::NonZero, BoolOp::Intersect, po::DEFAULT_PRECISION)?; + Ok(if po::try_area(&r, FillRule::NonZero)? <= 1e-6 { PathData::default() } else { r }) } // ---------- Line Cut ---------- @@ -198,24 +203,26 @@ fn line_cut(s: &mut Session, p: &Value) -> Result { } /// `pd` split by the line: the pieces on both sides, `None` when the line misses it. -fn split_by_line(pd: &PathData, rule: FillRule, axis: &Axis) -> Option> { +fn split_by_line(pd: &PathData, rule: FillRule, axis: &Axis) -> Cut { if pd.is_closed() { let around = reach(pd); - let a = intersect(pd, rule, &axis.half_plane(1.0, around)); - let b = intersect(pd, rule, &axis.half_plane(-1.0, around)); + let a = intersect(pd, rule, &axis.half_plane(1.0, around))?; + let b = intersect(pd, rule, &axis.half_plane(-1.0, around))?; if a.is_empty() || b.is_empty() { - return None; + return Ok(None); } - return Some(components(&a).into_iter().chain(components(&b)).collect()); + return Ok(Some(components(&a).into_iter().chain(components(&b)).collect())); } // Open paths: the parts on each side (each pass removes the other side). - let a = erase_outline(pd, f64::INFINITY, &|q| axis.side(q) < 0.0)?; - let b = erase_outline(pd, f64::INFINITY, &|q| axis.side(q) >= 0.0)?; - Some(if pd.subpaths.len() == 1 { + let (Some(a), Some(b)) = (erase_outline(pd, f64::INFINITY, &|q| axis.side(q) < 0.0), erase_outline(pd, f64::INFINITY, &|q| axis.side(q) >= 0.0)) + else { + return Ok(None); + }; + Ok(Some(if pd.subpaths.len() == 1 { a.into_iter().chain(b).collect() } else { vec![PathData::new(a.into_iter().chain(b).flat_map(|p| p.subpaths).collect())] - }) + })) } // ---------- Rectangle Cut ---------- @@ -238,25 +245,30 @@ fn rect_cut(s: &mut Session, p: &Value) -> Result { } /// `pd` cropped to `rect`: `None` when it lies inside, no pieces when it lies outside. -fn crop(pd: &PathData, rule: FillRule, rect: Rect) -> Option> { - let b = pd.bounds()?; +fn crop(pd: &PathData, rule: FillRule, rect: Rect) -> Cut { + let Some(b) = pd.bounds() else { return Ok(None) }; if rect.inflate(1e-9, 1e-9).contains_rect(b) { - return None; + return Ok(None); } if pd.is_closed() { - let r = - intersect(pd, rule, &polygon(&[rect.origin(), Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1), Point::new(rect.x0, rect.y1)])); - return Some(if r.is_empty() { vec![] } else { vec![r] }); + let r = intersect( + pd, + rule, + &polygon(&[rect.origin(), Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1), Point::new(rect.x0, rect.y1)]), + )?; + return Ok(Some(if r.is_empty() { vec![] } else { vec![r] })); } let inside = rect.inflate(1e-9, 1e-9); // Sampled at least every half of the rectangle's shorter side, so a line through it is found. let step = rect.width().min(rect.height()) / 2.0; - let pieces = erase_outline(pd, step, &|q| !(q.x >= inside.x0 && q.x <= inside.x1 && q.y >= inside.y0 && q.y <= inside.y1))?; - Some(if pd.subpaths.len() == 1 || pieces.is_empty() { + let Some(pieces) = erase_outline(pd, step, &|q| !(q.x >= inside.x0 && q.x <= inside.x1 && q.y >= inside.y0 && q.y <= inside.y1)) else { + return Ok(None); + }; + Ok(Some(if pd.subpaths.len() == 1 || pieces.is_empty() { pieces } else { vec![PathData::new(pieces.into_iter().flat_map(|p| p.subpaths).collect())] - }) + })) } // ---------- Mirror & Cut ---------- @@ -287,7 +299,7 @@ fn mirror_cut(s: &mut Session, p: &Value) -> Result { other => return Err(bad(C, format!("unknown axis `{other}` (vertical|horizontal|free)"))), }; let sign = keep_sign(&axis, str_param(p, "keep")).map_err(|e| bad(C, e))?; - run_cut(s, C, "Mirror & Cut", &|pd, rule| Some(mirror(pd, rule, &axis, sign))) + run_cut(s, C, "Mirror & Cut", &|pd, rule| mirror(pd, rule, &axis, sign).map(Some)) } /// The sign of [`Axis::side`] on the side `keep` names (default: left, or top when the axis runs @@ -309,22 +321,22 @@ fn keep_sign(axis: &Axis, keep: Option<&str>) -> std::result::Result Vec { +fn mirror(pd: &PathData, rule: FillRule, axis: &Axis, sign: f64) -> std::result::Result, po::PathOpsError> { let m = axis.reflection(); if pd.is_closed() { - let kept = intersect(pd, rule, &axis.half_plane(sign, reach(pd))); + let kept = intersect(pd, rule, &axis.half_plane(sign, reach(pd)))?; if kept.is_empty() { - return vec![]; + return Ok(vec![]); } - let joined = po::boolean(&kept, FillRule::NonZero, &kept.transformed(m), FillRule::NonZero, BoolOp::Union); - return if joined.is_empty() { vec![] } else { vec![joined] }; + let joined = po::try_boolean(&kept, FillRule::NonZero, &kept.transformed(m), FillRule::NonZero, BoolOp::Union, po::DEFAULT_PRECISION)?; + return Ok(if joined.is_empty() { vec![] } else { vec![joined] }); } let kept = match erase_outline(pd, f64::INFINITY, &|q| axis.side(q) * sign < 0.0) { Some(pieces) => pieces.into_iter().flat_map(|p| p.subpaths).collect(), None => pd.subpaths.clone(), }; let subs: Vec = kept.iter().flat_map(|sp| mirror_open(sp, axis, m)).collect(); - if subs.is_empty() { vec![] } else { vec![PathData::new(subs)] } + Ok(if subs.is_empty() { vec![] } else { vec![PathData::new(subs)] }) } /// An open run and its reflection, joined where its ends touch the axis (closed when both do); @@ -399,4 +411,24 @@ mod tests { let out = mirror_open(&sp, &axis, axis.reflection()); assert_eq!((out.len(), out[0].closed, out[0].anchors.len()), (1, false, 3)); } + + /// A shape the booleans refuse (here: so far from the origin that the sweep tolerance exceeds the precision) is an error, + /// not "no pieces": that would make Mirror & Cut delete it and Rectangle Cut crop it to nothing. + #[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), + ]); + 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()); + // 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); + assert_eq!(split_by_line(&near, FillRule::NonZero, &axis).unwrap().map(|p| p.len()), Some(2)); + } } diff --git a/crates/pathops/src/boolean.rs b/crates/pathops/src/boolean.rs index 2e7b1b793..27294956e 100644 --- a/crates/pathops/src/boolean.rs +++ b/crates/pathops/src/boolean.rs @@ -143,9 +143,18 @@ pub(crate) fn eps_for<'a>(paths: impl IntoIterator) -> Resul } } } - Ok((m * f64::EPSILON * 64.0).max(1e-6)) + 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 + // 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); + } + Ok(eps) } +/// The widest sweep tolerance [`eps_for`] accepts: [`DEFAULT_PRECISION`], reached by coordinates of about 7e11. +const MAX_SWEEP_EPS: f64 = DEFAULT_PRECISION; + fn el_points(el: &kurbo::PathEl) -> impl Iterator { use kurbo::PathEl::*; let v: [Option; 3] = match *el { @@ -329,6 +338,13 @@ impl Arrangement { self.contours_where(|w| pred(&self.mask(w))) } + /// [`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 + /// 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) + } + /// All distinct non-empty coverage masks that occur next to some edge, sorted. pub(crate) fn distinct_masks(&self) -> Vec> { let mut set = BTreeSet::new(); @@ -670,7 +686,7 @@ pub fn boolean(a: &PathData, a_rule: FillRule, b: &PathData, b_rule: FillRule, o /// Boolean operation with an explicit refit `precision`; errors on non-finite input. pub fn try_boolean(a: &PathData, a_rule: FillRule, b: &PathData, b_rule: FillRule, op: BoolOp, precision: f64) -> Result { let arr = Arrangement::new(vec![(fill_bezpath(a), a_rule), (fill_bezpath(b), b_rule)])?; - let c = arr.contours(|m| op.apply(m[0], m[1])); + let c = arr.try_contours(|m| op.apply(m[0], m[1]))?; let tidy = Tidy { precision, ..arr.tidy }; Ok(all_contours_to_path(&c, &tidy)) } @@ -680,7 +696,7 @@ pub fn try_boolean(a: &PathData, a_rule: FillRule, b: &PathData, b_rule: FillRul pub fn try_boolean_n(paths: &[(&PathData, FillRule)], pred: impl Fn(&[bool]) -> bool) -> Result { let bps: Vec<(BezPath, FillRule)> = paths.iter().map(|(p, r)| (fill_bezpath(p), *r)).collect(); let arr = Arrangement::new(bps)?; - Ok(all_contours_to_path(&arr.contours(pred), &arr.tidy)) + Ok(all_contours_to_path(&arr.try_contours(pred)?, &arr.tidy)) } /// N-ary boolean: the area where `pred(inside_flags)` holds, `inside_flags[i]` telling whether a @@ -751,7 +767,8 @@ 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)?; - Ok(c.contours().map(|ct| ct.path.area()).sum::().abs()) + let area = c.contours().map(|ct| ct.path.area()).sum::().abs(); + if area.is_finite() { Ok(area) } else { Err(PathOpsError::NonFinite) } } /// Unsigned filled area of a path under a fill rule (normalises first, so it is exact for @@ -816,4 +833,67 @@ mod tests { lens.close_path(); assert_eq!(tidy.repair(&lens), lens); } + + fn square(x: f64, y: f64, size: f64) -> PathData { + PathData::single(vectorcraft_geom::SubPath::new( + [(x, y), (x + size, y), (x + size, y + size), (x, y + size)] + .iter() + .map(|&(x, y)| vectorcraft_geom::Anchor::corner(Point::new(x, y))) + .collect(), + true, + )) + } + + #[test] + fn coordinates_wider_than_the_precision_are_refused_not_emptied() { + // At 1e15 the sweep tolerance is far above a 10 unit square: both squares used to vanish and the call said `Ok`. + let near = square(0.0, 0.0, 10.0); + let far = square(1e15, 1e15, 10.0); + for op in [BoolOp::Union, BoolOp::Intersect, BoolOp::Difference, BoolOp::Xor] { + assert_eq!( + try_boolean(&near, FillRule::NonZero, &far, FillRule::NonZero, op, DEFAULT_PRECISION), + Err(PathOpsError::Degenerate), + "{op:?}" + ); + } + assert_eq!(try_area(&far, FillRule::NonZero), Err(PathOpsError::Degenerate)); + assert_eq!(try_normalize(&far, FillRule::NonZero), Err(PathOpsError::Degenerate)); + // The infallible entry points keep answering with an empty path. + assert!(boolean(&near, FillRule::NonZero, &far, FillRule::NonZero, BoolOp::Union).is_empty()); + } + + #[test] + fn coordinates_near_the_float_limit_do_not_reach_the_sweep() { + // A 1e308 rectangle made linesweeper panic (a caught panic here, an abort on the web). + let huge = square(0.0, 0.0, 1e308); + let small = square(0.0, 0.0, 10.0); + assert_eq!(try_area(&huge, FillRule::NonZero), Err(PathOpsError::Degenerate)); + assert_eq!(try_boolean(&huge, FillRule::NonZero, &small, FillRule::NonZero, BoolOp::Union, DEFAULT_PRECISION), Err(PathOpsError::Degenerate)); + assert_eq!(try_unite_all(&[(&huge, FillRule::NonZero), (&small, FillRule::NonZero)]), Err(PathOpsError::Degenerate)); + } + + #[test] + fn large_but_sensible_coordinates_still_work() { + // Well inside the limit: far-out but sensible offsets. + for at in [1e5, 1e7] { + let u = try_boolean( + &square(at, at, 10.0), + FillRule::NonZero, + &square(at + 5.0, at, 10.0), + FillRule::NonZero, + BoolOp::Union, + DEFAULT_PRECISION, + ) + .unwrap(); + let a = try_area(&u, FillRule::NonZero).unwrap(); + assert!((a - 150.0).abs() < 1.0, "at {at}: area {a}"); + } + } + + #[test] + 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()); + assert_eq!(arr.try_contours(|_| panic!("simulated panic in the sweep")).err(), Some(PathOpsError::Degenerate)); + } } From 0a2935cf8f984ddbfcb1959f4ff44166dd9efcc9 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Wed, 4 Nov 2026 01:49:40 +0100 Subject: [PATCH 2/2] Booleans: limit of one unit, crop outside shortcut, tests (#5465) Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/cut.rs | 24 ++++++++++++++++-------- crates/pathops/src/boolean.rs | 21 +++++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) 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());