mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 04:48:12 +08:00
Support linear bezier curve segments in the Bezier math library (#717)
* Support Linear line segments, add linear section to interactive docs * Fix regression, customize points in UI examples, add optional subdivisions to length, minor refactors * Refactor ExamplePane, use better example curves * Update consts.rs comments * Code review changes * Address PR comments * Code review Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
ad7097ea92
commit
00cc50d531
+16
-15
@@ -1,20 +1,21 @@
|
||||
/// Implementation constants
|
||||
// Implementation constants
|
||||
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
/// A stricter constant used to determine if `f64`s are equivalent.
|
||||
pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6;
|
||||
/// Number of distances used in search algorithm for `project`.
|
||||
pub const NUM_DISTANCES: usize = 5;
|
||||
/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple.
|
||||
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
|
||||
|
||||
/// Method argument defaults
|
||||
pub const REDUCE_STEP_SIZE_DEFAULT: f64 = 0.01;
|
||||
// Method argument defaults
|
||||
|
||||
/// Default `t` value used for the `curve_through_points` functions
|
||||
/// Default `t` value used for the `curve_through_points` functions.
|
||||
pub const DEFAULT_T_VALUE: f64 = 0.5;
|
||||
|
||||
/// Default LUT step size in `compute_lookup_table` function
|
||||
/// Default LUT step size in `compute_lookup_table` function.
|
||||
pub const DEFAULT_LUT_STEP_SIZE: i32 = 10;
|
||||
|
||||
/// Number of subdivisions used in `length` calculation
|
||||
pub const LENGTH_SUBDIVISIONS: i32 = 1000;
|
||||
|
||||
/// Number of distances used in search algorithm for `project`
|
||||
pub const NUM_DISTANCES: usize = 5;
|
||||
|
||||
/// Constants used to determine if `f64`'s are equivalent
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
/// Default number of subdivisions used in `length` calculation.
|
||||
pub const DEFAULT_LENGTH_SUBDIVISIONS: i32 = 1000;
|
||||
/// Default step size for `reduce` function.
|
||||
pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01;
|
||||
|
||||
+161
-77
@@ -4,11 +4,13 @@ mod consts;
|
||||
mod utils;
|
||||
|
||||
use consts::*;
|
||||
|
||||
use glam::{DMat2, DVec2};
|
||||
|
||||
/// Representation of the handle point(s) in a bezier segment.
|
||||
#[derive(Copy, Clone)]
|
||||
enum BezierHandles {
|
||||
Linear,
|
||||
/// Handles for a quadratic curve.
|
||||
Quadratic {
|
||||
/// Point representing the location of the single handle.
|
||||
@@ -59,6 +61,25 @@ pub struct Bezier {
|
||||
}
|
||||
|
||||
impl Bezier {
|
||||
// TODO: Consider removing this function
|
||||
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
|
||||
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::new(x1, y1),
|
||||
handles: BezierHandles::Linear,
|
||||
end: DVec2::new(x2, y2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
|
||||
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
|
||||
Bezier {
|
||||
start: p1,
|
||||
handles: BezierHandles::Linear,
|
||||
end: p2,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider removing this function
|
||||
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
|
||||
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
|
||||
@@ -148,14 +169,15 @@ impl Bezier {
|
||||
// TODO: Allow modifying the viewport, width and height
|
||||
let m_path = format!("M {} {}", self.start.x, self.start.y);
|
||||
let handles_path = match self.handles {
|
||||
BezierHandles::Linear => "L".to_string(),
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
format!("Q {} {}", handle.x, handle.y)
|
||||
format!("Q {} {},", handle.x, handle.y)
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
format!("C {} {}, {} {}", handle_start.x, handle_start.y, handle_end.x, handle_end.y)
|
||||
format!("C {} {}, {} {},", handle_start.x, handle_start.y, handle_end.x, handle_end.y)
|
||||
}
|
||||
};
|
||||
let curve_path = format!("{}, {} {}", handles_path, self.end.x, self.end.y);
|
||||
let curve_path = format!("{} {} {}", handles_path, self.end.x, self.end.y);
|
||||
format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}px"><path d="{} {} {}" stroke="black" fill="transparent"/></svg>"#,
|
||||
0, 0, 100, 100, 100, 100, "\n", m_path, curve_path
|
||||
@@ -172,9 +194,12 @@ impl Bezier {
|
||||
self.end = e;
|
||||
}
|
||||
|
||||
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment.
|
||||
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic.
|
||||
pub fn set_handle_start(&mut self, h1: DVec2) {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => {
|
||||
self.handles = BezierHandles::Quadratic { handle: h1 };
|
||||
}
|
||||
BezierHandles::Quadratic { ref mut handle } => {
|
||||
*handle = h1;
|
||||
}
|
||||
@@ -184,9 +209,15 @@ impl Bezier {
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the coordinates of the second handle point. This will convert a quadratic segment into a cubic one.
|
||||
/// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point.
|
||||
pub fn set_handle_end(&mut self, h2: DVec2) {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => {
|
||||
self.handles = BezierHandles::Cubic {
|
||||
handle_start: self.start,
|
||||
handle_end: h2,
|
||||
};
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 };
|
||||
}
|
||||
@@ -207,39 +238,45 @@ impl Bezier {
|
||||
}
|
||||
|
||||
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
|
||||
pub fn handle_start(&self) -> DVec2 {
|
||||
pub fn handle_start(&self) -> Option<DVec2> {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { handle } => handle,
|
||||
BezierHandles::Cubic { handle_start, .. } => handle_start,
|
||||
BezierHandles::Linear => None,
|
||||
BezierHandles::Quadratic { handle } => Some(handle),
|
||||
BezierHandles::Cubic { handle_start, .. } => Some(handle_start),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
|
||||
pub fn handle_end(&self) -> Option<DVec2> {
|
||||
match self.handles {
|
||||
BezierHandles::Linear { .. } => None,
|
||||
BezierHandles::Quadratic { .. } => None,
|
||||
BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coordinates of all points in an array of 4 optional points.
|
||||
/// For a quadratic segment, the order of the points will be: `start`, `handle`, `end`. The fourth element will be `None`.
|
||||
/// For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
|
||||
pub fn get_points(&self) -> [Option<DVec2>; 4] {
|
||||
/// Get an iterator over the coordinates of all points in a vector.
|
||||
/// - For a linear segment, the order of the points will be: `start`, `end`.
|
||||
/// - For a quadratic segment, the order of the points will be: `start`, `handle`, `end`.
|
||||
/// - For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
|
||||
pub fn get_points(&self) -> impl Iterator<Item = DVec2> {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { handle } => [Some(self.start), Some(handle), Some(self.end), None],
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [Some(self.start), Some(handle_start), Some(handle_end), Some(self.end)],
|
||||
BezierHandles::Linear => [self.start, self.end, DVec2::ZERO, DVec2::ZERO].into_iter().take(2),
|
||||
BezierHandles::Quadratic { handle } => [self.start, handle, self.end, DVec2::ZERO].into_iter().take(3),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [self.start, handle_start, handle_end, self.end].into_iter().take(4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the point on the curve based on the `t`-value provided.
|
||||
/// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>.
|
||||
fn unrestricted_compute(&self, t: f64) -> DVec2 {
|
||||
fn unrestricted_evaluate(&self, t: f64) -> DVec2 {
|
||||
// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>.
|
||||
|
||||
let t_squared = t * t;
|
||||
let one_minus_t = 1.0 - t;
|
||||
let squared_one_minus_t = one_minus_t * one_minus_t;
|
||||
|
||||
match self.handles {
|
||||
BezierHandles::Linear => self.start.lerp(self.end, t),
|
||||
BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2.0 * one_minus_t * t * handle + t_squared * self.end,
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let t_cubed = t_squared * t;
|
||||
@@ -251,9 +288,9 @@ impl Bezier {
|
||||
|
||||
/// Calculate the point on the curve based on the `t`-value provided.
|
||||
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
||||
pub fn compute(&self, t: f64) -> DVec2 {
|
||||
pub fn evaluate(&self, t: f64) -> DVec2 {
|
||||
assert!((0.0..=1.0).contains(&t));
|
||||
self.unrestricted_compute(t)
|
||||
self.unrestricted_evaluate(t)
|
||||
}
|
||||
|
||||
/// Return a selection of equidistant points on the bezier curve.
|
||||
@@ -264,90 +301,102 @@ impl Bezier {
|
||||
let mut steps_array = Vec::with_capacity((steps_unwrapped + 1) as usize);
|
||||
|
||||
for t in 0..steps_unwrapped + 1 {
|
||||
steps_array.push(self.compute(f64::from(t) * ratio))
|
||||
steps_array.push(self.evaluate(f64::from(t) * ratio))
|
||||
}
|
||||
|
||||
steps_array
|
||||
}
|
||||
|
||||
/// Return an approximation of the length of the bezier curve.
|
||||
pub fn length(&self) -> f64 {
|
||||
// Code example from <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>.
|
||||
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is 1000.
|
||||
pub fn length(&self, num_subdivisions: Option<i32>) -> f64 {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => self.start.distance(self.end),
|
||||
_ => {
|
||||
// Code example from <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>.
|
||||
|
||||
// We will use an approximate approach where
|
||||
// we split the curve into many subdivisions
|
||||
// and calculate the euclidean distance between the two endpoints of the subdivision
|
||||
let lookup_table = self.compute_lookup_table(Some(LENGTH_SUBDIVISIONS));
|
||||
let mut approx_curve_length = 0.0;
|
||||
let mut prev_point = lookup_table[0];
|
||||
// calculate approximate distance between subdivision
|
||||
for curr_point in lookup_table.iter().skip(1) {
|
||||
// calculate distance of subdivision
|
||||
approx_curve_length += (*curr_point - prev_point).length();
|
||||
// update the prev point
|
||||
prev_point = *curr_point;
|
||||
// We will use an approximate approach where we split the curve into many subdivisions
|
||||
// and calculate the euclidean distance between the two endpoints of the subdivision
|
||||
let lookup_table = self.compute_lookup_table(Some(num_subdivisions.unwrap_or(DEFAULT_LENGTH_SUBDIVISIONS)));
|
||||
let mut approx_curve_length = 0.0;
|
||||
let mut previous_point = lookup_table[0];
|
||||
// Calculate approximate distance between subdivision
|
||||
for current_point in lookup_table.iter().skip(1) {
|
||||
// Calculate distance of subdivision
|
||||
approx_curve_length += (*current_point - previous_point).length();
|
||||
// Update the previous point
|
||||
previous_point = *current_point;
|
||||
}
|
||||
|
||||
approx_curve_length
|
||||
}
|
||||
}
|
||||
|
||||
approx_curve_length
|
||||
}
|
||||
|
||||
/// Returns a vector representing the derivative at the point designated by `t` on the curve.
|
||||
pub fn derivative(&self, t: f64) -> DVec2 {
|
||||
let one_minus_t = 1. - t;
|
||||
/// Returns a Bezier representing the derivative of the original curve.
|
||||
/// - This function returns `None` for a linear segment.
|
||||
pub fn derivative(&self) -> Option<Bezier> {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => None,
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let p1_minus_p0 = handle - self.start;
|
||||
let p2_minus_p1 = self.end - handle;
|
||||
2. * one_minus_t * p1_minus_p0 + 2. * t * p2_minus_p1
|
||||
Some(Bezier::from_linear_dvec2(2. * p1_minus_p0, 2. * p2_minus_p1))
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let p1_minus_p0 = handle_start - self.start;
|
||||
let p2_minus_p1 = handle_end - handle_start;
|
||||
let p3_minus_p2 = self.end - handle_end;
|
||||
3. * one_minus_t * one_minus_t * p1_minus_p0 + 6. * t * one_minus_t * p2_minus_p1 + 3. * t * t * p3_minus_p2
|
||||
Some(Bezier::from_quadratic_dvec2(3. * p1_minus_p0, 3. * p2_minus_p1, 3. * p3_minus_p2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve.
|
||||
pub fn tangent(&self, t: f64) -> DVec2 {
|
||||
self.derivative(t).normalize()
|
||||
match self.handles {
|
||||
BezierHandles::Linear => self.end - self.start,
|
||||
_ => self.derivative().unwrap().evaluate(t),
|
||||
}
|
||||
.normalize()
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve.
|
||||
pub fn normal(&self, t: f64) -> DVec2 {
|
||||
let derivative = self.derivative(t);
|
||||
derivative.normalize().perp()
|
||||
self.tangent(t).perp()
|
||||
}
|
||||
|
||||
/// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`.
|
||||
pub fn split(&self, t: f64) -> [Bezier; 2] {
|
||||
let split_point = self.compute(t);
|
||||
|
||||
let t_squared = t * t;
|
||||
let t_minus_one = t - 1.;
|
||||
let squared_t_minus_one = t_minus_one * t_minus_one;
|
||||
let split_point = self.evaluate(t);
|
||||
|
||||
match self.handles {
|
||||
BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)],
|
||||
// TODO: Actually calculate the correct handle locations
|
||||
BezierHandles::Quadratic { handle } => [
|
||||
Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point),
|
||||
Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end),
|
||||
],
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [
|
||||
Bezier::from_cubic_dvec2(
|
||||
self.start,
|
||||
t * handle_start - t_minus_one * self.start,
|
||||
t_squared * handle_end - 2. * t * t_minus_one * handle_start + squared_t_minus_one * self.start,
|
||||
split_point,
|
||||
),
|
||||
Bezier::from_cubic_dvec2(
|
||||
split_point,
|
||||
t_squared * self.end - 2. * t * t_minus_one * handle_end + squared_t_minus_one * handle_start,
|
||||
t * self.end - t_minus_one * handle_end,
|
||||
self.end,
|
||||
),
|
||||
],
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let t_minus_one = t - 1.;
|
||||
[
|
||||
Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point),
|
||||
Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end),
|
||||
]
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let t_minus_one = t - 1.;
|
||||
[
|
||||
Bezier::from_cubic_dvec2(
|
||||
self.start,
|
||||
t * handle_start - t_minus_one * self.start,
|
||||
(t * t) * handle_end - 2. * t * t_minus_one * handle_start + (t_minus_one * t_minus_one) * self.start,
|
||||
split_point,
|
||||
),
|
||||
Bezier::from_cubic_dvec2(
|
||||
split_point,
|
||||
(t * t) * self.end - 2. * t * t_minus_one * handle_end + (t_minus_one * t_minus_one) * handle_start,
|
||||
t * self.end - t_minus_one * handle_end,
|
||||
self.end,
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +428,7 @@ impl Bezier {
|
||||
iteration_limit,
|
||||
} = options;
|
||||
|
||||
// TODO: Consider optimizations from precomputing useful values, or using the GPU
|
||||
// First find the closest point from the results of a lookup table
|
||||
let lut = self.compute_lookup_table(Some(lut_size));
|
||||
let (minimum_position, minimum_distance) = utils::get_closest_point_in_lut(&lut, point);
|
||||
@@ -421,7 +471,7 @@ impl Bezier {
|
||||
if step_index == 0 {
|
||||
distance = *table_distance;
|
||||
} else {
|
||||
distance = point.distance(self.compute(iterator_t));
|
||||
distance = point.distance(self.evaluate(iterator_t));
|
||||
*table_distance = distance;
|
||||
}
|
||||
if distance < new_minimum_distance {
|
||||
@@ -456,13 +506,14 @@ impl Bezier {
|
||||
}
|
||||
}
|
||||
|
||||
self.compute(final_t)
|
||||
self.evaluate(final_t)
|
||||
}
|
||||
|
||||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
|
||||
/// The local extrema are defined to be points at which the derivative of the curve is equal to zero.
|
||||
fn unrestricted_local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => [Vec::new(), Vec::new()],
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let a = handle - self.start;
|
||||
let b = self.end - handle;
|
||||
@@ -499,6 +550,7 @@ impl Bezier {
|
||||
let transformed_start = transformation_function(self.start);
|
||||
let transformed_end = transformation_function(self.end);
|
||||
match self.handles {
|
||||
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let transformed_handle = transformation_function(handle);
|
||||
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
|
||||
@@ -522,7 +574,10 @@ impl Bezier {
|
||||
self.apply_transformation(&|point| point + translation)
|
||||
}
|
||||
|
||||
/// Returns a list of points where the provided line segment intersects with the Bezier curve.
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
// TODO: Change this to `intersect(&self, other: &Bezier)` to also work on quadratic and cubic segments
|
||||
// TODO: (or keep this and add two more functions that perform the logic, and make the `intersect` function call the correct one)
|
||||
/// Returns a list of points where the provided line segment intersects with the Bezier curve. If the provided segment is colinear with the bezier, zero intersection points will be returned.
|
||||
/// - `line` - A line segment expected to be received in the format of `[start_point, end_point]`.
|
||||
pub fn intersect_line_segment(&self, line: [DVec2; 2]) -> Vec<DVec2> {
|
||||
// Rotate the bezier and the line by the angle that the line makes with the x axis
|
||||
@@ -538,6 +593,12 @@ impl Bezier {
|
||||
|
||||
// Compute the roots of the resulting bezier curve
|
||||
let list_intersection_t = match translated_bezier.handles {
|
||||
BezierHandles::Linear => {
|
||||
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
|
||||
let a = translated_bezier.end.y - translated_bezier.start.y;
|
||||
let b = translated_bezier.start.y;
|
||||
utils::solve_linear(a, b)
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
|
||||
let b = 2. * (handle.y - translated_bezier.start.y);
|
||||
@@ -558,13 +619,14 @@ impl Bezier {
|
||||
utils::solve_cubic(a, b, c, d)
|
||||
}
|
||||
};
|
||||
|
||||
let min = line[0].min(line[1]);
|
||||
let max = line[0].max(line[1]);
|
||||
|
||||
list_intersection_t
|
||||
.iter()
|
||||
.filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
|
||||
.map(|&t| self.unrestricted_compute(t))
|
||||
.map(|&t| self.unrestricted_evaluate(t))
|
||||
.filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all())
|
||||
.collect::<Vec<DVec2>>()
|
||||
}
|
||||
@@ -594,7 +656,12 @@ impl Bezier {
|
||||
/// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`.
|
||||
/// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time.
|
||||
pub fn reduce(&self, step_size: Option<f64>) -> Vec<Bezier> {
|
||||
let step_size = step_size.unwrap_or(REDUCE_STEP_SIZE_DEFAULT);
|
||||
// A linear segment is scalable, so return itself
|
||||
if let BezierHandles::Linear = self.handles {
|
||||
return vec![*self];
|
||||
}
|
||||
|
||||
let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE);
|
||||
|
||||
let mut extrema: Vec<f64> = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
|
||||
extrema.append(&mut vec![0., 1.]);
|
||||
@@ -675,13 +742,13 @@ mod tests {
|
||||
let p3 = DVec2::new(160., 170.);
|
||||
|
||||
let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None);
|
||||
assert!(compare_points(bezier1.compute(0.5), p2));
|
||||
assert!(compare_points(bezier1.evaluate(0.5), p2));
|
||||
|
||||
let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8));
|
||||
assert!(compare_points(bezier2.compute(0.8), p2));
|
||||
assert!(compare_points(bezier2.evaluate(0.8), p2));
|
||||
|
||||
let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.));
|
||||
assert!(compare_points(bezier3.compute(0.), p2));
|
||||
assert!(compare_points(bezier3.evaluate(0.), p2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -691,13 +758,13 @@ mod tests {
|
||||
let p3 = DVec2::new(160., 160.);
|
||||
|
||||
let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.));
|
||||
assert!(compare_points(bezier1.compute(0.3), p2));
|
||||
assert!(compare_points(bezier1.evaluate(0.3), p2));
|
||||
|
||||
let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7));
|
||||
assert!(compare_points(bezier2.compute(0.8), p2));
|
||||
assert!(compare_points(bezier2.evaluate(0.8), p2));
|
||||
|
||||
let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7));
|
||||
assert!(compare_points(bezier3.compute(0.), p2));
|
||||
assert!(compare_points(bezier3.evaluate(0.), p2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -711,6 +778,23 @@ mod tests {
|
||||
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
|
||||
assert!(bezier2.project(DVec2::new(100., 0.), project_options) == DVec2::new(0., 0.));
|
||||
}
|
||||
#[test]
|
||||
fn intersect_line_segment_linear() {
|
||||
let p1 = DVec2::new(30., 60.);
|
||||
let p2 = DVec2::new(140., 120.);
|
||||
|
||||
// Intersection at edge of curve
|
||||
let bezier1 = Bezier::from_linear_dvec2(p1, p2);
|
||||
let line1 = [DVec2::new(20., 60.), DVec2::new(70., 60.)];
|
||||
let intersections1 = bezier1.intersect_line_segment(line1);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(intersections1[0], DVec2::new(30., 60.)));
|
||||
|
||||
// Intersection in the middle of curve
|
||||
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
|
||||
let intersections2 = bezier1.intersect_line_segment(line2);
|
||||
assert!(compare_points(intersections2[0], DVec2::new(96., 96.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_line_segment_quadratic() {
|
||||
|
||||
+42
-28
@@ -1,3 +1,5 @@
|
||||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
|
||||
use glam::{BVec2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
@@ -40,15 +42,18 @@ pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (i32, f64) {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// TODO: Use an `Option` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax + b`.
|
||||
pub fn solve_linear(a: f64, b: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
if a != 0. {
|
||||
// There exist roots when `a` is not 0
|
||||
if a.abs() > MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots.push(-b / a);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax^2 + bx + c`.
|
||||
/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes.
|
||||
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec<f64> {
|
||||
@@ -76,35 +81,39 @@ fn cube_root(f: f64) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>.
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
if p == 0. {
|
||||
if p.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// Handle when p is approximately 0
|
||||
roots.push(cube_root(-q));
|
||||
} else if q == 0. {
|
||||
} else if q.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// Handle when q is approximately 0
|
||||
if p < 0. {
|
||||
roots.push((-p).powf(1. / 2.));
|
||||
}
|
||||
} else if discriminant == 0. {
|
||||
} else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// When discriminant is 0 (check for approximation because of floating point errors), all roots are real, and 2 are repeated
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let a_divided_by_3 = a / 3.;
|
||||
// all roots are real, and 2 are repeated
|
||||
|
||||
roots.push(2. * cube_root(-q_divided_by_2) - a_divided_by_3);
|
||||
roots.push(cube_root(q_divided_by_2) - a_divided_by_3);
|
||||
} else if discriminant > 0. {
|
||||
// one real and two imaginary roots
|
||||
// When discriminant > 0, there is one real and two imaginary roots
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let square_root_discriminant = discriminant.powf(1. / 2.);
|
||||
|
||||
roots.push(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
|
||||
} else {
|
||||
// three real roots
|
||||
// Otherwise, discriminant < 0 and there are three real roots
|
||||
let p_divided_by_3 = p / 3.;
|
||||
let a_divided_by_3 = a / 3.;
|
||||
let cube_root_r = (-p_divided_by_3).powf(1. / 2.);
|
||||
let phi = (-q / (2. * cube_root_r.powi(3))).acos();
|
||||
|
||||
let two_times_cube_root_r = 2. * cube_root_r;
|
||||
// three real roots
|
||||
roots.push(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
|
||||
@@ -112,10 +121,11 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `ax^3 + bx^2 + ct + d`.
|
||||
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
|
||||
if a.abs() <= 1e-5 {
|
||||
if b.abs() <= 1e-5 {
|
||||
if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// If both a and b are approximately 0, treat as a linear problem
|
||||
solve_linear(c, d)
|
||||
} else {
|
||||
@@ -161,44 +171,48 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
|
||||
/// Compare vectors of `f64`s with a provided max absolute value difference.
|
||||
fn f64_compare_vector(vec1: Vec<f64>, vec2: Vec<f64>, max_abs_diff: f64) -> bool {
|
||||
vec1.len() == vec2.len() && vec1.into_iter().zip(vec2.into_iter()).all(|(a, b)| f64_compare(a, b, max_abs_diff))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_linear() {
|
||||
// Line that is on the x-axis
|
||||
assert!(solve_linear(0., 0.).is_empty());
|
||||
// Line that is parallel to but not on the x-axis
|
||||
assert!(solve_linear(0., 1.).is_empty());
|
||||
// Line with a non-zero slope
|
||||
assert!(solve_linear(2., -8.) == vec![4.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_cubic() {
|
||||
// discriminant == 0
|
||||
let roots1 = solve_cubic(1., 0., 0., 0.);
|
||||
assert!(roots1.len() == 1);
|
||||
assert!(roots1[0] == 0.);
|
||||
assert!(roots1 == vec![0.]);
|
||||
|
||||
let roots2 = solve_cubic(1., 3., 0., -4.);
|
||||
assert!(roots2.len() == 2);
|
||||
assert!(roots2[0] == 1.);
|
||||
assert!(roots2[1] == -2.);
|
||||
assert!(roots2 == vec![1., -2.]);
|
||||
|
||||
// p == 0
|
||||
let roots3 = solve_cubic(1., 0., 0., -1.);
|
||||
assert!(roots3.len() == 1);
|
||||
assert!(roots3[0] == 1.);
|
||||
assert!(roots3 == vec![1.]);
|
||||
|
||||
// discriminant > 0
|
||||
let roots4 = solve_cubic(1., 3., 0., 2.);
|
||||
assert!(roots4.len() == 1);
|
||||
assert!(f64_compare(roots4[0], -3.196, MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// discriminant < 0
|
||||
let roots5 = solve_cubic(1., 3., 0., -1.);
|
||||
assert!(roots5.len() == 3);
|
||||
assert!(f64_compare(roots5[0], 0.532, MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(f64_compare(roots5[1], -2.879, MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(f64_compare(roots5[2], -0.653, MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(f64_compare_vector(roots5, vec![0.532, -2.879, -0.653], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// quadratic
|
||||
let roots6 = solve_cubic(0., 3., 0., -3.);
|
||||
assert!(roots6.len() == 2);
|
||||
assert!(roots6[0] == 1.);
|
||||
assert!(roots6[1] == -1.);
|
||||
assert!(roots6 == vec![1., -1.]);
|
||||
|
||||
// linear
|
||||
let roots7 = solve_cubic(0., 0., 1., -1.);
|
||||
assert!(roots7.len() == 1);
|
||||
assert!(roots7[0] == 1.);
|
||||
assert!(roots7 == vec![1.]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user