Bezier-rs: Updated Bezier function signatures to accept TValue (#967)

* Create helper for converting d to t values

* Add euclidean option for tangent and normal

* Modified bezier functions signatures to accept ComputeType

* Stylistic changes per review

* Added ComputeType documentation

* Renamed ComputeType to TValue

* Fixed comments

* Fixed failing unit tests

* Code review

* Fix comments in code review

* Renamed compute_type_to_parametric to t_value_to_parametric

---------

Co-authored-by: Linda Zheng <thelindazheng@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Rob Nadal 2023-02-13 12:31:51 -05:00 committed by Keavon Chambers
parent f0ad4c91d3
commit a64c856ec4
25 changed files with 456 additions and 433 deletions

View file

@ -1,6 +1,6 @@
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::ComputeType; use bezier_rs::TValue;
use document_legacy::{LayerId, Operation}; use document_legacy::{LayerId, Operation};
use graphene_std::vector::consts::ManipulatorType; use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup; use graphene_std::vector::manipulator_group::ManipulatorGroup;
@ -296,7 +296,7 @@ impl ShapeEditor {
for bezier_id in document.layer(layer_path).ok()?.as_subpath()?.bezier_iter() { for bezier_id in document.layer(layer_path).ok()?.as_subpath()?.bezier_iter() {
let bezier = bezier_id.internal; let bezier = bezier_id.internal;
let t = bezier.project(layer_pos, projection_options); let t = bezier.project(layer_pos, projection_options);
let layerspace = bezier.evaluate(ComputeType::Parametric(t)); let layerspace = bezier.evaluate(TValue::Parametric(t));
let screenspace = transform.transform_point2(layerspace); let screenspace = transform.transform_point2(layerspace);
let distance_squared = screenspace.distance_squared(position); let distance_squared = screenspace.distance_squared(position);
@ -314,7 +314,7 @@ impl ShapeEditor {
pub fn split(&self, document: &Document, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) { pub fn split(&self, document: &Document, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) {
for layer_path in &self.selected_layers { for layer_path in &self.selected_layers {
if let Some((bezier_id, t)) = self.closest_segment(document, layer_path, position, tolerance) { if let Some((bezier_id, t)) = self.closest_segment(document, layer_path, position, tolerance) {
let [first, second] = bezier_id.internal.split(t); let [first, second] = bezier_id.internal.split(TValue::Parametric(t));
// Adjust the first manipulator group's out handle // Adjust the first manipulator group's out handle
let out_handle = Operation::SetManipulatorPoints { let out_handle = Operation::SetManipulatorPoints {

View file

@ -203,7 +203,7 @@ impl Bezier {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::ComputeType; use crate::utils::TValue;
use super::compare::compare_points; use super::compare::compare_points;
use super::*; use super::*;
@ -215,13 +215,13 @@ mod tests {
let p3 = DVec2::new(160., 170.); let p3 = DVec2::new(160., 170.);
let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None); let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None);
assert!(compare_points(bezier1.evaluate(ComputeType::Parametric(0.5)), p2)); assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.5)), p2));
let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8));
assert!(compare_points(bezier2.evaluate(ComputeType::Parametric(0.8)), p2)); assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2));
let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.));
assert!(compare_points(bezier3.evaluate(ComputeType::Parametric(0.)), p2)); assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2));
} }
#[test] #[test]
@ -231,12 +231,12 @@ mod tests {
let p3 = DVec2::new(160., 160.); let p3 = DVec2::new(160., 160.);
let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.)); let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.));
assert!(compare_points(bezier1.evaluate(ComputeType::Parametric(0.3)), p2)); assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.3)), p2));
let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7));
assert!(compare_points(bezier2.evaluate(ComputeType::Parametric(0.8)), p2)); assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2));
let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7));
assert!(compare_points(bezier3.evaluate(ComputeType::Parametric(0.)), p2)); assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2));
} }
} }

View file

@ -1,9 +1,49 @@
use crate::utils::{f64_compare, ComputeType}; use crate::utils::{f64_compare, TValue};
use super::*; use super::*;
/// Functionality relating to looking up properties of the `Bezier` or points along the `Bezier`. /// Functionality relating to looking up properties of the `Bezier` or points along the `Bezier`.
impl Bezier { impl Bezier {
/// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value.
pub fn euclidean_to_parametric(&self, ratio: f64, error: f64) -> f64 {
let mut low = 0.;
let mut mid = 0.;
let mut high = 1.;
let total_length = self.length(None);
while low < high {
mid = (low + high) / 2.;
let test_ratio = self.trim(TValue::Parametric(0.), TValue::Parametric(mid)).length(None) / total_length;
if f64_compare(test_ratio, ratio, error) {
break;
} else if test_ratio < ratio {
low = mid;
} else {
high = mid;
}
}
mid
}
/// Convert a [TValue] to a parametric `t`-value.
pub(crate) fn t_value_to_parametric(&self, t: TValue) -> f64 {
match t {
TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t));
t
}
TValue::Euclidean(t) => {
assert!((0.0..=1.).contains(&t));
self.euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND)
}
TValue::EuclideanWithinError { t, error } => {
assert!((0.0..=1.).contains(&t));
self.euclidean_to_parametric(t, error)
}
}
}
/// Calculate the point on the curve based on the `t`-value provided. /// Calculate the point on the curve based on the `t`-value provided.
pub(crate) fn unrestricted_parametric_evaluate(&self, t: f64) -> DVec2 { pub(crate) fn unrestricted_parametric_evaluate(&self, t: f64) -> DVec2 {
// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>. // Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>.
@ -23,48 +63,11 @@ impl Bezier {
} }
} }
/// Calculate the point along the curve that is a factor of `d` away from the start. /// Calculate the coordinates of the point `t` along the curve.
pub(crate) fn unrestricted_euclidean_evaluate(&self, d: f64, error: f64) -> DVec2 {
if let BezierHandles::Linear = self.handles {
return self.unrestricted_parametric_evaluate(d);
}
let mut low = 0.;
let mut mid = 0.;
let mut high = 1.;
let total_length = self.length(None);
while low < high {
mid = (low + high) / 2.;
let test_d = self.trim(0., mid).length(None) / total_length;
if f64_compare(test_d, d, error) {
break;
} else if test_d < d {
low = mid;
} else {
high = mid;
}
}
self.unrestricted_parametric_evaluate(mid)
}
/// Calculate the point on the curve based on the `t`-value provided.
/// Expects `t` to be within the inclusive range `[0, 1]`. /// Expects `t` to be within the inclusive range `[0, 1]`.
pub fn evaluate(&self, t: ComputeType) -> DVec2 { pub fn evaluate(&self, t: TValue) -> DVec2 {
match t { let t = self.t_value_to_parametric(t);
ComputeType::Parametric(t) => { self.unrestricted_parametric_evaluate(t)
assert!((0.0..=1.).contains(&t));
self.unrestricted_parametric_evaluate(t)
}
ComputeType::Euclidean(t) => {
assert!((0.0..=1.).contains(&t));
self.unrestricted_euclidean_evaluate(t, 0.0001)
}
ComputeType::EuclideanWithinError { t, epsilon } => {
assert!((0.0..=1.).contains(&t));
self.unrestricted_euclidean_evaluate(t, epsilon)
}
}
} }
/// Return a selection of equidistant points on the bezier curve. /// Return a selection of equidistant points on the bezier curve.
@ -75,7 +78,7 @@ impl Bezier {
let mut steps_array = Vec::with_capacity(steps_unwrapped + 1); let mut steps_array = Vec::with_capacity(steps_unwrapped + 1);
for t in 0..steps_unwrapped + 1 { for t in 0..steps_unwrapped + 1 {
steps_array.push(self.evaluate(ComputeType::Parametric(f64::from(t as i32) * ratio))) steps_array.push(self.evaluate(TValue::Parametric(f64::from(t as i32) * ratio)))
} }
steps_array steps_array
@ -107,7 +110,7 @@ impl Bezier {
} }
} }
/// Returns the `t` value that corresponds to the closest point on the curve to the provided point. /// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point.
/// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure. /// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure.
pub fn project(&self, point: DVec2, options: ProjectionOptions) -> f64 { pub fn project(&self, point: DVec2, options: ProjectionOptions) -> f64 {
let ProjectionOptions { let ProjectionOptions {
@ -162,7 +165,7 @@ impl Bezier {
if step_index == 0 { if step_index == 0 {
distance = *table_distance; distance = *table_distance;
} else { } else {
distance = point.distance(self.evaluate(ComputeType::Parametric(iterator_t))); distance = point.distance(self.evaluate(TValue::Parametric(iterator_t)));
*table_distance = distance; *table_distance = distance;
} }
if distance < new_minimum_distance { if distance < new_minimum_distance {
@ -212,17 +215,17 @@ mod tests {
let p4 = DVec2::new(30., 21.); let p4 = DVec2::new(30., 21.);
let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert_eq!(bezier1.evaluate(ComputeType::Parametric(0.5)), DVec2::new(12.5, 6.25)); assert_eq!(bezier1.evaluate(TValue::Parametric(0.5)), DVec2::new(12.5, 6.25));
let bezier2 = Bezier::from_cubic_dvec2(p1, p2, p3, p4); let bezier2 = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert_eq!(bezier2.evaluate(ComputeType::Parametric(0.5)), DVec2::new(16.5, 9.625)); assert_eq!(bezier2.evaluate(TValue::Parametric(0.5)), DVec2::new(16.5, 9.625));
} }
#[test] #[test]
fn test_compute_lookup_table() { fn test_compute_lookup_table() {
let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.); let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.);
let lookup_table1 = bezier1.compute_lookup_table(Some(2)); let lookup_table1 = bezier1.compute_lookup_table(Some(2));
assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(ComputeType::Parametric(0.5)), bezier1.end()]); assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(TValue::Parametric(0.5)), bezier1.end()]);
let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.); let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.);
let lookup_table2 = bezier2.compute_lookup_table(Some(4)); let lookup_table2 = bezier2.compute_lookup_table(Some(4));
@ -230,9 +233,9 @@ mod tests {
lookup_table2, lookup_table2,
vec![ vec![
bezier2.start(), bezier2.start(),
bezier2.evaluate(ComputeType::Parametric(0.25)), bezier2.evaluate(TValue::Parametric(0.25)),
bezier2.evaluate(ComputeType::Parametric(0.50)), bezier2.evaluate(TValue::Parametric(0.50)),
bezier2.evaluate(ComputeType::Parametric(0.75)), bezier2.evaluate(TValue::Parametric(0.75)),
bezier2.end() bezier2.end()
] ]
); );

View file

@ -1,15 +1,16 @@
use super::*; use super::*;
use crate::utils::ComputeType; use crate::utils::TValue;
use glam::DMat2; use glam::DMat2;
use std::ops::Range; use std::ops::Range;
/// Functionality that solve for various curve information such as derivative, tangent, intersect, etc. /// Functionality that solve for various curve information such as derivative, tangent, intersect, etc.
impl Bezier { impl Bezier {
/// Returns a list of lists of points representing the De Casteljau points for all iterations at the point corresponding to `t` using De Casteljau's algorithm. /// Returns a list of lists of points representing the De Casteljau points for all iterations at the point `t` along the curve using De Casteljau's algorithm.
/// The `i`th element of the list represents the set of points in the `i`th iteration. /// The `i`th element of the list represents the set of points in the `i`th iteration.
/// More information on the algorithm can be found in the [De Casteljau section](https://pomax.github.io/bezierinfo/#decasteljau) in Pomax's primer. /// More information on the algorithm can be found in the [De Casteljau section](https://pomax.github.io/bezierinfo/#decasteljau) in Pomax's primer.
pub fn de_casteljau_points(&self, t: f64) -> Vec<Vec<DVec2>> { pub fn de_casteljau_points(&self, t: TValue) -> Vec<Vec<DVec2>> {
let t = self.t_value_to_parametric(t);
let bezier_points = match self.handles { let bezier_points = match self.handles {
BezierHandles::Linear => vec![self.start, self.end], BezierHandles::Linear => vec![self.start, self.end],
BezierHandles::Quadratic { handle } => vec![self.start, handle, self.end], BezierHandles::Quadratic { handle } => vec![self.start, handle, self.end],
@ -30,7 +31,7 @@ impl Bezier {
de_casteljau_points de_casteljau_points
} }
/// Returns a Bezier representing the derivative of the original curve. /// Returns a [Bezier] representing the derivative of the original curve.
/// - This function returns `None` for a linear segment. /// - This function returns `None` for a linear segment.
pub fn derivative(&self) -> Option<Bezier> { pub fn derivative(&self) -> Option<Bezier> {
match self.handles { match self.handles {
@ -49,27 +50,29 @@ impl Bezier {
} }
} }
/// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve. /// Returns a normalized unit vector representing the tangent at the point `t` along the curve.
pub fn tangent(&self, t: f64) -> DVec2 { pub fn tangent(&self, t: TValue) -> DVec2 {
let t = self.t_value_to_parametric(t);
match self.handles { match self.handles {
BezierHandles::Linear => self.end - self.start, BezierHandles::Linear => self.end - self.start,
_ => self.derivative().unwrap().evaluate(ComputeType::Parametric(t)), _ => self.derivative().unwrap().evaluate(TValue::Parametric(t)),
} }
.normalize() .normalize()
} }
/// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve. /// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve.
pub fn normal(&self, t: f64) -> DVec2 { pub fn normal(&self, t: TValue) -> DVec2 {
self.tangent(t).perp() self.tangent(t).perp()
} }
/// Returns the curvature, a scalar value for the derivative at the given `t`-value along the curve. /// Returns the curvature, a scalar value for the derivative at the point `t` along the curve.
/// Curvature is 1 over the radius of a circle with an equivalent derivative. /// Curvature is 1 over the radius of a circle with an equivalent derivative.
pub fn curvature(&self, t: f64) -> f64 { pub fn curvature(&self, t: TValue) -> f64 {
let t = self.t_value_to_parametric(t);
let (d, dd) = match &self.derivative() { let (d, dd) = match &self.derivative() {
Some(first_derivative) => match first_derivative.derivative() { Some(first_derivative) => match first_derivative.derivative() {
Some(second_derivative) => (first_derivative.evaluate(ComputeType::Parametric(t)), second_derivative.evaluate(ComputeType::Parametric(t))), Some(second_derivative) => (first_derivative.evaluate(TValue::Parametric(t)), second_derivative.evaluate(TValue::Parametric(t))),
None => (first_derivative.evaluate(ComputeType::Parametric(t)), first_derivative.end - first_derivative.start), None => (first_derivative.evaluate(TValue::Parametric(t)), first_derivative.end - first_derivative.start),
}, },
None => (self.end - self.start, DVec2::new(0., 0.)), None => (self.end - self.start, DVec2::new(0., 0.)),
}; };
@ -129,7 +132,7 @@ impl Bezier {
let extrema = self.local_extrema(); let extrema = self.local_extrema();
for t_values in extrema { for t_values in extrema {
for t in t_values { for t in t_values {
let point = self.evaluate(ComputeType::Parametric(t)); let point = self.evaluate(TValue::Parametric(t));
// Update bounding box if new min/max is found. // Update bounding box if new min/max is found.
endpoints_min = endpoints_min.min(point); endpoints_min = endpoints_min.min(point);
endpoints_max = endpoints_max.max(point); endpoints_max = endpoints_max.max(point);
@ -178,7 +181,7 @@ impl Bezier {
} }
} }
/// Returns list of `t`-values representing the inflection points of the curve. /// Returns list of parametric `t`-values representing the inflection points of the curve.
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
pub fn inflections(&self) -> Vec<f64> { pub fn inflections(&self) -> Vec<f64> {
self.unrestricted_inflections().into_iter().filter(|&t| t > 0. && t < 1.).collect::<Vec<f64>>() self.unrestricted_inflections().into_iter().filter(|&t| t > 0. && t < 1.).collect::<Vec<f64>>()
@ -213,8 +216,8 @@ impl Bezier {
} }
// Split curves in half and repeat with the combinations of the two halves of each curve // Split curves in half and repeat with the combinations of the two halves of each curve
let [split_1_a, split_1_b] = self.split(0.5); let [split_1_a, split_1_b] = self.split(TValue::Parametric(0.5));
let [split_2_a, split_2_b] = other.split(0.5); let [split_2_a, split_2_b] = other.split(TValue::Parametric(0.5));
[ [
split_1_a.intersections_between_subcurves(self_start_t..self_mid_t, &split_2_a, other_start_t..other_mid_t, error), split_1_a.intersections_between_subcurves(self_start_t..self_mid_t, &split_2_a, other_start_t..other_mid_t, error),
@ -229,7 +232,7 @@ impl Bezier {
} }
// TODO: Use an `impl Iterator` return type instead of a `Vec` // 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 /// Returns a list of filtered parametric `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 /// 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. /// 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. /// The returned `t` values are with respect to the current bezier, not the provided parameter.
@ -242,8 +245,6 @@ impl Bezier {
let mut intersection_t_values = self.unfiltered_intersections(other, error); let mut intersection_t_values = self.unfiltered_intersections(other, error);
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); 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| { 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) { if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
accumulator.pop(); accumulator.pop();
@ -334,7 +335,7 @@ impl Bezier {
} }
// TODO: Use an `impl Iterator` return type instead of a `Vec` // TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. /// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point.
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
pub fn self_intersections(&self, error: Option<f64>) -> Vec<[f64; 2]> { pub fn self_intersections(&self, error: Option<f64>) -> Vec<[f64; 2]> {
if self.handles == BezierHandles::Linear || matches!(self.handles, BezierHandles::Quadratic { .. }) { if self.handles == BezierHandles::Linear || matches!(self.handles, BezierHandles::Quadratic { .. }) {
@ -362,7 +363,7 @@ impl Bezier {
.collect() .collect()
} }
/// Returns a list of `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners. /// Returns a list of parametric `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners.
pub fn rectangle_intersections(&self, corner1: DVec2, corner2: DVec2) -> Vec<f64> { pub fn rectangle_intersections(&self, corner1: DVec2, corner2: DVec2) -> Vec<f64> {
[ [
Bezier::from_linear_coordinates(corner1.x, corner1.y, corner2.x, corner1.y), Bezier::from_linear_coordinates(corner1.x, corner1.y, corner2.x, corner1.y),
@ -384,7 +385,7 @@ mod tests {
#[test] #[test]
fn test_de_casteljau_points() { fn test_de_casteljau_points() {
let bezier = Bezier::from_cubic_coordinates(0., 0., 0., 100., 100., 100., 100., 0.); let bezier = Bezier::from_cubic_coordinates(0., 0., 0., 100., 100., 100., 100., 0.);
let de_casteljau_points = bezier.de_casteljau_points(0.5); let de_casteljau_points = bezier.de_casteljau_points(TValue::Parametric(0.5));
let expected_de_casteljau_points = vec![ let expected_de_casteljau_points = vec![
vec![DVec2::new(0., 0.), DVec2::new(0., 100.), DVec2::new(100., 100.), DVec2::new(100., 0.)], vec![DVec2::new(0., 0.), DVec2::new(0., 100.), DVec2::new(100., 100.), DVec2::new(100., 0.)],
vec![DVec2::new(0., 50.), DVec2::new(50., 100.), DVec2::new(100., 50.)], vec![DVec2::new(0., 50.), DVec2::new(50., 100.), DVec2::new(100., 50.)],
@ -393,7 +394,7 @@ mod tests {
]; ];
assert_eq!(&de_casteljau_points, &expected_de_casteljau_points); assert_eq!(&de_casteljau_points, &expected_de_casteljau_points);
assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(TValue::Parametric(0.5)));
} }
#[test] #[test]
@ -433,16 +434,16 @@ mod tests {
let linear = Bezier::from_linear_dvec2(p1, p2); let linear = Bezier::from_linear_dvec2(p1, p2);
let unit_slope = DVec2::new(30., 20.).normalize(); let unit_slope = DVec2::new(30., 20.).normalize();
assert_eq!(linear.tangent(0.), unit_slope); assert_eq!(linear.tangent(TValue::Parametric(0.)), unit_slope);
assert_eq!(linear.tangent(1.), unit_slope); assert_eq!(linear.tangent(TValue::Parametric(1.)), unit_slope);
let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert_eq!(quadratic.tangent(0.), DVec2::new(60., 40.).normalize()); assert_eq!(quadratic.tangent(TValue::Parametric(0.)), DVec2::new(60., 40.).normalize());
assert_eq!(quadratic.tangent(1.), DVec2::new(40., 60.).normalize()); assert_eq!(quadratic.tangent(TValue::Parametric(1.)), DVec2::new(40., 60.).normalize());
let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert_eq!(cubic.tangent(0.), DVec2::new(90., 60.).normalize()); assert_eq!(cubic.tangent(TValue::Parametric(0.)), DVec2::new(90., 60.).normalize());
assert_eq!(cubic.tangent(1.), DVec2::new(30., 120.).normalize()); assert_eq!(cubic.tangent(TValue::Parametric(1.)), DVec2::new(30., 120.).normalize());
} }
#[test] #[test]
@ -455,16 +456,16 @@ mod tests {
let linear = Bezier::from_linear_dvec2(p1, p2); let linear = Bezier::from_linear_dvec2(p1, p2);
let unit_slope = DVec2::new(-20., 30.).normalize(); let unit_slope = DVec2::new(-20., 30.).normalize();
assert_eq!(linear.normal(0.), unit_slope); assert_eq!(linear.normal(TValue::Parametric(0.)), unit_slope);
assert_eq!(linear.normal(1.), unit_slope); assert_eq!(linear.normal(TValue::Parametric(1.)), unit_slope);
let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert_eq!(quadratic.normal(0.), DVec2::new(-40., 60.).normalize()); assert_eq!(quadratic.normal(TValue::Parametric(0.)), DVec2::new(-40., 60.).normalize());
assert_eq!(quadratic.normal(1.), DVec2::new(-60., 40.).normalize()); assert_eq!(quadratic.normal(TValue::Parametric(1.)), DVec2::new(-60., 40.).normalize());
let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert_eq!(cubic.normal(0.), DVec2::new(-60., 90.).normalize()); assert_eq!(cubic.normal(TValue::Parametric(0.)), DVec2::new(-60., 90.).normalize());
assert_eq!(cubic.normal(1.), DVec2::new(-120., 30.).normalize()); assert_eq!(cubic.normal(TValue::Parametric(1.)), DVec2::new(-120., 30.).normalize());
} }
#[test] #[test]
@ -475,24 +476,24 @@ mod tests {
let p4 = DVec2::new(50., 10.); let p4 = DVec2::new(50., 10.);
let linear = Bezier::from_linear_dvec2(p1, p2); let linear = Bezier::from_linear_dvec2(p1, p2);
assert_eq!(linear.curvature(0.), 0.); assert_eq!(linear.curvature(TValue::Parametric(0.)), 0.);
assert_eq!(linear.curvature(0.5), 0.); assert_eq!(linear.curvature(TValue::Parametric(0.5)), 0.);
assert_eq!(linear.curvature(1.), 0.); assert_eq!(linear.curvature(TValue::Parametric(1.)), 0.);
let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert!(compare_f64s(quadratic.curvature(0.), 0.0125)); assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.)), 0.0125));
assert!(compare_f64s(quadratic.curvature(0.5), 0.035355)); assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.5)), 0.035355));
assert!(compare_f64s(quadratic.curvature(1.), 0.0125)); assert!(compare_f64s(quadratic.curvature(TValue::Parametric(1.)), 0.0125));
let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert!(compare_f64s(cubic.curvature(0.), 0.016667)); assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.)), 0.016667));
assert!(compare_f64s(cubic.curvature(0.5), 0.)); assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.5)), 0.));
assert!(compare_f64s(cubic.curvature(1.), 0.)); assert!(compare_f64s(cubic.curvature(TValue::Parametric(1.)), 0.));
// The curvature at an inflection point is zero // The curvature at an inflection point is zero
let inflection_curve = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.); let inflection_curve = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.);
let inflections = inflection_curve.inflections(); let inflections = inflection_curve.inflections();
assert_eq!(inflection_curve.curvature(inflections[0]), 0.); assert_eq!(inflection_curve.curvature(TValue::Parametric(inflections[0])), 0.);
} }
#[test] #[test]
@ -609,12 +610,12 @@ mod tests {
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.); let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
let intersections1 = bezier.intersections(&line1, None, None); let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1); assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), DVec2::new(30., 60.))); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), DVec2::new(30., 60.)));
// Intersection in the middle of curve // Intersection in the middle of curve
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None, None); let intersections2 = bezier.intersections(&line2, None, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.))); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(96., 96.)));
} }
#[test] #[test]
@ -628,12 +629,12 @@ mod tests {
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.); let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
let intersections1 = bezier.intersections(&line1, None, None); let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1); assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1)); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), p1));
// Intersection in the middle of curve // Intersection in the middle of curve
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None, None); let intersections2 = bezier.intersections(&line2, None, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354))); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354)));
} }
#[test] #[test]
@ -648,14 +649,14 @@ mod tests {
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.); let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
let intersections1 = bezier.intersections(&line1, None, None); let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1); assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1)); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), p1));
// Intersection at edge and in middle of curve, Discriminant < 0 // Intersection at edge and in middle of curve, Discriminant < 0
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None, None); let intersections2 = bezier.intersections(&line2, None, None);
assert!(intersections2.len() == 2); assert!(intersections2.len() == 2);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), p1)); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), p1));
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[1])), DVec2::new(85.84, 85.84))); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[1])), DVec2::new(85.84, 85.84)));
} }
#[test] #[test]
@ -672,7 +673,7 @@ mod tests {
let intersections = bezier.intersections(&line, None, None); let intersections = bezier.intersections(&line, None, None);
assert_eq!(intersections.len(), 1); assert_eq!(intersections.len(), 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections[0])), p4)); assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections[0])), p4));
} }
#[test] #[test]
@ -699,8 +700,8 @@ mod tests {
let intersections1 = bezier1.intersections(&bezier2, None, None); let intersections1 = bezier1.intersections(&bezier2, None, None);
let intersections2 = bezier2.intersections(&bezier1, 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 intersections1_points: Vec<DVec2> = intersections1.iter().map(|&t| bezier1.evaluate(TValue::Parametric(t))).collect();
let intersections2_points: Vec<DVec2> = intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).rev().collect(); let intersections2_points: Vec<DVec2> = intersections2.iter().map(|&t| bezier2.evaluate(TValue::Parametric(t))).rev().collect();
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.)); assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
} }
@ -710,8 +711,8 @@ mod tests {
let bezier = Bezier::from_cubic_coordinates(160., 180., 170., 10., 30., 90., 180., 140.); let bezier = Bezier::from_cubic_coordinates(160., 180., 170., 10., 30., 90., 180., 140.);
let intersections = bezier.self_intersections(Some(0.5)); let intersections = bezier.self_intersections(Some(0.5));
assert!(compare_vec_of_points( assert!(compare_vec_of_points(
intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[0]))).collect(), intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[0]))).collect(),
intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[1]))).collect(), intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[1]))).collect(),
2. 2.
)); ));
assert!(Bezier::from_linear_coordinates(160., 180., 170., 10.).self_intersections(None).is_empty()); assert!(Bezier::from_linear_coordinates(160., 180., 170., 10.).self_intersections(None).is_empty());

View file

@ -1,14 +1,15 @@
use super::*; use super::*;
use crate::utils::{f64_compare, ComputeType}; use crate::utils::{f64_compare, TValue};
use glam::DMat2; use glam::DMat2;
use std::f64::consts::PI; use std::f64::consts::PI;
/// Functionality that transform Beziers, such as split, reduce, offset, etc. /// Functionality that transform Beziers, such as split, reduce, offset, etc.
impl Bezier { impl Bezier {
/// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`. /// Returns the pair of Bezier curves that result from splitting the original curve at the point `t` along the curve.
pub fn split(&self, t: f64) -> [Bezier; 2] { pub fn split(&self, t: TValue) -> [Bezier; 2] {
let split_point = self.evaluate(ComputeType::Parametric(t)); let t = self.t_value_to_parametric(t);
let split_point = self.evaluate(TValue::Parametric(t));
match self.handles { match self.handles {
BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)], BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)],
@ -49,11 +50,13 @@ impl Bezier {
} }
} }
/// Returns the Bezier curve representing the sub-curve starting at the point corresponding to `t1` and ending at the point corresponding to `t2`. /// Returns the Bezier curve representing the sub-curve starting at the point `t1` and ending at the point `t2` along the curve.
pub fn trim(&self, t1: f64, t2: f64) -> Bezier { /// When `t1 < t2`, returns the reversed sub-curve starting at `t2` and ending at `t1`.
pub fn trim(&self, t1: TValue, t2: TValue) -> Bezier {
let (t1, t2) = (self.t_value_to_parametric(t1), self.t_value_to_parametric(t2));
// If t1 is equal to t2, return a bezier comprised entirely of the same point // If t1 is equal to t2, return a bezier comprised entirely of the same point
if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) { if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) {
let point = self.evaluate(ComputeType::Parametric(t1)); let point = self.evaluate(TValue::Parametric(t1));
return match self.handles { return match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(point, point), BezierHandles::Linear => Bezier::from_linear_dvec2(point, point),
BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point), BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point),
@ -63,7 +66,7 @@ impl Bezier {
// Depending on the order of `t1` and `t2`, determine which half of the split we need to keep // Depending on the order of `t1` and `t2`, determine which half of the split we need to keep
let t1_split_side = usize::from(t1 <= t2); let t1_split_side = usize::from(t1 <= t2);
let t2_split_side = usize::from(t1 > t2); let t2_split_side = usize::from(t1 > t2);
let bezier_starting_at_t1 = self.split(t1)[t1_split_side]; let bezier_starting_at_t1 = self.split(TValue::Parametric(t1))[t1_split_side];
// Adjust the ratio `t2` to its corresponding value on the new curve that was split on `t1` // Adjust the ratio `t2` to its corresponding value on the new curve that was split on `t1`
let adjusted_t2 = if t1 < t2 || t1 == 0. { let adjusted_t2 = if t1 < t2 || t1 == 0. {
// Case where we took the split from t1 to the end // Case where we took the split from t1 to the end
@ -73,7 +76,7 @@ impl Bezier {
// Case where we took the split from the beginning to `t1` // Case where we took the split from the beginning to `t1`
t2 / t1 t2 / t1
}; };
let result = bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]; let result = bezier_starting_at_t1.split(TValue::Parametric(adjusted_t2))[t2_split_side];
if t2 < t1 { if t2 < t1 {
return result.reverse(); return result.reverse();
} }
@ -132,8 +135,8 @@ impl Bezier {
} }
} }
// Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon. // Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon.
let normal_0 = self.normal(0.); let normal_0 = self.normal(TValue::Parametric(0.));
let normal_1 = self.normal(1.); let normal_1 = self.normal(TValue::Parametric(1.));
let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).acos(); let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).acos();
endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE
} }
@ -169,7 +172,7 @@ impl Bezier {
extrema.windows(2).for_each(|t_pair| { extrema.windows(2).for_each(|t_pair| {
let t_subcurve_start = t_pair[0]; let t_subcurve_start = t_pair[0];
let t_subcurve_end = t_pair[1]; let t_subcurve_end = t_pair[1];
let subcurve = self.trim(t_subcurve_start, t_subcurve_end); let subcurve = self.trim(TValue::Parametric(t_subcurve_start), TValue::Parametric(t_subcurve_end));
// Perform no processing on the subcurve if it's already scalable. // Perform no processing on the subcurve if it's already scalable.
if subcurve.is_scalable() { if subcurve.is_scalable() {
result_beziers.push(subcurve); result_beziers.push(subcurve);
@ -177,7 +180,7 @@ impl Bezier {
return; return;
} }
// According to <https://pomax.github.io/bezierinfo/#offsetting>, it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments. // According to <https://pomax.github.io/bezierinfo/#offsetting>, it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments.
let [first_half, second_half] = subcurve.split(0.5); let [first_half, second_half] = subcurve.split(TValue::Parametric(0.5));
if first_half.is_scalable() && second_half.is_scalable() { if first_half.is_scalable() && second_half.is_scalable() {
result_beziers.push(first_half); result_beziers.push(first_half);
result_beziers.push(second_half); result_beziers.push(second_half);
@ -191,14 +194,14 @@ impl Bezier {
let mut t1 = 0.; let mut t1 = 0.;
let mut t2 = step_size; let mut t2 = step_size;
while t2 <= 1. + step_size { while t2 <= 1. + step_size {
segment = subcurve.trim(t1, f64::min(t2, 1.)); segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(f64::min(t2, 1.)));
if !segment.is_scalable() { if !segment.is_scalable() {
t2 -= step_size; t2 -= step_size;
// If the previous step does not exist, the start of the subcurve is irreducible. // If the previous step does not exist, the start of the subcurve is irreducible.
// Otherwise, add the valid segment from the previous step to the result. // Otherwise, add the valid segment from the previous step to the result.
if f64::abs(t1 - t2) >= step_size { if f64::abs(t1 - t2) >= step_size {
segment = subcurve.trim(t1, t2); segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(t2));
result_beziers.push(segment); result_beziers.push(segment);
result_t_values.push(t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)); result_t_values.push(t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start));
} else { } else {
@ -210,7 +213,7 @@ impl Bezier {
} }
// Collect final remainder of the curve. // Collect final remainder of the curve.
if t1 < 1. { if t1 < 1. {
segment = subcurve.trim(t1, 1.); segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(1.));
if segment.is_scalable() { if segment.is_scalable() {
result_beziers.push(segment); result_beziers.push(segment);
result_t_values.push(t_subcurve_end); result_t_values.push(t_subcurve_end);
@ -236,8 +239,8 @@ impl Bezier {
fn scale(&self, distance: f64) -> Bezier { fn scale(&self, distance: f64) -> Bezier {
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
let normal_start = self.normal(0.); let normal_start = self.normal(TValue::Parametric(0.));
let normal_end = self.normal(1.); let normal_end = self.normal(TValue::Parametric(1.));
// If normal unit vectors are equal, then the lines are parallel // If normal unit vectors are equal, then the lines are parallel
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
@ -263,30 +266,30 @@ impl Bezier {
pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier { pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier {
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
let normal_start = self.normal(0.); let normal_start = self.normal(TValue::Parametric(0.));
let normal_end = self.normal(1.); let normal_end = self.normal(TValue::Parametric(1.));
// If normal unit vectors are equal, then the lines are parallel // If normal unit vectors are equal, then the lines are parallel
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(0.), false, start_distance); let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(TValue::Parametric(0.)), false, start_distance);
let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(1.), false, end_distance); let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(TValue::Parametric(1.)), false, end_distance);
return match self.handles { return match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
let handle_closest_t = self.project(handle, ProjectionOptions::default()); let handle_closest_t = self.project(handle, ProjectionOptions::default());
let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance; let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance;
let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(handle_closest_t), false, handle_scale_distance); let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(TValue::Parametric(handle_closest_t)), false, handle_scale_distance);
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end) Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
} }
BezierHandles::Cubic { handle_start, handle_end } => { BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default()); let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default());
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance; let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(handle_start_closest_t), false, handle_start_scale_distance); let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance);
let handle_end_closest_t = self.project(handle_start, ProjectionOptions::default()); let handle_end_closest_t = self.project(handle_start, ProjectionOptions::default());
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance; let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(handle_end_closest_t), false, handle_end_scale_distance); let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end) Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
} }
}; };
@ -399,7 +402,7 @@ impl Bezier {
match maximize_arcs { match maximize_arcs {
ArcStrategy::Automatic => { ArcStrategy::Automatic => {
let (auto_arcs, final_low_t) = self.approximate_curve_with_arcs(0., 1., error, max_iterations, true); let (auto_arcs, final_low_t) = self.approximate_curve_with_arcs(0., 1., error, max_iterations, true);
let arc_approximations = self.split(final_low_t)[1].arcs(ArcsOptions { let arc_approximations = self.split(TValue::Parametric(final_low_t))[1].arcs(ArcsOptions {
strategy: ArcStrategy::FavorCorrectness, strategy: ArcStrategy::FavorCorrectness,
error, error,
max_iterations, max_iterations,
@ -444,9 +447,9 @@ impl Bezier {
// Inner loop to find the next maximal segment of the curve that can be approximated with a circular arc // Inner loop to find the next maximal segment of the curve that can be approximated with a circular arc
while iterations <= max_iterations { while iterations <= max_iterations {
iterations += 1; iterations += 1;
let p1 = self.evaluate(ComputeType::Parametric(low)); let p1 = self.evaluate(TValue::Parametric(low));
let p2 = self.evaluate(ComputeType::Parametric(middle)); let p2 = self.evaluate(TValue::Parametric(middle));
let p3 = self.evaluate(ComputeType::Parametric(high)); let p3 = self.evaluate(TValue::Parametric(high));
let wrapped_center = utils::compute_circle_center_from_points(p1, p2, p3); let wrapped_center = utils::compute_circle_center_from_points(p1, p2, p3);
// If the segment is linear, move on to next segment // If the segment is linear, move on to next segment
@ -486,8 +489,8 @@ impl Bezier {
}; };
// Use points in between low, middle, and high to evaluate how well the arc approximates the curve // Use points in between low, middle, and high to evaluate how well the arc approximates the curve
let e1 = self.evaluate(ComputeType::Parametric((low + middle) / 2.)); let e1 = self.evaluate(TValue::Parametric((low + middle) / 2.));
let e2 = self.evaluate(ComputeType::Parametric((middle + high) / 2.)); let e2 = self.evaluate(TValue::Parametric((middle + high) / 2.));
// Iterate until we find the largest good approximation such that the next iteration is not a good approximation with an arc // Iterate until we find the largest good approximation such that the next iteration is not a good approximation with an arc
if utils::f64_compare(radius, e1.distance(center), error) && utils::f64_compare(radius, e2.distance(center), error) { if utils::f64_compare(radius, e1.distance(center), error) && utils::f64_compare(radius, e2.distance(center), error) {
@ -537,7 +540,7 @@ impl Bezier {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::ComputeType; use crate::utils::TValue;
use super::compare::{compare_arcs, compare_vector_of_beziers}; use super::compare::{compare_arcs, compare_vector_of_beziers};
use super::*; use super::*;
@ -545,37 +548,37 @@ mod tests {
#[test] #[test]
fn test_split() { fn test_split() {
let line = Bezier::from_linear_coordinates(25., 25., 75., 75.); let line = Bezier::from_linear_coordinates(25., 25., 75., 75.);
let [part1, part2] = line.split(0.5); let [part1, part2] = line.split(TValue::Parametric(0.5));
assert_eq!(part1.start(), line.start()); assert_eq!(part1.start(), line.start());
assert_eq!(part1.end(), line.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part1.end(), line.evaluate(TValue::Parametric(0.5)));
assert_eq!(part1.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.25))); assert_eq!(part1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.25)));
assert_eq!(part2.start(), line.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part2.start(), line.evaluate(TValue::Parametric(0.5)));
assert_eq!(part2.end(), line.end()); assert_eq!(part2.end(), line.end());
assert_eq!(part2.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.75))); assert_eq!(part2.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.75)));
let quad_bezier = Bezier::from_quadratic_coordinates(10., 10., 50., 50., 90., 10.); let quad_bezier = Bezier::from_quadratic_coordinates(10., 10., 50., 50., 90., 10.);
let [part3, part4] = quad_bezier.split(0.5); let [part3, part4] = quad_bezier.split(TValue::Parametric(0.5));
assert_eq!(part3.start(), quad_bezier.start()); assert_eq!(part3.start(), quad_bezier.start());
assert_eq!(part3.end(), quad_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part3.end(), quad_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part3.evaluate(ComputeType::Parametric(0.5)), quad_bezier.evaluate(ComputeType::Parametric(0.25))); assert_eq!(part3.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(part4.start(), quad_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part4.start(), quad_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part4.end(), quad_bezier.end()); assert_eq!(part4.end(), quad_bezier.end());
assert_eq!(part4.evaluate(ComputeType::Parametric(0.5)), quad_bezier.evaluate(ComputeType::Parametric(0.75))); assert_eq!(part4.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.75)));
let cubic_bezier = Bezier::from_cubic_coordinates(10., 10., 50., 50., 90., 10., 40., 50.); let cubic_bezier = Bezier::from_cubic_coordinates(10., 10., 50., 50., 90., 10., 40., 50.);
let [part5, part6] = cubic_bezier.split(0.5); let [part5, part6] = cubic_bezier.split(TValue::Parametric(0.5));
assert_eq!(part5.start(), cubic_bezier.start()); assert_eq!(part5.start(), cubic_bezier.start());
assert_eq!(part5.end(), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part5.end(), cubic_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part5.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.25))); assert_eq!(part5.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(part6.start(), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part6.start(), cubic_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part6.end(), cubic_bezier.end()); assert_eq!(part6.end(), cubic_bezier.end());
assert_eq!(part6.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.75))); assert_eq!(part6.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.75)));
} }
#[test] #[test]
@ -586,24 +589,24 @@ mod tests {
let bezier_quadratic = Bezier::from_quadratic_dvec2(start, DVec2::new(140., 30.), end); let bezier_quadratic = Bezier::from_quadratic_dvec2(start, DVec2::new(140., 30.), end);
// Test splitting a quadratic bezier at the startpoint // Test splitting a quadratic bezier at the startpoint
let [point_bezier1, remainder1] = bezier_quadratic.split(0.); let [point_bezier1, remainder1] = bezier_quadratic.split(TValue::Parametric(0.));
assert_eq!(point_bezier1, Bezier::from_quadratic_dvec2(start, start, start)); assert_eq!(point_bezier1, Bezier::from_quadratic_dvec2(start, start, start));
assert!(remainder1.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); assert!(remainder1.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE));
// Test splitting a quadratic bezier at the endpoint // Test splitting a quadratic bezier at the endpoint
let [remainder2, point_bezier2] = bezier_quadratic.split(1.); let [remainder2, point_bezier2] = bezier_quadratic.split(TValue::Parametric(1.));
assert_eq!(point_bezier2, Bezier::from_quadratic_dvec2(end, end, end)); assert_eq!(point_bezier2, Bezier::from_quadratic_dvec2(end, end, end));
assert!(remainder2.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); assert!(remainder2.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE));
let bezier_cubic = Bezier::from_cubic_dvec2(start, DVec2::new(60., 140.), DVec2::new(150., 30.), end); let bezier_cubic = Bezier::from_cubic_dvec2(start, DVec2::new(60., 140.), DVec2::new(150., 30.), end);
// Test splitting a cubic bezier at the startpoint // Test splitting a cubic bezier at the startpoint
let [point_bezier3, remainder3] = bezier_cubic.split(0.); let [point_bezier3, remainder3] = bezier_cubic.split(TValue::Parametric(0.));
assert_eq!(point_bezier3, Bezier::from_cubic_dvec2(start, start, start, start)); assert_eq!(point_bezier3, Bezier::from_cubic_dvec2(start, start, start, start));
assert!(remainder3.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE)); assert!(remainder3.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE));
// Test splitting a cubic bezier at the endpoint // Test splitting a cubic bezier at the endpoint
let [remainder4, point_bezier4] = bezier_cubic.split(1.); let [remainder4, point_bezier4] = bezier_cubic.split(TValue::Parametric(1.));
assert_eq!(point_bezier4, Bezier::from_cubic_dvec2(end, end, end, end)); assert_eq!(point_bezier4, Bezier::from_cubic_dvec2(end, end, end, end));
assert!(remainder4.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE)); assert!(remainder4.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE));
} }
@ -611,39 +614,39 @@ mod tests {
#[test] #[test]
fn test_trim() { fn test_trim() {
let line = Bezier::from_linear_coordinates(80., 80., 40., 40.); let line = Bezier::from_linear_coordinates(80., 80., 40., 40.);
let trimmed1 = line.trim(0.25, 0.75); let trimmed1 = line.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
assert_eq!(trimmed1.start(), line.evaluate(ComputeType::Parametric(0.25))); assert_eq!(trimmed1.start(), line.evaluate(TValue::Parametric(0.25)));
assert_eq!(trimmed1.end(), line.evaluate(ComputeType::Parametric(0.75))); assert_eq!(trimmed1.end(), line.evaluate(TValue::Parametric(0.75)));
assert_eq!(trimmed1.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.5))); assert_eq!(trimmed1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.5)));
let quadratic_bezier = Bezier::from_quadratic_coordinates(80., 80., 40., 40., 70., 70.); let quadratic_bezier = Bezier::from_quadratic_coordinates(80., 80., 40., 40., 70., 70.);
let trimmed2 = quadratic_bezier.trim(0.25, 0.75); let trimmed2 = quadratic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(ComputeType::Parametric(0.25))); assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(ComputeType::Parametric(0.75))); assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(TValue::Parametric(0.75)));
assert_eq!(trimmed2.evaluate(ComputeType::Parametric(0.5)), quadratic_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(trimmed2.evaluate(TValue::Parametric(0.5)), quadratic_bezier.evaluate(TValue::Parametric(0.5)));
let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.); let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.);
let trimmed3 = cubic_bezier.trim(0.25, 0.75); let trimmed3 = cubic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
assert_eq!(trimmed3.start(), cubic_bezier.evaluate(ComputeType::Parametric(0.25))); assert_eq!(trimmed3.start(), cubic_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(trimmed3.end(), cubic_bezier.evaluate(ComputeType::Parametric(0.75))); assert_eq!(trimmed3.end(), cubic_bezier.evaluate(TValue::Parametric(0.75)));
assert_eq!(trimmed3.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(trimmed3.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.5)));
} }
#[test] #[test]
fn test_trim_t2_greater_than_t1() { fn test_trim_t2_greater_than_t1() {
// Test trimming quadratic curve when t2 > t1 // Test trimming quadratic curve when t2 > t1
let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.); let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.);
let trim1 = bezier_quadratic.trim(0.25, 0.75); let trim1 = bezier_quadratic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
let trim2 = bezier_quadratic.trim(0.75, 0.25).reverse(); let trim2 = bezier_quadratic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)).reverse();
assert!(trim1.abs_diff_eq(&trim2, MAX_ABSOLUTE_DIFFERENCE)); assert!(trim1.abs_diff_eq(&trim2, MAX_ABSOLUTE_DIFFERENCE));
// Test trimming cubic curve when t2 > t1 // Test trimming cubic curve when t2 > t1
let bezier_cubic = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.); let bezier_cubic = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let trim3 = bezier_cubic.trim(0.25, 0.75); let trim3 = bezier_cubic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
let trim4 = bezier_cubic.trim(0.75, 0.25).reverse(); let trim4 = bezier_cubic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)).reverse();
assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE)); assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE));
} }
@ -704,7 +707,7 @@ mod tests {
assert!(reduced_curves assert!(reduced_curves
.iter() .iter()
.zip(helper_t_values.windows(2)) .zip(helper_t_values.windows(2))
.all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(t_pair[0], t_pair[1]), MAX_ABSOLUTE_DIFFERENCE))) .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)))
} }
#[test] #[test]
@ -744,23 +747,23 @@ mod tests {
// Assert the first length-wise piece of the outline is 10 units from the line // Assert the first length-wise piece of the outline is 10 units from the line
assert!(f64_compare( assert!(f64_compare(
outline[0].evaluate(ComputeType::Parametric(0.25)).distance(line.evaluate(ComputeType::Parametric(0.25))), outline[0].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))),
10., 10.,
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
)); // f64 )); // f64
// Assert the first cap touches the line end point at the halfway point // Assert the first cap touches the line end point at the halfway point
assert!(outline[1].evaluate(ComputeType::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); assert!(outline[1].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
// Assert the second length-wise piece of the outline is 10 units from the line // Assert the second length-wise piece of the outline is 10 units from the line
assert!(f64_compare( assert!(f64_compare(
outline[2].evaluate(ComputeType::Parametric(0.25)).distance(line.evaluate(ComputeType::Parametric(0.75))), outline[2].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))),
10., 10.,
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
)); // f64 )); // f64
// Assert the second cap touches the line start point at the halfway point // Assert the second cap touches the line start point at the halfway point
assert!(outline[3].evaluate(ComputeType::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); assert!(outline[3].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
} }
#[test] #[test]
@ -778,17 +781,17 @@ mod tests {
// Assert the scaled bezier is 30 units from the line // Assert the scaled bezier is 30 units from the line
assert!(f64_compare( assert!(f64_compare(
scaled_bezier.evaluate(ComputeType::Parametric(0.)).distance(bezier.evaluate(ComputeType::Parametric(0.))), scaled_bezier.evaluate(TValue::Parametric(0.)).distance(bezier.evaluate(TValue::Parametric(0.))),
30., 30.,
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
)); ));
assert!(f64_compare( assert!(f64_compare(
scaled_bezier.evaluate(ComputeType::Parametric(1.)).distance(bezier.evaluate(ComputeType::Parametric(1.))), scaled_bezier.evaluate(TValue::Parametric(1.)).distance(bezier.evaluate(TValue::Parametric(1.))),
30., 30.,
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
)); ));
assert!(f64_compare( assert!(f64_compare(
scaled_bezier.evaluate(ComputeType::Parametric(0.5)).distance(bezier.evaluate(ComputeType::Parametric(0.5))), scaled_bezier.evaluate(TValue::Parametric(0.5)).distance(bezier.evaluate(TValue::Parametric(0.5))),
30., 30.,
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
)); ));

View file

@ -10,6 +10,8 @@ pub const NUM_DISTANCES: usize = 5;
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; 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 /// Minimum allowable separation between adjacent `t` values when calculating curve intersections
pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3; pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3;
/// Default error bound for `t_value_to_parametric` function when TValue argument is Euclidean
pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001;
// Method argument defaults // Method argument defaults

View file

@ -7,4 +7,4 @@ mod utils;
pub use bezier::*; pub use bezier::*;
pub use subpath::*; pub use subpath::*;
pub use utils::ComputeType; pub use utils::TValue;

View file

@ -1,5 +1,5 @@
use super::*; use super::*;
use crate::{ComputeType, ProjectionOptions}; use crate::{ProjectionOptions, TValue};
use glam::DVec2; use glam::DVec2;
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
@ -22,7 +22,7 @@ impl Subpath {
.iter() .iter()
.map(|bezier| { .map(|bezier| {
let project_t = bezier.project(point, options); let project_t = bezier.project(point, options);
(bezier.evaluate(ComputeType::Parametric(project_t)).distance(point), project_t) (bezier.evaluate(TValue::Parametric(project_t)).distance(point), project_t)
}) })
.enumerate() .enumerate()
.min_by(|(_, (distance1, _)), (_, (distance2, _))| distance1.total_cmp(distance2)) .min_by(|(_, (distance1, _)), (_, (distance2, _))| distance1.total_cmp(distance2))

View file

@ -1,14 +1,14 @@
use super::*; use super::*;
use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
use crate::utils::f64_compare; use crate::utils::f64_compare;
use crate::ComputeType; use crate::TValue;
impl Subpath { impl Subpath {
/// Inserts a `ManipulatorGroup` at a certain point along the subpath based on the parametric `t`-value provided. /// Inserts a `ManipulatorGroup` at a certain point along the subpath based on the parametric `t`-value provided.
/// Expects `t` to be within the inclusive range `[0, 1]`. /// Expects `t` to be within the inclusive range `[0, 1]`.
pub fn insert(&mut self, t: ComputeType) { pub fn insert(&mut self, t: TValue) {
match t { match t {
ComputeType::Parametric(t) => { TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t)); assert!((0.0..=1.).contains(&t));
let number_of_curves = self.len_segments() as f64; let number_of_curves = self.len_segments() as f64;
@ -25,7 +25,7 @@ impl Subpath {
// But the above if case would catch that, since `target_curve_t` would be 0. // But the above if case would catch that, since `target_curve_t` would be 0.
let curve = self.iter().nth(target_curve_index as usize).unwrap(); let curve = self.iter().nth(target_curve_index as usize).unwrap();
let [first, second] = curve.split(target_curve_t); let [first, second] = curve.split(TValue::Parametric(target_curve_t));
let new_group = ManipulatorGroup { let new_group = ManipulatorGroup {
anchor: first.end(), anchor: first.end(),
in_handle: first.handle_end(), in_handle: first.handle_end(),
@ -37,8 +37,8 @@ impl Subpath {
self.manipulator_groups[((target_curve_index as usize) + 2) % number_of_groups].in_handle = second.handle_end(); self.manipulator_groups[((target_curve_index as usize) + 2) % number_of_groups].in_handle = second.handle_end();
} }
// TODO: change this implementation to Euclidean compute // TODO: change this implementation to Euclidean compute
ComputeType::Euclidean(_t) => {} TValue::Euclidean(_t) => {}
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
} }
} }
} }
@ -94,9 +94,9 @@ mod tests {
#[test] #[test]
fn insert_in_first_segment_of_open_subpath() { fn insert_in_first_segment_of_open_subpath() {
let mut subpath = set_up_open_subpath(); let mut subpath = set_up_open_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.2)); let location = subpath.evaluate(TValue::Parametric(0.2));
let split_pair = subpath.iter().next().unwrap().split((0.2 * 3.) % 1.); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.));
subpath.insert(ComputeType::Parametric(0.2)); subpath.insert(TValue::Parametric(0.2));
assert_eq!(subpath.manipulator_groups[1].anchor, location); assert_eq!(subpath.manipulator_groups[1].anchor, location);
assert_eq!(split_pair[0], subpath.iter().next().unwrap()); assert_eq!(split_pair[0], subpath.iter().next().unwrap());
assert_eq!(split_pair[1], subpath.iter().nth(1).unwrap()); assert_eq!(split_pair[1], subpath.iter().nth(1).unwrap());
@ -105,9 +105,9 @@ mod tests {
#[test] #[test]
fn insert_in_last_segment_of_open_subpath() { fn insert_in_last_segment_of_open_subpath() {
let mut subpath = set_up_open_subpath(); let mut subpath = set_up_open_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.9)); let location = subpath.evaluate(TValue::Parametric(0.9));
let split_pair = subpath.iter().nth(2).unwrap().split((0.9 * 3.) % 1.); let split_pair = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.9 * 3.) % 1.));
subpath.insert(ComputeType::Parametric(0.9)); subpath.insert(TValue::Parametric(0.9));
assert_eq!(subpath.manipulator_groups[3].anchor, location); assert_eq!(subpath.manipulator_groups[3].anchor, location);
assert_eq!(split_pair[0], subpath.iter().nth(2).unwrap()); assert_eq!(split_pair[0], subpath.iter().nth(2).unwrap());
assert_eq!(split_pair[1], subpath.iter().nth(3).unwrap()); assert_eq!(split_pair[1], subpath.iter().nth(3).unwrap());
@ -117,8 +117,8 @@ mod tests {
fn insert_at_exisiting_manipulator_group_of_open_subpath() { fn insert_at_exisiting_manipulator_group_of_open_subpath() {
// This will do nothing to the subpath // This will do nothing to the subpath
let mut subpath = set_up_open_subpath(); let mut subpath = set_up_open_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.75)); let location = subpath.evaluate(TValue::Parametric(0.75));
subpath.insert(ComputeType::Parametric(0.75)); subpath.insert(TValue::Parametric(0.75));
assert_eq!(subpath.manipulator_groups[3].anchor, location); assert_eq!(subpath.manipulator_groups[3].anchor, location);
assert_eq!(subpath.manipulator_groups.len(), 5); assert_eq!(subpath.manipulator_groups.len(), 5);
assert_eq!(subpath.len_segments(), 4); assert_eq!(subpath.len_segments(), 4);
@ -127,9 +127,9 @@ mod tests {
#[test] #[test]
fn insert_at_last_segment_of_closed_subpath() { fn insert_at_last_segment_of_closed_subpath() {
let mut subpath = set_up_closed_subpath(); let mut subpath = set_up_closed_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.9)); let location = subpath.evaluate(TValue::Parametric(0.9));
let split_pair = subpath.iter().nth(3).unwrap().split((0.9 * 4.) % 1.); let split_pair = subpath.iter().nth(3).unwrap().split(TValue::Parametric((0.9 * 4.) % 1.));
subpath.insert(ComputeType::Parametric(0.9)); subpath.insert(TValue::Parametric(0.9));
assert_eq!(subpath.manipulator_groups[4].anchor, location); assert_eq!(subpath.manipulator_groups[4].anchor, location);
assert_eq!(split_pair[0], subpath.iter().nth(3).unwrap()); assert_eq!(split_pair[0], subpath.iter().nth(3).unwrap());
assert_eq!(split_pair[1], subpath.iter().nth(4).unwrap()); assert_eq!(split_pair[1], subpath.iter().nth(4).unwrap());
@ -140,8 +140,8 @@ mod tests {
fn insert_at_last_manipulator_group_of_closed_subpath() { fn insert_at_last_manipulator_group_of_closed_subpath() {
// This will do nothing to the subpath // This will do nothing to the subpath
let mut subpath = set_up_closed_subpath(); let mut subpath = set_up_closed_subpath();
let location = subpath.evaluate(ComputeType::Parametric(1.)); let location = subpath.evaluate(TValue::Parametric(1.));
subpath.insert(ComputeType::Parametric(1.)); subpath.insert(TValue::Parametric(1.));
assert_eq!(subpath.manipulator_groups[0].anchor, location); assert_eq!(subpath.manipulator_groups[0].anchor, location);
assert_eq!(subpath.manipulator_groups.len(), 4); assert_eq!(subpath.manipulator_groups.len(), 4);
assert!(subpath.closed); assert!(subpath.closed);

View file

@ -1,26 +1,26 @@
use super::*; use super::*;
use crate::consts::MIN_SEPERATION_VALUE; use crate::consts::MIN_SEPERATION_VALUE;
use crate::ComputeType; use crate::TValue;
use glam::DVec2; use glam::DVec2;
impl Subpath { impl Subpath {
/// Calculate the point on the subpath based on the parametric `t`-value provided. /// Calculate the point on the subpath based on the parametric `t`-value provided.
/// Expects `t` to be within the inclusive range `[0, 1]`. /// Expects `t` to be within the inclusive range `[0, 1]`.
pub fn evaluate(&self, t: ComputeType) -> DVec2 { pub fn evaluate(&self, t: TValue) -> DVec2 {
match t { match t {
ComputeType::Parametric(t) => { TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t)); assert!((0.0..=1.).contains(&t));
if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) {
curve.evaluate(ComputeType::Parametric(target_curve_t)) curve.evaluate(TValue::Parametric(target_curve_t))
} else { } else {
self.iter().last().unwrap().evaluate(ComputeType::Parametric(1.)) self.iter().last().unwrap().evaluate(TValue::Parametric(1.))
} }
} }
// TODO: change this implementation to Euclidean compute // TODO: change this implementation to Euclidean compute
ComputeType::Euclidean(_t) => self.iter().next().unwrap().evaluate(ComputeType::Parametric(0.)), TValue::Euclidean(_t) => self.iter().next().unwrap().evaluate(TValue::Parametric(0.)),
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
} }
} }
@ -54,35 +54,35 @@ impl Subpath {
intersection_t_values intersection_t_values
} }
pub fn tangent(&self, t: ComputeType) -> DVec2 { pub fn tangent(&self, t: TValue) -> DVec2 {
match t { match t {
ComputeType::Parametric(t) => { TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t)); assert!((0.0..=1.).contains(&t));
if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) {
curve.tangent(target_curve_t) curve.tangent(TValue::Parametric(target_curve_t))
} else { } else {
self.iter().last().unwrap().tangent(1.) self.iter().last().unwrap().tangent(TValue::Parametric(1.))
} }
} }
ComputeType::Euclidean(_t) => unimplemented!(), TValue::Euclidean(_t) => unimplemented!(),
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
} }
} }
pub fn normal(&self, t: ComputeType) -> DVec2 { pub fn normal(&self, t: TValue) -> DVec2 {
match t { match t {
ComputeType::Parametric(t) => { TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t)); assert!((0.0..=1.).contains(&t));
if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) {
curve.normal(target_curve_t) curve.normal(TValue::Parametric(target_curve_t))
} else { } else {
self.iter().last().unwrap().normal(1.) self.iter().last().unwrap().normal(TValue::Parametric(1.))
} }
} }
ComputeType::Euclidean(_t) => unimplemented!(), TValue::Euclidean(_t) => unimplemented!(),
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
} }
} }
} }
@ -124,16 +124,16 @@ mod tests {
); );
let t0 = 0.; let t0 = 0.;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t0)), bezier.evaluate(ComputeType::Parametric(t0))); assert_eq!(subpath.evaluate(TValue::Parametric(t0)), bezier.evaluate(TValue::Parametric(t0)));
let t1 = 0.25; let t1 = 0.25;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t1)), bezier.evaluate(ComputeType::Parametric(t1))); assert_eq!(subpath.evaluate(TValue::Parametric(t1)), bezier.evaluate(TValue::Parametric(t1)));
let t2 = 0.50; let t2 = 0.50;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t2)), bezier.evaluate(ComputeType::Parametric(t2))); assert_eq!(subpath.evaluate(TValue::Parametric(t2)), bezier.evaluate(TValue::Parametric(t2)));
let t3 = 1.; let t3 = 1.;
assert_eq!(subpath.evaluate(ComputeType::Parametric(t3)), bezier.evaluate(ComputeType::Parametric(t3))); assert_eq!(subpath.evaluate(TValue::Parametric(t3)), bezier.evaluate(TValue::Parametric(t3)));
} }
#[test] #[test]
@ -176,43 +176,38 @@ mod tests {
let t0 = 0.; let t0 = 0.;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t0)), subpath.evaluate(TValue::Parametric(t0)),
linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t0))), linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
let t1 = 0.25; let t1 = 0.25;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t1)), subpath.evaluate(TValue::Parametric(t1)),
linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t1))), linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
let t2 = 0.50; let t2 = 0.50;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t2)), subpath.evaluate(TValue::Parametric(t2)),
quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t2))), quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
let t3 = 0.75; let t3 = 0.75;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t3)), subpath.evaluate(TValue::Parametric(t3)),
quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t3))), quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
let t4 = 1.0; let t4 = 1.0;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(subpath.evaluate(TValue::Parametric(t4)), quadratic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all());
subpath.evaluate(ComputeType::Parametric(t4)),
quadratic_bezier.evaluate(ComputeType::Parametric(1.)),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
// Test closed subpath // Test closed subpath
@ -221,19 +216,14 @@ mod tests {
let t5 = 2. / 3.; let t5 = 2. / 3.;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
subpath.evaluate(ComputeType::Parametric(t5)), subpath.evaluate(TValue::Parametric(t5)),
cubic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t5))), cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
let t6 = 1.; let t6 = 1.;
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(subpath.evaluate(TValue::Parametric(t6)), cubic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all());
subpath.evaluate(ComputeType::Parametric(t6)),
cubic_bezier.evaluate(ComputeType::Parametric(1.)),
MAX_ABSOLUTE_DIFFERENCE
)
.all());
} }
#[test] #[test]
@ -281,22 +271,22 @@ mod tests {
let subpath_intersections = subpath.intersections(&line, None, None); let subpath_intersections = subpath.intersections(&line, None, None);
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])), cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])), subpath.evaluate(TValue::Parametric(subpath_intersections[0])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])), quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])), subpath.evaluate(TValue::Parametric(subpath_intersections[1])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])), quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])), subpath.evaluate(TValue::Parametric(subpath_intersections[2])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
@ -348,15 +338,15 @@ mod tests {
let subpath_intersections = subpath.intersections(&line, None, None); let subpath_intersections = subpath.intersections(&line, None, None);
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])), cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])), subpath.evaluate(TValue::Parametric(subpath_intersections[0])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])), quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])), subpath.evaluate(TValue::Parametric(subpath_intersections[1])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
@ -407,22 +397,22 @@ mod tests {
let subpath_intersections = subpath.intersections(&line, None, None); let subpath_intersections = subpath.intersections(&line, None, None);
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])), cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])), subpath.evaluate(TValue::Parametric(subpath_intersections[0])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])), quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])), subpath.evaluate(TValue::Parametric(subpath_intersections[1])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());
assert!(utils::dvec2_compare( assert!(utils::dvec2_compare(
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])), quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])),
subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])), subpath.evaluate(TValue::Parametric(subpath_intersections[2])),
MAX_ABSOLUTE_DIFFERENCE MAX_ABSOLUTE_DIFFERENCE
) )
.all()); .all());

View file

@ -1,14 +1,14 @@
use super::*; use super::*;
use crate::ComputeType; use crate::TValue;
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc. /// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
impl Subpath { impl Subpath {
/// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`. /// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`.
/// If the original Subpath was closed, a single open Subpath will be returned. /// If the original Subpath was closed, a single open Subpath will be returned.
/// If the original Subpath was open, two open Subpaths will be returned. /// If the original Subpath was open, two open Subpaths will be returned.
pub fn split(&self, t: ComputeType) -> (Subpath, Option<Subpath>) { pub fn split(&self, t: TValue) -> (Subpath, Option<Subpath>) {
match t { match t {
ComputeType::Parametric(t) => { TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t)); assert!((0.0..=1.).contains(&t));
let number_of_curves = self.len_segments() as f64; let number_of_curves = self.len_segments() as f64;
@ -22,7 +22,7 @@ impl Subpath {
let optional_curve = self.iter().nth(target_curve_index as usize); let optional_curve = self.iter().nth(target_curve_index as usize);
let curve = optional_curve.unwrap_or_else(|| self.iter().last().unwrap()); let curve = optional_curve.unwrap_or_else(|| self.iter().last().unwrap());
let [first_bezier, second_bezier] = curve.split(if t == 1. { t } else { target_curve_t }); let [first_bezier, second_bezier] = curve.split(TValue::Parametric(if t == 1. { t } else { target_curve_t }));
let mut clone = self.manipulator_groups.clone(); let mut clone = self.manipulator_groups.clone();
let (mut first_split, mut second_split) = if t > 0. { let (mut first_split, mut second_split) = if t > 0. {
@ -83,8 +83,8 @@ impl Subpath {
} }
} }
// TODO: change this implementation to Euclidean compute // TODO: change this implementation to Euclidean compute
ComputeType::Euclidean(_t) => todo!(), TValue::Euclidean(_t) => todo!(),
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
} }
} }
} }
@ -140,9 +140,9 @@ mod tests {
#[test] #[test]
fn split_an_open_subpath() { fn split_an_open_subpath() {
let subpath = set_up_open_subpath(); let subpath = set_up_open_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.2)); let location = subpath.evaluate(TValue::Parametric(0.2));
let split_pair = subpath.iter().next().unwrap().split((0.2 * 3.) % 1.); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.));
let (first, second) = subpath.split(ComputeType::Parametric(0.2)); let (first, second) = subpath.split(TValue::Parametric(0.2));
assert!(second.is_some()); assert!(second.is_some());
let second = second.unwrap(); let second = second.unwrap();
assert_eq!(first.manipulator_groups[1].anchor, location); assert_eq!(first.manipulator_groups[1].anchor, location);
@ -154,9 +154,9 @@ mod tests {
#[test] #[test]
fn split_at_start_of_an_open_subpath() { fn split_at_start_of_an_open_subpath() {
let subpath = set_up_open_subpath(); let subpath = set_up_open_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.)); let location = subpath.evaluate(TValue::Parametric(0.));
let split_pair = subpath.iter().next().unwrap().split(0.); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.));
let (first, second) = subpath.split(ComputeType::Parametric(0.)); let (first, second) = subpath.split(TValue::Parametric(0.));
assert!(second.is_some()); assert!(second.is_some());
let second = second.unwrap(); let second = second.unwrap();
assert_eq!( assert_eq!(
@ -175,9 +175,9 @@ mod tests {
#[test] #[test]
fn split_at_end_of_an_open_subpath() { fn split_at_end_of_an_open_subpath() {
let subpath = set_up_open_subpath(); let subpath = set_up_open_subpath();
let location = subpath.evaluate(ComputeType::Parametric(1.)); let location = subpath.evaluate(TValue::Parametric(1.));
let split_pair = subpath.iter().last().unwrap().split(1.); let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.));
let (first, second) = subpath.split(ComputeType::Parametric(1.)); let (first, second) = subpath.split(TValue::Parametric(1.));
assert!(second.is_some()); assert!(second.is_some());
let second = second.unwrap(); let second = second.unwrap();
assert_eq!(first.manipulator_groups[3].anchor, location); assert_eq!(first.manipulator_groups[3].anchor, location);
@ -196,9 +196,9 @@ mod tests {
#[test] #[test]
fn split_a_closed_subpath() { fn split_a_closed_subpath() {
let subpath = set_up_closed_subpath(); let subpath = set_up_closed_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.2)); let location = subpath.evaluate(TValue::Parametric(0.2));
let split_pair = subpath.iter().next().unwrap().split((0.2 * 4.) % 1.); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.));
let (first, second) = subpath.split(ComputeType::Parametric(0.2)); let (first, second) = subpath.split(TValue::Parametric(0.2));
assert!(second.is_none()); assert!(second.is_none());
assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[0].anchor, location);
assert_eq!(first.manipulator_groups[5].anchor, location); assert_eq!(first.manipulator_groups[5].anchor, location);
@ -210,8 +210,8 @@ mod tests {
#[test] #[test]
fn split_at_start_of_a_closed_subpath() { fn split_at_start_of_a_closed_subpath() {
let subpath = set_up_closed_subpath(); let subpath = set_up_closed_subpath();
let location = subpath.evaluate(ComputeType::Parametric(0.)); let location = subpath.evaluate(TValue::Parametric(0.));
let (first, second) = subpath.split(ComputeType::Parametric(0.)); let (first, second) = subpath.split(TValue::Parametric(0.));
assert!(second.is_none()); assert!(second.is_none());
assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[0].anchor, location);
assert_eq!(first.manipulator_groups[4].anchor, location); assert_eq!(first.manipulator_groups[4].anchor, location);
@ -224,8 +224,8 @@ mod tests {
#[test] #[test]
fn split_at_end_of_a_closed_subpath() { fn split_at_end_of_a_closed_subpath() {
let subpath = set_up_closed_subpath(); let subpath = set_up_closed_subpath();
let location = subpath.evaluate(ComputeType::Parametric(1.)); let location = subpath.evaluate(TValue::Parametric(1.));
let (first, second) = subpath.split(ComputeType::Parametric(1.)); let (first, second) = subpath.split(TValue::Parametric(1.));
assert!(second.is_none()); assert!(second.is_none());
assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[0].anchor, location);
assert_eq!(first.manipulator_groups[4].anchor, location); assert_eq!(first.manipulator_groups[4].anchor, location);

View file

@ -4,10 +4,18 @@ use glam::{BVec2, DMat2, DVec2};
use std::f64::consts::PI; use std::f64::consts::PI;
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
pub enum ComputeType { /// A structure which can be used to reference a particular point along a `Bezier`.
/// Assuming a 2-dimensional Bezier is represented as a parametric curve defined by components `(x(f(t), y(f(t))))`, this structure defines variants for `f(t)`.
/// - The `Parametric` variant represents the point calculated using the parametric equation of the curve at argument `t`. That is, `f(t) = t`. Speed along the curve's parametric form is not constant. `t` must lie in the range `[0, 1]`.
/// - The `Euclidean` variant represents the point calculated at a distance ratio `t` along the arc length of the curve in the range `[0, 1]`. Speed is constant along the curve's arc length.
/// - E.g. If `d` is the distance from the start point of a `Bezier` to a certain point along the curve, and `l` is the total arc length of the curve, that certain point lies at a distance ratio `t = d / l`.
/// - All `Bezier` functions will implicitly convert a Euclidean [TValue] argument to a parametric `t`-value using binary search, computed within a particular error. That is, a point at distance ratio `t*`,
/// satisfying `|t* - t| <= error`. The default error is `0.001`. Given this requires a lengthier calculation, it is not recommended to use the `Euclidean` or `EuclideanWithinError` variants frequently in computationally intensive tasks.
/// - The `EuclideanWithinError` variant functions exactly as the `Euclidean` variant, but allows the `error` to be customized when computing `t` internally.
pub enum TValue {
Parametric(f64), Parametric(f64),
Euclidean(f64), Euclidean(f64),
EuclideanWithinError { t: f64, epsilon: f64 }, EuclideanWithinError { t: f64, error: f64 },
} }
/// Helper to perform the computation of a and c, where b is the provided point on the curve. /// Helper to perform the computation of a and c, where b is the provided point on the curve.

View file

@ -1,7 +1,7 @@
import { WasmBezier } from "@/../wasm/pkg"; import { WasmBezier } from "@/../wasm/pkg";
import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features"; import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features";
import { renderDemo } from "@/utils/render"; import { renderDemo } from "@/utils/render";
import { getConstructorKey, getCurveType, BezierCallback, BezierCurveType, SliderOption, WasmBezierManipulatorKey, ComputeType, Demo } from "@/utils/types"; import { getConstructorKey, getCurveType, BezierCallback, BezierCurveType, SliderOption, WasmBezierManipulatorKey, TVariant, Demo } from "@/utils/types";
const SELECTABLE_RANGE = 10; const SELECTABLE_RANGE = 10;
@ -24,7 +24,7 @@ class BezierDemo extends HTMLElement implements Demo {
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
computeType!: ComputeType; tVariant!: TVariant;
// Data // Data
bezier!: WasmBezier; bezier!: WasmBezier;
@ -40,12 +40,12 @@ class BezierDemo extends HTMLElement implements Demo {
sliderUnits!: Record<string, string | string[]>; sliderUnits!: Record<string, string | string[]>;
static get observedAttributes(): string[] { static get observedAttributes(): string[] {
return ["computetype"]; return ["tvariant"];
} }
attributeChangedCallback(name: string, oldValue: string, newValue: string): void { attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
if (name === "computetype" && oldValue) { if (name === "tvariant" && oldValue) {
this.computeType = (newValue || "Parametric") as ComputeType; this.tVariant = (newValue || "Parametric") as TVariant;
const figure = this.querySelector("figure") as HTMLElement; const figure = this.querySelector("figure") as HTMLElement;
this.drawDemo(figure); this.drawDemo(figure);
} }
@ -57,7 +57,7 @@ class BezierDemo extends HTMLElement implements Demo {
this.key = this.getAttribute("key") as BezierFeatureKey; this.key = this.getAttribute("key") as BezierFeatureKey;
this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.computeType = (this.getAttribute("computetype") || "Parametric") as ComputeType; this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant;
this.callback = bezierFeatures[this.key].callback as BezierCallback; this.callback = bezierFeatures[this.key].callback as BezierCallback;
const curveType = getCurveType(this.points.length); const curveType = getCurveType(this.points.length);
@ -78,7 +78,7 @@ class BezierDemo extends HTMLElement implements Demo {
} }
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void { drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void {
figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation, this.computeType); figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation, this.tVariant);
} }
onMouseDown(event: MouseEvent): void { onMouseDown(event: MouseEvent): void {

View file

@ -1,6 +1,6 @@
import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features"; import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features";
import { renderDemoPane } from "@/utils/render"; import { renderDemoPane } from "@/utils/render";
import { BezierCurveType, BEZIER_CURVE_TYPE, ComputeType, BezierDemoOptions, SliderOption, Demo, DemoPane, BezierDemoArgs } from "@/utils/types"; import { BezierCurveType, BEZIER_CURVE_TYPE, TVariant, BezierDemoOptions, SliderOption, Demo, DemoPane, BezierDemoArgs } from "@/utils/types";
const demoDefaults = { const demoDefaults = {
Linear: { Linear: {
@ -36,24 +36,23 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
chooseComputeType!: boolean; chooseTVariant!: boolean;
// Data // Data
demos!: BezierDemoArgs[]; demos!: BezierDemoArgs[];
id!: string; id!: string;
computeType!: ComputeType; tVariant!: TVariant;
connectedCallback(): void { connectedCallback(): void {
this.computeType = "Parametric"; this.tVariant = "Parametric";
this.key = (this.getAttribute("name") || "") as BezierFeatureKey; this.key = (this.getAttribute("name") || "") as BezierFeatureKey;
this.id = `bezier/${this.key}`; this.id = `bezier/${this.key}`;
this.name = bezierFeatures[this.key].name; this.name = bezierFeatures[this.key].name;
this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]"); this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.chooseComputeType = this.getAttribute("chooseComputeType") === "true"; this.chooseTVariant = this.getAttribute("chooseTVariant") === "true";
// Use quadratic slider options as a default if sliders are not provided for the other curve types. // Use quadratic slider options as a default if sliders are not provided for the other curve types.
const defaultSliderOptions: SliderOption[] = this.demoOptions.Quadratic?.sliderOptions || []; const defaultSliderOptions: SliderOption[] = this.demoOptions.Quadratic?.sliderOptions || [];
this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => { this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => {
@ -80,7 +79,7 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
bezierDemo.setAttribute("key", this.key); bezierDemo.setAttribute("key", this.key);
bezierDemo.setAttribute("sliderOptions", JSON.stringify(demo.sliderOptions)); bezierDemo.setAttribute("sliderOptions", JSON.stringify(demo.sliderOptions));
bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
bezierDemo.setAttribute("computetype", this.computeType); bezierDemo.setAttribute("tvariant", this.tVariant);
return bezierDemo; return bezierDemo;
} }
} }

View file

@ -2,7 +2,7 @@ import { WasmSubpath } from "@/../wasm/pkg";
import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features"; import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features";
import { renderDemo } from "@/utils/render"; import { renderDemo } from "@/utils/render";
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, ComputeType } from "@/utils/types"; import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, TVariant } from "@/utils/types";
const SELECTABLE_RANGE = 10; const SELECTABLE_RANGE = 10;
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"]; const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
@ -21,7 +21,7 @@ class SubpathDemo extends HTMLElement {
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
computeType!: ComputeType; tVariant!: TVariant;
// Data // Data
subpath!: WasmSubpath; subpath!: WasmSubpath;
@ -37,12 +37,12 @@ class SubpathDemo extends HTMLElement {
sliderUnits!: Record<string, string | string[]>; sliderUnits!: Record<string, string | string[]>;
static get observedAttributes(): string[] { static get observedAttributes(): string[] {
return ["computetype"]; return ["tvariant"];
} }
attributeChangedCallback(name: string, oldValue: string, newValue: string): void { attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
if (name === "computetype" && oldValue) { if (name === "tvariant" && oldValue) {
this.computeType = (newValue || "Parametric") as ComputeType; this.tVariant = (newValue || "Parametric") as TVariant;
const figure = this.querySelector("figure") as HTMLElement; const figure = this.querySelector("figure") as HTMLElement;
this.drawDemo(figure); this.drawDemo(figure);
} }
@ -55,7 +55,7 @@ class SubpathDemo extends HTMLElement {
this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.closed = this.getAttribute("closed") === "true"; this.closed = this.getAttribute("closed") === "true";
this.computeType = (this.getAttribute("computetype") || "Parametric") as ComputeType; this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant;
this.callback = subpathFeatures[this.key].callback as SubpathCallback; this.callback = subpathFeatures[this.key].callback as SubpathCallback;
this.subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance; this.subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
@ -73,7 +73,7 @@ class SubpathDemo extends HTMLElement {
} }
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void { drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void {
figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation, this.computeType); figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation, this.tVariant);
} }
onMouseDown(event: MouseEvent): void { onMouseDown(event: MouseEvent): void {

View file

@ -1,6 +1,6 @@
import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features"; import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features";
import { renderDemoPane } from "@/utils/render"; import { renderDemoPane } from "@/utils/render";
import { ComputeType, Demo, DemoPane, SliderOption, SubpathDemoArgs } from "@/utils/types"; import { TVariant, Demo, DemoPane, SliderOption, SubpathDemoArgs } from "@/utils/types";
class SubpathDemoPane extends HTMLElement implements DemoPane { class SubpathDemoPane extends HTMLElement implements DemoPane {
// Props // Props
@ -12,14 +12,14 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
chooseComputeType!: boolean; chooseTVariant!: boolean;
// Data // Data
demos!: SubpathDemoArgs[]; demos!: SubpathDemoArgs[];
id!: string; id!: string;
computeType!: ComputeType; tVariant!: TVariant;
connectedCallback(): void { connectedCallback(): void {
this.demos = [ this.demos = [
@ -47,14 +47,13 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
closed: true, closed: true,
}, },
]; ];
this.computeType = "Parametric"; this.tVariant = "Parametric";
this.key = (this.getAttribute("name") || "") as SubpathFeatureKey; this.key = (this.getAttribute("name") || "") as SubpathFeatureKey;
this.id = `subpath/${this.key}`; this.id = `subpath/${this.key}`;
this.name = subpathFeatures[this.key].name; this.name = subpathFeatures[this.key].name;
this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.chooseComputeType = this.getAttribute("chooseComputeType") === "true"; this.chooseTVariant = this.getAttribute("chooseTVariant") === "true";
this.render(); this.render();
} }
@ -71,7 +70,7 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
subpathDemo.setAttribute("key", this.key); subpathDemo.setAttribute("key", this.key);
subpathDemo.setAttribute("sliderOptions", JSON.stringify(this.sliderOptions)); subpathDemo.setAttribute("sliderOptions", JSON.stringify(this.sliderOptions));
subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
subpathDemo.setAttribute("computetype", this.computeType); subpathDemo.setAttribute("tvariant", this.tVariant);
return subpathDemo; return subpathDemo;
} }
} }

View file

@ -1,6 +1,6 @@
import { WasmBezier } from "@/../wasm/pkg"; import { WasmBezier } from "@/../wasm/pkg";
import { tSliderOptions, tErrorOptions, tMinimumSeperationOptions } from "@/utils/options"; import { tSliderOptions, errorOptions, minimumSeparationOptions } from "@/utils/options";
import { ComputeType, BezierDemoOptions, WasmBezierInstance, BezierCallback } from "@/utils/types"; import { TVariant, BezierDemoOptions, WasmBezierInstance, BezierCallback } from "@/utils/types";
const bezierFeatures = { const bezierFeatures = {
constructor: { constructor: {
@ -67,13 +67,13 @@ const bezierFeatures = {
}, },
evaluate: { evaluate: {
name: "Evaluate", name: "Evaluate",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => bezier.evaluate(options.computeArgument, computeType), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.evaluate(options.t, tVariant),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], sliderOptions: [tSliderOptions],
}, },
}, },
chooseComputeType: true, chooseTVariant: true,
}, },
"lookup-table": { "lookup-table": {
name: "Lookup Table", name: "Lookup Table",
@ -118,25 +118,27 @@ const bezierFeatures = {
}, },
tangent: { tangent: {
name: "Tangent", name: "Tangent",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.tangent(options.t), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.tangent(options.t, tVariant),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], sliderOptions: [tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
normal: { normal: {
name: "Normal", name: "Normal",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.normal(options.t), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.normal(options.t, tVariant),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], sliderOptions: [tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
curvature: { curvature: {
name: "Curvature", name: "Curvature",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.curvature(options.t), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.curvature(options.t, tVariant),
demoOptions: { demoOptions: {
Linear: { Linear: {
disabled: true, disabled: true,
@ -145,19 +147,21 @@ const bezierFeatures = {
sliderOptions: [tSliderOptions], sliderOptions: [tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
split: { split: {
name: "Split", name: "Split",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.split(options.t), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.split(options.t, tVariant),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], sliderOptions: [tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
trim: { trim: {
name: "Trim", name: "Trim",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.trim(options.t1, options.t2), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.trim(options.t1, options.t2, tVariant),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ sliderOptions: [
@ -178,6 +182,7 @@ const bezierFeatures = {
], ],
}, },
}, },
chooseTVariant: true,
}, },
project: { project: {
name: "Project", name: "Project",
@ -400,11 +405,11 @@ const bezierFeatures = {
[180, 10], [180, 10],
[90, 120], [90, 120],
]; ];
return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_seperation); return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_separation);
}, },
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tErrorOptions, tMinimumSeperationOptions], sliderOptions: [errorOptions, minimumSeparationOptions],
}, },
}, },
}, },
@ -417,11 +422,11 @@ const bezierFeatures = {
[40, 120], [40, 120],
[175, 140], [175, 140],
]; ];
return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_seperation); return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_separation);
}, },
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tErrorOptions, tMinimumSeperationOptions], sliderOptions: [errorOptions, minimumSeparationOptions],
}, },
}, },
}, },
@ -430,7 +435,7 @@ const bezierFeatures = {
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.intersect_self(options.error), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.intersect_self(options.error),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tErrorOptions], sliderOptions: [errorOptions],
}, },
Cubic: { Cubic: {
customPoints: [ customPoints: [
@ -470,12 +475,13 @@ const bezierFeatures = {
}, },
"de-casteljau-points": { "de-casteljau-points": {
name: "De Casteljau Points", name: "De Casteljau Points",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.de_casteljau_points(options.t), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.de_casteljau_points(options.t, tVariant),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], sliderOptions: [tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
}; };
@ -485,6 +491,6 @@ export type BezierFeatureOptions = {
callback: BezierCallback; callback: BezierCallback;
demoOptions?: Partial<BezierDemoOptions>; demoOptions?: Partial<BezierDemoOptions>;
triggerOnMouseMove?: boolean; triggerOnMouseMove?: boolean;
chooseComputeType?: boolean; chooseTVariant?: boolean;
}; };
export default bezierFeatures as Record<BezierFeatureKey, BezierFeatureOptions>; export default bezierFeatures as Record<BezierFeatureKey, BezierFeatureOptions>;

View file

@ -1,5 +1,5 @@
import { tSliderOptions } from "@/utils/options"; import { tSliderOptions } from "@/utils/options";
import { ComputeType, SliderOption, SubpathCallback, WasmSubpathInstance } from "@/utils/types"; import { TVariant, SliderOption, SubpathCallback, WasmSubpathInstance } from "@/utils/types";
const subpathFeatures = { const subpathFeatures = {
constructor: { constructor: {
@ -8,10 +8,10 @@ const subpathFeatures = {
}, },
insert: { insert: {
name: "Insert", name: "Insert",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => subpath.insert(options.computeArgument, computeType), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.insert(options.t, tVariant),
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], sliderOptions: [tSliderOptions],
// TODO: Uncomment this after implementing the Euclidean version // TODO: Uncomment this after implementing the Euclidean version
// chooseComputeType: true, // chooseTVariant: true,
}, },
length: { length: {
name: "Length", name: "Length",
@ -19,9 +19,9 @@ const subpathFeatures = {
}, },
evaluate: { evaluate: {
name: "Evaluate", name: "Evaluate",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => subpath.evaluate(options.computeArgument, computeType), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.evaluate(options.t, tVariant),
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], sliderOptions: [tSliderOptions],
chooseComputeType: true, chooseTVariant: true,
}, },
project: { project: {
name: "Project", name: "Project",
@ -68,10 +68,10 @@ const subpathFeatures = {
}, },
split: { split: {
name: "Split", name: "Split",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => subpath.split(options.computeArgument, computeType), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.split(options.t, tVariant),
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], sliderOptions: [tSliderOptions],
// TODO: Uncomment this after implementing the Euclidean version // TODO: Uncomment this after implementing the Euclidean version
// chooseComputeType: true, // chooseTVariant: true,
}, },
}; };
@ -81,6 +81,6 @@ export type SubpathFeatureOptions = {
callback: SubpathCallback; callback: SubpathCallback;
sliderOptions?: SliderOption[]; sliderOptions?: SliderOption[];
triggerOnMouseMove?: boolean; triggerOnMouseMove?: boolean;
chooseComputeType?: boolean; chooseTVariant?: boolean;
}; };
export default subpathFeatures as Record<SubpathFeatureKey, SubpathFeatureOptions>; export default subpathFeatures as Record<SubpathFeatureKey, SubpathFeatureOptions>;

View file

@ -31,7 +31,7 @@ function renderBezierPane(featureName: BezierFeatureKey, container: HTMLElement
demo.setAttribute("name", featureName); demo.setAttribute("name", featureName);
demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {})); demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {}));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
demo.setAttribute("chooseComputeType", String(feature.chooseComputeType)); demo.setAttribute("chooseTVariant", String(feature.chooseTVariant));
container?.append(demo); container?.append(demo);
} }
@ -42,7 +42,7 @@ function renderSubpathPane(featureName: SubpathFeatureKey, container: HTMLElemen
demo.setAttribute("name", featureName); demo.setAttribute("name", featureName);
demo.setAttribute("sliderOptions", JSON.stringify(feature.sliderOptions || [])); demo.setAttribute("sliderOptions", JSON.stringify(feature.sliderOptions || []));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
demo.setAttribute("chooseComputeType", String(feature.chooseComputeType)); demo.setAttribute("chooseTVariant", String(feature.chooseTVariant));
container?.append(demo); container?.append(demo);
} }

View file

@ -30,7 +30,7 @@ body > h2 {
justify-content: center; justify-content: center;
} }
.compute-type-choice { .t-variant-choice {
margin-top: 20px; margin-top: 20px;
} }

View file

@ -6,7 +6,7 @@ export const tSliderOptions = {
variable: "t", variable: "t",
}; };
export const tErrorOptions = { export const errorOptions = {
variable: "error", variable: "error",
min: 0.1, min: 0.1,
max: 2, max: 2,
@ -14,7 +14,7 @@ export const tErrorOptions = {
default: 0.5, default: 0.5,
}; };
export const tMinimumSeperationOptions = { export const minimumSeparationOptions = {
variable: "minimum_seperation", variable: "minimum_seperation",
min: 0.001, min: 0.001,
max: 0.25, max: 0.25,

View file

@ -1,4 +1,4 @@
import { ComputeType, Demo, DemoPane, SliderOption } from "@/utils/types"; import { TVariant, Demo, DemoPane, SliderOption } from "@/utils/types";
export function renderDemo(demo: Demo): void { export function renderDemo(demo: Demo): void {
const header = document.createElement("h4"); const header = document.createElement("h4");
@ -53,27 +53,27 @@ export function renderDemoPane(demoPane: DemoPane): void {
header.className = "demo-pane-header"; header.className = "demo-pane-header";
header.append(headerAnchorLink); header.append(headerAnchorLink);
const computeTypeContainer = document.createElement("div"); const tVariantContainer = document.createElement("div");
computeTypeContainer.className = "compute-type-choice"; tVariantContainer.className = "t-variant-choice";
const computeTypeLabel = document.createElement("strong"); const tVariantLabel = document.createElement("strong");
computeTypeLabel.innerText = "ComputeType:"; tVariantLabel.innerText = "TValue Variant:";
computeTypeContainer.append(computeTypeLabel); tVariantContainer.append(tVariantLabel);
const radioInputs = ["Parametric", "Euclidean"].map((computeType) => { const radioInputs = ["Parametric", "Euclidean"].map((tVariant) => {
const id = `${demoPane.id}-${computeType}`; const id = `${demoPane.id}-${tVariant}`;
const radioInput = document.createElement("input"); const radioInput = document.createElement("input");
radioInput.type = "radio"; radioInput.type = "radio";
radioInput.id = id; radioInput.id = id;
radioInput.value = computeType; radioInput.value = tVariant;
radioInput.name = "ComputeType"; radioInput.name = `TVariant - ${demoPane.id}`;
radioInput.checked = computeType === "Parametric"; radioInput.checked = tVariant === "Parametric";
computeTypeContainer.append(radioInput); tVariantContainer.append(radioInput);
const label = document.createElement("label"); const label = document.createElement("label");
label.htmlFor = id; label.htmlFor = id;
label.innerText = computeType; label.innerText = tVariant;
computeTypeContainer.append(label); tVariantContainer.append(label);
return radioInput; return radioInput;
}); });
@ -88,16 +88,16 @@ export function renderDemoPane(demoPane: DemoPane): void {
radioInputs.forEach((radioInput: HTMLElement) => { radioInputs.forEach((radioInput: HTMLElement) => {
radioInput.addEventListener("input", (event: Event): void => { radioInput.addEventListener("input", (event: Event): void => {
demoPane.computeType = (event.target as HTMLInputElement).value as ComputeType; demoPane.tVariant = (event.target as HTMLInputElement).value as TVariant;
demoComponent.setAttribute("computetype", demoPane.computeType); demoComponent.setAttribute("tvariant", demoPane.tVariant);
}); });
}); });
demoRow.append(demoComponent); demoRow.append(demoComponent);
}); });
container.append(header); container.append(header);
if (demoPane.chooseComputeType) { if (demoPane.chooseTVariant) {
container.append(computeTypeContainer); container.append(tVariantContainer);
} }
container.append(demoRow); container.append(demoRow);

View file

@ -11,10 +11,10 @@ export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_ou
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const; export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number]; export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number];
export type ComputeType = "Euclidean" | "Parametric"; export type TVariant = "Euclidean" | "Parametric";
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number], computeType?: ComputeType) => string; export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number], tVariant?: TVariant) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number], computeType?: ComputeType) => string; export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number], tVariant?: TVariant) => string;
export type BezierDemoOptions = { export type BezierDemoOptions = {
[key in BezierCurveType]: { [key in BezierCurveType]: {
@ -85,7 +85,7 @@ export interface DemoPane extends HTMLElement {
name: string; name: string;
demos: DemoArgs[]; demos: DemoArgs[];
id: string; id: string;
chooseComputeType: boolean; chooseTVariant: boolean;
computeType: ComputeType; tVariant: TVariant;
buildDemo(demo: DemoArgs): Demo; buildDemo(demo: DemoArgs): Demo;
} }

View file

@ -1,5 +1,5 @@
use crate::svg_drawing::*; use crate::svg_drawing::*;
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ComputeType, ProjectionOptions}; use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions, TValue};
use glam::DVec2; use glam::DVec2;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -41,6 +41,14 @@ fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy
} }
} }
fn parse_t_variant(t_variant: &String, t: f64) -> TValue {
match t_variant.as_str() {
"Parametric" => TValue::Parametric(t),
"Euclidean" => TValue::Euclidean(t),
_ => panic!("Unexpected TValue string: '{}'", t_variant),
}
}
#[wasm_bindgen] #[wasm_bindgen]
impl WasmBezier { impl WasmBezier {
/// Expect js_points to be a list of 2 pairs. /// Expect js_points to be a list of 2 pairs.
@ -128,13 +136,10 @@ impl WasmBezier {
wrap_svg_tag(format!("{bezier}{}", draw_text(format!("Length: {:.2}", self.0.length(None)), TEXT_OFFSET_X, TEXT_OFFSET_Y, BLACK))) wrap_svg_tag(format!("{bezier}{}", draw_text(format!("Length: {:.2}", self.0.length(None)), TEXT_OFFSET_X, TEXT_OFFSET_Y, BLACK)))
} }
pub fn evaluate(&self, t: f64, compute_type: String) -> String { pub fn evaluate(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path(); let bezier = self.get_bezier_path();
let point = match compute_type.as_str() { let t = parse_t_variant(&t_variant, raw_t);
"Euclidean" => self.0.evaluate(ComputeType::Euclidean(t)), let point = self.0.evaluate(t);
"Parametric" => self.0.evaluate(ComputeType::Parametric(t)),
_ => panic!("Unexpected ComputeType string: '{}'", compute_type),
};
let content = format!("{bezier}{}", draw_circle(point, 4., RED, 1.5, WHITE)); let content = format!("{bezier}{}", draw_circle(point, 4., RED, 1.5, WHITE));
wrap_svg_tag(content) wrap_svg_tag(content)
} }
@ -169,11 +174,12 @@ impl WasmBezier {
wrap_svg_tag(content) wrap_svg_tag(content)
} }
pub fn tangent(&self, t: f64) -> String { pub fn tangent(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path(); let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let tangent_point = self.0.tangent(t); let tangent_point = self.0.tangent(t);
let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let intersection_point = self.0.evaluate(t);
let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR;
let content = format!( let content = format!(
@ -185,11 +191,12 @@ impl WasmBezier {
wrap_svg_tag(content) wrap_svg_tag(content)
} }
pub fn normal(&self, t: f64) -> String { pub fn normal(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path(); let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let normal_point = self.0.normal(t); let normal_point = self.0.normal(t);
let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let intersection_point = self.0.evaluate(t);
let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR;
let content = format!( let content = format!(
@ -201,11 +208,13 @@ impl WasmBezier {
wrap_svg_tag(content) wrap_svg_tag(content)
} }
pub fn curvature(&self, t: f64) -> String { pub fn curvature(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path(); let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let radius = 1. / self.0.curvature(t); let radius = 1. / self.0.curvature(t);
let normal_point = self.0.normal(t); let normal_point = self.0.normal(t);
let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let intersection_point = self.0.evaluate(t);
let curvature_center = intersection_point + normal_point * radius; let curvature_center = intersection_point + normal_point * radius;
@ -219,7 +228,8 @@ impl WasmBezier {
wrap_svg_tag(content) wrap_svg_tag(content)
} }
pub fn split(&self, t: f64) -> String { pub fn split(&self, raw_t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, raw_t);
let beziers: [Bezier; 2] = self.0.split(t); let beziers: [Bezier; 2] = self.0.split(t);
let mut original_bezier_svg = String::new(); let mut original_bezier_svg = String::new();
@ -252,7 +262,8 @@ impl WasmBezier {
wrap_svg_tag(format!("{original_bezier_svg}{bezier_svg_1}{bezier_svg_2}")) wrap_svg_tag(format!("{original_bezier_svg}{bezier_svg_1}{bezier_svg_2}"))
} }
pub fn trim(&self, t1: f64, t2: f64) -> String { pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String {
let (t1, t2) = (parse_t_variant(&t_variant, raw_t1), parse_t_variant(&t_variant, raw_t2));
let trimmed_bezier = self.0.trim(t1, t2); let trimmed_bezier = self.0.trim(t1, t2);
let mut trimmed_bezier_svg = String::new(); let mut trimmed_bezier_svg = String::new();
@ -269,7 +280,7 @@ impl WasmBezier {
pub fn project(&self, x: f64, y: f64) -> String { pub fn project(&self, x: f64, y: f64) -> String {
let projected_t_value = self.0.project(DVec2::new(x, y), ProjectionOptions::default()); let projected_t_value = self.0.project(DVec2::new(x, y), ProjectionOptions::default());
let projected_point = self.0.evaluate(ComputeType::Parametric(projected_t_value)); let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value));
let bezier = self.get_bezier_path(); let bezier = self.get_bezier_path();
let content = format!("{bezier}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); let content = format!("{bezier}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),);
@ -285,7 +296,7 @@ impl WasmBezier {
.zip([RED, GREEN]) .zip([RED, GREEN])
.flat_map(|(t_value_list, color)| { .flat_map(|(t_value_list, color)| {
t_value_list.iter().map(|&t_value| { t_value_list.iter().map(|&t_value| {
let point = self.0.evaluate(ComputeType::Parametric(t_value)); let point = self.0.evaluate(TValue::Parametric(t_value));
draw_circle(point, 3., color, 1.5, WHITE) draw_circle(point, 3., color, 1.5, WHITE)
}) })
}) })
@ -320,7 +331,7 @@ impl WasmBezier {
let circles: String = inflections let circles: String = inflections
.iter() .iter()
.map(|&t_value| { .map(|&t_value| {
let point = self.0.evaluate(ComputeType::Parametric(t_value)); let point = self.0.evaluate(TValue::Parametric(t_value));
draw_circle(point, 3., RED, 1.5, WHITE) draw_circle(point, 3., RED, 1.5, WHITE)
}) })
.fold("".to_string(), |acc, circle| acc + &circle); .fold("".to_string(), |acc, circle| acc + &circle);
@ -328,7 +339,8 @@ impl WasmBezier {
wrap_svg_tag(content) wrap_svg_tag(content)
} }
pub fn de_casteljau_points(&self, t: f64) -> String { pub fn de_casteljau_points(&self, raw_t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, raw_t);
let points: Vec<Vec<DVec2>> = self.0.de_casteljau_points(t); let points: Vec<Vec<DVec2>> = self.0.de_casteljau_points(t);
let bezier_svg = self.get_bezier_path(); let bezier_svg = self.get_bezier_path();
@ -413,7 +425,7 @@ impl WasmBezier {
.intersect(&line, None, None) .intersect(&line, None, None)
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE) draw_circle(*point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));
@ -433,7 +445,7 @@ impl WasmBezier {
.intersect(&quadratic, Some(error), Some(minimum_separation)) .intersect(&quadratic, Some(error), Some(minimum_separation))
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE) draw_circle(*point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));
@ -453,7 +465,7 @@ impl WasmBezier {
.intersect(&cubic, Some(error), Some(minimum_separation)) .intersect(&cubic, Some(error), Some(minimum_separation))
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE) draw_circle(*point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));
@ -469,7 +481,7 @@ impl WasmBezier {
.self_intersections(Some(error)) .self_intersections(Some(error))
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(intersection_t[0])); let point = &self.0.evaluate(TValue::Parametric(intersection_t[0]));
draw_circle(*point, 4., RED, 1.5, WHITE) draw_circle(*point, 4., RED, 1.5, WHITE)
}) })
.fold(bezier_curve_svg, |acc, item| format!("{acc}{item}")); .fold(bezier_curve_svg, |acc, item| format!("{acc}{item}"));
@ -497,7 +509,7 @@ impl WasmBezier {
.rectangle_intersections(points[0], points[1]) .rectangle_intersections(points[0], points[1])
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE) draw_circle(*point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));

View file

@ -1,6 +1,6 @@
use crate::svg_drawing::*; use crate::svg_drawing::*;
use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, ProjectionOptions, Subpath}; use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, TValue};
use glam::DVec2; use glam::DVec2;
use std::fmt::Write; use std::fmt::Write;
@ -56,20 +56,20 @@ impl WasmSubpath {
subpath_svg subpath_svg
} }
pub fn insert(&self, t: f64, compute_type: String) -> String { pub fn insert(&self, t: f64, t_variant: String) -> String {
let mut subpath = self.0.clone(); let mut subpath = self.0.clone();
let point = match compute_type.as_str() { let point = match t_variant.as_str() {
"Euclidean" => { "Euclidean" => {
let parameter = ComputeType::Euclidean(t); let parameter = TValue::Euclidean(t);
subpath.insert(parameter); subpath.insert(parameter);
self.0.evaluate(parameter) self.0.evaluate(parameter)
} }
"Parametric" => { "Parametric" => {
let parameter = ComputeType::Parametric(t); let parameter = TValue::Parametric(t);
subpath.insert(parameter); subpath.insert(parameter);
self.0.evaluate(parameter) self.0.evaluate(parameter)
} }
_ => panic!("Unexpected ComputeType string: '{}'", compute_type), _ => panic!("Unexpected TValue string: '{}'", t_variant),
}; };
let point_text = draw_circle(point, 4., RED, 1.5, WHITE); let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
@ -81,19 +81,19 @@ impl WasmSubpath {
wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text)) wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text))
} }
pub fn evaluate(&self, t: f64, compute_type: String) -> String { pub fn evaluate(&self, t: f64, t_variant: String) -> String {
let point = match compute_type.as_str() { let point = match t_variant.as_str() {
"Euclidean" => self.0.evaluate(ComputeType::Euclidean(t)), "Euclidean" => self.0.evaluate(TValue::Euclidean(t)),
"Parametric" => self.0.evaluate(ComputeType::Parametric(t)), "Parametric" => self.0.evaluate(TValue::Parametric(t)),
_ => panic!("Unexpected ComputeType string: '{}'", compute_type), _ => panic!("Unexpected TValue string: '{}'", t_variant),
}; };
let point_text = draw_circle(point, 4., RED, 1.5, WHITE); let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text)) wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
} }
pub fn tangent(&self, t: f64) -> String { pub fn tangent(&self, t: f64) -> String {
let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let intersection_point = self.0.evaluate(TValue::Parametric(t));
let tangent_point = self.0.tangent(ComputeType::Parametric(t)); let tangent_point = self.0.tangent(TValue::Parametric(t));
let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR;
let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE);
@ -103,8 +103,8 @@ impl WasmSubpath {
} }
pub fn normal(&self, t: f64) -> String { pub fn normal(&self, t: f64) -> String {
let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let intersection_point = self.0.evaluate(TValue::Parametric(t));
let normal_point = self.0.normal(ComputeType::Parametric(t)); let normal_point = self.0.normal(TValue::Parametric(t));
let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR;
let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE);
@ -115,7 +115,7 @@ impl WasmSubpath {
pub fn project(&self, x: f64, y: f64) -> String { pub fn project(&self, x: f64, y: f64) -> String {
let (segment_index, projected_t) = self.0.project(DVec2::new(x, y), ProjectionOptions::default()).unwrap(); let (segment_index, projected_t) = self.0.project(DVec2::new(x, y), ProjectionOptions::default()).unwrap();
let projected_point = self.0.evaluate(ComputeType::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64))); let projected_point = self.0.evaluate(TValue::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64)));
let subpath_svg = self.to_default_svg(); let subpath_svg = self.to_default_svg();
let content = format!("{subpath_svg}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); let content = format!("{subpath_svg}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),);
@ -143,7 +143,7 @@ impl WasmSubpath {
.intersections(&line, None, None) .intersections(&line, None, None)
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(point, 4., RED, 1.5, WHITE) draw_circle(point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));
@ -172,7 +172,7 @@ impl WasmSubpath {
.intersections(&line, None, None) .intersections(&line, None, None)
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(point, 4., RED, 1.5, WHITE) draw_circle(point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));
@ -201,7 +201,7 @@ impl WasmSubpath {
.intersections(&line, None, None) .intersections(&line, None, None)
.iter() .iter()
.map(|intersection_t| { .map(|intersection_t| {
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t)); let point = self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(point, 4., RED, 1.5, WHITE) draw_circle(point, 4., RED, 1.5, WHITE)
}) })
.fold(String::new(), |acc, item| format!("{acc}{item}")); .fold(String::new(), |acc, item| format!("{acc}{item}"));
@ -209,11 +209,11 @@ impl WasmSubpath {
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
} }
pub fn split(&self, t: f64, compute_type: String) -> String { pub fn split(&self, t: f64, t_variant: String) -> String {
let (main_subpath, optional_subpath) = match compute_type.as_str() { let (main_subpath, optional_subpath) = match t_variant.as_str() {
"Euclidean" => self.0.split(ComputeType::Euclidean(t)), "Euclidean" => self.0.split(TValue::Euclidean(t)),
"Parametric" => self.0.split(ComputeType::Parametric(t)), "Parametric" => self.0.split(TValue::Parametric(t)),
_ => panic!("Unexpected ComputeType string: '{}'", compute_type), _ => panic!("Unexpected ComputeType string: '{}'", t_variant),
}; };
let mut main_subpath_svg = String::new(); let mut main_subpath_svg = String::new();