address review: Offset Paths copies are budgeted by the geometry offset_path returns

This commit is contained in:
Dylan M. Taylor
2026-11-11 14:50:06 +00:00
parent 9d8611299b
commit 3cf99b92bb
4 changed files with 36 additions and 1 deletions
Generated
+1
View File
@@ -1758,6 +1758,7 @@ dependencies = [
"effectcraft-raster",
"kurbo",
"linesweeper",
"log",
"tiny-skia 0.12.0",
]
+1
View File
@@ -14,5 +14,6 @@ effectcraft-geom = { workspace = true }
effectcraft-keyframe = { workspace = true }
effectcraft-raster = { workspace = true }
kurbo = { workspace = true }
log = { workspace = true }
tiny-skia = { workspace = true }
linesweeper = "0.5"
+15 -1
View File
@@ -351,6 +351,11 @@ fn round_sub(sub: &Sub, r: f64, out: &mut BezPath) {
/// join. `copies` copies are made, copy *i* (0-based) offset by `amount · (i + 1 + copy_offset)`,
/// all returned in the same compound path.
pub fn offset(paths: &[BezPath], amount: f64, join: Join, miter_limit: f64, copies: f64, copy_offset: f64) -> Vec<BezPath> {
offset_within(paths, amount, join, miter_limit, copies, copy_offset, MAX_OPERATOR_SEGMENTS)
}
/// [`offset`] with each path's result held to `max` elements (its first copy aside).
fn offset_within(paths: &[BezPath], amount: f64, join: Join, miter_limit: f64, copies: f64, copy_offset: f64, max: usize) -> Vec<BezPath> {
let n = pieces_within_budget(paths, copies.round().clamp(1.0, 1000.0) as usize);
paths
.iter()
@@ -358,7 +363,16 @@ pub fn offset(paths: &[BezPath], amount: f64, join: Join, miter_limit: f64, copi
let mut out = BezPath::new();
for i in 0..n {
let d = amount * (i as f64 + 1.0 + copy_offset);
out.extend(crate::offset::offset_path(p, d, join, miter_limit).elements().iter().copied());
let copy = crate::offset::offset_path(p, d, join, miter_limit);
// The budget is spent on what `offset_path` returns (an open subpath comes back as
// a closed outline of many segments; a zero offset as the path itself): the first
// copy is always kept, and a copy that would pass the budget is not made, so the
// work stays within a small multiple of it.
if i > 0 && out.elements().len().saturating_add(copy.elements().len()) > max {
log::warn!("path: Offset Paths made {i} of {n} copies; more would pass {max} segments");
break;
}
out.extend(copy.elements().iter().copied());
}
out
})
+19
View File
@@ -182,6 +182,25 @@ fn offset_copies() {
assert!(close(o[0].bounding_box().width(), 140.0, 1e-3));
}
#[test]
fn offset_copies_are_budgeted_by_the_output_they_return() {
// An open one-segment subpath comes back as a closed outline of several elements, so the copies
// stop when the returned elements pass the budget, not when the input segments would.
let line = || {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.line_to((50.0, 0.0));
p
};
let one = offset_within(&[line()], 5.0, Join::Round, 4.0, 1.0, 0.0, usize::MAX)[0].elements().len();
assert!(one > 4, "{one}");
let o = offset_within(&[line()], 5.0, Join::Round, 4.0, 50.0, 0.0, one * 3);
let moves = o[0].elements().iter().filter(|e| matches!(e, PathEl::MoveTo(_))).count();
assert!((1..50).contains(&moves) && o[0].elements().len() <= one * 4, "{moves} contours, {} elements", o[0].elements().len());
// The first copy always comes back, however small the budget.
assert!(!offset_within(&[line()], 5.0, Join::Round, 4.0, 5.0, 0.0, 0)[0].elements().is_empty());
}
#[test]
fn offset_open_path_outlines() {
let mut l = BezPath::new();