use arrayvec::ArrayVec;
use kurbo::{CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Shape, Vec2};
use polycool::{Cubic, Poly, Quadratic};
use smallvec::SmallVec;
pub mod line;
pub(crate) mod transverse;
#[derive(Clone, Debug, PartialEq)]
struct CurveEntry<T> {
end: f64,
order: T,
}
type CurveOrderEntry = CurveEntry<Order>;
impl CurveOrderEntry {
fn flip(&self) -> Self {
Self {
end: self.end,
order: match self.order {
Order::Right => Order::Left,
Order::Ish => Order::Ish,
Order::Left => Order::Right,
},
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Metrics {
pub entries: Vec<MetricsEntry>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MetricsEntry {
pub end: f64,
pub depth: u32,
}
impl MetricsEntry {
fn new(end: f64, status: MetricsStatus) -> Self {
Self {
end,
depth: status.depth,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
struct MetricsStatus {
depth: u32,
}
impl MetricsStatus {
fn bump(&self) -> Self {
Self {
depth: self.depth + 1,
}
}
}
#[derive(Clone, Debug)]
pub struct CurveOrder {
start: f64,
cmps: SmallVec<[CurveOrderEntry; 3]>,
pub metrics: Metrics,
}
pub struct CurveOrderIter<'a, T> {
next: f64,
iter: std::slice::Iter<'a, CurveEntry<T>>,
}
#[derive(Clone, Copy, Debug)]
pub enum CurveInteraction {
Cross(f64),
Touch(f64),
}
impl<T: Copy> Iterator for CurveOrderIter<'_, T> {
type Item = (f64, f64, T);
fn next(&mut self) -> Option<Self::Item> {
let next_entry = self.iter.next()?;
let start = self.next;
self.next = next_entry.end;
Some((start, next_entry.end, next_entry.order))
}
}
impl CurveOrder {
fn new(start: f64) -> Self {
CurveOrder {
start,
cmps: SmallVec::new(),
metrics: Metrics::default(),
}
}
fn push(&mut self, end: f64, order: Order, status: MetricsStatus) {
self.metrics.entries.push(MetricsEntry::new(end, status));
if let Some(last) = self.cmps.last_mut() {
match (last.order, order) {
(Order::Right, Order::Right)
| (Order::Left, Order::Left)
| (Order::Ish, Order::Ish) => {
debug_assert!(end >= last.end);
last.end = end;
}
(Order::Right, Order::Left) | (Order::Left, Order::Right) => {
debug_assert!(end >= last.end);
self.cmps.push(CurveOrderEntry {
end,
order: Order::Ish,
});
self.cmps.push(CurveOrderEntry { end, order });
}
_ => {
debug_assert!(end >= last.end);
if end > last.end {
self.cmps.push(CurveOrderEntry { end, order });
}
}
}
} else {
self.cmps.push(CurveOrderEntry { end, order });
}
}
pub fn iter(&self) -> CurveOrderIter<'_, Order> {
CurveOrderIter {
next: self.start,
iter: self.cmps.iter(),
}
}
pub fn flip(&self) -> Self {
Self {
start: self.start,
cmps: self.cmps.iter().map(CurveOrderEntry::flip).collect(),
metrics: self.metrics.clone(),
}
}
pub fn next_touch_after(&self, y: f64) -> Option<CurveInteraction> {
let mut iter = self
.iter()
.skip_while(|(_start, end, _order)| end <= &y)
.skip_while(|(_start, _end, order)| *order == Order::Left);
let (y0, y1, order) = iter.next()?;
if order == Order::Right {
return Some(CurveInteraction::Cross(y));
}
let (_, next_y1, next_order) = iter.next()?;
let cross_y = (y0 + y1) / 2.0;
match next_order {
Order::Right => Some(CurveInteraction::Cross(cross_y)),
Order::Left => {
if y < y0 {
Some(CurveInteraction::Touch(cross_y))
} else {
self.next_touch_after(next_y1)
}
}
Order::Ish => {
unreachable!();
}
}
}
pub fn with_y_slop(self, slop: f64) -> CurveOrder {
if slop == 0.0 {
return self;
}
let mut ret = SmallVec::<[CurveOrderEntry; 3]>::new();
let last_end = self.cmps.last().unwrap().end;
if self.start == last_end || self.cmps.len() <= 1 {
return self;
}
for (_start, end, order) in self.iter() {
if order == Order::Ish {
let new_end = (end + slop).min(last_end);
if let Some(last) = ret.last_mut()
&& last.order == Order::Ish
{
last.end = new_end;
} else {
ret.push(CurveOrderEntry {
end: new_end,
order: Order::Ish,
});
}
} else {
let new_end = end - slop;
if new_end > self.start && ret.last().is_none_or(|last| last.end < new_end) {
ret.push(CurveOrderEntry {
end: new_end,
order,
});
}
}
}
if let Some(last) = ret.last_mut()
&& last.end < last_end
{
last.end = last_end;
}
if ret.is_empty() {
ret.push(CurveOrderEntry {
end: last_end,
order: Order::Ish,
});
}
CurveOrder {
start: self.start,
cmps: ret,
metrics: Metrics::default(),
}
}
pub fn check_invariants(&self) {
let mut cmps = self.cmps.iter();
let mut last = cmps.next().unwrap();
for cmp in cmps {
assert!(last.end <= cmp.end);
assert!(last.order != cmp.order);
assert!(last.order == Order::Ish || cmp.order == Order::Ish);
last = cmp;
}
}
pub fn order_at(&self, y: f64) -> Order {
self.iter()
.find(|(_start, end, _order)| end >= &y)
.unwrap()
.2
}
pub fn entry_at(&self, y: f64) -> (f64, f64, Order) {
self.iter().find(|(_start, end, _order)| *end > y).unwrap()
}
pub fn order_after(&self, y: f64) -> Order {
if y == self.cmps.last().unwrap().end {
self.cmps.last().unwrap().order
} else {
self.iter()
.skip_while(|(_start, end, _order)| end <= &y)
.find(|(_start, _end, order)| *order != Order::Ish)
.map_or(Order::Ish, |(_start, _end, order)| order)
}
}
}
pub fn solve_t_for_y(c: CubicBez, y: f64) -> f64 {
debug_assert!(
c.p0.y <= y && y <= c.p3.y && c.p0.y < c.p3.y,
"invalid y ({y}) for curve ({c:?})"
);
if y == c.p0.y {
return 0.0;
}
if y == c.p3.y {
return 1.0;
}
let c3 = c.p3.y - 3.0 * c.p2.y + 3.0 * c.p1.y - c.p0.y;
let c2 = 3.0 * (c.p2.y - 2.0 * c.p1.y + c.p0.y);
let c1 = 3.0 * (c.p1.y - c.p0.y);
let c0 = c.p0.y - y;
let cubic = Cubic::new([c0, c1, c2, c3]);
let eps = 1e-12;
let roots = cubic.roots_between(0.0, 1.0, eps);
if !roots.is_empty() {
return roots[0];
}
debug_assert_eq!(cubic.eval(0.0).signum(), cubic.eval(1.0).signum());
if (y - c.p0.y).abs() <= (y - c.p3.y).abs() {
0.0
} else {
1.0
}
}
pub fn solve_x_for_y(c: CubicBez, y: f64) -> f64 {
c.eval(solve_t_for_y(c, y)).x
}
pub fn y_subsegment(c: CubicBez, y0: f64, y1: f64) -> CubicBez {
debug_assert!(y0 < y1);
debug_assert!(c.p0.y <= y0 && y1 <= c.p3.y);
let t0 = solve_t_for_y(c, y0);
let t1 = solve_t_for_y(c, y1);
let mut ret = c.subsegment(t0..t1);
ret.p0.y = y0;
ret.p3.y = y1;
ret
}
#[derive(Clone, Copy, Debug)]
pub struct QuadraticSigns {
pub smaller_root: f64,
pub bigger_root: f64,
pub initial_sign: f64,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Order {
Right,
Ish,
Left,
}
impl Order {
pub fn flip(self) -> Order {
match self {
Order::Right => Order::Left,
Order::Ish => Order::Ish,
Order::Left => Order::Right,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MaybeOrder {
Right,
Ish,
Left,
Unknown,
}
impl From<Order> for MaybeOrder {
fn from(ord: Order) -> Self {
match ord {
Order::Right => MaybeOrder::Right,
Order::Ish => MaybeOrder::Ish,
Order::Left => MaybeOrder::Left,
}
}
}
impl TryFrom<MaybeOrder> for Order {
type Error = ();
fn try_from(ord: MaybeOrder) -> Result<Self, Self::Error> {
match ord {
MaybeOrder::Right => Ok(Order::Right),
MaybeOrder::Ish => Ok(Order::Ish),
MaybeOrder::Left => Ok(Order::Left),
MaybeOrder::Unknown => Err(()),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Direction {
Increasing,
Decreasing,
}
#[derive(Clone, Copy, Debug)]
enum Bound {
Left,
Right,
}
#[derive(Clone, Debug, Default)]
pub struct ApproxComparison<const CAP: usize> {
entries: ArrayVec<CurveEntry<MaybeOrder>, CAP>,
}
type QuadApproxComparison = ApproxComparison<27>;
struct MergedRangeIter<S, T, I, J> {
a_iter: I,
b_iter: J,
a_cur: Option<(f64, S)>,
b_cur: Option<(f64, T)>,
}
impl<S, T, I, J> MergedRangeIter<S, T, I, J>
where
I: Iterator<Item = (f64, S)>,
J: Iterator<Item = (f64, T)>,
S: Clone,
T: Clone,
{
fn new(mut a: I, mut b: J) -> Self {
Self {
a_cur: a.next(),
b_cur: b.next(),
a_iter: a,
b_iter: b,
}
}
}
impl<S, T, I, J> Iterator for MergedRangeIter<S, T, I, J>
where
I: Iterator<Item = (f64, S)>,
J: Iterator<Item = (f64, T)>,
S: Clone,
T: Clone,
{
type Item = (f64, S, T);
fn next(&mut self) -> Option<Self::Item> {
if let (Some((a, a_payload)), Some((b, b_payload))) =
(self.a_cur.clone(), self.b_cur.clone())
{
let end = a.min(b);
if a == end {
self.a_cur = self.a_iter.next();
}
if b == end {
self.b_cur = self.b_iter.next();
}
Some((end, a_payload, b_payload))
} else {
None
}
}
}
impl<const CAP: usize> ApproxComparison<CAP> {
fn push(&mut self, end: f64, order: MaybeOrder) {
debug_assert!(self.entries.last().is_none_or(|e| e.end < end));
if let Some(e) = self.entries.last_mut()
&& e.order == order
{
e.end = end;
} else {
self.entries.push(CurveEntry { end, order });
}
}
fn merge_from<const A: usize, const B: usize>(
a: &ApproxComparison<A>,
b: &ApproxComparison<B>,
merge: impl Fn(MaybeOrder, MaybeOrder) -> MaybeOrder,
) -> Self {
let mut a_iter = a.entries.iter();
let mut b_iter = b.entries.iter();
let mut ret = Self::default();
let mut a_cur = a_iter.next();
let mut b_cur = b_iter.next();
while let (Some(a), Some(b)) = (a_cur, b_cur) {
let end = a.end.min(b.end);
ret.push(end, merge(a.order, b.order));
if a.end == end {
a_cur = a_iter.next();
}
if b.end == end {
b_cur = b_iter.next();
}
}
ret
}
fn iter(&self, start: f64) -> CurveOrderIter<'_, MaybeOrder> {
CurveOrderIter {
next: start,
iter: self.entries.iter(),
}
}
}
pub(crate) fn quadratic_signs(q: Quadratic) -> (bool, f64, f64) {
let &[c, b, a] = q.coeffs();
let disc = b * b - 4.0 * a * c;
let initially_negative = a < 0.0
|| (a == 0.0 && b != 0.0 && a.signum() == -1.0)
|| (a == 0.0 && b == 0.0 && c < 0.0);
let numerator = if disc.is_finite() {
if disc <= 0.0 {
return (!initially_negative, f64::NEG_INFINITY, f64::NEG_INFINITY);
} else {
-0.5 * (b + disc.sqrt().copysign(b))
}
} else {
let scaled_disc = 1.0 - (c / b / b * a) * 4.0;
if !scaled_disc.is_finite() {
if c * a > 0.0 {
return (!initially_negative, f64::NEG_INFINITY, f64::NEG_INFINITY);
} else {
return (initially_negative, f64::NEG_INFINITY, f64::NEG_INFINITY);
}
}
-0.5 * b * (1.0 + scaled_disc.sqrt())
};
let r1 = numerator / a;
let r2 = c / numerator;
let (r1, r2) = if r1 <= r2 { (r1, r2) } else { (r2, r1) };
(!initially_negative, r1, r2)
}
fn push_quadratic_sign<const CAP: usize, T>(
q: Quadratic,
neg: Order,
pos: Order,
y0: f64,
y1: f64,
out: &mut ArrayVec<CurveEntry<T>, CAP>,
) where
T: PartialEq,
Order: Into<T>,
{
let mut push = |end: f64, order: Order| {
let order = order.into();
debug_assert!(out.last().is_none_or(|e| e.end < end));
if let Some(e) = out.last_mut()
&& e.order == order
{
e.end = end;
} else {
out.push(CurveEntry { end, order });
}
};
let (initially_positive, r1, r2) = quadratic_signs(q);
let (initial_sign, next_sign) = if initially_positive {
(pos, neg)
} else {
(neg, pos)
};
let r1 = r1.min(y1);
let r2 = r2.min(y1);
if r1 > y0 {
push(r1, initial_sign);
}
if r2 > r1 && r2 > y0 {
push(r2, next_sign);
}
if y1 > r2 {
push(y1, initial_sign);
}
}
fn push_two_level_quadratic_signs(
q: Quadratic,
lower: f64,
upper: f64,
y0: f64,
y1: f64,
out: &mut QuadApproxComparison,
) {
debug_assert!(lower < upper);
let mut push = |end: f64, order: MaybeOrder| {
if end > y0
&& out
.entries
.last()
.is_none_or(|last| last.end < y1 && last.end < end)
{
out.push(end.min(y1), order);
}
};
let lower_q = raise_quadratic(q, -lower);
let upper_q = raise_quadratic(q, -upper);
let (lower_initially_positive, r_lower, s_lower) = quadratic_signs(lower_q);
let (upper_initially_positive, r_upper, s_upper) = quadratic_signs(upper_q);
use MaybeOrder::*;
if upper_initially_positive {
debug_assert!(r_upper <= r_lower || r_lower.is_infinite());
debug_assert!(s_lower <= s_upper);
push(r_upper, Left);
push(r_lower, Unknown);
push(s_lower, Right);
push(s_upper, Unknown);
} else {
debug_assert!(r_upper >= r_lower || r_upper.is_infinite());
debug_assert!(s_lower >= s_upper);
push(r_lower, Right);
push(r_upper, Unknown);
push(s_upper, Left);
push(s_lower, Unknown);
}
let final_sign = if upper_initially_positive && lower_initially_positive {
Left
} else if !upper_initially_positive && !lower_initially_positive {
Right
} else {
Unknown
};
push(y1, final_sign);
}
#[derive(Clone, Copy, Debug)]
pub struct QuadraticApprox {
pub q: Quadratic,
pub dmin: f64,
pub dmax: f64,
pub almost_horiz: Option<(Direction, f64, f64)>,
}
fn raise_quadratic(q: Quadratic, c: f64) -> Quadratic {
let &[c_old, b, a] = q.coeffs();
Quadratic::new([c_old + c, b, a])
}
fn shift_quadratic(q: Quadratic, shift: f64) -> Quadratic {
let &[c, b, a] = q.coeffs();
Quadratic::new([c + a * shift * shift - b * shift, b - 2.0 * shift * a, a])
}
impl QuadraticApprox {
pub fn from_cubic(c: CubicBez, almost_horiz: Option<Direction>) -> Self {
let q = fit_quadratic(c);
let &[c0, c1, c2] = q.coeffs();
let y = cubic_from_bez_y(c);
let x = cubic_from_bez_x(c);
let [y0, y1, y2, y3] = *y.coeffs();
let [x0, x1, x2, x3] = *x.coeffs();
let error = Poly::new([
c0 + c1 * y0 + c2 * y0 * y0 - x0,
c1 * y1 + c2 * (2.0 * y0 * y1) - x1,
c1 * y2 + c2 * (2.0 * y0 * y2 + y1 * y1) - x2,
c1 * y3 + c2 * 2.0 * (y0 * y3 + y1 * y2) - x3,
c2 * (2.0 * y1 * y3 + y2 * y2),
c2 * 2.0 * y2 * y3,
c2 * y3 * y3,
]);
let extrema = error.deriv().roots_between(0.0, 1.0, 1e-8);
let err0 = c.p0.x - q.eval(c.p0.y);
let err1 = c.p3.x - q.eval(c.p3.y);
let mut dmin = 0.0f64.min(err0).min(err1);
let mut dmax = 0.0f64.max(err0).max(err1);
for t in extrema {
let e = -error.eval(t);
dmin = dmin.min(e);
dmax = dmax.max(e);
}
QuadraticApprox {
q,
dmin,
dmax,
almost_horiz: almost_horiz.map(|d| (d, c.p0.x, c.p3.x)),
}
}
pub fn shift_y(&self, amount: f64) -> Self {
Self {
q: shift_quadratic(self.q, amount),
..*self
}
}
fn initial_bound(&self, which: Bound, tolerance: f64) -> Option<Quadratic> {
match (self.almost_horiz, which) {
(Some((Direction::Increasing, x, _)), Bound::Left) => {
Some(Quadratic::new([x - tolerance, 0.0, 0.0]))
}
(Some((Direction::Decreasing, x, _)), Bound::Right) => {
Some(Quadratic::new([x + tolerance, 0.0, 0.0]))
}
_ => None,
}
}
fn final_bound(&self, which: Bound, tolerance: f64) -> Option<Quadratic> {
match (self.almost_horiz, which) {
(Some((Direction::Increasing, _, x)), Bound::Right) => {
Some(Quadratic::new([x + tolerance, 0.0, 0.0]))
}
(Some((Direction::Decreasing, _, x)), Bound::Left) => {
Some(Quadratic::new([x - tolerance, 0.0, 0.0]))
}
_ => None,
}
}
fn middle_bound(&self, which: Bound, tolerance: f64, outer_bound: bool) -> Quadratic {
let shift = match (self.almost_horiz, which) {
(Some((Direction::Increasing, ..)), Bound::Right)
| (Some((Direction::Decreasing, ..)), Bound::Left) => -tolerance,
(Some((Direction::Increasing, ..)), Bound::Left)
| (Some((Direction::Decreasing, ..)), Bound::Right) => tolerance,
_ => 0.0,
};
let factor = if outer_bound { 1.0 } else { -1.0 };
let raise = match which {
Bound::Left => factor * self.dmin - tolerance,
Bound::Right => factor * self.dmax + tolerance,
};
shift_quadratic(raise_quadratic(self.q, raise), shift)
}
#[allow(dead_code)]
fn compare(
&self,
other: &QuadraticApprox,
sure_tolerance: f64,
ish_tolerance: f64,
y0: f64,
y1: f64,
) -> QuadApproxComparison {
if self.almost_horiz.is_none() && other.almost_horiz.is_none() {
let diff = *other - *self;
let mut out = QuadApproxComparison::default();
push_two_level_quadratic_signs(
diff.q,
-diff.dmax - sure_tolerance,
-diff.dmin + sure_tolerance,
y0,
y1,
&mut out,
);
return out;
}
use MaybeOrder::*;
let our_right_their_left = self.compare_one_side(other, sure_tolerance, y0, y1, true);
let their_right_our_left = other.compare_one_side(self, sure_tolerance, y0, y1, true);
let sure_order = QuadApproxComparison::merge_from(
&our_right_their_left,
&their_right_our_left,
|ortl, trol| match (ortl, trol) {
(Left, _) => {
debug_assert_eq!(trol, Ish);
Left
}
(_, Left) => {
debug_assert_eq!(ortl, Ish);
Right
}
_ => Ish,
},
);
let our_right_their_left = self.compare_one_side(other, ish_tolerance, y0, y1, false);
let their_right_our_left = other.compare_one_side(self, ish_tolerance, y0, y1, false);
let ish_order = QuadApproxComparison::merge_from(
&our_right_their_left,
&their_right_our_left,
|ortl, trol| match (ortl, trol) {
(Left, _) => Left,
(_, Left) => Right,
_ => Ish,
},
);
QuadApproxComparison::merge_from(&sure_order, &ish_order, |sure, ish| match (sure, ish) {
(MaybeOrder::Left | MaybeOrder::Right, _) => sure,
(_, MaybeOrder::Ish) => MaybeOrder::Ish,
_ => MaybeOrder::Unknown,
})
}
fn compare_one_side(
&self,
other: &QuadraticApprox,
tolerance: f64,
mut y0: f64,
y1: f64,
outer_bound: bool,
) -> QuadApproxComparison {
let mut buf = ApproxComparison::default();
let r = self.initial_bound(Bound::Right, tolerance);
let l = other.initial_bound(Bound::Left, tolerance);
if r.is_some() || l.is_some() {
let r = r.unwrap_or_else(|| self.middle_bound(Bound::Right, tolerance, outer_bound));
let l = l.unwrap_or_else(|| other.middle_bound(Bound::Left, tolerance, outer_bound));
let end = (y0 + tolerance).min(y1);
push_quadratic_sign(r - l, Order::Left, Order::Ish, y0, end, &mut buf.entries);
y0 = end;
}
let r = self.final_bound(Bound::Right, tolerance);
let l = other.final_bound(Bound::Left, tolerance);
let end = if r.is_some() || l.is_some() {
(y1 - tolerance).max(y0)
} else {
y1
};
if end > y0 {
let r = self.middle_bound(Bound::Right, tolerance, outer_bound);
let l = other.middle_bound(Bound::Left, tolerance, outer_bound);
push_quadratic_sign(r - l, Order::Left, Order::Ish, y0, end, &mut buf.entries);
y0 = end;
}
if y0 < y1 {
let r = r.unwrap_or_else(|| self.middle_bound(Bound::Right, tolerance, outer_bound));
let l = l.unwrap_or_else(|| other.middle_bound(Bound::Left, tolerance, outer_bound));
push_quadratic_sign(r - l, Order::Left, Order::Ish, y0, y1, &mut buf.entries);
}
buf
}
#[cfg(test)]
fn eval(&self, y: f64) -> f64 {
self.q.eval(y)
}
}
impl std::ops::Sub for QuadraticApprox {
type Output = Self;
fn sub(self, other: Self) -> Self {
debug_assert!(self.almost_horiz.is_none() && other.almost_horiz.is_none());
QuadraticApprox {
q: self.q - other.q,
dmin: self.dmin - other.dmax,
dmax: self.dmax - other.dmin,
almost_horiz: None,
}
}
}
fn interpolate_quadratic(x0: f64, y0: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> Quadratic {
debug_assert!(x0 < x1 && x1 < x2);
let dx10 = x1 - x0;
let dx21 = x2 - x1;
let dx20 = x2 - x0;
let denom0 = dx10 * dx20;
let factor0 = y0 / denom0;
let ell0 = [x1 * x2 * factor0, -(x1 + x2) * factor0, factor0];
let denom1 = -dx10 * dx21;
let factor1 = y1 / denom1;
let ell1 = [x0 * x2 * factor1, -(x0 + x2) * factor1, factor1];
let denom2 = dx21 * dx20;
let factor2 = y2 / denom2;
let ell2 = [x0 * x1 * factor2, -(x0 + x1) * factor2, factor2];
Quadratic::new([
ell0[0] + ell1[0] + ell2[0],
ell0[1] + ell1[1] + ell2[1],
ell0[2] + ell1[2] + ell2[2],
])
}
fn fit_quadratic(c: CubicBez) -> Quadratic {
let t0 = 0.0;
let t1 = 0.3;
let t2 = 0.7;
let t3 = 1.0;
let p0 = c.eval(t0);
let p1 = c.eval(t1);
let p2 = c.eval(t2);
let p3 = c.eval(t3);
if p1.y - p0.y < 1e-12 || p2.y - p1.y < 1e-12 {
return Quadratic::new([
(p0.x * p3.y - p3.x * p0.y) / (p3.y - p0.y),
(p3.x - p0.x) / (p3.y - p0.y),
0.0,
]);
}
let p = interpolate_quadratic(p0.y, p0.x, p1.y, p1.x, p2.y, p2.x);
let q = interpolate_quadratic(p0.y, 1.0, p1.y, -1.0, p2.y, 1.0);
let e = (p.eval(p3.y) - p3.x) / (q.eval(p3.y) + 1.0);
let p = p.coeffs();
let q = q.coeffs();
Quadratic::new([p[0] - q[0] * e, p[1] - q[1] * e, p[2] - q[2] * e])
}
#[allow(dead_code)]
fn fit_quadratic_with_endpoints_and_area(c: CubicBez) -> Quadratic {
let seg = PathSeg::Cubic(c);
let close_seg = PathSeg::Line(Line::new(c.p3, c.p0));
let area = seg.area() + close_seg.area();
let dy = c.p3.y - c.p0.y;
let c2 = -6. * area / dy.powi(3);
let c1 = (c.p3.x - c.p0.x - (c.p3.y.powi(2) - c.p0.y.powi(2)) * c2) / dy;
let c0 = c.p0.x - c1 * c.p0.y - c2 * c.p0.y.powi(2);
Quadratic::new([c0, c1, c2])
}
pub fn quad_parameters(q: QuadBez) -> (Vec2, Vec2, Vec2) {
let c0 = q.p0.to_vec2();
let c1 = (q.p1 - q.p0) * 2.0;
let c2 = c0 - q.p1.to_vec2() * 2.0 + q.p2.to_vec2();
(c0, c1, c2)
}
fn l_infty_distance(p0: Point, p1: Point) -> f64 {
(p0.x - p1.x).abs().max((p0.y - p1.y).abs())
}
fn max_distance(c0: CubicBez, c1: CubicBez) -> f64 {
l_infty_distance(c0.p0, c1.p0)
.max(l_infty_distance(c0.p1, c1.p1))
.max(l_infty_distance(c0.p2, c1.p2))
.max(l_infty_distance(c0.p3, c1.p3))
}
#[derive(Clone, Debug)]
struct CubicParams {
c: CubicBez,
dir: Option<Direction>,
approx: QuadraticApprox,
}
#[allow(clippy::too_many_arguments)]
fn intersect_cubics_rec(
cubic0: &CubicParams,
cubic1: &CubicParams,
use_precomputed_approx: bool,
y0: f64,
y1: f64,
sure_tolerance: f64,
ish_tolerance: f64,
out: &mut CurveOrder,
status: MetricsStatus,
) {
let mut c0 = y_subsegment(cubic0.c, y0, y1);
let mut c1 = y_subsegment(cubic1.c, y0, y1);
if max_distance(c0, c1) <= ish_tolerance {
out.push(y1, Order::Ish, status);
return;
}
if y1 - y0 < ish_tolerance {
let b0 = Shape::bounding_box(&c0);
let b1 = Shape::bounding_box(&c1);
let order = if b1.min_x() >= b0.max_x() + ish_tolerance {
Order::Left
} else if b0.min_x() >= b1.max_x() + ish_tolerance {
Order::Right
} else {
Order::Ish
};
out.push(y1, order, status);
return;
}
let y_mid = (y0 + y1) / 2.0;
c0.p0.y -= y_mid;
c0.p1.y -= y_mid;
c0.p2.y -= y_mid;
c0.p3.y -= y_mid;
c1.p0.y -= y_mid;
c1.p1.y -= y_mid;
c1.p2.y -= y_mid;
c1.p3.y -= y_mid;
let (ep0, ep1) = if use_precomputed_approx {
let orig_y_mid0 = (cubic0.c.p0.y + cubic0.c.p3.y) / 2.0;
let orig_y_mid1 = (cubic1.c.p0.y + cubic1.c.p3.y) / 2.0;
(
cubic0.approx.shift_y(orig_y_mid0 - y_mid),
cubic1.approx.shift_y(orig_y_mid1 - y_mid),
)
} else {
(
QuadraticApprox::from_cubic(c0, cubic0.dir),
QuadraticApprox::from_cubic(c1, cubic1.dir),
)
};
let mut signs = ep0.compare(&ep1, sure_tolerance, ish_tolerance, y0 - y_mid, y1 - y_mid);
for entry in &mut signs.entries {
entry.end = (entry.end + y_mid).clamp(y0, y1);
}
signs.entries.last_mut().unwrap().end = y1;
for (y0, y1, order) in signs.iter(y0) {
match order {
MaybeOrder::Right => {
debug_assert!(solve_x_for_y(cubic0.c, y0) > solve_x_for_y(cubic1.c, y0));
debug_assert!(solve_x_for_y(cubic0.c, y1) > solve_x_for_y(cubic1.c, y1));
}
MaybeOrder::Left => {
debug_assert!(solve_x_for_y(cubic0.c, y0) < solve_x_for_y(cubic1.c, y0));
debug_assert!(solve_x_for_y(cubic0.c, y1) < solve_x_for_y(cubic1.c, y1));
}
_ => {}
}
}
for (new_y0, new_y1, order) in signs.iter(y0) {
if new_y0 == new_y1 {
continue;
}
if let Ok(order) = order.try_into() {
out.push(new_y1, order, status);
} else {
let mid = 0.5 * (new_y0 + new_y1);
if y1 - y0 <= ish_tolerance
|| (ep0.dmax + ep1.dmax <= 2.0 * ish_tolerance
&& ep0.dmin + ep1.dmin >= -2.0 * ish_tolerance)
{
out.push(new_y1, Order::Ish, status);
} else if new_y1 - new_y0 < 0.5 * (y1 - y0) {
intersect_cubics_rec(
cubic0,
cubic1,
false,
new_y0,
new_y1,
sure_tolerance,
ish_tolerance,
out,
status.bump(),
);
} else {
intersect_cubics_rec(
cubic0,
cubic1,
false,
new_y0,
mid,
sure_tolerance,
ish_tolerance,
out,
status.bump(),
);
intersect_cubics_rec(
cubic0,
cubic1,
false,
mid,
new_y1,
sure_tolerance,
ish_tolerance,
out,
status.bump(),
);
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct AlmostHorizontalDecomposition {
pieces: ArrayVec<CubicParams, 6>,
}
impl AlmostHorizontalDecomposition {
pub fn from_monotone_cubic(c: CubicBez) -> Self {
const THRESHOLD: f64 = 16.0;
let dx = cubic_from_bez_x(c).deriv();
let dy = cubic_from_bez_y(c).deriv();
let increasing = dx - dy * THRESHOLD;
let decreasing = dy * (-THRESHOLD) - dx;
let mut increasing_buf = ApproxComparison::<3>::default();
let mut decreasing_buf = ApproxComparison::<3>::default();
push_quadratic_sign(
increasing,
Order::Ish,
Order::Right,
0.0,
1.0,
&mut increasing_buf.entries,
);
push_quadratic_sign(
decreasing,
Order::Ish,
Order::Left,
0.0,
1.0,
&mut decreasing_buf.entries,
);
use MaybeOrder::*;
let subdivision =
ApproxComparison::<6>::merge_from(&increasing_buf, &decreasing_buf, |inc, dec| match (
inc, dec,
) {
(Right, _) => Right,
(_, Left) => Left,
_ => Ish,
});
let mut last_y = c.p0.y;
let mut last_end = 0.0;
let orig_c = c;
let mut ret = ArrayVec::default();
for (_, end, order) in subdivision.iter(0.0) {
let start = last_end;
let mut c = if start == 0.0 && end == 1.0 {
orig_c
} else {
orig_c.subsegment(start..end)
};
c.p0.y = c.p0.y.clamp(last_y, orig_c.p3.y);
c.p1.y = c.p1.y.clamp(c.p0.y, orig_c.p3.y);
c.p3.y = c.p3.y.clamp(last_y, orig_c.p3.y);
c.p2.y = c.p2.y.clamp(last_y, c.p3.y);
last_y = c.p3.y;
let approx_horiz = match order {
Right => {
c.p1.x = c.p1.x.max(c.p0.x);
c.p2.x = c.p2.x.min(c.p3.x);
Some(Direction::Increasing)
}
Ish | Unknown => None,
Left => {
c.p1.x = c.p1.x.min(c.p0.x);
c.p2.x = c.p2.x.max(c.p3.x);
Some(Direction::Decreasing)
}
};
if c.p0.y != c.p3.y {
let mut recentered_c = c;
let y_mid = (c.p0.y + c.p3.y) / 2.0;
recentered_c.p0.y -= y_mid;
recentered_c.p1.y -= y_mid;
recentered_c.p2.y -= y_mid;
recentered_c.p3.y -= y_mid;
let p = CubicParams {
c,
dir: approx_horiz,
approx: QuadraticApprox::from_cubic(recentered_c, approx_horiz),
};
ret.push(p);
last_end = end;
}
}
ret.last_mut().unwrap().c.p3 = orig_c.p3;
Self { pieces: ret }
}
}
pub fn intersect_cubics(
c0: CubicBez,
c1: CubicBez,
sure_tolerance: f64,
ish_tolerance: f64,
) -> CurveOrder {
intersect_cubics_with_precomputed_decomp(
c0,
&AlmostHorizontalDecomposition::from_monotone_cubic(c0),
c1,
&AlmostHorizontalDecomposition::from_monotone_cubic(c1),
sure_tolerance,
ish_tolerance,
)
}
pub fn intersect_cubics_with_precomputed_decomp(
c0: CubicBez,
c0_div: &AlmostHorizontalDecomposition,
c1: CubicBez,
c1_div: &AlmostHorizontalDecomposition,
sure_tolerance: f64,
ish_tolerance: f64,
) -> CurveOrder {
let y0 = c0.p0.y.max(c1.p0.y);
let y1 = c0.p3.y.min(c1.p3.y);
let mut ret = CurveOrder::new(y0);
if y0 < y1 {
let c0_div = c0_div
.pieces
.iter()
.filter(|cp| cp.c.p3.y > y0)
.map(|cp| (cp.c.p3.y, cp));
let c1_div = c1_div
.pieces
.iter()
.filter(|cp| cp.c.p3.y > y0)
.map(|cp| (cp.c.p3.y, cp));
for (_, c0, c1) in MergedRangeIter::new(c0_div, c1_div) {
let y0 = c0.c.p0.y.max(c1.c.p0.y);
let y1 = c0.c.p3.y.min(c1.c.p3.y);
if y0 < y1 {
intersect_cubics_rec(
c0,
c1,
true,
y0,
y1,
sure_tolerance,
ish_tolerance,
&mut ret,
MetricsStatus::default(),
);
}
}
} else if y0 == y1 {
debug_assert!(c0.p0.y == c1.p3.y || c0.p3.y == c1.p0.y);
let (x0, x1) = if c0.p0.y == c1.p3.y {
(c0.p0.x, c1.p3.x)
} else {
(c0.p3.x, c1.p0.x)
};
let order = if x0 < x1 - ish_tolerance {
Order::Left
} else if x0 > x1 + ish_tolerance {
Order::Right
} else {
Order::Ish
};
ret.push(y1, order, MetricsStatus::default());
}
debug_assert_eq!(ret.cmps.last().unwrap().end, y1);
ret
}
fn cubic_from_bez(a0: f64, a1: f64, a2: f64, a3: f64) -> Cubic {
let c3 = a3 - 3.0 * a2 + 3.0 * a1 - a0;
let c2 = 3.0 * (a2 - 2.0 * a1 + a0);
let c1 = 3.0 * (a1 - a0);
let c0 = a0;
Cubic::new([c0, c1, c2, c3])
}
fn cubic_from_bez_x(c: CubicBez) -> Cubic {
cubic_from_bez(c.p0.x, c.p1.x, c.p2.x, c.p3.x)
}
fn cubic_from_bez_y(c: CubicBez) -> Cubic {
cubic_from_bez(c.p0.y, c.p1.y, c.p2.y, c.p3.y)
}
#[cfg(any(test, feature = "arbitrary"))]
#[doc(hidden)]
pub mod arbtests {
use super::{cubic_from_bez_x, cubic_from_bez_y};
use arbitrary::Unstructured;
use kurbo::ParamCurve as _;
use super::solve_t_for_y;
pub fn solve_for_t(u: &mut Unstructured) -> Result<(), arbitrary::Error> {
let c = crate::arbitrary::monotonic_bezier(u)?;
if c.p0.y == c.p3.y {
return Ok(());
}
let accuracy = 1e-9;
let max_coeff = cubic_from_bez_x(c)
.max_abs_coefficient()
.max(cubic_from_bez_y(c).max_abs_coefficient());
let threshold = accuracy * max_coeff.max(1.0);
let y = crate::arbitrary::float_in_range(c.p0.y, c.p3.y, u)?;
let t = solve_t_for_y(c, y);
assert!((c.eval(t).y - y).abs() <= threshold);
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn solve_for_t_accuracy() {
let c = CubicBez {
p0: (0.5, 0.0).into(),
p1: (0.19531249655301508, 0.6666666864572713).into(),
p2: (0.00781250000181899, 1.0).into(),
p3: (0.0, 1.0).into(),
};
let y0 = 0.0;
let y1 = 0.9999999406281859;
let c0 = c.subsegment(solve_t_for_y(c, y0)..solve_t_for_y(c, y1));
assert!((dbg!(c0.p3.y) - dbg!(y1)).abs() < 1e-8);
let y1 = 0.9999999;
let c0 = c.subsegment(solve_t_for_y(c, y0)..solve_t_for_y(c, y1));
assert!((dbg!(c0.p3.y) - dbg!(y1)).abs() < 1e-8);
}
#[test]
fn solve_for_t_accuracy2() {
let c = CubicBez {
p0: (0.9999999406281859, 0.0).into(),
p1: (0.011764705882352941, 0.011764705882352941).into(),
p2: (0.011764705882352941, 0.023391003460207612).into(),
p3: (0.011764705882352941, 0.03488052106655811).into(),
};
let y = 0.012468608207852864;
let t = solve_t_for_y(c, y);
assert!(dbg!(c.eval(t).y - y).abs() < 1e-8);
}
#[test]
fn solve_for_t_accuracy3() {
let c = CubicBez {
p0: (0.1443023801753, -0.00034678801186988073).into(),
p1: (0.3760345290602, -0.00011491438194505266).into(),
p2: (0.6070068772783, 0.00011680717653411721).into(),
p3: (0.8372203215247, 0.0003483767633017387).into(),
};
let y = 0.00016929232880729566;
let t = solve_t_for_y(c, y);
assert!(dbg!(c.eval(t).y - y).abs() < 1e-8);
}
#[test]
fn solve_for_t_arbtest() {
arbtest::arbtest(arbtests::solve_for_t);
}
#[test]
fn non_touching() {
let sure_tol = 0.01;
let ish_tol = 0.03;
let a = Line::new((-1e6, 0.0), (0.0, 1.0));
let b = Line::new((ish_tol * 1.5, 0.0), (ish_tol * 1.5, 1.0));
let a = PathSeg::Line(a).to_cubic();
let b = PathSeg::Line(b).to_cubic();
let cmp = intersect_cubics(a, b, sure_tol, ish_tol);
assert_eq!(cmp.order_at(1.0), Order::Left);
}
#[test]
fn almost_touching() {
let sure_tol = 0.25;
let ish_tol = 0.3;
let a = CubicBez::new((-1.0, 0.0), (0.5, 0.6), (0.5, 0.4), (-1.0, 1.0));
let b = CubicBez::new((0.3, 0.0), (0.3, 1.0 / 3.0), (0.3, 2.0 / 3.0), (0.3, 1.0));
let cmp = intersect_cubics(a, b, sure_tol, ish_tol);
assert_eq!(cmp.iter().count(), 3);
}
#[test]
fn quad_approximation() {
let c = CubicBez {
p0: (0.1443023801753, -0.00034678801186988073).into(),
p1: (0.3760345290602, -0.00011491438194505266).into(),
p2: (0.6070068772783, 0.00011680717653411721).into(),
p3: (0.8372203215247, 0.0003483767633017387).into(),
};
let y = 0.00016929232880729566;
let t = solve_t_for_y(c, y);
dbg!(t, c.eval(t));
let ep = QuadraticApprox::from_cubic(c, None);
dbg!(y);
dbg!(ep);
dbg!(ep.eval(y));
dbg!(solve_x_for_y(c, y));
let diff = solve_x_for_y(c, y) - ep.eval(y);
assert!(diff <= ep.dmax);
assert!(diff >= ep.dmin);
}
#[test]
fn test_loop() {
let c0 = CubicBez {
p0: (0.5, 0.0).into(),
p1: (0.0014551791831513819, 0.9999847770668531).into(),
p2: (0.9999999850988388, 1.0).into(),
p3: (0.0, 1.0).into(),
};
let c1 = CubicBez {
p0: (0.5, 0.0).into(),
p1: (0.0014551791831513819, 0.9999847770668531).into(),
p2: (1.0, 0.9999999999999717).into(),
p3: (0.0, 0.9999999999999717).into(),
};
dbg!(intersect_cubics(c0, c1, 0.5e-6, 1.5e-6));
}
#[test]
fn y_slop_single_seg() {
let order = CurveOrder {
start: 0.0,
cmps: [CurveOrderEntry {
end: 1.0,
order: Order::Right,
}]
.into_iter()
.collect(),
metrics: Default::default(),
};
assert_eq!(order.clone().with_y_slop(0.5).cmps, order.cmps);
}
#[test]
fn y_slop_single_short_seg() {
let order = CurveOrder {
start: 0.0,
cmps: [CurveOrderEntry {
end: 0.2,
order: Order::Right,
}]
.into_iter()
.collect(),
metrics: Default::default(),
};
assert_eq!(order.clone().with_y_slop(0.5).cmps, order.cmps);
}
#[test]
fn y_slop_short_start_order() {
let order = CurveOrder {
start: 0.0,
cmps: [
CurveOrderEntry {
end: 0.1,
order: Order::Right,
},
CurveOrderEntry {
end: 1.0,
order: Order::Ish,
},
]
.into_iter()
.collect(),
metrics: Default::default(),
};
assert_eq!(
order.clone().with_y_slop(0.5).cmps.as_slice(),
&[CurveOrderEntry {
end: 1.0,
order: Order::Ish
}]
);
}
#[test]
fn y_slop_short_end_order() {
let order = CurveOrder {
start: 0.0,
cmps: [
CurveOrderEntry {
end: 0.9,
order: Order::Ish,
},
CurveOrderEntry {
end: 1.0,
order: Order::Left,
},
]
.into_iter()
.collect(),
metrics: Default::default(),
};
assert_eq!(
order.clone().with_y_slop(0.5).cmps.as_slice(),
&[CurveOrderEntry {
end: 1.0,
order: Order::Ish
}]
);
}
#[test]
fn y_slop_no_height() {
let order = CurveOrder {
start: 1.0,
cmps: [CurveOrderEntry {
end: 1.0,
order: Order::Right,
}]
.into_iter()
.collect(),
metrics: Default::default(),
};
assert_eq!(order.clone().with_y_slop(0.5).cmps, order.cmps);
}
#[test]
fn curve_order_no_overlap() {
let c0 = CubicBez {
p0: (0.0, 0.0).into(),
p1: (0.0, 0.0).into(),
p2: (0.0, 1.0).into(),
p3: (0.0, 1.0).into(),
};
let c1 = CubicBez {
p0: (1.0, 1.0).into(),
p1: (1.0, 1.0).into(),
p2: (1.0, 2.0).into(),
p3: (1.0, 2.0).into(),
};
let order = intersect_cubics(c0, c1, 0.125, 0.25);
assert_eq!(order.start, 1.0);
assert_eq!(
&order.cmps[..],
&[CurveOrderEntry {
end: 1.0,
order: Order::Left
}]
);
let c1 = CubicBez {
p0: (1.0, 1.0).into(),
p1: (1.0, 1.0).into(),
p2: (0.0, 2.0).into(),
p3: (0.0, 2.0).into(),
};
let order = intersect_cubics(c0, c1, 0.125, 0.25);
assert_eq!(order.start, 1.0);
assert_eq!(
&order.cmps[..],
&[CurveOrderEntry {
end: 1.0,
order: Order::Left
}]
);
let c1 = CubicBez {
p0: (0.1, 1.0).into(),
p1: (0.1, 1.0).into(),
p2: (0.0, 2.0).into(),
p3: (0.0, 2.0).into(),
};
let order = intersect_cubics(c0, c1, 0.125, 0.25);
assert_eq!(order.start, 1.0);
assert_eq!(
&order.cmps[..],
&[CurveOrderEntry {
end: 1.0,
order: Order::Ish
}]
);
}
#[test]
fn painted_dreams_deep_recursion() {
let sure_tol = 0.5e-7;
let ish_tol = 1.5e-7;
let c0 = CubicBez::new(
(268.2700181987094, 465.9600610772311),
(261.5036543284458, 474.04917281766745),
(255.01588390924064, 481.16474559246694),
(248.76144328725377, 487.4191862144538),
);
let c1 = CubicBez::new(
(268.2700181987094, 465.9600610772311),
(261.508835610275, 474.04297901254995),
(255.02582000674386, 481.1538483487484),
(248.77581162941254, 487.404817872295),
);
dbg!(intersect_cubics(c0, c1, sure_tol, ish_tol));
}
#[test]
fn quadratic_comparison() {
let c0 = CubicBez::new((0.0, 0.0), (10.0, 10.0), (20.0, 20.0), (30.0, 30.0));
let c1 = CubicBez::new((30.0, 0.0), (20.0, 10.0), (10.0, 20.0), (0.0, 30.0));
let sure_tolerance = 1.0;
let ish_tolerance = 2.0;
let qa0 = QuadraticApprox::from_cubic(c0, None);
let qa1 = QuadraticApprox::from_cubic(c1, None);
assert_eq!(
3,
qa0.compare(&qa1, sure_tolerance, ish_tolerance, 0.0, 30.0)
.entries
.len()
);
let c0 = CubicBez::new((0.0, 0.0), (100.0, 10.0), (200.0, 20.0), (300.0, 30.0));
let c1 = CubicBez::new((3.0, 0.0), (103.0, 10.0), (203.0, 20.0), (303.0, 30.0));
let qa0 = QuadraticApprox::from_cubic(c0, Some(Direction::Increasing));
let qa1 = QuadraticApprox::from_cubic(c1, Some(Direction::Increasing));
assert_eq!(
1,
qa0.compare(&qa1, sure_tolerance, ish_tolerance, 0.0, 30.0)
.entries
.len()
);
let c1 = CubicBez::new((150.0, 0.0), (150.0, 10.0), (150.0, 20.0), (150.0, 30.0));
let qa0 = QuadraticApprox::from_cubic(c0, Some(Direction::Increasing));
let qa1 = QuadraticApprox::from_cubic(c1, Some(Direction::Increasing));
assert_eq!(
3,
qa0.compare(&qa1, sure_tolerance, ish_tolerance, 0.0, 30.0)
.entries
.len()
);
}
#[test]
fn break_horizontal() {
let c0 = CubicBez::new((0.0, 0.0), (10.0, 10.0), (20.0, 20.0), (30.0, 30.0));
dbg!(AlmostHorizontalDecomposition::from_monotone_cubic(c0));
let c0 = CubicBez::new((0.0, 0.0), (10.0, 10.0), (20.0, 20.0), (500.0, 30.0));
dbg!(AlmostHorizontalDecomposition::from_monotone_cubic(c0));
let c0 = CubicBez::new((0.0, 0.0), (2000.0, 10.0), (-1000.0, 20.0), (1000.0, 30.0));
dbg!(AlmostHorizontalDecomposition::from_monotone_cubic(c0));
let c0 = CubicBez::new((0.0, 0.0), (2000.0, 0.0), (-1000.0, 30.0), (1000.0, 30.0));
dbg!(AlmostHorizontalDecomposition::from_monotone_cubic(c0));
}
}