Files
vectorcraft/crates/geom/src/corners.rs
T

558 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Live Corners on any path: the anchors where two sides, straight or curved, meet at an angle,
//! and the path with those corners cut round, inverted round or chamfered (a curved side trimmed
//! to what the cut leaves of it). Rectangles cut theirs the same way.
use kurbo::{CubicBez, ParamCurve, ParamCurveArclen, ParamCurveDeriv, Point, Vec2};
use crate::path::{Anchor, AnchorKind, PathData, SubPath};
use crate::shapes::{CornerKind, KAPPA};
/// Lengths and radii at or below this are none at all.
const EPS: f64 = 1e-9;
/// Sides closer than this to straight on (or folded back), as the sine of their angle, make no corner.
const MIN_SINE: f64 = 1e-6;
/// How closely arc lengths along curved sides are measured (points).
const ARCLEN_ACCURACY: f64 = 1e-6;
/// A side of a corner: the segment from the corner's anchor to its neighbour, as a cubic leaving
/// the corner (a straight side has its handles at its ends).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Side {
pub curve: CubicBez,
}
impl Side {
/// Is the side straight, its handles on its ends?
fn straight(&self) -> bool {
self.curve.p1.distance(self.curve.p0) <= EPS && self.curve.p2.distance(self.curve.p3) <= EPS
}
/// The unit direction the side leaves the corner in: towards its first control point not on
/// the corner. `None` when the side has no length.
fn direction(&self) -> Option<Vec2> {
let c = self.curve;
let d = [c.p1, c.p2, c.p3].into_iter().map(|q| q - c.p0).find(|d| d.hypot() > EPS)?;
let d = d / d.hypot();
d.x.is_finite().then_some(d)
}
/// The side's length.
fn length(&self) -> f64 {
if self.straight() {
self.curve.p0.distance(self.curve.p3)
} else {
crate::path::measured_arclen(&kurbo::PathSeg::Cubic(self.curve), ARCLEN_ACCURACY)
}
}
/// The parameter `s` along the side from the corner, the point there and the direction the
/// side runs on in (away from the corner).
fn at_length(&self, s: f64, u: Vec2) -> (f64, Point, Vec2) {
let c = self.curve;
if self.straight() {
let len = c.p0.distance(c.p3);
let t = if len > EPS { (s / len).clamp(0.0, 1.0) } else { 0.0 };
return (t, c.p0 + u * s, u);
}
let t = c.inv_arclen(s, ARCLEN_ACCURACY).clamp(0.0, 1.0);
let d = c.deriv().eval(t).to_vec2();
let dir = if d.hypot() > EPS { d / d.hypot() } else { u };
(t, c.eval(t), dir)
}
}
/// A corner Live Corners can cut: an anchor that isn't smooth, where two sides (straight or
/// curved) meet at an angle (#1144).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Corner {
/// The anchor's index in its path, counting the anchors of every subpath in order.
pub index: usize,
/// Where the sides meet.
pub at: Point,
/// Unit directions the sides leave the corner in: towards the previous anchor, then towards
/// the next one.
pub u: Vec2,
pub v: Vec2,
/// The sides' lengths (along curves), in the same order.
pub lu: f64,
pub lv: f64,
/// The sides themselves, leaving the corner, in the same order.
pub side_u: Side,
pub side_v: Side,
/// The anchor indices (in the path) of the anchors at the other ends of the sides, in the
/// same order.
pub before: usize,
pub after: usize,
}
impl Corner {
/// The corner of anchor `i` of `sp`, whose index in the path is `index`; `None` when the
/// anchor is an end of an open subpath or is smooth, when a side has no length, or when the
/// sides leave it straight on.
fn of(sp: &SubPath, i: usize, index: usize) -> Option<Self> {
let n = sp.anchors.len();
let (prev_i, next_i) = if sp.closed { ((i + n).checked_sub(1)? % n, (i + 1) % n) } else { (i.checked_sub(1)?, i + 1) };
let (a, prev, next) = (sp.anchors.get(i)?, sp.anchors.get(prev_i)?, sp.anchors.get(next_i)?);
let first = index.checked_sub(i)?;
let (before, after) = (first + prev_i, first + next_i);
if a.kind == AnchorKind::Smooth {
return None;
}
let side_u = Side { curve: CubicBez::new(a.p, a.h_in, prev.h_out, prev.p) };
let side_v = Side { curve: CubicBez::new(a.p, a.h_out, next.h_in, next.p) };
let (u, v) = (side_u.direction()?, side_v.direction()?);
let (lu, lv) = (side_u.length(), side_v.length());
if !(lu > EPS && lv > EPS && lu.is_finite() && lv.is_finite()) {
return None;
}
let c = Self { index, at: a.p, u, v, lu, lv, side_u, side_v, before, after };
(c.sin() > MIN_SINE).then_some(c)
}
/// The cosine of the angle between the sides.
pub fn cos(&self) -> f64 {
self.u.dot(self.v)
}
/// The sine of the angle between the sides.
pub fn sin(&self) -> f64 {
self.u.cross(self.v).abs()
}
/// The angle between the sides, in degrees (90 at a rectangle's corners).
pub fn angle_deg(&self) -> f64 {
self.sin().atan2(self.cos()).to_degrees()
}
/// How far from the corner a round cut of radius `r` meets each side: r / tan(angle / 2).
pub fn setback(&self, r: f64) -> f64 {
r * (1.0 + self.cos()) / self.sin()
}
/// The radius of a round cut that meets each side `s` from the corner (the inverse of
/// [`Corner::setback`]).
pub fn radius_at_setback(&self, s: f64) -> f64 {
s * self.sin() / (1.0 + self.cos())
}
/// The largest radius the corner draws whatever its neighbours do: its cut then reaches
/// halfway along the shorter side, so neighbouring corners at their largest meet without
/// overlapping (half the shorter side at a rectangle's corners).
pub fn max_radius(&self) -> f64 {
self.radius_at_setback(self.lu.min(self.lv) / 2.0)
}
/// The largest radius the corner draws when the cuts of the corners at the other ends of its
/// sides reach `before` along side u and `after` along side v: its cut reaches halfway along
/// a side, or further, up to where the other end's cut begins, so all of a side whose other
/// end isn't cut (#2332).
pub fn max_radius_beside(&self, before: f64, after: f64) -> f64 {
let room = |len: f64, other: f64| (len - other.max(0.0)).max(len / 2.0);
self.radius_at_setback(room(self.lu, before).min(room(self.lv, after)))
}
/// The largest radius the corner draws when it and the corners for which `edited` holds are
/// set to one radius together, the other corners of `corners` keeping their `radii` (by
/// anchor index). A side shared with an edited corner takes both cuts as far as they fit
/// side by side, or halfway each; on a side to any other corner the cut reaches up to where
/// that corner's cut begins ([`Corner::max_radius_beside`]).
pub fn max_shared_radius(&self, corners: &[Corner], radii: &[f64], edited: impl Fn(usize) -> bool) -> f64 {
let side = |len: f64, other: usize| match find(corners, other) {
Some(c) if other == self.index || edited(other) => {
let both = self.setback(1.0) + c.setback(1.0);
let apart = if both > EPS { len / both } else { f64::INFINITY };
apart.max(self.radius_at_setback(len / 2.0))
}
_ => self.radius_at_setback((len - requested_setback(corners, radii, other)).max(len / 2.0)),
};
side(self.lu, self.before).min(side(self.lv, self.after))
}
/// The point `k` along the bisector, measured in (u + v): the centre of a round cut of radius
/// `r` is at `k = r / sin`.
pub fn on_bisector(&self, k: f64) -> Point {
self.at + (self.u + self.v) * k
}
/// How much the radius grows when the centre of the round cut moves by `d`: `d`'s part along
/// the bisector, in radius units.
pub fn radius_change(&self, d: Vec2) -> f64 {
d.dot(self.u + self.v) * self.sin() / (2.0 + 2.0 * self.cos())
}
/// The cut of radius `radius` (> 0, fitted): where it meets each side, `setback` along it,
/// and the cut's own curve between them, its handles leaving the sides smoothly.
fn cut(&self, radius: f64, kind: CornerKind) -> Cut {
let s = self.setback(radius);
let (tu, start, du) = self.side_u.at_length(s, self.u);
let (tv, end, dv) = self.side_v.at_length(s, self.v);
let cos = du.dot(dv);
let (h_out, h_in) = match kind {
// An arc tangent to both sides, turning by 180° less the angle between them there.
CornerKind::Round => {
let (hu, hv) = self.round_handles(radius, (start, du), (end, dv));
(start - du * hu, end - dv * hv)
}
// An arc centred on the corner through both ends, spanning the angle.
CornerKind::InvertedRound => {
let h = s * arc_handle(cos);
(start + square_to(du, dv) * h, end + square_to(dv, du) * h)
}
CornerKind::Chamfer => (start, end),
};
Cut { tu, tv, start, h_out, h_in, end }
}
}
impl Corner {
/// The handle lengths of a round cut of `radius` between `start` and `end` (each a point and
/// the direction its side runs on in there). Between straight sides they are the arc's. A
/// curved side has turned by the time the cut meets it, so the arc's handles would reach past
/// where the tangents at the cut's ends cross and the cut would stick out of the corner
/// (#1512): there each handle is the share an arc takes of its tangent, up to that crossing
/// (a third of the way to the other end where the tangents don't cross between them).
fn round_handles(&self, radius: f64, (start, du): (Point, Vec2), (end, dv): (Point, Vec2)) -> (f64, f64) {
let cos = du.dot(dv);
let arc = arc_handle(-cos);
if self.side_u.straight() && self.side_v.straight() {
return (radius * arc, radius * arc);
}
// start − du·a = end − dv·b is where the tangents cross.
let (cross, w) = (du.cross(dv), start - end);
let (a, b) = (w.cross(dv) / cross, w.cross(du) / cross);
if cross.abs() > MIN_SINE && a > EPS && b > EPS && a.is_finite() && b.is_finite() {
// An arc's handle over its tangent's length, radius · tan(turn / 2).
let share = arc * (1.0 - cos) / cross.abs();
(a * share, b * share)
} else {
let third = start.distance(end) / 3.0;
(third, third)
}
}
}
/// A corner cut: the parameters where it meets its sides (from the corner), its two new anchors
/// and the handles between them.
#[derive(Clone, Copy, Debug)]
struct Cut {
tu: f64,
tv: f64,
start: Point,
h_out: Point,
h_in: Point,
end: Point,
}
/// The unit direction square to unit `a`, on `b`'s side.
fn square_to(a: Vec2, b: Vec2) -> Vec2 {
let w = b - a * a.dot(b);
let l = w.hypot();
if l > EPS { w / l } else { Vec2::ZERO }
}
/// The handle length, per unit of radius, of a cubic drawing a circular arc that spans an angle
/// whose cosine is `cos`: 4/3 · tan(angle / 4) ([`KAPPA`] for a quarter circle).
fn arc_handle(cos: f64) -> f64 {
if cos.abs() < 1e-12 {
return KAPPA;
}
let cos = cos.clamp(-1.0, 1.0);
let (sin_half, cos_half) = (((1.0 - cos) / 2.0).max(0.0).sqrt(), ((1.0 + cos) / 2.0).max(0.0).sqrt());
4.0 / 3.0 * sin_half / (1.0 + cos_half)
}
/// The corners of `path` Live Corners can cut, in anchor order.
pub fn path_corners(path: &PathData) -> Vec<Corner> {
let mut out = vec![];
let mut first = 0;
for sp in &path.subpaths {
out.extend((0..sp.anchors.len()).filter_map(|i| Corner::of(sp, i, first + i)));
first += sp.anchors.len();
}
out
}
/// The corner at anchor `index` among `corners` (in anchor order).
fn find(corners: &[Corner], index: usize) -> Option<&Corner> {
corners.binary_search_by_key(&index, |c| c.index).ok().and_then(|i| corners.get(i))
}
/// How far along each side the cut of the corner at anchor `index` asks to reach, with the radius
/// in `radii` there (0 for an anchor that is no corner, or a radius that is negative or not a
/// number).
fn requested_setback(corners: &[Corner], radii: &[f64], index: usize) -> f64 {
find(corners, index).map_or(0.0, |c| c.setback(radii.get(index).copied().unwrap_or(0.0).max(0.0)))
}
/// The radius each of `corners` (in anchor order) is drawn with, given the radii by anchor index:
/// its own, no larger than leaves room for the cuts of its neighbours ([`Corner::max_radius_beside`]
/// with the setbacks their radii ask for), so a corner whose neighbours are square rounds until
/// its cut reaches them (#2332) and two neighbours never cut past each other.
pub fn fitted_radii(corners: &[Corner], radii: &[f64]) -> Vec<f64> {
corners
.iter()
.map(|c| {
let own = radii.get(c.index).copied().unwrap_or(0.0).max(0.0);
let max = c.max_radius_beside(requested_setback(corners, radii, c.before), requested_setback(corners, radii, c.after));
own.min(max)
})
.collect()
}
/// `path` with each of its corners ([`path_corners`]) cut by the radius in `radii` and the kind in
/// `kinds` at its anchor index (missing: 0 and round), each radius no larger than fits
/// ([`fitted_radii`]). A cut corner becomes two anchors, and a curved side between corners is
/// trimmed to what is left of it; a closed subpath whose first corner is cut starts where that
/// cut ends, so the cut closes it. Also returns, per subpath, the anchor index in `path` each
/// anchor comes from.
pub fn cut_corners(path: &PathData, radii: &[f64], kinds: &[CornerKind]) -> (PathData, Vec<Vec<usize>>) {
let corners = path_corners(path);
let fitted = fitted_radii(&corners, radii);
let mut corners = corners.iter().zip(fitted).peekable();
let (mut subpaths, mut sources) = (Vec::with_capacity(path.subpaths.len()), Vec::with_capacity(path.subpaths.len()));
let mut first = 0;
for sp in &path.subpaths {
let n = sp.anchors.len();
// The cut of each anchor, if any.
let cuts: Vec<Option<Cut>> = (0..n)
.map(|i| {
let index = first + i;
let (c, radius) = corners.next_if(|(c, _)| c.index == index)?;
(radius > EPS).then(|| c.cut(radius, kinds.get(index).copied().unwrap_or_default()))
})
.collect();
// Each segment from anchor j, trimmed to the part the cuts at its ends leave: its handles
// (out of its first anchor, into its last), `None` when it is untouched.
let trimmed: Vec<Option<(Point, Point)>> = (0..n)
.map(|j| {
let k = (j + 1) % n;
let (a, b) = (sp.anchors.get(j)?, sp.anchors.get(k)?);
let (from, to) = (cuts.get(j).copied().flatten(), cuts.get(k).copied().flatten());
if (from.is_none() && to.is_none()) || (k == 0 && !sp.closed) {
return None;
}
// A straight segment stays straight, its handles on its ends.
if !a.has_out() && !b.has_in() {
return Some((from.map_or(a.p, |c| c.end), to.map_or(b.p, |c| c.start)));
}
let (s0, s1) = (from.map_or(0.0, |c| c.tv), to.map_or(1.0, |c| 1.0 - c.tu));
let sub = CubicBez::new(a.p, a.h_out, b.h_in, b.p).subsegment(s0..s1.max(s0));
Some((sub.p1, sub.p2))
})
.collect();
let (mut anchors, mut from) = (Vec::with_capacity(n), Vec::with_capacity(n));
let mut closing = None;
for (i, a) in sp.anchors.iter().enumerate() {
let index = first + i;
let h_in = trimmed.get((i + n - 1) % n).copied().flatten().map_or(a.h_in, |(_, h)| h);
let h_out = trimmed.get(i).copied().flatten().map_or(a.h_out, |(h, _)| h);
match cuts.get(i).copied().flatten() {
Some(c) => {
let start = Anchor { p: c.start, h_in, h_out: c.h_out, kind: AnchorKind::Corner };
let end = Anchor { p: c.end, h_in: c.h_in, h_out, kind: AnchorKind::Corner };
if i == 0 && sp.closed {
closing = Some(start);
} else {
anchors.push(start);
from.push(index);
}
anchors.push(end);
from.push(index);
}
None => {
anchors.push(Anchor { h_in, h_out, ..*a });
from.push(index);
}
}
}
if let Some(a) = closing {
anchors.push(a);
from.push(first);
}
first += n;
subpaths.push(SubPath::new(anchors, sp.closed));
sources.push(from);
}
(PathData::new(subpaths), sources)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shapes;
use kurbo::Rect;
fn near(a: Point, b: Point) -> bool {
a.distance(b) < 1e-9
}
/// A five-pointed star's tips are acute corners, its inner corners obtuse: all ten cut, each
/// a circle of the radius tangent to both sides.
#[test]
fn a_star_has_ten_corners_cut_as_circles() {
let star = shapes::star(Point::new(100.0, 100.0), 50.0, 25.0, 5, 0.0);
let corners = path_corners(&star);
assert_eq!(corners.len(), 10);
let angles: Vec<_> = corners.iter().map(|c| c.angle_deg()).collect();
assert!(angles.iter().step_by(2).all(|a| *a < 90.0), "tips are acute: {angles:?}");
assert!(angles.iter().skip(1).step_by(2).all(|a| *a > 90.0), "inner corners are obtuse: {angles:?}");
let (cut, from) = cut_corners(&star, &[4.0; 10], &[]);
assert_eq!(cut.anchor_count(), 20);
assert_eq!(from, [vec![0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 0]]);
for c in &corners {
// Both ends of the cut lie `setback` along the sides, as far from the arc's centre as
// the radius, and that centre is `radius` from either side.
let t = c.setback(4.0);
let o = c.on_bisector(4.0 / c.sin());
for p in [c.at + c.u * t, c.at + c.v * t] {
assert!((p.distance(o) - 4.0).abs() < 1e-9);
assert!(cut.anchors().any(|(_, _, a)| near(a.p, p)), "{p:?} is an anchor");
}
}
}
#[test]
fn radii_stop_at_half_the_shorter_side_per_corner() {
let tri = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(100.0, 0.0), Point::new(0.0, 40.0)], true));
let corners = path_corners(&tri);
assert_eq!(corners.len(), 3);
for c in &corners {
let max = c.max_radius();
assert!((c.setback(max) - c.lu.min(c.lv) / 2.0).abs() < 1e-9);
}
// The right angle at (0, 0) takes up to 20, half its shorter side.
assert!((corners[0].max_radius() - 20.0).abs() < 1e-9);
}
#[test]
fn ends_curves_smooth_and_straight_anchors_are_no_corners() {
// An open zig-zag: its ends aren't corners, nor is the anchor its sides run straight on through.
let open = PathData::single(SubPath::polyline(
&[Point::new(0.0, 0.0), Point::new(50.0, 0.0), Point::new(100.0, 0.0), Point::new(100.0, 50.0), Point::new(150.0, 80.0)],
false,
));
assert_eq!(path_corners(&open).iter().map(|c| c.index).collect::<Vec<_>>(), [2, 3]);
// Smooth anchors aren't corners.
assert!(path_corners(&shapes::ellipse(Rect::new(0.0, 0.0, 10.0, 10.0))).is_empty());
// A second subpath counts on from the first one's anchors.
let two = PathData::new(vec![
SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(0.0, 10.0)], true),
SubPath::polyline(&[Point::new(50.0, 0.0), Point::new(60.0, 0.0), Point::new(50.0, 10.0)], true),
]);
let (cut, from) = cut_corners(&two, &[0.0, 0.0, 0.0, 1.0], &[]);
assert_eq!(from, [vec![0, 1, 2], vec![3, 4, 5, 3]]);
assert_eq!(cut.anchor_count(), 7);
}
/// The square `0..100` with its right side bulging out as a curve (#1144).
fn bulging_square() -> PathData {
let mut sp = SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(100.0, 0.0), Point::new(100.0, 100.0), Point::new(0.0, 100.0)], true);
sp.anchors[1].h_out = Point::new(140.0, 30.0);
sp.anchors[2].h_in = Point::new(140.0, 70.0);
PathData::single(sp)
}
/// Corners where a curve meets a line are cut too: the cut ends on the path, leaves both
/// sides smoothly, and what is left of the curve still follows it (#1144).
#[test]
fn corners_next_to_curves_are_cut_along_them() {
let path = bulging_square();
let corners = path_corners(&path);
assert_eq!(corners.iter().map(|c| c.index).collect::<Vec<_>>(), [0, 1, 2, 3]);
let side = kurbo::CubicBez::new(Point::new(100.0, 0.0), Point::new(140.0, 30.0), Point::new(140.0, 70.0), Point::new(100.0, 100.0));
// The curved side is longer than its chord, and limits its corners by its length.
assert!(corners[1].lv > 100.0 + 1.0);
let (cut, from) = cut_corners(&path, &[10.0; 4], &[]);
assert_eq!(from, [vec![0, 1, 1, 2, 2, 3, 3, 0]]);
let sp = &cut.subpaths[0];
let on_side = |p: Point| (0..=1000).map(|i| side.eval(f64::from(i) / 1000.0).distance(p)).fold(f64::MAX, f64::min);
// Corner 1's cut ends on the curve and corner 2's starts on it, and the trimmed curve
// between them stays on the original one.
let (a, b) = (sp.anchors[2], sp.anchors[3]);
assert!(on_side(a.p) < 0.1 && on_side(b.p) < 0.1);
let trimmed = kurbo::CubicBez::new(a.p, a.h_out, b.h_in, b.p);
for t in [0.25, 0.5, 0.75] {
assert!(on_side(trimmed.eval(t)) < 0.1);
}
// Every cut leaves its sides smoothly: each anchor's handles are in line.
for (i, x) in sp.anchors.iter().enumerate() {
let (into, out) = if x.has_in() && x.has_out() {
(x.p - x.h_in, x.h_out - x.p)
} else if x.has_out() {
(x.p - sp.anchors[(i + sp.anchors.len() - 1) % sp.anchors.len()].p, x.h_out - x.p)
} else {
(x.h_in - x.p, x.p - sp.anchors[(i + 1) % sp.anchors.len()].p)
};
assert!(into.normalize().cross(out.normalize()).abs() < 1e-6 && into.dot(out) > 0.0, "anchor {i}");
}
}
/// A round cut next to a curve stays inside its corner, however large (#1512): its handles
/// stop short of where the tangents at its ends cross, so it doesn't stick out past the sides.
#[test]
fn a_round_cut_next_to_a_curve_stays_inside_its_corner() {
// A square whose top side arches up, meeting the upright sides at an angle.
let mut sp =
SubPath::polyline(&[Point::new(100.0, 150.0), Point::new(300.0, 150.0), Point::new(300.0, 300.0), Point::new(100.0, 300.0)], true);
sp.anchors[0].h_out = Point::new(120.0, 20.0);
sp.anchors[1].h_in = Point::new(280.0, 20.0);
let path = PathData::single(sp);
let bounds = path.bounds().unwrap().inflate(1e-6, 1e-6);
for radius in [10.0, 60.0, 200.0, 1e6] {
let (cut, _) = cut_corners(&path, &[radius, radius], &[]);
let b = cut.bounds().unwrap();
assert!(bounds.union(b) == bounds, "radius {radius}: {b:?} is outside {bounds:?}");
// Each cut's handles end before its tangents cross: the cut turns one way only.
let a = &cut.subpaths[0].anchors;
for (from, to) in [(a[a.len() - 1], a[0]), (a[1], a[2])] {
let (t0, t1, chord) = (from.h_out - from.p, to.p - to.h_in, to.p - from.p);
assert!(t0.cross(chord) * chord.cross(t1) > 0.0 && t0.cross(t1) * t0.cross(chord) > 0.0, "radius {radius}: {from:?} {to:?}");
}
}
}
/// Inverted round and chamfer cuts end where the round one does, at any angle.
#[test]
fn kinds_share_their_ends() {
let star = shapes::star(Point::new(0.0, 0.0), 50.0, 25.0, 5, 0.0);
let ends = |k: CornerKind| -> Vec<Point> { cut_corners(&star, &[3.0; 10], &[k; 10]).0.anchors().map(|(_, _, a)| a.p).collect() };
let round = ends(CornerKind::Round);
assert_eq!(ends(CornerKind::InvertedRound), round);
assert_eq!(ends(CornerKind::Chamfer), round);
// The inverted arc stays on the circle about the corner.
let c = path_corners(&star)[0];
let cut = c.cut(3.0, CornerKind::InvertedRound);
let bez = kurbo::CubicBez::new(cut.start, cut.h_out, cut.h_in, cut.end);
let r = c.setback(3.0);
for t in [0.25, 0.5, 0.75] {
use kurbo::ParamCurve;
assert!((bez.eval(t).distance(c.at) - r).abs() < r * 1e-3);
}
}
/// #2332: a corner rounds until its cut reaches its neighbours' (all of a side whose other
/// end is square); neighbours that both ask for more than half a side meet halfway.
#[test]
fn a_corner_rounds_up_to_its_neighbours_cuts() {
let square = shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0));
let corners = path_corners(&square);
let drawn = |radii: [f64; 4]| fitted_radii(&corners, &radii);
assert_eq!(drawn([0.0, 500.0, 0.0, 0.0]), [0.0, 100.0, 0.0, 0.0]);
assert_eq!(drawn([30.0, 500.0, 0.0, 0.0]), [30.0, 70.0, 0.0, 0.0]);
assert_eq!(drawn([80.0, 500.0, 0.0, 0.0]), [50.0, 50.0, 0.0, 0.0]);
assert_eq!(drawn([500.0; 4]), [50.0; 4]);
assert_eq!(drawn([f64::NAN, 120.0, -1.0, 0.0]), [0.0, 100.0, 0.0, 0.0]);
// The lone cut is a quarter circle from one neighbour to the other.
let (cut, _) = cut_corners(&square, &[0.0, 500.0, 0.0, 0.0], &[]);
let ends: Vec<Point> = cut.anchors().map(|(_, _, a)| a.p).collect();
assert_eq!(ends, [Point::new(0.0, 0.0), Point::new(0.0, 0.0), Point::new(100.0, 100.0), Point::new(100.0, 100.0), Point::new(0.0, 100.0)]);
// Dragged together with its neighbour, a corner meets it halfway; beside a square one,
// it goes all the way.
let tr = corners[1];
assert!((tr.max_shared_radius(&corners, &[0.0; 4], |k| k == 0) - 50.0).abs() < 1e-9);
assert!((tr.max_shared_radius(&corners, &[0.0; 4], |_| false) - 100.0).abs() < 1e-9);
assert!((tr.max_shared_radius(&corners, &[0.0, 0.0, 40.0, 0.0], |_| false) - 60.0).abs() < 1e-9);
}
}