Skip to content

Commit

Permalink
Merge pull request #307 from dfrg/reverse-subpaths
Browse files Browse the repository at this point in the history
Add reverse_subpaths() method to BezPath
  • Loading branch information
dfrg authored Oct 3, 2023
2 parents ad67725 + a989b4b commit b1dfb53
Showing 1 changed file with 113 additions and 0 deletions.
113 changes: 113 additions & 0 deletions src/bezpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,58 @@ impl BezPath {
}
cbox.unwrap_or_default()
}

/// Returns a new path with the winding direction of all subpaths reversed.
pub fn reverse_subpaths(&self) -> BezPath {
let elements = self.elements();
let mut start_ix = 1;
let mut start_pt = Point::default();
let mut reversed = BezPath(Vec::with_capacity(elements.len()));
for (ix, el) in elements.iter().enumerate() {
match el {
PathEl::MoveTo(pt) => {
if start_ix < ix {
reverse_subpath(start_pt, &elements[start_ix..ix], &mut reversed);
}
start_pt = *pt;
start_ix = ix + 1;
}
PathEl::ClosePath => {
if start_ix < ix {
reverse_subpath(start_pt, &elements[start_ix..ix], &mut reversed);
reversed.push(PathEl::ClosePath);
}
start_ix = ix + 1;
}
_ => {}
}
}
if start_ix < elements.len() {
reverse_subpath(start_pt, &elements[start_ix..], &mut reversed);
}
reversed
}
}

/// Helper for reversing a subpath.
///
/// The `els` parameter must not contain any `MoveTo` or `ClosePath` elements.
fn reverse_subpath(start_pt: Point, els: &[PathEl], reversed: &mut BezPath) {
let end_pt = els.last().and_then(|el| el.end_point()).unwrap_or(start_pt);
reversed.push(PathEl::MoveTo(end_pt));
for (ix, el) in els.iter().enumerate().rev() {
let end_pt = if ix > 0 {
els[ix - 1].end_point().unwrap()
} else {
start_pt
};
match el {
PathEl::LineTo(_) => reversed.push(PathEl::LineTo(end_pt)),
PathEl::QuadTo(c0, _) => reversed.push(PathEl::QuadTo(*c0, end_pt)),
PathEl::CurveTo(c0, c1, _) => reversed.push(PathEl::CurveTo(*c1, *c0, end_pt)),
_ => panic!("reverse_subpath expects MoveTo and ClosePath to be removed"),
}
}
}

impl FromIterator<PathEl> for BezPath {
Expand Down Expand Up @@ -1535,4 +1587,65 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(segments, get_segs);
}

#[test]
fn test_control_box() {
// a sort of map ping looking thing drawn with a single cubic
// cbox is wildly different than tight box
let path = BezPath::from_svg("M200,300 C50,50 350,50 200,300").unwrap();
assert_eq!(Rect::new(50.0, 50.0, 350.0, 300.0), path.control_box());
assert!(path.control_box().area() > path.bounding_box().area());
}

#[test]
fn test_reverse_unclosed() {
let path = BezPath::from_svg("M10,10 Q40,40 60,10 L100,10 C125,10 150,50 125,60").unwrap();
let reversed = path.reverse_subpaths();
assert_eq!(
"M125,60 C150,50 125,10 100,10 L60,10 Q40,40 10,10",
reversed.to_svg()
);
}

#[test]
fn test_reverse_closed_triangle() {
let path = BezPath::from_svg("M100,100 L150,200 L50,200 Z").unwrap();
let reversed = path.reverse_subpaths();
assert_eq!("M50,200 L150,200 L100,100 Z", reversed.to_svg());
}

#[test]
fn test_reverse_closed_shape() {
let path = BezPath::from_svg(
"M125,100 Q200,150 175,300 C150,150 50,150 25,300 Q0,150 75,100 L100,50 Z",
)
.unwrap();
let reversed = path.reverse_subpaths();
assert_eq!(
"M100,50 L75,100 Q0,150 25,300 C50,150 150,150 175,300 Q200,150 125,100 Z",
reversed.to_svg()
);
}

#[test]
fn test_reverse_multiple_subpaths() {
let svg = "M10,10 Q40,40 60,10 L100,10 C125,10 150,50 125,60 M100,100 L150,200 L50,200 Z M125,100 Q200,150 175,300 C150,150 50,150 25,300 Q0,150 75,100 L100,50 Z";
let expected_svg = "M125,60 C150,50 125,10 100,10 L60,10 Q40,40 10,10 M50,200 L150,200 L100,100 Z M100,50 L75,100 Q0,150 25,300 C50,150 150,150 175,300 Q200,150 125,100 Z";
let path = BezPath::from_svg(svg).unwrap();
let reversed = path.reverse_subpaths();
assert_eq!(expected_svg, reversed.to_svg());
}

/// https://github.com/fonttools/fonttools/blob/bf265ce49e0cae6f032420a4c80c31d8e16285b8/Tests/pens/reverseContourPen_test.py#L7
#[test]
fn test_reverse_lines() {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((1.0, 1.0));
path.line_to((2.0, 2.0));
path.line_to((3.0, 3.0));
path.close_path();
let rev = path.reverse_subpaths();
assert_eq!("M3,3 L2,2 L1,1 L0,0 Z", rev.to_svg());
}
}

0 comments on commit b1dfb53

Please sign in to comment.