blob: 3a8a860fbc4d4612811762104e673e0cd4f60c5f [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::mem;
use smallvec::SmallVec;
use crate::math;
const TOLERANCE: f32 = 0.005;
fn angle(origin: math::Vec, p0: math::Vec, p1: math::Vec) -> f32 {
let d0 = p0 - origin;
let d1 = p1 - origin;
let cross = d0.x * d1.y - d0.y * d1.x;
let dot = d0.x * d1.x + d0.y * d1.y;
cross.atan2(dot)
}
fn line_segment_intersection(
segment0: [math::Vec; 2],
segment1: [math::Vec; 2],
) -> Option<math::Vec> {
let d10 = segment0[1] - segment0[0];
let d32 = segment1[1] - segment1[0];
let denom = d10.x * d32.y - d32.x * d10.y;
if denom == 0.0 {
return None;
}
let denom_is_pos = denom > 0.0;
let d02 = segment0[0] - segment1[0];
let s_numer = d10.x * d02.y - d10.y * d02.x;
if (s_numer < 0.0) == denom_is_pos {
return None;
}
let t_numer = d32.x * d02.y - d32.y * d02.x;
if (t_numer < 0.0) == denom_is_pos {
return None;
}
if (s_numer > denom) == denom_is_pos || (t_numer > denom) == denom_is_pos {
return None;
}
let t = t_numer / denom;
Some(segment0[0] + d10 * t)
}
fn align(point: math::Vec, line: [math::Vec; 2]) -> math::Vec {
let angle = -(line[1].y - line[0].y).atan2(line[1].x - line[0].x);
math::Vec::new(
(point.x - line[0].x) * angle.cos() - (point.y - line[0].y) * angle.sin(),
(point.x - line[0].x) * angle.sin() + (point.y - line[0].y) * angle.cos(),
)
}
fn approx_eq(p0: math::Vec, p1: math::Vec) -> bool {
(p0.x - p1.x).abs() <= TOLERANCE && (p0.y - p1.y).abs() <= TOLERANCE
}
fn left_different(points: &[math::Vec]) -> [math::Vec; 2] {
points
.windows(2)
.find_map(|window| {
if let [p0, p1] = *window {
(!approx_eq(p0, p1)).then(|| [p0, p1])
} else {
unreachable!()
}
})
.expect("Bezier cannot be a point")
}
pub fn right_different(points: &[math::Vec]) -> [math::Vec; 2] {
points
.windows(2)
.rev()
.find_map(|window| {
if let [p0, p1] = *window {
(!approx_eq(p0, p1)).then(|| [p0, p1])
} else {
unreachable!()
}
})
.expect("Bezier cannot be a point")
}
fn normal(p0: math::Vec, p1: math::Vec, c: f32) -> Option<math::Vec> {
let d = (p1 - p0) * c;
if d.x.abs() == 0.0 && d.y.abs() == 0.0 {
return None;
}
let q = (d.x * d.x + d.y * d.y).sqrt();
Some(math::Vec::new(-d.y / q, d.x / q))
}
fn normal_left(points: &[math::Vec]) -> math::Vec {
let c = points.len() as f32 - 1.0;
let [p0, p1] = left_different(points);
normal(p0, p1, c).expect("Bezier cannot be a point")
}
fn normal_right(points: &[math::Vec]) -> math::Vec {
let c = points.len() as f32 - 1.0;
let [p0, p1] = right_different(points);
normal(p0, p1, c).expect("Bezier cannot be a point")
}
fn derive(points: &[math::Vec]) -> SmallVec<[math::Vec; 3]> {
let mut derived = SmallVec::new();
for window in points.windows(2) {
if let [p0, p1] = *window {
derived.push((p1 - p0) * (points.len() - 1) as f32);
}
}
derived
}
fn derive_left(points: &[math::Vec]) -> math::Vec {
let [p0, p1] = left_different(points);
(p1 - p0) * (points.len() - 1) as f32
}
fn derive_right(points: &[math::Vec]) -> math::Vec {
let [p0, p1] = right_different(points);
(p1 - p0) * (points.len() - 1) as f32
}
fn droots(vals: &[f32]) -> SmallVec<[f32; 2]> {
let mut droots = SmallVec::new();
match *vals {
[a, b] => {
if a != b {
droots.push(a / (a - b));
}
}
[a, b, c] => {
let d = a - 2.0 * b + c;
if d != 0.0 {
let m1 = -(b * b - a * c).sqrt();
let m2 = -a + b;
droots.push(-(m1 + m2) / d);
droots.push(-(-m1 + m2) / d);
} else if b != c && d == 0.0 {
droots.push((2.0 * b - c) / (2.0 * (b - c)));
}
}
_ => (),
}
droots
}
fn hull(points: &[math::Vec; 4], t: f32) -> SmallVec<[math::Vec; 10]> {
let mut hull = SmallVec::new();
let mut points = SmallVec::from(*points);
let mut next_points = SmallVec::new();
hull.extend(points.iter().copied());
while points.len() > 1 {
next_points.clear();
for window in points.windows(2) {
if let [p0, p1] = *window {
let point = p0.lerp(p1, t);
hull.push(point);
next_points.push(point);
}
}
mem::swap(&mut points, &mut next_points);
}
hull
}
fn split(points: &[math::Vec; 4], t0: f32, t1: f32) -> [math::Vec; 4] {
let hull0 = hull(points, t0);
let right = [hull0[9], hull0[8], hull0[6], hull0[3]];
let t1 = (t1 - t0) / (1.0 - t0);
let hull1 = hull(&right, t1);
[hull1[0], hull1[4], hull1[7], hull1[9]]
}
fn line_intersection(
p0: math::Vec,
p1: math::Vec,
p2: math::Vec,
p3: math::Vec,
) -> Option<math::Vec> {
let nx =
(p0.x * p1.y - p0.y * p1.x) * (p2.x - p3.x) - (p0.x - p1.x) * (p2.x * p3.y - p2.y * p3.x);
let ny =
(p0.x * p1.y - p0.y * p1.x) * (p2.y - p3.y) - (p0.y - p1.y) * (p2.x * p3.y - p2.y * p3.x);
let d = (p0.x - p1.x) * (p2.y - p3.y) - (p0.y - p1.y) * (p2.x - p3.x);
if d == 0.0 {
return None;
}
Some(math::Vec::new(nx / d, ny / d))
}
fn scale(points: &[math::Vec; 4], dist: f32) -> Option<[math::Vec; 4]> {
if dist == 0.0 {
return Some(*points);
}
let normals = [normal_left(points), normal_right(points)];
let offset = [points[0] + normals[0] * 10.0, points[3] + normals[1] * 10.0];
let origin = line_intersection(offset[0], points[0], offset[1], points[3])?;
let mut new_points = [math::Vec::default(); 4];
new_points[0] = points[0] + normals[0] * dist;
new_points[3] = points[3] + normals[1] * dist;
new_points[1] =
line_intersection(new_points[0], new_points[0] + derive_left(points), origin, points[1])?;
new_points[2] =
line_intersection(new_points[3], new_points[3] + derive_right(points), origin, points[2])?;
Some(new_points)
}
#[derive(Clone, Debug)]
pub enum Bezier {
Line([math::Vec; 2]),
Cubic([math::Vec; 4]),
}
impl Bezier {
fn is_linear(&self) -> bool {
match self {
Self::Line(_) => true,
Self::Cubic(points) => points
.iter()
.copied()
.all(|point| align(point, [points[0], points[3]]).y.abs() < TOLERANCE),
}
}
fn is_simple(&self) -> bool {
match self {
Self::Line(_) => true,
Self::Cubic(points) => {
let a0 = angle(points[0], points[3], points[1]);
let a1 = angle(points[0], points[3], points[2]);
if (a0 > 0.0 && a1 < 0.0 || a0 < 0.0 && a1 > 0.0) && (a1 - a0).abs() >= TOLERANCE {
return false;
}
let n0 = normal(points[0], points[1], 3.0);
let n1 = normal(points[2], points[3], 3.0);
if let (Some(n0), Some(n1)) = (n0, n1) {
let s = n0.x * n1.x + n0.y * n1.y;
s.clamp(-1.0, 1.0).acos().abs() < std::f32::consts::PI / 3.0
} else {
false
}
}
}
}
pub fn points(&self) -> &[math::Vec] {
match self {
Self::Line(points) => points,
Self::Cubic(points) => points,
}
}
fn points_mut(&mut self) -> &mut [math::Vec] {
match self {
Self::Line(points) => points,
Self::Cubic(points) => points,
}
}
pub fn left_different(&self) -> [math::Vec; 2] {
left_different(self.points())
}
pub fn right_different(&self) -> [math::Vec; 2] {
right_different(self.points())
}
pub fn normalize(&self) -> Option<Self> {
match *self {
Self::Line([p0, p1]) => {
if !approx_eq(p0, p1) {
Some(self.clone())
} else {
None
}
}
Self::Cubic([p0, p1, p2, p3]) => {
if approx_eq(p0, p1) && approx_eq(p2, p3) {
Self::Line([p0, p3]).normalize()
} else {
Some(self.clone())
}
}
}
}
fn as_segment(&self) -> [math::Vec; 2] {
match self {
Self::Line(points) => *points,
Self::Cubic([p0, _, _, p3]) => [*p0, *p3],
}
}
pub fn intersect(&self, other: &Self) -> bool {
line_segment_intersection(self.as_segment(), other.as_segment()).is_some()
}
fn map(&self, mut f: impl FnMut(math::Vec) -> math::Vec) -> Self {
let mut clone = self.clone();
for point in clone.points_mut() {
*point = f(*point);
}
clone
}
pub fn rev(&self) -> Self {
match self {
Self::Line([p0, p1]) => Self::Line([*p1, *p0]),
Self::Cubic([p0, p1, p2, p3]) => Self::Cubic([*p3, *p2, *p1, *p0]),
}
}
fn extrema(&self) -> SmallVec<[f32; 8]> {
let mut extrema: SmallVec<[f32; 8]> = SmallVec::new();
let is_unit = |t: f32| (t.is_finite() && (0.0..=1.0).contains(&t)).then(|| t.abs());
if let Self::Cubic(points) = self {
let d1 = derive(points);
let d2 = derive(&d1);
let d1x: SmallVec<[_; 3]> = d1.iter().map(|p| p.x).collect();
let d2x: SmallVec<[_; 2]> = d2.iter().map(|p| p.x).collect();
let d1y: SmallVec<[_; 3]> = d1.iter().map(|p| p.y).collect();
let d2y: SmallVec<[_; 2]> = d2.iter().map(|p| p.y).collect();
extrema.extend(droots(&d1x).into_iter().filter_map(is_unit));
extrema.extend(droots(&d2x).into_iter().filter_map(is_unit));
extrema.extend(droots(&d1y).into_iter().filter_map(is_unit));
extrema.extend(droots(&d2y).into_iter().filter_map(is_unit));
}
extrema.sort_by(|&a, b| a.partial_cmp(b).unwrap());
extrema.dedup();
extrema
}
fn reduce(&self) -> SmallVec<[Self; 16]> {
const STEP: f32 = 0.01;
let mut extrema = self.extrema();
if extrema.first().map(|&e| e <= STEP) != Some(true) {
extrema.insert(0, 0.0);
}
if extrema.last().map(|&e| (e - 1.0).abs() <= STEP) != Some(true) {
extrema.push(1.0);
}
let mut pass0: SmallVec<[_; 8]> = SmallVec::new();
let mut pass1 = SmallVec::new();
if let Self::Cubic(points) = self {
for window in extrema.windows(2) {
if let [t0, t1] = *window {
pass0.push(split(points, t0, t1));
}
}
'outer: for segment_pass0 in pass0 {
let mut t0 = 0.0;
let mut t1 = 0.0;
while t1 <= 1.0 - TOLERANCE {
t1 = t0 + STEP;
while t1 <= 1.0 + STEP - TOLERANCE {
let segment = split(&segment_pass0, t0, t1);
let bezier = Self::Cubic(segment);
if !bezier.is_simple() {
t1 -= STEP;
if (t1 - t0).abs() < STEP {
if let Some(normalized) = Self::Cubic(segment_pass0).normalize() {
if t0 == 0.0 {
pass1.push(normalized);
}
}
continue 'outer;
}
pass1.push(Self::Cubic(split(&segment_pass0, t0, t1)));
t0 = t1;
break;
}
t1 += STEP;
}
}
if t0 < 1.0 {
pass1.push(Self::Cubic(split(&segment_pass0, t0, 1.0)));
}
}
}
if pass1.is_empty() {
pass1.push(self.clone());
}
pass1
}
pub fn offset(&self, dist: f32) -> SmallVec<[Self; 16]> {
if dist.is_sign_negative() {
return self.rev().offset(-dist);
}
let mut curves = SmallVec::new();
if self.is_linear() {
let normal = normal_left(self.points());
curves.push(
self.map(|point| {
math::Vec::new(point.x + dist * normal.x, point.y + dist * normal.y)
}),
);
return curves;
}
self.reduce()
.iter()
.filter_map(|bezier| {
bezier
.normalize()
.map(|bezier| {
if bezier.is_linear() {
bezier.offset(dist)[0].clone()
} else if let Self::Cubic(points) = bezier {
if let Some(scaled) = scale(&points, dist) {
Self::Cubic(scaled)
} else {
Self::Line([points[0], points[3]]).offset(dist)[0].clone()
}
} else {
unreachable!()
}
})
.as_ref()
.and_then(Bezier::normalize)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => {{
assert!(
($a - $b).abs() < TOLERANCE,
"assertion failed: `(left !== right)` \
(left: `{:?}`, right: `{:?}`)",
$a,
$b,
);
}};
}
#[test]
fn offset_line() {
let line = Bezier::Line([math::Vec::new(-0.5, -0.5), math::Vec::new(0.5, 0.5)]);
let pos_offset = line.offset(2.0f32.sqrt() / 2.0);
assert_eq!(pos_offset.len(), 1);
assert_eq!(pos_offset[0].points().len(), 2);
assert_approx_eq!(pos_offset[0].points()[0].x, -1.0);
assert_approx_eq!(pos_offset[0].points()[0].y, 0.0);
assert_approx_eq!(pos_offset[0].points()[1].x, 0.0);
assert_approx_eq!(pos_offset[0].points()[1].y, 1.0);
let neg_offset = line.offset(-(2.0f32.sqrt()) / 2.0);
assert_eq!(neg_offset.len(), 1);
assert_eq!(neg_offset[0].points().len(), 2);
assert_approx_eq!(neg_offset[0].points()[1].x, 0.0);
assert_approx_eq!(neg_offset[0].points()[1].y, -1.0);
assert_approx_eq!(neg_offset[0].points()[0].x, 1.0);
assert_approx_eq!(neg_offset[0].points()[0].y, 0.0);
}
}