| // 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); |
| } |
| } |