use std::{borrow::Cow, ops::Range};
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez, Shape};
use ordered_float::OrderedFloat;
pub fn erase_open_corners(path: &BezPath) -> Option<BezPath> {
CornerErasureCtx::new(path).erase_corners()
}
struct CornerErasureCtx<'a> {
els: Cow<'a, [PathEl]>,
}
impl<'a> CornerErasureCtx<'a> {
fn new(path: &'a BezPath) -> Self {
CornerErasureCtx {
els: path.elements().into(),
}
}
fn is_closed(&self) -> bool {
matches!(self.els.last(), Some(PathEl::ClosePath))
}
fn n_possible_candidates(&self) -> usize {
if self.els.last() == Some(&PathEl::ClosePath) {
self.els.len() - 2
} else {
self.els.len().saturating_sub(3)
}
}
fn erase_corners(mut self) -> Option<BezPath> {
if (self.is_closed() && self.els.len() < 5) || self.els.len() < 4 {
return None;
}
let mut made_changes = false;
let mut ix = 0;
while ix <= self.n_possible_candidates() {
made_changes |= self.erase_open_corner_if_present(ix);
ix += 1;
}
if !made_changes {
return None;
}
Some(BezPath::from_vec(self.els.into_owned()))
}
fn erase_open_corner_if_present(&mut self, seg_ix: usize) -> bool {
let Some(maybe_corner) = self.possible_corner(seg_ix) else {
return false;
};
log::trace!(
"considering ({}..{})",
maybe_corner.one.end(),
maybe_corner.two.start()
);
if !maybe_corner.points_are_right_of_line() {
log::trace!(
"crossing points {} and {} not on same side of line",
maybe_corner.point_before_line(),
maybe_corner.point_after_line()
);
return false;
}
let Some(intersection) = maybe_corner.intersection() else {
log::trace!("no intersections");
return false;
};
let Intersection { t0, t1, .. } = intersection;
let t0_inv = 1.0 - t0;
log::trace!("found intersections at {t0} and {t1}");
if ((t0_inv < 0.5 && t1 < 0.5)
|| (t0_inv < 0.3 && t1 < 0.99)
|| (t0_inv < 0.99 && t1 < 0.3))
&& t0_inv > 0.001
&& t1 > 0.001
{
log::debug!("found an open corner");
let new_prev = maybe_corner.one.subsegment(0.0..intersection.t0);
let first_ix = maybe_corner.first_el_idx;
self.overwrite_el(first_ix, new_prev);
let new_next = maybe_corner.two.subsegment(intersection.t1..1.0);
let next_seg_el_ix = self.wrapping_el_idx(first_ix + 2);
self.overwrite_el(next_seg_el_ix, new_next);
let line_seg_el_ix = self.wrapping_el_idx(first_ix + 1);
self.els.to_mut().remove(line_seg_el_ix);
if self.is_closed() {
let second_last = self.els.len() - 2;
let last_pt = self.els[second_last].end_point().unwrap();
*self.els.to_mut().get_mut(0).unwrap() = PathEl::MoveTo(last_pt);
}
return true;
}
false
}
fn overwrite_el(&mut self, el_ix: usize, new_seg: PathSeg) {
match (new_seg, self.els.to_mut().get_mut(el_ix).unwrap()) {
(PathSeg::Line(new), PathEl::LineTo(end)) => {
*end = new.p1;
}
(PathSeg::Quad(new), PathEl::QuadTo(p1, p2)) => {
*p1 = new.p1;
*p2 = new.p2;
}
(PathSeg::Cubic(new), PathEl::CurveTo(p1, p2, p3)) => {
*p1 = new.p1;
*p2 = new.p2;
*p3 = new.p3;
}
_ => panic!("el/seg mismatch"),
}
}
fn possible_corner(&self, i: usize) -> Option<PossibleCorner> {
if i >= self.els.len().saturating_sub(2) {
return None;
}
let prev_seg_start = match i {
0 if !self.is_closed() => return None,
0 => self.els.len() - 3,
_ => i - 1,
};
let first_el_idx = self.wrapping_el_idx(prev_seg_start + 1);
let start = self.els.get(prev_seg_start).and_then(PathEl::end_point)?;
let one = self
.wrapping_get_el(prev_seg_start + 1)
.and_then(|el| make_seg(el, start))?;
if let PathEl::LineTo(line_to) = self.wrapping_get_el(prev_seg_start + 2)? {
let two = self
.wrapping_get_el(prev_seg_start + 3)
.and_then(|el| make_seg(el, line_to))?;
return Some(PossibleCorner {
one,
two,
first_el_idx,
});
}
None
}
fn wrapping_get_el(&self, i: usize) -> Option<PathEl> {
self.els.get(self.wrapping_el_idx(i)).copied()
}
fn wrapping_el_idx(&self, i: usize) -> usize {
if !self.is_closed() {
return i;
}
if i == 0 {
return self.els.len().saturating_sub(2);
}
if i >= self.els.len() - 1 {
i % (self.els.len() - 1) + 1
} else {
i
}
}
}
#[derive(Clone, Debug)]
struct PossibleCorner {
first_el_idx: usize,
one: PathSeg,
two: PathSeg,
}
impl PossibleCorner {
fn point_before_line(&self) -> Point {
match self.one {
PathSeg::Line(line) => line.p0,
PathSeg::Quad(quad) => quad.p1,
PathSeg::Cubic(cube) => cube.p2,
}
}
fn point_after_line(&self) -> Point {
match self.two {
PathSeg::Line(line) => line.p1,
PathSeg::Quad(quad) => quad.p1,
PathSeg::Cubic(cube) => cube.p1,
}
}
fn line(&self) -> Line {
Line::new(self.one.end(), self.two.start())
}
fn points_are_right_of_line(&self) -> bool {
let prev_point = self.point_before_line();
let next_point = self.point_after_line();
let line = self.line();
!(point_is_left_of_line(line, prev_point) || point_is_left_of_line(line, next_point))
}
fn intersection(&self) -> Option<Intersection> {
let candidate = seg_seg_intersection(self.one, self.two)?;
let p1 = self.one.eval(candidate.t0);
let p2 = self.two.eval(candidate.t1);
let dist = p1.distance(p2);
if dist < 0.2 { Some(candidate) } else { None }
}
}
fn make_seg(element: PathEl, p0: Point) -> Option<PathSeg> {
match element {
PathEl::LineTo(p1) => Some(Line::new(p0, p1).into()),
PathEl::QuadTo(p1, p2) => Some(QuadBez::new(p0, p1, p2).into()),
PathEl::CurveTo(p1, p2, p3) => Some(CubicBez::new(p0, p1, p2, p3).into()),
_ => None,
}
}
fn point_is_left_of_line(line: Line, point: Point) -> bool {
let Line { p0: a, p1: b } = line;
(b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x) >= 0.0
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Intersection {
t0: f64,
t1: f64,
}
impl Intersection {
fn unique_key(&self) -> (u64, u64) {
(
(self.t0 / PY_ACCURACY) as u64,
(self.t1 / PY_ACCURACY) as u64,
)
}
}
fn seg_seg_intersection(seg1: PathSeg, seg2: PathSeg) -> Option<Intersection> {
let hit = match (seg1, seg2) {
(PathSeg::Line(line), seg) => seg
.intersect_line(line)
.iter()
.min_by_key(|hit| OrderedFloat(hit.line_t))
.map(|hit| Intersection {
t0: hit.line_t,
t1: hit.segment_t,
}),
(seg, PathSeg::Line(line)) => seg
.intersect_line(line)
.iter()
.min_by_key(|hit| OrderedFloat(hit.segment_t))
.map(|hit| Intersection {
t0: hit.segment_t,
t1: hit.line_t,
}),
(bez0, bez1) => return curve_curve_intersection_py(bez0, bez1),
}?;
if let (PathSeg::Line(l1), PathSeg::Line(l2)) = (seg1, seg2) {
let pt = l1.eval(hit.t0);
if py_isclose(l1.end().x, l1.start().x) || py_isclose(l2.start().x, l2.end().x) {
return Some(hit);
}
if !(l1.p0.points_are_on_same_side(pt, l1.p1) && l2.p1.points_are_on_same_side(pt, l2.p0)) {
return None;
}
}
Some(hit)
}
fn py_isclose(a: f64, b: f64) -> bool {
const TOLERANCE: f64 = 1e-09;
(a - b).abs() <= (TOLERANCE * a.abs().max(b.abs()))
}
trait SameSide {
fn points_are_on_same_side(&self, a: Point, b: Point) -> bool;
}
impl SameSide for Point {
fn points_are_on_same_side(&self, a: Point, b: Point) -> bool {
let x_diff = (a.x - self.x) * (b.x - self.x);
let y_diff = (a.y - self.y) * (b.y - self.y);
x_diff > 0.0 || y_diff > 0.0
}
}
const PY_ACCURACY: f64 = 1e-3;
fn curve_curve_intersection_py(seg1: PathSeg, seg2: PathSeg) -> Option<Intersection> {
let mut result = Vec::new();
curve_curve_py_impl(seg1, seg2, &(0.0..1.0), &(0.0..1.0), &mut result);
result.first().copied()
}
fn curve_curve_py_impl(
seg1: PathSeg,
seg2: PathSeg,
range1: &Range<f64>,
range2: &Range<f64>,
buf: &mut Vec<Intersection>,
) {
fn midpoint(range: &Range<f64>) -> f64 {
0.5 * (range.start + range.end)
}
let bounds1 = seg1.bounding_box();
let bounds2 = seg2.bounding_box();
if !bounds1.overlaps(bounds2) {
return;
}
if bounds1.area() < PY_ACCURACY && bounds2.area() < PY_ACCURACY {
let hit = Intersection {
t0: midpoint(range1),
t1: midpoint(range2),
};
let key = hit.unique_key();
if !buf.iter().any(|x| x.unique_key() == key) {
buf.push(hit);
}
return;
}
let (seg1_1, seg1_2) = seg1.subdivide();
let seg1_1_range = range1.start..midpoint(range1);
let seg1_2_range = midpoint(range1)..range1.end;
let (seg2_1, seg2_2) = seg2.subdivide();
let seg2_1_range = range2.start..midpoint(range2);
let seg2_2_range = midpoint(range2)..range2.end;
curve_curve_py_impl(seg1_1, seg2_1, &seg1_1_range, &seg2_1_range, buf);
curve_curve_py_impl(seg1_2, seg2_1, &seg1_2_range, &seg2_1_range, buf);
curve_curve_py_impl(seg1_1, seg2_2, &seg1_1_range, &seg2_2_range, buf);
curve_curve_py_impl(seg1_2, seg2_2, &seg1_2_range, &seg2_2_range, buf);
}
#[cfg(test)]
mod tests {
use kurbo::Vec2;
use write_fonts::OtRound;
use super::*;
#[allow(non_snake_case)]
mod python_test_glyphs {
pub(super) fn space() -> kurbo::BezPath {
kurbo::BezPath::new()
}
pub(super) fn hasCornerGlyph() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((20.0, 0.0));
path.line_to((179.0, 0.0));
path.line_to((60.0, 353.0));
path.line_to((198.0, 360.0));
path.line_to((20.0, 0.0));
path.close_path();
path
}
pub(super) fn curvyCornerGlyph() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((400.0, 0.0));
path.curve_to((400.0, 100.0), (450.0, 300.0), (300.0, 300.0));
path.line_to((200.0, 100.0));
path.curve_to((250.0, 100.0), (450.0, 150.0), (450.0, 50.0));
path.line_to((400.0, 0.0));
path.close_path();
path
}
pub(super) fn doubleCornerGlyph() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((100.0, 0.0));
path.line_to((400.0, 0.0));
path.line_to((400.0, 500.0));
path.line_to((500.0, 400.0));
path.line_to((0.0, 400.0));
path.line_to((100.0, 500.0));
path.line_to((100.0, 0.0));
path.close_path();
path
}
pub(super) fn doubleCornerGlyphTrickyBitInMiddle() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((100.0, 500.0));
path.line_to((100.0, 0.0));
path.line_to((400.0, 0.0));
path.line_to((400.0, 500.0));
path.line_to((500.0, 400.0));
path.line_to((0.0, 400.0));
path.line_to((100.0, 500.0));
path.close_path();
path
}
pub(super) fn curveCorner() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((316.0, 437.0));
path.curve_to(
(388.67761, 437.0),
(446.1305580343, 401.4757887467),
(475.0, 344.0),
);
path.line_to((588.0, 407.0));
path.line_to((567.0, 260.0));
path.curve_to((567.0, 414.0), (464.0, 510.0), (316.0, 510.0));
path.line_to((316.0, 437.0));
path.close_path();
path
}
pub(super) fn dotabove_ar() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((58.0, -58.0));
path.curve_to((90.0, -58.0), (116.0, -32.0), (116.0, 0.0));
path.curve_to((116.0, 32.0), (90.0, 58.0), (58.0, 58.0));
path.curve_to((26.0, 58.0), (0.0, 32.0), (0.0, 0.0));
path.curve_to((0.0, -32.0), (26.0, -58.0), (58.0, -58.0));
path.close_path();
path
}
pub(super) fn sofiaSans() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((190.0, 327.0));
path.line_to((199.0, 497.0));
path.line_to((199.0, 488.0));
path.line_to((32.0, 503.0));
path.line_to((190.0, 327.0));
path.close_path();
path
}
pub(super) fn largeCrossing() -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
path.move_to((378.0, 615.0));
path.line_to((344.0, 706.0));
path.line_to((444.0, 660.0));
path.line_to((281.0, 699.0));
path.line_to((378.0, 615.0));
path.close_path();
path
}
}
macro_rules! assert_approx {
($left:expr_2021, $right:expr_2021) => {
assert!(($left - $right).abs() < 1e-4, "{} !~= {}", $left, $right)
};
}
#[test]
fn ctx_test_open() {
let mut path = kurbo::BezPath::new();
path.move_to((10., 10.));
path.line_to((20., 20.));
path.line_to((30., 30.));
path.line_to((40., 40.));
let ctx = CornerErasureCtx::new(&path);
assert!(ctx.possible_corner(0).is_none());
let first = ctx.possible_corner(1).unwrap();
assert_eq!(first.one, Line::new((10., 10.), (20., 20.)).into());
assert_eq!(first.two, Line::new((30., 30.), (40., 40.,)).into());
assert!(ctx.possible_corner(2).is_none());
}
#[test]
fn ctx_test_closed() {
let mut path = kurbo::BezPath::new();
path.move_to((10., 10.));
path.line_to((20., 20.));
path.line_to((30., 30.));
path.quad_to((50., 50.), (100., 100.));
path.line_to((10., 10.));
path.close_path();
let ctx = CornerErasureCtx::new(&path);
assert_eq!(
ctx.wrapping_get_el(1).unwrap(),
PathEl::LineTo((20., 20.).into())
);
assert_eq!(
ctx.wrapping_get_el(2).unwrap(),
PathEl::LineTo((30., 30.).into())
);
assert_eq!(
ctx.wrapping_get_el(3).unwrap(),
PathEl::QuadTo((50., 50.).into(), (100., 100.).into())
);
assert_eq!(
ctx.wrapping_get_el(4).unwrap(),
PathEl::LineTo((10., 10.).into())
);
assert_eq!(
ctx.wrapping_get_el(5).unwrap(),
PathEl::LineTo((20., 20.).into())
);
assert_eq!(
ctx.wrapping_get_el(6).unwrap(),
PathEl::LineTo((30., 30.).into())
);
let candi = ctx.possible_corner(0).unwrap();
assert_eq!(candi.line(), Line::new((10., 10.), (20., 20.)));
assert_eq!(candi.one.start(), (100., 100.).into());
assert_eq!(candi.two.end(), (30., 30.).into());
let candi = ctx.possible_corner(1).unwrap();
assert_eq!(candi.line(), Line::new((20., 20.), (30., 30.)));
assert_eq!(candi.one.start(), (10., 10.).into());
assert_eq!(candi.two.end(), (100., 100.).into());
assert!(ctx.possible_corner(2).is_none());
let candi = ctx.possible_corner(3).unwrap();
assert_eq!(candi.line(), Line::new((100., 100.), (10., 10.)));
assert_eq!(candi.one.start(), (30., 30.).into());
assert_eq!(candi.two.end(), (20., 20.).into());
assert!(ctx.possible_corner(4).is_none());
}
#[test]
fn test_empty_glyph() {
let glyph = python_test_glyphs::space();
let reveresed = glyph.reverse_subpaths();
for g in [glyph, reveresed] {
assert!(erase_open_corners(&g).is_none());
}
}
#[test]
fn test_corner_glyph() {
let glyph = python_test_glyphs::hasCornerGlyph();
let after = erase_open_corners(&glyph).unwrap();
let new_pt = after.segments().nth(1).unwrap().end();
assert_approx!(new_pt.x, 114.5417);
assert_approx!(new_pt.y, 191.2080);
}
#[test]
fn test_curve_curve_glyph() {
let glyph = python_test_glyphs::curvyCornerGlyph();
let after = erase_open_corners(&glyph);
let new_pt = after.unwrap().segments().next().unwrap().start();
assert_approx!(new_pt.x, 406.4859);
assert_approx!(new_pt.y, 104.5666);
}
#[test]
fn test_double_corner_glyph() {
let glyph = python_test_glyphs::doubleCornerGlyph();
let after = erase_open_corners(&glyph).unwrap();
let &[
PathEl::MoveTo(one),
PathEl::LineTo(two),
PathEl::LineTo(tre),
PathEl::LineTo(four),
PathEl::LineTo(end),
PathEl::ClosePath,
] = after.elements()
else {
panic!("wrong path elements: {:?}", after.elements())
};
assert_eq!(
[one, two, tre, four],
[
Point::new(100., 0.,),
Point::new(400., 0.,),
Point::new(400., 400.,),
Point::new(100., 400.,)
]
);
assert_eq!(one, end);
assert!(erase_open_corners(&glyph.reverse_subpaths()).is_none());
}
#[test]
fn test_double_corner_glyph_wrap() {
let glyph = python_test_glyphs::doubleCornerGlyphTrickyBitInMiddle();
let after = erase_open_corners(&glyph).unwrap();
let &[
PathEl::MoveTo(one),
PathEl::LineTo(two),
PathEl::LineTo(tre),
PathEl::LineTo(four),
PathEl::LineTo(end),
PathEl::ClosePath,
] = after.elements()
else {
panic!("wrong path elements: {:?}", after.elements())
};
assert_eq!(
[one, two, tre, four],
[
Point::new(100., 400.,),
Point::new(100., 0.,),
Point::new(400., 0.,),
Point::new(400., 400.,),
]
);
assert_eq!(one, end);
}
#[test]
fn test_curve_corner() {
let glyph = python_test_glyphs::curveCorner();
let after = erase_open_corners(&glyph).unwrap();
let PathEl::CurveTo(pt, _, _) = after.elements()[3] else {
panic!("nope")
};
assert_approx!(pt.x, 501.81019);
assert_approx!(pt.y, 462.5782);
assert!(erase_open_corners(&glyph.reverse_subpaths()).is_none());
}
#[test]
fn test_circle_no_overlap() {
let glyph = python_test_glyphs::dotabove_ar();
assert!(erase_open_corners(&glyph).is_none())
}
#[test]
fn test_self_loop() {
let glyph = python_test_glyphs::sofiaSans();
let after = erase_open_corners(&glyph).unwrap();
assert!(after.elements().len() == glyph.elements().len() - 1);
}
#[test]
fn large_crossing() {
let glyph = python_test_glyphs::largeCrossing();
let after = erase_open_corners(&glyph).unwrap();
assert!(after.elements().len() == glyph.elements().len() - 1);
}
#[test]
fn kurbo_411() {
let candidate = PossibleCorner {
first_el_idx: 0,
one: CubicBez::new(
(452.0, 240.0),
(462.667, 78.667),
(480.667, -146.333),
(506.0, -435.0),
)
.into(),
two: Line::new((385.0, 146.0), (438., 243.)).into(),
};
assert!(candidate.intersection().is_none());
}
#[test]
fn joan_four_sc_ss10() {
let mut path = BezPath::new();
path.move_to((332.0, 155.0));
path.line_to((317.0, 184.0));
path.line_to((509.0, 184.0)); path.line_to((493.0, 168.0));
path.line_to((493.0, 412.0)); path.line_to((514.0, 405.0));
path.line_to((332.0, 155.0)); path.close_path();
let after = erase_open_corners(&path).unwrap();
let pts = after
.segments()
.map(|seg| seg.start().ot_round())
.collect::<Vec<_>>();
assert_eq!(pts, [(353, 184), (493, 184), (493, 376)]);
}
#[test]
fn closed_curved_triangle() {
let mut path = BezPath::new();
path.move_to((332.0, 155.0));
path.line_to((317.0, 184.0));
path.curve_to((317., 202.), (509., 206.), (509.0, 184.0)); path.line_to((493.0, 168.0));
path.curve_to((470., 168.), (501., 402.), (493.0, 412.0)); path.line_to((514.0, 405.0));
path.curve_to((505., 405.), (332., 238.), (332.0, 155.0)); path.close_path();
let after = erase_open_corners(&path).unwrap();
let pts = after
.segments()
.map(|seg| seg.start().ot_round())
.collect::<Vec<_>>();
assert_eq!(pts, [(341, 194), (485, 195), (494, 389)]);
}
#[test]
fn only_two_segments() {
let mut path = BezPath::new();
path.move_to((10., 10.));
path.line_to((13., 10.));
path.curve_to((11., 8.), (11., 8.), (10., 10.));
path.close_path();
assert!(erase_open_corners(&path).is_none());
}
#[test]
fn seg_seg_intersect_order() {
let _ = env_logger::builder().is_test(true).try_init();
let seg1 = PathSeg::Cubic(CubicBez::new(
(21.0, 34.0),
(21.0, 33.0),
(21.0, 33.0),
(22.0, 33.0),
));
let seg2 = Line::new((22.0, 32.0), (21.0, 34.0));
let raw_intersections = seg1.intersect_line(seg2);
assert_eq!(raw_intersections.len(), 2);
let one_intersection = seg_seg_intersection(seg1, seg2.into()).unwrap();
assert_eq!(
one_intersection.t0,
raw_intersections
.iter()
.min_by_key(|hit| OrderedFloat(hit.segment_t))
.unwrap()
.segment_t
);
let reverse_intersection = seg_seg_intersection(seg2.into(), seg1).unwrap();
assert_eq!(
reverse_intersection.t0,
raw_intersections
.iter()
.min_by_key(|hit| OrderedFloat(hit.line_t))
.unwrap()
.line_t
)
}
#[test]
fn curve_curve_intersect_order() {
let _ = env_logger::builder().is_test(true).try_init();
let seg1 = CubicBez::new(
(336.0, 150.0),
(340.0, 151.0),
(341.0, 151.0),
(339.0, 152.0),
)
.into();
let seg2 = CubicBez::new(
(340.0, 152.0),
(340.0, 151.0),
(338.0, 149.0),
(335.0, 148.0),
)
.into();
let hit = curve_curve_intersection_py(seg1, seg2).unwrap();
assert_eq!(hit.t1, 0.29296875);
}
#[test]
fn corner_with_t() {
let _ = env_logger::builder().is_test(true).try_init();
let mut path = BezPath::new();
path.move_to((11.0, 4.0));
path.line_to((17.0, 34.0));
path.line_to((8.0, 29.0));
path.line_to((7.0, 27.0));
path.curve_to((7.0, 27.0), (6.0, 25.0), (6.0, 24.0));
path.line_to((9.0, 25.0));
path.line_to((7.0, 24.0));
path.line_to((1.0, 24.0));
path.line_to((11.0, 4.0));
path.close_path();
assert!(erase_open_corners(&path).is_none());
}
#[test]
fn corner_exactly_on_line() {
let mut path = BezPath::new();
path.move_to((339.0, 141.0));
path.line_to((354.0, 0.));
path.line_to((328.0, 0.));
path.line_to((328.0, 208.));
path.line_to((384.0, 208.));
path.curve_to((364.91, 186.86), (346.24, 168.53), (328.0, 153.0));
path.line_to((339.0, 141.0));
path.close_path();
assert!(erase_open_corners(&path).is_none());
}
#[test]
fn same_sidedness() {
let origin = Point { x: 1.0, y: 1.0 };
assert!(
origin.points_are_on_same_side(
origin + Vec2::new(1.0, 1.0),
origin + Vec2::new(1.0, -1.0)
)
);
assert!(origin.points_are_on_same_side(
origin + Vec2::new(-1.0, 1.0),
origin + Vec2::new(-1.0, -1.0)
));
assert!(
origin.points_are_on_same_side(
origin + Vec2::new(-1.0, 1.0),
origin + Vec2::new(-1.0, 1.0)
)
);
assert!(origin.points_are_on_same_side(
origin + Vec2::new(-1.0, -1.0),
origin + Vec2::new(1.0, -1.0)
));
assert!(
!origin.points_are_on_same_side(
origin + Vec2::new(-1.0, 1.0),
origin + Vec2::new(1.0, -1.0)
)
);
assert!(
!origin.points_are_on_same_side(
origin + Vec2::new(0.0, -1.0),
origin + Vec2::new(0.0, 1.0)
)
);
assert!(
!origin.points_are_on_same_side(
origin + Vec2::new(-1.0, 0.0),
origin + Vec2::new(1.0, 0.0)
)
);
}
}