mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 21:50:16 +00:00
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:
committed by
bflatastic
co-authored by
Claude Sonnet 5.5
parent
e951095617
commit
0a2935cf8f
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user