mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
Offset Path: an inset keeps the outline of open paths (#6427)
A negative offset kept only the faces inside the closed fill, so the stroke of every open subpath was dropped and an open line inset to nothing. Inset the closed subpaths, outline the open ones with a 2|delta| butt stroke as a positive offset does, and unite the two.
This commit is contained in:
@@ -126,21 +126,49 @@ fn path_is_finite(path: &PathData) -> bool {
|
||||
/// Fallible [`offset_path`] that returns an error when the input is non-finite or exceeds
|
||||
/// the complexity bound ([`MAX_OFFSET_SEGMENT_PAIRS`]), or when planar arrangement fails.
|
||||
pub fn try_offset_path(path: &PathData, delta: f64, join: Join, miter_limit: f64) -> Result<PathData, PathOpsError> {
|
||||
let src = std::cell::LazyCell::new(|| SegmentTree::new(boxed_segments(&path.to_bezpath())));
|
||||
offset_with_check(path, delta, join, miter_limit, |p, r| any_within(&src, p, r))
|
||||
offset_with_check(path, delta, join, miter_limit, |part| {
|
||||
let bp = part.to_bezpath();
|
||||
let src = std::cell::LazyCell::new(move || SegmentTree::new(boxed_segments(&bp)));
|
||||
move |p, r| any_within(&src, p, r)
|
||||
})
|
||||
}
|
||||
|
||||
/// [`try_offset_path`] with the test of whether some segment of `path` lies closer than a radius to a point.
|
||||
fn offset_with_check(
|
||||
/// [`try_offset_path`] with `within_of(part)`, the test of whether some segment of `part` lies
|
||||
/// closer than a radius to a point.
|
||||
fn offset_with_check<W: Fn(kurbo::Point, f64) -> bool>(
|
||||
path: &PathData,
|
||||
delta: f64,
|
||||
join: Join,
|
||||
miter_limit: f64,
|
||||
within_of: impl Fn(&PathData) -> W,
|
||||
) -> Result<PathData, PathOpsError> {
|
||||
if !delta.is_finite() || !path_is_finite(path) {
|
||||
return Err(PathOpsError::NonFinite);
|
||||
}
|
||||
// An inset keeps only faces inside the closed fill, so it would drop the outline of every
|
||||
// open subpath (#6427). Inset the closed subpaths, outline the open ones as a positive offset
|
||||
// does, and unite the two.
|
||||
if delta < 0.0 && path.subpaths.iter().any(|s| s.anchors.len() > 1 && !close_if_ends_meet(s).closed) {
|
||||
let (closed, open): (Vec<SubPath>, Vec<SubPath>) = path.subpaths.iter().cloned().partition(|s| close_if_ends_meet(s).closed);
|
||||
let (closed, open) = (PathData::new(closed), PathData::new(open));
|
||||
let inset = offset_region(&closed, delta, join, miter_limit, within_of(&closed))?;
|
||||
let outline = offset_region(&open, -delta, join, miter_limit, within_of(&open))?;
|
||||
if inset.is_empty() {
|
||||
return Ok(outline);
|
||||
}
|
||||
return crate::boolean::try_unite_all(&[(&inset, FillRule::NonZero), (&outline, FillRule::NonZero)]);
|
||||
}
|
||||
offset_region(path, delta, join, miter_limit, within_of(path))
|
||||
}
|
||||
|
||||
/// [`offset_with_check`] of a finite `path` with the `within` test of its own segments.
|
||||
fn offset_region(
|
||||
path: &PathData,
|
||||
delta: f64,
|
||||
join: Join,
|
||||
miter_limit: f64,
|
||||
within: impl Fn(kurbo::Point, f64) -> bool,
|
||||
) -> Result<PathData, PathOpsError> {
|
||||
if !delta.is_finite() || !path_is_finite(path) {
|
||||
return Err(PathOpsError::NonFinite);
|
||||
}
|
||||
if path.is_empty() {
|
||||
return Ok(PathData::default());
|
||||
}
|
||||
@@ -630,14 +658,17 @@ mod tests {
|
||||
fn offsets_with_the_tree_are_identical_to_offsets_with_the_scan() {
|
||||
let (mut checks, mut hits, mut misses) = (0, 0, 0);
|
||||
for path in offset_inputs() {
|
||||
let segs = boxed_segments(&path.to_bezpath());
|
||||
for delta in [0.0, 0.05, 1.0, 5.0, 60.0, -0.05, -1.0, -5.0, -60.0] {
|
||||
for join in [Join::Round, Join::Miter, Join::Bevel] {
|
||||
let found = std::cell::Cell::new((0, 0));
|
||||
let by_scan = offset_with_check(&path, delta, join, 4.0, |p, r| {
|
||||
let v = any_within_by_scan(&segs, p, r);
|
||||
found.set((found.get().0 + usize::from(v), found.get().1 + usize::from(!v)));
|
||||
v
|
||||
let by_scan = offset_with_check(&path, delta, join, 4.0, |part| {
|
||||
let segs = boxed_segments(&part.to_bezpath());
|
||||
let found = &found;
|
||||
move |p, r| {
|
||||
let v = any_within_by_scan(&segs, p, r);
|
||||
found.set((found.get().0 + usize::from(v), found.get().1 + usize::from(!v)));
|
||||
v
|
||||
}
|
||||
});
|
||||
checks += 1;
|
||||
hits += found.get().0;
|
||||
|
||||
@@ -316,6 +316,24 @@ fn offset_of_an_open_path_whose_ends_meet_is_the_closed_shape_offset() {
|
||||
assert_eq!(o.subpaths.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_offset_outlines_an_open_path_as_a_positive_one_does() {
|
||||
// Issue #6427 (A4): an inset of an open path was empty. Open subpaths are outlined with a
|
||||
// stroke of width 2|delta| whichever way the offset goes.
|
||||
let line = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(100.0, 0.0)], false));
|
||||
for delta in [2.0, -2.0] {
|
||||
let o = offset_path(&line, delta, Join::Miter, 4.0);
|
||||
assert!(close(ar(&o), 400.0, 1e-6), "delta {delta}: {}", ar(&o));
|
||||
assert_eq!(o.subpaths.len(), 1, "delta {delta}");
|
||||
}
|
||||
// With a closed shape beside it, the shape is inset and the line still outlined.
|
||||
let mut both = rect(0.0, 0.0, 20.0, 10.0);
|
||||
both.subpaths.push(SubPath::polyline(&[Point::new(100.0, 0.0), Point::new(150.0, 0.0)], false));
|
||||
let o = offset_path(&both, -2.0, Join::Miter, 4.0);
|
||||
assert!(close(ar(&o), 16.0 * 6.0 + 50.0 * 4.0, 1e-6), "{}", ar(&o));
|
||||
assert_eq!(o.subpaths.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_stroke_of_line() {
|
||||
let l = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(30.0, 40.0)], false));
|
||||
|
||||
Reference in New Issue
Block a user