Skip to content

Commit 6836244

Browse files
authored
Close subpaths when computing area for expansion (#580)
Previously the area() method on the Shape trait was used to compute the area for the purposes of determining the orientation for expansion. However, this method is not valid unless the input has closed subpaths, which can lead to inconsistent results. This patch adds an iterator to close subpaths, and computes area based on that. The iterator is made publicly available, as it may be handy.
1 parent c907a3a commit 6836244

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

kurbo/src/bezpath.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,73 @@ impl Mul<&BezPath> for TranslateScale {
793793
}
794794
}
795795

796+
/// Close all open subpaths in a path, expressed as iterator transform.
797+
pub(crate) fn close_subpaths<I>(elements: I) -> CloseSubpaths<I::IntoIter>
798+
where
799+
I: IntoIterator<Item = PathEl>,
800+
{
801+
CloseSubpaths {
802+
elements: elements.into_iter(),
803+
state: CloseSubpathState::Start,
804+
}
805+
}
806+
807+
/// An iterator that closes all open subpaths.
808+
///
809+
/// This struct is created by the [`close_subpaths`] function.
810+
#[derive(Clone)]
811+
pub(crate) struct CloseSubpaths<I: Iterator<Item = PathEl>> {
812+
elements: I,
813+
state: CloseSubpathState,
814+
}
815+
816+
#[derive(Clone)]
817+
enum CloseSubpathState {
818+
Start,
819+
InSubpath,
820+
ClosedLast,
821+
PendingMoveTo(Point),
822+
}
823+
824+
impl<I: Iterator<Item = PathEl>> Iterator for CloseSubpaths<I> {
825+
type Item = PathEl;
826+
827+
fn next(&mut self) -> Option<PathEl> {
828+
match self.state {
829+
CloseSubpathState::Start => {
830+
let el = self.elements.next();
831+
if !matches!(el, Some(PathEl::ClosePath) | None) {
832+
self.state = CloseSubpathState::InSubpath;
833+
}
834+
el
835+
}
836+
CloseSubpathState::InSubpath => {
837+
let el = self.elements.next();
838+
match el {
839+
None => {
840+
self.state = CloseSubpathState::ClosedLast;
841+
Some(PathEl::ClosePath)
842+
}
843+
Some(PathEl::MoveTo(point)) => {
844+
self.state = CloseSubpathState::PendingMoveTo(point);
845+
Some(PathEl::ClosePath)
846+
}
847+
Some(PathEl::ClosePath) => {
848+
self.state = CloseSubpathState::Start;
849+
el
850+
}
851+
_ => el,
852+
}
853+
}
854+
CloseSubpathState::ClosedLast => None,
855+
CloseSubpathState::PendingMoveTo(point) => {
856+
self.state = CloseSubpathState::Start;
857+
Some(PathEl::MoveTo(point))
858+
}
859+
}
860+
}
861+
}
862+
796863
/// Transform an iterator over path elements into one over path
797864
/// segments.
798865
///
@@ -2183,4 +2250,62 @@ mod tests {
21832250
assert!(bez.contains((100.0, 299.9).into()));
21842251
assert!(bez.contains((100.0, 300.0).into()));
21852252
}
2253+
2254+
#[test]
2255+
fn check_close_subpaths() {
2256+
let mut bez = BezPath::new();
2257+
bez.move_to((10.0, 10.0));
2258+
bez.line_to((100.0, 20.0));
2259+
bez.line_to((60.0, 100.0));
2260+
let elements = close_subpaths(&bez).collect::<Vec<_>>();
2261+
bez.close_path();
2262+
assert_eq!(&elements, bez.elements());
2263+
2264+
// Test implicit closepath in middle of path
2265+
let mut bez2 = BezPath::new();
2266+
bez2.move_to((10.0, 10.0));
2267+
bez2.line_to((100.0, 20.0));
2268+
bez2.line_to((60.0, 100.0));
2269+
bez2.move_to((110.0, 10.0));
2270+
bez2.line_to((200.0, 20.0));
2271+
bez2.line_to((160.0, 100.0));
2272+
let elements2 = close_subpaths(&bez2).collect::<Vec<_>>();
2273+
let mut bez3 = BezPath::new();
2274+
bez3.move_to((10.0, 10.0));
2275+
bez3.line_to((100.0, 20.0));
2276+
bez3.line_to((60.0, 100.0));
2277+
bez3.close_path();
2278+
bez3.move_to((110.0, 10.0));
2279+
bez3.line_to((200.0, 20.0));
2280+
bez3.line_to((160.0, 100.0));
2281+
bez3.close_path();
2282+
assert_eq!(&elements2, bez3.elements());
2283+
}
2284+
2285+
#[test]
2286+
fn close_subpaths_does_not_duplicate_existing_closepath() {
2287+
let path = BezPath::from_vec(vec![
2288+
PathEl::MoveTo((0.0, 0.0).into()),
2289+
PathEl::LineTo((10.0, 0.0).into()),
2290+
PathEl::LineTo((10.0, 10.0).into()),
2291+
PathEl::ClosePath,
2292+
PathEl::MoveTo((20.0, 0.0).into()),
2293+
PathEl::LineTo((30.0, 0.0).into()),
2294+
]);
2295+
2296+
let closed = close_subpaths(path.iter()).collect::<Vec<_>>();
2297+
2298+
assert_eq!(
2299+
closed,
2300+
vec![
2301+
PathEl::MoveTo((0.0, 0.0).into()),
2302+
PathEl::LineTo((10.0, 0.0).into()),
2303+
PathEl::LineTo((10.0, 10.0).into()),
2304+
PathEl::ClosePath,
2305+
PathEl::MoveTo((20.0, 0.0).into()),
2306+
PathEl::LineTo((30.0, 0.0).into()),
2307+
PathEl::ClosePath,
2308+
]
2309+
);
2310+
}
21862311
}

kurbo/src/expand.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use crate::{
55
Affine, Arc, BezPath, CubicBez, Join, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point,
6-
QuadBez, Shape, Vec2,
6+
QuadBez, Shape, Vec2, bezpath::close_subpaths, segments,
77
};
88

99
#[cfg(not(feature = "std"))]
@@ -131,7 +131,7 @@ pub fn expand_path(
131131
miter_limit: f64,
132132
tolerance: f64,
133133
) -> BezPath {
134-
if path.area() >= 0.0 {
134+
if segments(close_subpaths(path.path_elements(tolerance))).area() >= 0.0 {
135135
expand = -expand;
136136
}
137137
expand_path_signed(path, expand, join, miter_limit, tolerance)
@@ -584,4 +584,21 @@ mod tests {
584584

585585
assert_eq!(actual, expected);
586586
}
587+
588+
#[test]
589+
fn expand_open_subpath_uses_implicit_close_for_area_sign() {
590+
let mut open = BezPath::new();
591+
open.move_to((100.0, 100.0));
592+
open.line_to((110.0, 100.0));
593+
open.line_to((100.0, 110.0));
594+
595+
let mut closed = open.clone();
596+
closed.close_path();
597+
598+
let expand = Diagonal2::new(1.0, 1.0);
599+
let open_expanded = expand_path(open, expand, Join::Miter, 4.0, 1e-3);
600+
let closed_expanded = expand_path(closed, expand, Join::Miter, 4.0, 1e-3);
601+
602+
assert_eq!(open_expanded, closed_expanded);
603+
}
587604
}

0 commit comments

Comments
 (0)