diff --git a/crates/pathops/src/lib.rs b/crates/pathops/src/lib.rs index 553fce04..74a120b1 100644 --- a/crates/pathops/src/lib.rs +++ b/crates/pathops/src/lib.rs @@ -59,6 +59,13 @@ pub fn closed(path: &BezPath) -> BezPath { /// Every coordinate is finite and of a sane magnitude (the sweep needs bounded input). pub use figcraft_geom::measure::finite_path as finite; +/// Run the sweep library on `f`. Version 0.4 can panic on rare near-degenerate input (an internal +/// `unwrap` while ordering curves, hit by some font outlines): that is bad input, not a crash. +// shortcut: no catch where panics abort (wasm32); drop this once a linesweeper release no longer panics there. +fn sweep(f: impl FnOnce() -> Result) -> Result { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(Err(PathOpsError::BadInput)) +} + fn binary(a: &BezPath, b: &BezPath, op: linesweeper::BinaryOp) -> Result { if a.elements().is_empty() && b.elements().is_empty() { return Ok(BezPath::new()); @@ -66,7 +73,7 @@ fn binary(a: &BezPath, b: &BezPath, op: linesweeper::BinaryOp) -> Result Result, PathOpsE let bbox = closed_paths.iter().filter(|p| !p.elements().is_empty()).map(Shape::bounding_box).reduce(|a, b| a.union(b)); let m = bbox.map(|b| b.x0.abs().max(b.x1.abs()).max(b.y0.abs()).max(b.y1.abs())).unwrap_or(0.0); let eps = (m * f64::EPSILON * 64.0).max(1e-6); + let even_odd: Vec = paths.iter().map(|(_, e)| *e).collect(); + sweep(|| arrange(&closed_paths, &even_odd, eps)) +} + +/// [`faces`] of the closed `paths`, swept with tolerance `eps`. +fn arrange(closed_paths: &[BezPath], even_odd: &[bool], eps: f64) -> Result, PathOpsError> { let topo = linesweeper::topology::Topology::::from_paths(closed_paths.iter().enumerate().map(|(i, p)| (p, i)), eps) .map_err(|_| PathOpsError::BadInput)?; - let even_odd: Vec = paths.iter().map(|(_, e)| *e).collect(); let cover_of = |w: &Windings| -> Vec { w.0.iter() .enumerate() @@ -441,6 +453,13 @@ mod tests { p.area().abs() } + #[test] + fn a_panic_in_the_sweep_is_bad_input() { + let r: Result<(), PathOpsError> = sweep(|| panic!("sweep bug")); + assert!(matches!(r, Err(PathOpsError::BadInput))); + assert!(matches!(sweep(|| Ok(1)), Ok(1))); + } + #[test] fn union_intersect_subtract_exclude() { let a = sq(0.0, 0.0, 10.0);