Bezier-rs: Add parametric evaluate and line intersect to subpath (#852)

* add slider to subpath component + change evaluate to take an enum

Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com>

wip - add intersect to subpath, TODO fix bug

Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com>

add unit tests to subpath intersections

stress, testing

Co-authored-by: Hannah Li <hannahli2010@gmail.com>

* add parametric eval impl to subpath

* add line intersection to subpath

* Uncomment and #[ignore] disabled tests

* Reorder a few imports

* change subpath:eval slider to radio button

* fixed bug with solve_cubic, fixed unit tests, improved intersection accuracy

* fix failing test

Co-authored-by: Hannah Li <hannahli2010@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Thomas Cheng 2022-12-11 00:41:02 -05:00 committed by Keavon Chambers
parent 9a4af4f87a
commit 52cc770a1e
13 changed files with 713 additions and 54 deletions

View file

@ -222,11 +222,36 @@ impl Bezier {
}
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of filtered `t` values that correspond to intersection points between the current bezier curve and the provided one
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum seperation value. If the difference
/// between 2 adjacent `t` values is lesss than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
/// The returned `t` values are with respect to the current bezier, not the provided parameter.
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
/// - `minimum_seperation` - The minimum difference between adjacent `t` values in sorted order
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> {
// TODO: Consider using the `intersections_between_vectors_of_curves` helper function here
// Otherwise, use bounding box to determine intersections
let mut intersection_t_values = self.unfiltered_intersections(other, error);
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
// println!("<<<<< intersection_t_values :: {:?}", intersection_t_values);
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
accumulator.pop();
}
accumulator.push(*t);
accumulator
})
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of `t` values that correspond to intersection points between the current bezier curve and the provided one. The returned `t` values are with respect to the current bezier, not the provided parameter.
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
pub fn intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<f64> {
fn unfiltered_intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<f64> {
let error = error.unwrap_or(0.5);
if other.handles == BezierHandles::Linear {
// Rotate the bezier and the line by the angle that the line makes with the x axis
@ -295,7 +320,7 @@ impl Bezier {
let segment_pairs = subcurves1.iter().flat_map(move |(curve1, curve1_t_pair)| {
subcurves2
.iter()
.filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then(|| (curve1, curve1_t_pair, curve2, curve2_t_pair)))
.filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then_some((curve1, curve1_t_pair, curve2, curve2_t_pair)))
});
segment_pairs
.flat_map(|(curve1, curve1_t_pair, curve2, curve2_t_pair)| curve1.intersections_between_subcurves(curve1_t_pair.clone(), curve2, curve2_t_pair.clone(), error))
@ -563,13 +588,13 @@ mod tests {
// Intersection at edge of curve
let bezier = Bezier::from_linear_dvec2(p1, p2);
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
let intersections1 = bezier.intersections(&line1, None);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), DVec2::new(30., 60.)));
// Intersection in the middle of curve
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
let intersections2 = bezier.intersections(&line2, None, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.)));
}
@ -582,13 +607,13 @@ mod tests {
// Intersection at edge of curve
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
let intersections1 = bezier.intersections(&line1, None);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
// Intersection in the middle of curve
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
let intersections2 = bezier.intersections(&line2, None, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354)));
}
@ -602,30 +627,63 @@ mod tests {
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
// Intersection at edge of curve, Discriminant > 0
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
let intersections1 = bezier.intersections(&line1, None);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
// Intersection at edge and in middle of curve, Discriminant < 0
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
let intersections2 = bezier.intersections(&line2, None, None);
assert!(intersections2.len() == 2);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), p1));
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[1])), DVec2::new(85.84, 85.84)));
}
#[test]
fn test_intersect_curve_cubic_anchor_handle_overlap() {
// M31 94 C40 40 107 107 106 106
let p1 = DVec2::new(31., 94.);
let p2 = DVec2::new(40., 40.);
let p3 = DVec2::new(107., 107.);
let p4 = DVec2::new(106., 106.);
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let intersections = bezier.intersections(&line, None, None);
assert_eq!(intersections.len(), 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections[0])), p4));
}
#[test]
fn test_intersect_curve_cubic_edge_case() {
// M34 107 C40 40 120 120 102 29
let p1 = DVec2::new(34., 107.);
let p2 = DVec2::new(40., 40.);
let p3 = DVec2::new(120., 120.);
let p4 = DVec2::new(102., 29.);
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let intersections = bezier.intersections(&line, None, None);
assert_eq!(intersections.len(), 1);
}
#[test]
fn test_intersect_curve() {
let bezier1 = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let bezier2 = Bezier::from_quadratic_coordinates(175., 140., 20., 20., 120., 20.);
let intersections = bezier1.intersections(&bezier2, None);
let intersections2 = bezier2.intersections(&bezier1, None);
assert!(compare_vec_of_points(
intersections.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect(),
intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).collect(),
2.
));
let intersections1 = bezier1.intersections(&bezier2, None, None);
let intersections2 = bezier2.intersections(&bezier1, None, None);
let intersections1_points: Vec<DVec2> = intersections1.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect();
let intersections2_points: Vec<DVec2> = intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).rev().collect();
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
}
#[test]

View file

@ -8,6 +8,8 @@ pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6;
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.;
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3;
// Method argument defaults

View file

@ -1,5 +1,6 @@
use super::*;
use crate::consts::*;
use std::fmt::Write;
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
@ -40,6 +41,15 @@ impl Subpath {
self.manipulator_groups.len()
}
/// Returns the number of segments contained within the `Subpath`.
pub fn len_segments(&self) -> usize {
let mut number_of_curves = self.len();
if !self.closed {
number_of_curves -= 1
}
number_of_curves
}
/// Returns an iterator of the [Bezier]s along the `Subpath`.
pub fn iter(&self) -> SubpathIter {
SubpathIter { sub_path: self, index: 0 }

View file

@ -1,5 +1,6 @@
mod core;
mod lookup;
mod solvers;
mod structs;
pub use structs::*;

View file

@ -0,0 +1,405 @@
use super::*;
use crate::{consts::MIN_SEPERATION_VALUE, ComputeType};
use glam::DVec2;
impl Subpath {
/// Calculate the point on the subpath based on the parametric `t`-value provided.
/// Expects `t` to be within the inclusive range `[0, 1]`.
pub fn evaluate(&self, t: ComputeType) -> DVec2 {
match t {
ComputeType::Parametric(t) => {
assert!((0.0..=1.).contains(&t));
let number_of_curves = self.len_segments() as f64;
let scaled_t = t * number_of_curves;
let target_curve_index = scaled_t.floor() as i32;
let target_curve_t = scaled_t % 1.;
if let Some(curve) = self.iter().nth(target_curve_index as usize) {
curve.evaluate(ComputeType::Parametric(target_curve_t))
} else {
self.iter().last().unwrap().evaluate(ComputeType::Parametric(1.))
}
}
// TODO: change this implementation to Euclidean compute
ComputeType::Euclidean(_t) => self.iter().next().unwrap().evaluate(ComputeType::Parametric(0.)),
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(),
}
}
/// Calculates the intersection points the subpath has with a given line and returns a list of parameteric `t`-values.
/// This function expects the following:
/// - other: a [Bezier] curve to check intersections against
/// - error: an optional f64 value to provide an error bound
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> {
// TODO: account for either euclidean or parametric type
let number_of_curves = self.len_segments() as f64;
let intersection_t_values: Vec<f64> = self
.iter()
.enumerate()
.flat_map(|(index, bezier)| {
bezier
.intersections(other, error, minimum_seperation)
.into_iter()
.map(|t| ((index as f64) + t) / number_of_curves)
.collect::<Vec<f64>>()
})
.collect();
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
accumulator.pop();
}
accumulator.push(*t);
accumulator
});
intersection_t_values
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Bezier;
use glam::DVec2;
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
use crate::utils;
fn normalize_t(n: i64, t: f64) -> f64 {
t * (n as f64) % 1.
}
#[test]
fn evaluate_one_subpath_curve() {
let start = DVec2::new(20., 30.);
let end = DVec2::new(60., 45.);
let handle = DVec2::new(75., 85.);
let bezier = Bezier::from_quadratic_dvec2(start, handle, end);
let subpath = Subpath::new(
vec![
ManipulatorGroup {
anchor: start,
in_handle: None,
out_handle: Some(handle),
},
ManipulatorGroup {
anchor: end,
in_handle: None,
out_handle: Some(handle),
},
],
false,
);
let t0 = 0.;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t0)), bezier.evaluate(ComputeType::Parametric(t0)));
let t1 = 0.25;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t1)), bezier.evaluate(ComputeType::Parametric(t1)));
let t2 = 0.50;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t2)), bezier.evaluate(ComputeType::Parametric(t2)));
let t3 = 1.;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t3)), bezier.evaluate(ComputeType::Parametric(t3)));
}
#[test]
fn evaluate_multiple_subpath_curves() {
let start = DVec2::new(20., 30.);
let middle = DVec2::new(70., 70.);
let end = DVec2::new(60., 45.);
let handle1 = DVec2::new(75., 85.);
let handle2 = DVec2::new(40., 30.);
let handle3 = DVec2::new(10., 10.);
let linear_bezier = Bezier::from_linear_dvec2(start, middle);
let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end);
let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start);
let mut subpath = Subpath::new(
vec![
ManipulatorGroup {
anchor: start,
in_handle: Some(handle3),
out_handle: None,
},
ManipulatorGroup {
anchor: middle,
in_handle: None,
out_handle: Some(handle1),
},
ManipulatorGroup {
anchor: end,
in_handle: None,
out_handle: Some(handle2),
},
],
false,
);
// Test open subpath
let mut n = (subpath.len() as i64) - 1;
let t0 = 0.;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t0)),
linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t0))),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
let t1 = 0.25;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t1)),
linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t1))),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
let t2 = 0.50;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t2)),
quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t2))),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
let t3 = 0.75;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t3)),
quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t3))),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
let t4 = 1.0;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t4)),
quadratic_bezier.evaluate(ComputeType::Parametric(1.)),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
// Test closed subpath
subpath.closed = true;
n = subpath.len() as i64;
let t5 = 2. / 3.;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t5)),
cubic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t5))),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
let t6 = 1.;
assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t6)),
cubic_bezier.evaluate(ComputeType::Parametric(1.)),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
}
#[test]
fn intersection_linear_multiple_subpath_curves_test_one() {
// M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z
let cubic_start = DVec2::new(35., 125.);
let cubic_handle_1 = DVec2::new(40., 40.);
let cubic_handle_2 = DVec2::new(120., 120.);
let cubic_end = DVec2::new(43., 43.);
let quadratic_1_handle = DVec2::new(175., 90.);
let quadratic_end = DVec2::new(145., 150.);
let quadratic_2_handle = DVec2::new(70., 185.);
let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end);
let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end);
let subpath = Subpath::new(
vec![
ManipulatorGroup {
anchor: cubic_start,
in_handle: None,
out_handle: Some(cubic_handle_1),
},
ManipulatorGroup {
anchor: cubic_end,
in_handle: Some(cubic_handle_2),
out_handle: None,
},
ManipulatorGroup {
anchor: quadratic_end,
in_handle: Some(quadratic_1_handle),
out_handle: Some(quadratic_2_handle),
},
],
true,
);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let cubic_intersections = cubic_bezier.intersections(&line, None, None);
let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None);
let subpath_intersections = subpath.intersections(&line, None, None);
assert!(utils::dvec2_compare(
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
}
#[test]
fn intersection_linear_multiple_subpath_curves_test_two() {
// M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z
// M150 150 L 20 20
let cubic_start = DVec2::new(34., 107.);
let cubic_handle_1 = DVec2::new(40., 40.);
let cubic_handle_2 = DVec2::new(120., 120.);
let cubic_end = DVec2::new(102., 29.);
let quadratic_1_handle = DVec2::new(175., 90.);
let quadratic_end = DVec2::new(129., 171.);
let quadratic_2_handle = DVec2::new(70., 185.);
let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end);
let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end);
let subpath = Subpath::new(
vec![
ManipulatorGroup {
anchor: cubic_start,
in_handle: None,
out_handle: Some(cubic_handle_1),
},
ManipulatorGroup {
anchor: cubic_end,
in_handle: Some(cubic_handle_2),
out_handle: None,
},
ManipulatorGroup {
anchor: quadratic_end,
in_handle: Some(quadratic_1_handle),
out_handle: Some(quadratic_2_handle),
},
],
true,
);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let cubic_intersections = cubic_bezier.intersections(&line, None, None);
let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None);
let subpath_intersections = subpath.intersections(&line, None, None);
assert!(utils::dvec2_compare(
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
}
#[test]
fn intersection_linear_multiple_subpath_curves_test_three() {
// M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z
let cubic_start = DVec2::new(35., 125.);
let cubic_handle_1 = DVec2::new(40., 40.);
let cubic_handle_2 = DVec2::new(120., 120.);
let cubic_end = DVec2::new(44., 44.);
let quadratic_1_handle = DVec2::new(175., 90.);
let quadratic_end = DVec2::new(145., 150.);
let quadratic_2_handle = DVec2::new(70., 185.);
let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end);
let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end);
let subpath = Subpath::new(
vec![
ManipulatorGroup {
anchor: cubic_start,
in_handle: None,
out_handle: Some(cubic_handle_1),
},
ManipulatorGroup {
anchor: cubic_end,
in_handle: Some(cubic_handle_2),
out_handle: None,
},
ManipulatorGroup {
anchor: quadratic_end,
in_handle: Some(quadratic_1_handle),
out_handle: Some(quadratic_2_handle),
},
],
true,
);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let cubic_intersections = cubic_bezier.intersections(&line, None, None);
let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None);
let subpath_intersections = subpath.intersections(&line, None, None);
assert!(utils::dvec2_compare(
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
}
// TODO: add more intersection tests
}

View file

@ -1,4 +1,4 @@
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPERATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
use glam::{BVec2, DMat2, DVec2};
use std::f64::consts::PI;
@ -90,21 +90,17 @@ fn cube_root(f: f64) -> f64 {
/// 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.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
// Handle when p is approximately 0
roots.push(cube_root(-q));
} 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.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
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
// filter out repeated roots (ie. roots whose distance is less than some epsilon)
let q_divided_by_2 = q / 2.;
let a_divided_by_3 = a / 3.;
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);
let root_1 = 2. * cube_root(-q_divided_by_2) - a_divided_by_3;
let root_2 = cube_root(q_divided_by_2) - a_divided_by_3;
if (root_1 - root_2).abs() > MIN_SEPERATION_VALUE {
roots.push(root_1);
}
roots.push(root_2);
} else if discriminant > 0. {
// When discriminant > 0, there is one real and two imaginary roots
let q_divided_by_2 = q / 2.;
@ -139,6 +135,7 @@ pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
solve_quadratic(discriminant, 2. * b, c, d)
}
} else {
// convert at^3 + bt^2 + ct + d ==> t^3 + a't^2 + b't + c'
let new_a = b / a;
let new_b = c / a;
let new_c = d / a;

View file

@ -13,7 +13,7 @@
</div>
<h2>Subpaths</h2>
<div v-for="(feature, index) in subpathFeatures" :key="index">
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
<SubpathExamplePane :name="feature.name" :callback="feature.callback" :sliderOptions="feature.sliderOptions" :chooseComputeType="feature.chooseComputeType" />
</div>
</template>
@ -84,6 +84,14 @@ const tErrorOptions = {
default: 0.5,
};
const tMinimumSeperationOptions = {
variable: "minimum_seperation",
min: 0.001,
max: 0.25,
step: 0.001,
default: 0.05,
};
export default defineComponent({
data() {
return {
@ -369,6 +377,14 @@ export default defineComponent({
],
},
},
customPoints: {
Cubic: [
[31, 94],
[40, 40],
[107, 107],
[106, 106],
],
},
},
{
name: "Skewed Outline",
@ -479,11 +495,11 @@ export default defineComponent({
[180, 10],
[90, 120],
];
return bezier.intersect_quadratic_segment(quadratic, options.error);
return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_seperation);
},
exampleOptions: {
Quadratic: {
sliderOptions: [tErrorOptions],
sliderOptions: [tErrorOptions, tMinimumSeperationOptions],
},
},
},
@ -496,11 +512,11 @@ export default defineComponent({
[40, 120],
[175, 140],
];
return bezier.intersect_cubic_segment(cubic, options.error);
return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_seperation);
},
exampleOptions: {
Quadratic: {
sliderOptions: [tErrorOptions],
sliderOptions: [tErrorOptions, tMinimumSeperationOptions],
},
},
},
@ -558,6 +574,39 @@ export default defineComponent({
name: "Length",
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
},
{
name: "Evaluate",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => subpath.evaluate(options.computeArgument, computeType),
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }],
chooseComputeType: true,
},
{
name: "Intersect (Line Segment)",
callback: (subpath: WasmSubpathInstance): string =>
subpath.intersect_line_segment([
[150, 150],
[20, 20],
]),
},
{
name: "Intersect (Quadratic segment)",
callback: (subpath: WasmSubpathInstance): string =>
subpath.intersect_quadratic_segment([
[20, 80],
[180, 10],
[90, 120],
]),
},
{
name: "Intersect (Cubic segment)",
callback: (subpath: WasmSubpathInstance): string =>
subpath.intersect_cubic_segment([
[40, 20],
[100, 40],
[40, 120],
[175, 140],
]),
},
],
};
},

View file

@ -2,6 +2,10 @@
<div>
<h4 class="example-header">{{ title }}</h4>
<figure @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" class="example-figure" v-html="subpathSVG"></figure>
<div v-for="(slider, index) in sliderOptions" :key="index">
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ getSliderValue(sliderData[slider.variable], sliderUnits[slider.variable]) }}</div>
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
</div>
</div>
</template>
@ -11,7 +15,7 @@
import { defineComponent, PropType } from "vue";
import { WasmSubpath } from "@/../wasm/pkg";
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey } from "@/utils/types";
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, ComputeType } from "@/utils/types";
const SELECTABLE_RANGE = 10;
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
@ -22,14 +26,22 @@ export default defineComponent({
triples: { type: Array as PropType<Array<Array<number[] | undefined>>>, mutable: true, required: true },
closed: { type: Boolean as PropType<boolean>, default: false },
callback: { type: Function as PropType<SubpathCallback>, required: true },
sliderOptions: { type: Object as PropType<Array<SliderOption>>, default: () => ({}) },
computeType: { type: String as PropType<ComputeType>, default: "Parametric" },
},
data() {
const subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
const sliderData = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.default })));
const sliderUnits = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.unit })));
return {
subpath,
subpathSVG: this.callback(subpath),
subpathSVG: this.callback(subpath, sliderData, undefined, "Euclidean"),
activeIndex: undefined as number[] | undefined,
mutableTriples: JSON.parse(JSON.stringify(this.triples)),
sliderData,
sliderUnits,
};
},
methods: {
@ -55,9 +67,23 @@ export default defineComponent({
if (this.activeIndex) {
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
this.mutableTriples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
this.subpathSVG = this.callback(this.subpath);
this.subpathSVG = this.callback(this.subpath, this.sliderData, [mx, my], this.computeType);
}
},
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
},
watch: {
sliderData: {
handler() {
this.subpathSVG = this.callback(this.subpath, this.sliderData, undefined, this.computeType);
},
deep: true,
},
computeType: {
handler() {
this.subpathSVG = this.callback(this.subpath, this.sliderData, undefined, this.computeType);
},
},
},
});
</script>

View file

@ -1,9 +1,18 @@
<template>
<div>
<h3 class="example-pane-header">{{ name }}</h3>
<div v-if="chooseComputeType" class="compute-type-choice">
<strong>ComputeType:</strong>
<input type="radio" :id="`${id}-parametric`" value="Parametric" v-model="computeTypeChoice" />
<label :for="`${id}-parametric`">Parametric</label>
<input type="radio" :id="`${id}-euclidean`" value="Euclidean" v-model="computeTypeChoice" />
<label :for="`${id}-euclidean`">Euclidean</label>
</div>
<div class="example-row">
<div v-for="(example, index) in examples" :key="index">
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" />
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" :sliderOptions="sliderOptions" :computeType="computeTypeChoice" />
</div>
</div>
</div>
@ -14,7 +23,7 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { SubpathCallback } from "@/utils/types";
import { SubpathCallback, SliderOption, ComputeType } from "@/utils/types";
import SubpathExample from "@/components/SubpathExample.vue";
@ -22,6 +31,8 @@ export default defineComponent({
props: {
name: { type: String as PropType<string>, required: true },
callback: { type: Function as PropType<SubpathCallback>, required: true },
sliderOptions: { type: Array as PropType<Array<SliderOption>>, default: () => [] },
chooseComputeType: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
@ -50,6 +61,8 @@ export default defineComponent({
closed: true,
},
],
id: `${Math.random()}`.substring(2),
computeTypeChoice: "Parametric" as ComputeType,
};
},
components: {

View file

@ -14,7 +14,7 @@ export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number];
export type ComputeType = "Euclidean" | "Parametric";
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number], computeType?: ComputeType) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number], computeType?: ComputeType) => string;
export type ExampleOptions = {
[key in BezierCurveType]: {

View file

@ -41,10 +41,6 @@ fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy
}
}
fn wrap_svg_tag(contents: String) -> String {
format!("{}{}{}", SVG_OPEN_TAG, contents, SVG_CLOSE_TAG)
}
#[wasm_bindgen]
impl WasmBezier {
/// Expect js_points to be a list of 2 pairs.
@ -400,8 +396,8 @@ impl WasmBezier {
))
}
fn intersect(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
self.0.intersections(curve, error)
fn intersect(&self, curve: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
self.0.intersections(curve, error, minimum_separation)
}
pub fn intersect_line_segment(&self, js_points: &JsValue) -> String {
@ -414,7 +410,7 @@ impl WasmBezier {
line.to_svg(&mut line_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let intersections_svg = self
.intersect(&line, None)
.intersect(&line, None, None)
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t));
@ -424,7 +420,7 @@ impl WasmBezier {
wrap_svg_tag(format!("{bezier_curve_svg}{line_svg}{intersections_svg}"))
}
pub fn intersect_quadratic_segment(&self, js_points: &JsValue, error: f64) -> String {
pub fn intersect_quadratic_segment(&self, js_points: &JsValue, error: f64, minimum_separation: f64) -> String {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
let quadratic = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]);
@ -434,7 +430,7 @@ impl WasmBezier {
quadratic.to_svg(&mut quadratic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let intersections_svg = self
.intersect(&quadratic, Some(error))
.intersect(&quadratic, Some(error), Some(minimum_separation))
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t));
@ -444,7 +440,7 @@ impl WasmBezier {
wrap_svg_tag(format!("{bezier_curve_svg}{quadratic_svg}{intersections_svg}"))
}
pub fn intersect_cubic_segment(&self, js_points: &JsValue, error: f64) -> String {
pub fn intersect_cubic_segment(&self, js_points: &JsValue, error: f64, minimum_separation: f64) -> String {
let points: [DVec2; 4] = js_points.into_serde().unwrap();
let cubic = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]);
@ -454,7 +450,7 @@ impl WasmBezier {
cubic.to_svg(&mut cubic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let intersections_svg = self
.intersect(&cubic, Some(error))
.intersect(&cubic, Some(error), Some(minimum_separation))
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t));

View file

@ -1,9 +1,10 @@
use bezier_rs::{ManipulatorGroup, Subpath};
use crate::svg_drawing::*;
use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, Subpath};
use glam::DVec2;
use wasm_bindgen::prelude::*;
use crate::svg_drawing::*;
/// Wrapper of the `Subpath` struct to be used in JS.
#[wasm_bindgen]
pub struct WasmSubpath(Subpath);
@ -54,6 +55,103 @@ impl WasmSubpath {
pub fn length(&self) -> String {
let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK);
format!("{}{}{}{}", SVG_OPEN_TAG, self.to_default_svg(), length_text, SVG_CLOSE_TAG)
wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text))
}
pub fn evaluate(&self, t: f64, compute_type: String) -> String {
let point = match compute_type.as_str() {
"Euclidean" => self.0.evaluate(ComputeType::Euclidean(t)),
"Parametric" => self.0.evaluate(ComputeType::Parametric(t)),
_ => panic!("Unexpected ComputeType string: '{}'", compute_type),
};
let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
}
pub fn intersect_line_segment(&self, js_points: &JsValue) -> String {
let points: [DVec2; 2] = js_points.into_serde().unwrap();
let line = Bezier::from_linear_dvec2(points[0], points[1]);
let subpath_svg = self.to_default_svg();
let empty_string = String::new();
let mut line_svg = String::new();
line.to_svg(
&mut line_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
empty_string.clone(),
empty_string.clone(),
empty_string,
);
let intersections_svg = self
.0
.intersections(&line, None, None)
.iter()
.map(|intersection_t| {
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t));
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
}
pub fn intersect_quadratic_segment(&self, js_points: &JsValue) -> String {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
let line = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]);
let subpath_svg = self.to_default_svg();
let empty_string = String::new();
let mut line_svg = String::new();
line.to_svg(
&mut line_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
empty_string.clone(),
empty_string.clone(),
empty_string,
);
let intersections_svg = self
.0
.intersections(&line, None, None)
.iter()
.map(|intersection_t| {
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t));
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
}
pub fn intersect_cubic_segment(&self, js_points: &JsValue) -> String {
let points: [DVec2; 4] = js_points.into_serde().unwrap();
let line = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]);
let subpath_svg = self.to_default_svg();
let empty_string = String::new();
let mut line_svg = String::new();
line.to_svg(
&mut line_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
empty_string.clone(),
empty_string.clone(),
empty_string,
);
let intersections_svg = self
.0
.intersections(&line, None, None)
.iter()
.map(|intersection_t| {
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t));
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
}
}

View file

@ -26,6 +26,10 @@ pub const HANDLE_ATTRIBUTES: &str = "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\
pub const TEXT_OFFSET_X: f64 = 5.;
pub const TEXT_OFFSET_Y: f64 = 193.;
pub fn wrap_svg_tag(contents: String) -> String {
format!("{}{}{}", SVG_OPEN_TAG, contents, SVG_CLOSE_TAG)
}
/// Helper function to create an SVG text entity.
pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String {
format!(r#"<text x="{x_pos}" y="{y_pos}" fill="{fill}">{text}</text>"#)