Claude found kurbo panics on some curves I'm processing. This is Claude's summary of the issue:
fit_to_bezpath_opt panics on some well-formed, finite inputs. Reproduced on 0.13.0 and 0.13.1.
I hit it through simplify_bezpath with SimplifyOptLevel::Optimize, feeding it flattened polygon-offset contours.
Reproduction
use kurbo::simplify::SimplifyBezPath;
use kurbo::{BezPath, Point};
fn main() {
// A closed quadrilateral whose four vertices are nearly collinear.
let pts = [(1.0, 0.7), (4.1, 2.0), (4.4, 2.1), (0.0, 0.0)];
let mut path = BezPath::new();
path.move_to(Point::new(pts[0].0, pts[0].1));
for (x, y) in &pts[1..] {
path.line_to(Point::new(*x, *y));
}
path.close_path();
let _ = kurbo::fit_to_bezpath_opt(&SimplifyBezPath::new(path), 2.0); // panics
}
thread 'main' panicked at kurbo-0.13.1/src/fit.rs:646:61:
called `Option::unwrap()` on a `None` value
(fit.rs:650:61 on 0.13.0.) Expected: a BezPath, or a graceful fallback — not a panic.
Cause
The panic is the last statement of fit_to_bezpath_opt_inner:
let (c, _) = fit_to_cubic(source, t0..t1, accuracy).unwrap();
fit_to_cubic returns Option and is treated as fallible at every other call site in fit.rs. Here it's unwrapped,
assuming a segment that fit_opt_segment accepted at the smaller per-segment budget x must also fit at the larger
accuracy.
It need not, because fit_to_cubic changes strategy rather than just relaxing as accuracy grows:
if chord2 <= acc2 {
// Special case very short chords; try to fit a line.
return try_fit_line(source, accuracy, range, start.p, end.p);
}
A bigger accuracy makes this branch more likely, and try_fit_line returns None when the curve strays further
than accuracy from the chord. So the trigger is a sub-range with arc length ≫ accuracy but chord ≪ accuracy —
geometry that doubles back on itself, which is what a nearly-degenerate polyline is full of.
Notes
- It's an accuracy window, not a threshold: this input panics at
accuracy = 2.0 but succeeds at 0.5, 1.0, 4.0 and
8.0.
fit_to_bezpath handles the same input at every accuracy above, so it's specific to the _opt path.
- The input is minimal — no three-vertex subset reproduces, the unclosed form doesn't, and only this starting vertex
does.
Possible fix
Stop unwrapping and give the loop somewhere to go when a sub-range can't be fit — fall back to the chord, or propagate
the failure — matching how the other fit_to_cubic call sites already treat it.
Repro
kurbo_fit_opt_panic.rs.txt
Claude found kurbo panics on some curves I'm processing. This is Claude's summary of the issue:
fit_to_bezpath_optpanics on some well-formed, finite inputs. Reproduced on 0.13.0 and 0.13.1.I hit it through
simplify_bezpathwithSimplifyOptLevel::Optimize, feeding it flattened polygon-offset contours.Reproduction
(
fit.rs:650:61on 0.13.0.) Expected: aBezPath, or a graceful fallback — not a panic.Cause
The panic is the last statement of
fit_to_bezpath_opt_inner:fit_to_cubicreturnsOptionand is treated as fallible at every other call site infit.rs. Here it's unwrapped,assuming a segment that
fit_opt_segmentaccepted at the smaller per-segment budgetxmust also fit at the largeraccuracy.It need not, because
fit_to_cubicchanges strategy rather than just relaxing asaccuracygrows:A bigger
accuracymakes this branch more likely, andtry_fit_linereturnsNonewhen the curve strays furtherthan
accuracyfrom the chord. So the trigger is a sub-range with arc length ≫accuracybut chord ≪accuracy—geometry that doubles back on itself, which is what a nearly-degenerate polyline is full of.
Notes
accuracy = 2.0but succeeds at 0.5, 1.0, 4.0 and8.0.
fit_to_bezpathhandles the same input at every accuracy above, so it's specific to the_optpath.does.
Possible fix
Stop unwrapping and give the loop somewhere to go when a sub-range can't be fit — fall back to the chord, or propagate
the failure — matching how the other
fit_to_cubiccall sites already treat it.Repro
kurbo_fit_opt_panic.rs.txt