Bezier-rs: Add SubpathTValue and euclidean parameterization for subpaths (#1027)
* Added SubpathTValue and euclidean parameterization for subpaths * Small fix * Added bounds checking to get_segment * Code review * code review nit for clarity --------- Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
344f243432
commit
9a52cae9b9
|
|
@ -7,4 +7,4 @@ mod utils;
|
||||||
|
|
||||||
pub use bezier::*;
|
pub use bezier::*;
|
||||||
pub use subpath::*;
|
pub use subpath::*;
|
||||||
pub use utils::TValue;
|
pub use utils::{SubpathTValue, TValue};
|
||||||
|
|
|
||||||
|
|
@ -50,16 +50,12 @@ impl Subpath {
|
||||||
number_of_curves
|
number_of_curves
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_curve_parametric(&self, t: f64) -> (Option<Bezier>, f64) {
|
/// Returns a copy of the bezier segment at the given segment index, if this segment exists.
|
||||||
assert!((0.0..=1.).contains(&t));
|
pub fn get_segment(&self, segment_index: usize) -> Option<Bezier> {
|
||||||
|
if segment_index >= self.len_segments() {
|
||||||
let number_of_curves = self.len_segments() as f64;
|
return None;
|
||||||
let scaled_t = t * number_of_curves;
|
}
|
||||||
|
Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()]))
|
||||||
let target_curve_index = scaled_t.floor() as i32;
|
|
||||||
let target_curve_t = scaled_t % 1.;
|
|
||||||
|
|
||||||
(self.iter().nth(target_curve_index as usize), target_curve_t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{ProjectionOptions, TValue};
|
use crate::consts::DEFAULT_EUCLIDEAN_ERROR_BOUND;
|
||||||
|
use crate::utils::{SubpathTValue, TValue};
|
||||||
|
use crate::ProjectionOptions;
|
||||||
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`.
|
||||||
|
|
@ -10,6 +12,70 @@ impl Subpath {
|
||||||
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn global_euclidean_to_local_euclidean(&self, global_t: f64) -> (usize, f64) {
|
||||||
|
let lengths = self.iter().map(|bezier| bezier.length(None)).collect::<Vec<f64>>();
|
||||||
|
let total_length: f64 = lengths.iter().sum();
|
||||||
|
|
||||||
|
let mut accumulator = 0.;
|
||||||
|
for (index, length) in lengths.iter().enumerate() {
|
||||||
|
let length_ratio = length / total_length;
|
||||||
|
if accumulator <= global_t && global_t <= accumulator + length_ratio {
|
||||||
|
return (index, (global_t - accumulator) / length_ratio);
|
||||||
|
}
|
||||||
|
accumulator += length_ratio;
|
||||||
|
}
|
||||||
|
(0, 0.)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a [SubpathTValue] to a parametric `(segment_index, t)` tuple.
|
||||||
|
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
|
||||||
|
/// - If the argument is a variant containing a `segment_index`, asserts that the index references a valid segment on the curve.
|
||||||
|
pub(crate) fn t_value_to_parametric(&self, t: SubpathTValue) -> (usize, f64) {
|
||||||
|
assert!(self.len_segments() >= 1);
|
||||||
|
|
||||||
|
match t {
|
||||||
|
SubpathTValue::Parametric { segment_index, t } => {
|
||||||
|
assert!((0.0..=1.).contains(&t));
|
||||||
|
assert!((0..self.len_segments() - 1).contains(&segment_index));
|
||||||
|
(segment_index, t)
|
||||||
|
}
|
||||||
|
SubpathTValue::GlobalParametric(global_t) => {
|
||||||
|
assert!((0.0..=1.).contains(&global_t));
|
||||||
|
|
||||||
|
if global_t == 1. {
|
||||||
|
return (self.len_segments() - 1, 1.);
|
||||||
|
}
|
||||||
|
|
||||||
|
let scaled_t = global_t * self.len_segments() as f64;
|
||||||
|
let segment_index = scaled_t.floor() as usize;
|
||||||
|
let t = scaled_t - segment_index as f64;
|
||||||
|
|
||||||
|
(segment_index, t)
|
||||||
|
}
|
||||||
|
SubpathTValue::Euclidean { segment_index, t } => {
|
||||||
|
assert!((0.0..=1.).contains(&t));
|
||||||
|
assert!((0..self.len_segments()).contains(&segment_index));
|
||||||
|
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND))
|
||||||
|
}
|
||||||
|
SubpathTValue::GlobalEuclidean(t) => {
|
||||||
|
let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t);
|
||||||
|
(
|
||||||
|
segment_index,
|
||||||
|
self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, DEFAULT_EUCLIDEAN_ERROR_BOUND),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SubpathTValue::EuclideanWithinError { segment_index, t, error } => {
|
||||||
|
assert!((0.0..=1.).contains(&t));
|
||||||
|
assert!((0..self.len_segments()).contains(&segment_index));
|
||||||
|
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, error))
|
||||||
|
}
|
||||||
|
SubpathTValue::GlobalEuclideanWithinError { t, error } => {
|
||||||
|
let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t);
|
||||||
|
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point.
|
/// Returns the segment index and `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) -> Option<(usize, f64)> {
|
pub fn project(&self, point: DVec2, options: ProjectionOptions) -> Option<(usize, f64)> {
|
||||||
|
|
@ -34,6 +100,9 @@ impl Subpath {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||||
|
use crate::utils::f64_compare;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -113,4 +182,46 @@ mod tests {
|
||||||
subpath.closed = true;
|
subpath.closed = true;
|
||||||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_value_to_parametric_global_parametric_open_subpath() {
|
||||||
|
let mock_manipulator_group = ManipulatorGroup {
|
||||||
|
anchor: DVec2::new(0., 0.),
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: None,
|
||||||
|
};
|
||||||
|
let open_subpath = Subpath {
|
||||||
|
manipulator_groups: vec![mock_manipulator_group; 5],
|
||||||
|
closed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (segment_index, t) = open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7));
|
||||||
|
assert_eq!(segment_index, 2);
|
||||||
|
assert!(f64_compare(t, 0.8, MAX_ABSOLUTE_DIFFERENCE));
|
||||||
|
|
||||||
|
// The start and end points of an open subpath are NOT equivalent
|
||||||
|
assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.));
|
||||||
|
assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (3, 1.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_value_to_parametric_global_parametric_closed_subpath() {
|
||||||
|
let mock_manipulator_group = ManipulatorGroup {
|
||||||
|
anchor: DVec2::new(0., 0.),
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: None,
|
||||||
|
};
|
||||||
|
let closed_subpath = Subpath {
|
||||||
|
manipulator_groups: vec![mock_manipulator_group; 5],
|
||||||
|
closed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (segment_index, t) = closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7));
|
||||||
|
assert_eq!(segment_index, 3);
|
||||||
|
assert!(f64_compare(t, 0.5, MAX_ABSOLUTE_DIFFERENCE));
|
||||||
|
|
||||||
|
// The start and end points of a closed subpath are equivalent
|
||||||
|
assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.));
|
||||||
|
assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (4, 1.));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,39 @@
|
||||||
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::TValue;
|
use crate::{SubpathTValue, 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: TValue) {
|
pub fn insert(&mut self, t: SubpathTValue) {
|
||||||
match t {
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
TValue::Parametric(t) => {
|
|
||||||
assert!((0.0..=1.).contains(&t));
|
|
||||||
|
|
||||||
let number_of_curves = self.len_segments() as f64;
|
if f64_compare(t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(t, 1., MAX_ABSOLUTE_DIFFERENCE) {
|
||||||
let scaled_t = t * number_of_curves;
|
return;
|
||||||
|
|
||||||
let target_curve_index = scaled_t.floor() as i32;
|
|
||||||
let target_curve_t = scaled_t % 1.;
|
|
||||||
|
|
||||||
if f64_compare(target_curve_t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(target_curve_t, 1., MAX_ABSOLUTE_DIFFERENCE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The only case where `curve` would be `None` is if the provided argument was 1
|
|
||||||
// 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 [first, second] = curve.split(TValue::Parametric(target_curve_t));
|
|
||||||
let new_group = ManipulatorGroup {
|
|
||||||
anchor: first.end(),
|
|
||||||
in_handle: first.handle_end(),
|
|
||||||
out_handle: second.handle_start(),
|
|
||||||
};
|
|
||||||
let number_of_groups = self.manipulator_groups.len() + 1;
|
|
||||||
self.manipulator_groups.insert((target_curve_index as usize) + 1, new_group);
|
|
||||||
self.manipulator_groups[(target_curve_index as usize) % number_of_groups].out_handle = first.handle_start();
|
|
||||||
self.manipulator_groups[((target_curve_index as usize) + 2) % number_of_groups].in_handle = second.handle_end();
|
|
||||||
}
|
|
||||||
// TODO: change this implementation to Euclidean compute
|
|
||||||
TValue::Euclidean(_t) => {}
|
|
||||||
TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The only case where `curve` would be `None` is if the provided argument was 1
|
||||||
|
// But the above if case would catch that, since `target_curve_t` would be 0.
|
||||||
|
let curve = self.iter().nth(segment_index).unwrap();
|
||||||
|
|
||||||
|
let [first, second] = curve.split(TValue::Parametric(t));
|
||||||
|
let new_group = ManipulatorGroup {
|
||||||
|
anchor: first.end(),
|
||||||
|
in_handle: first.handle_end(),
|
||||||
|
out_handle: second.handle_start(),
|
||||||
|
};
|
||||||
|
let number_of_groups = self.manipulator_groups.len() + 1;
|
||||||
|
self.manipulator_groups.insert((segment_index) + 1, new_group);
|
||||||
|
self.manipulator_groups[segment_index % number_of_groups].out_handle = first.handle_start();
|
||||||
|
self.manipulator_groups[(segment_index + 2) % number_of_groups].in_handle = second.handle_end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::utils::SubpathTValue;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
|
|
@ -94,9 +83,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(TValue::Parametric(0.2));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2));
|
||||||
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.));
|
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.));
|
||||||
subpath.insert(TValue::Parametric(0.2));
|
subpath.insert(SubpathTValue::GlobalParametric(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 +94,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(TValue::Parametric(0.9));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.9));
|
||||||
let split_pair = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.9 * 3.) % 1.));
|
let split_pair = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.9 * 3.) % 1.));
|
||||||
subpath.insert(TValue::Parametric(0.9));
|
subpath.insert(SubpathTValue::GlobalParametric(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 +106,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(TValue::Parametric(0.75));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75));
|
||||||
subpath.insert(TValue::Parametric(0.75));
|
subpath.insert(SubpathTValue::GlobalParametric(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 +116,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(TValue::Parametric(0.9));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.9));
|
||||||
let split_pair = subpath.iter().nth(3).unwrap().split(TValue::Parametric((0.9 * 4.) % 1.));
|
let split_pair = subpath.iter().nth(3).unwrap().split(TValue::Parametric((0.9 * 4.) % 1.));
|
||||||
subpath.insert(TValue::Parametric(0.9));
|
subpath.insert(SubpathTValue::GlobalParametric(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 +129,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(TValue::Parametric(1.));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.));
|
||||||
subpath.insert(TValue::Parametric(1.));
|
subpath.insert(SubpathTValue::GlobalParametric(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);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::consts::MIN_SEPERATION_VALUE;
|
use crate::consts::MIN_SEPERATION_VALUE;
|
||||||
|
use crate::utils::SubpathTValue;
|
||||||
use crate::TValue;
|
use crate::TValue;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
@ -7,24 +8,12 @@ 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: TValue) -> DVec2 {
|
pub fn evaluate(&self, t: SubpathTValue) -> DVec2 {
|
||||||
match t {
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
TValue::Parametric(t) => {
|
self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t))
|
||||||
assert!((0.0..=1.).contains(&t));
|
|
||||||
|
|
||||||
if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) {
|
|
||||||
curve.evaluate(TValue::Parametric(target_curve_t))
|
|
||||||
} else {
|
|
||||||
self.iter().last().unwrap().evaluate(TValue::Parametric(1.))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: change this implementation to Euclidean compute
|
|
||||||
TValue::Euclidean(_t) => self.iter().next().unwrap().evaluate(TValue::Parametric(0.)),
|
|
||||||
TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the intersection points the subpath has with a given line and returns a list of parameteric `t`-values.
|
/// Calculates the intersection points the subpath has with a given curve and returns a list of parameteric `t`-values.
|
||||||
/// This function expects the following:
|
/// This function expects the following:
|
||||||
/// - other: a [Bezier] curve to check intersections against
|
/// - other: a [Bezier] curve to check intersections against
|
||||||
/// - error: an optional f64 value to provide an error bound
|
/// - error: an optional f64 value to provide an error bound
|
||||||
|
|
@ -54,36 +43,14 @@ impl Subpath {
|
||||||
intersection_t_values
|
intersection_t_values
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tangent(&self, t: TValue) -> DVec2 {
|
pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
|
||||||
match t {
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
TValue::Parametric(t) => {
|
self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
|
||||||
assert!((0.0..=1.).contains(&t));
|
|
||||||
|
|
||||||
if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) {
|
|
||||||
curve.tangent(TValue::Parametric(target_curve_t))
|
|
||||||
} else {
|
|
||||||
self.iter().last().unwrap().tangent(TValue::Parametric(1.))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TValue::Euclidean(_t) => unimplemented!(),
|
|
||||||
TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normal(&self, t: TValue) -> DVec2 {
|
pub fn normal(&self, t: SubpathTValue) -> DVec2 {
|
||||||
match t {
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
TValue::Parametric(t) => {
|
self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t))
|
||||||
assert!((0.0..=1.).contains(&t));
|
|
||||||
|
|
||||||
if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) {
|
|
||||||
curve.normal(TValue::Parametric(target_curve_t))
|
|
||||||
} else {
|
|
||||||
self.iter().last().unwrap().normal(TValue::Parametric(1.))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TValue::Euclidean(_t) => unimplemented!(),
|
|
||||||
TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,16 +91,16 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let t0 = 0.;
|
let t0 = 0.;
|
||||||
assert_eq!(subpath.evaluate(TValue::Parametric(t0)), bezier.evaluate(TValue::Parametric(t0)));
|
assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t0)), bezier.evaluate(TValue::Parametric(t0)));
|
||||||
|
|
||||||
let t1 = 0.25;
|
let t1 = 0.25;
|
||||||
assert_eq!(subpath.evaluate(TValue::Parametric(t1)), bezier.evaluate(TValue::Parametric(t1)));
|
assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t1)), bezier.evaluate(TValue::Parametric(t1)));
|
||||||
|
|
||||||
let t2 = 0.50;
|
let t2 = 0.50;
|
||||||
assert_eq!(subpath.evaluate(TValue::Parametric(t2)), bezier.evaluate(TValue::Parametric(t2)));
|
assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t2)), bezier.evaluate(TValue::Parametric(t2)));
|
||||||
|
|
||||||
let t3 = 1.;
|
let t3 = 1.;
|
||||||
assert_eq!(subpath.evaluate(TValue::Parametric(t3)), bezier.evaluate(TValue::Parametric(t3)));
|
assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t3)), bezier.evaluate(TValue::Parametric(t3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -176,7 +143,7 @@ mod tests {
|
||||||
|
|
||||||
let t0 = 0.;
|
let t0 = 0.;
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
subpath.evaluate(TValue::Parametric(t0)),
|
subpath.evaluate(SubpathTValue::GlobalParametric(t0)),
|
||||||
linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))),
|
linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
|
|
@ -184,7 +151,7 @@ mod tests {
|
||||||
|
|
||||||
let t1 = 0.25;
|
let t1 = 0.25;
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
subpath.evaluate(TValue::Parametric(t1)),
|
subpath.evaluate(SubpathTValue::GlobalParametric(t1)),
|
||||||
linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))),
|
linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
|
|
@ -192,7 +159,7 @@ mod tests {
|
||||||
|
|
||||||
let t2 = 0.50;
|
let t2 = 0.50;
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
subpath.evaluate(TValue::Parametric(t2)),
|
subpath.evaluate(SubpathTValue::GlobalParametric(t2)),
|
||||||
quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))),
|
quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
|
|
@ -200,14 +167,19 @@ mod tests {
|
||||||
|
|
||||||
let t3 = 0.75;
|
let t3 = 0.75;
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
subpath.evaluate(TValue::Parametric(t3)),
|
subpath.evaluate(SubpathTValue::GlobalParametric(t3)),
|
||||||
quadratic_bezier.evaluate(TValue::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(subpath.evaluate(TValue::Parametric(t4)), quadratic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all());
|
assert!(utils::dvec2_compare(
|
||||||
|
subpath.evaluate(SubpathTValue::GlobalParametric(t4)),
|
||||||
|
quadratic_bezier.evaluate(TValue::Parametric(1.)),
|
||||||
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
|
)
|
||||||
|
.all());
|
||||||
|
|
||||||
// Test closed subpath
|
// Test closed subpath
|
||||||
|
|
||||||
|
|
@ -216,14 +188,19 @@ mod tests {
|
||||||
|
|
||||||
let t5 = 2. / 3.;
|
let t5 = 2. / 3.;
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
subpath.evaluate(TValue::Parametric(t5)),
|
subpath.evaluate(SubpathTValue::GlobalParametric(t5)),
|
||||||
cubic_bezier.evaluate(TValue::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(subpath.evaluate(TValue::Parametric(t6)), cubic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all());
|
assert!(utils::dvec2_compare(
|
||||||
|
subpath.evaluate(SubpathTValue::GlobalParametric(t6)),
|
||||||
|
cubic_bezier.evaluate(TValue::Parametric(1.)),
|
||||||
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
|
)
|
||||||
|
.all());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -272,21 +249,21 @@ mod tests {
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
|
cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[0])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
|
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[1])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])),
|
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[2])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[2])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
@ -339,14 +316,14 @@ mod tests {
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
|
cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[0])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
|
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[1])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
@ -398,21 +375,21 @@ mod tests {
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
|
cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[0])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
|
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[1])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
||||||
assert!(utils::dvec2_compare(
|
assert!(utils::dvec2_compare(
|
||||||
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])),
|
quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])),
|
||||||
subpath.evaluate(TValue::Parametric(subpath_intersections[2])),
|
subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[2])),
|
||||||
MAX_ABSOLUTE_DIFFERENCE
|
MAX_ABSOLUTE_DIFFERENCE
|
||||||
)
|
)
|
||||||
.all());
|
.all());
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use super::Bezier;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use std::fmt::{Debug, Formatter, Result};
|
use std::fmt::{Debug, Formatter, Result};
|
||||||
|
|
||||||
|
|
@ -22,3 +24,18 @@ impl Debug for ManipulatorGroup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ManipulatorGroup {
|
||||||
|
pub fn to_bezier(&self, end_group: &ManipulatorGroup) -> Bezier {
|
||||||
|
let start = self.anchor;
|
||||||
|
let end = end_group.anchor;
|
||||||
|
let out_handle = self.out_handle;
|
||||||
|
let in_handle = end_group.in_handle;
|
||||||
|
|
||||||
|
match (out_handle, in_handle) {
|
||||||
|
(Some(handle1), Some(handle2)) => Bezier::from_cubic_dvec2(start, handle1, handle2, end),
|
||||||
|
(Some(handle), None) | (None, Some(handle)) => Bezier::from_quadratic_dvec2(start, handle, end),
|
||||||
|
(None, None) => Bezier::from_linear_dvec2(start, end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,85 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::TValue;
|
use crate::utils::SubpathTValue;
|
||||||
|
use crate::utils::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: TValue) -> (Subpath, Option<Subpath>) {
|
pub fn split(&self, t: SubpathTValue) -> (Subpath, Option<Subpath>) {
|
||||||
match t {
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
TValue::Parametric(t) => {
|
let curve = self.get_segment(segment_index).unwrap();
|
||||||
assert!((0.0..=1.).contains(&t));
|
|
||||||
|
|
||||||
let number_of_curves = self.len_segments() as f64;
|
let [first_bezier, second_bezier] = curve.split(TValue::Parametric(t));
|
||||||
let scaled_t = t * number_of_curves;
|
|
||||||
|
|
||||||
let target_curve_index = scaled_t.floor() as i32;
|
let mut clone = self.manipulator_groups.clone();
|
||||||
let target_curve_t = scaled_t % 1.;
|
// Split the manipulator group list such that the split location is between the last and first elements of the two split halves
|
||||||
let num_manipulator_groups = self.manipulator_groups.len();
|
// If the split is on an anchor point, include this anchor point in the first half of the split, except for the first manipulator group which we want in the second group
|
||||||
|
let (mut first_split, mut second_split) = if !(t == 0. && segment_index == 0) {
|
||||||
|
let clone2 = clone.split_off(self.len().min(segment_index + 1 + (t == 1.) as usize));
|
||||||
|
(clone, clone2)
|
||||||
|
} else {
|
||||||
|
(vec![], clone)
|
||||||
|
};
|
||||||
|
|
||||||
// The only case where `curve` would be `None` is if the provided argument was 1
|
// If the subpath is closed and the split point is the start or end of the Subpath
|
||||||
let optional_curve = self.iter().nth(target_curve_index as usize);
|
if self.closed && ((t == 0. && segment_index == 0) || (t == 1. && segment_index == self.len_segments() - 1)) {
|
||||||
let curve = optional_curve.unwrap_or_else(|| self.iter().last().unwrap());
|
// The entire vector of manipulator groups will be in the second_split
|
||||||
|
// Add a new manipulator group with the same anchor as the first node to represent the end of the now opened subpath
|
||||||
let [first_bezier, second_bezier] = curve.split(TValue::Parametric(if t == 1. { t } else { target_curve_t }));
|
let last_curve = self.iter().last().unwrap();
|
||||||
|
first_split.push(ManipulatorGroup {
|
||||||
let mut clone = self.manipulator_groups.clone();
|
anchor: first_bezier.end(),
|
||||||
let (mut first_split, mut second_split) = if t > 0. {
|
in_handle: last_curve.handle_end(),
|
||||||
let clone2 = clone.split_off(num_manipulator_groups.min((target_curve_index as usize) + 1));
|
out_handle: None,
|
||||||
(clone, clone2)
|
});
|
||||||
} else {
|
} else {
|
||||||
(vec![], clone)
|
if !first_split.is_empty() {
|
||||||
};
|
let num_elements = first_split.len();
|
||||||
|
first_split[num_elements - 1].out_handle = first_bezier.handle_start();
|
||||||
if self.closed && (t == 0. || t == 1.) {
|
|
||||||
// The entire vector of manipulator groups will be in the second_split because target_curve_index == 0.
|
|
||||||
// Add a new manipulator group with the same anchor as the first node to represent the end of the now opened subpath
|
|
||||||
let last_curve = self.iter().last().unwrap();
|
|
||||||
first_split.push(ManipulatorGroup {
|
|
||||||
anchor: first_bezier.end(),
|
|
||||||
in_handle: last_curve.handle_end(),
|
|
||||||
out_handle: None,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if !first_split.is_empty() {
|
|
||||||
let num_elements = first_split.len();
|
|
||||||
first_split[num_elements - 1].out_handle = first_bezier.handle_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !second_split.is_empty() {
|
|
||||||
second_split[0].in_handle = second_bezier.handle_end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push new manipulator groups to represent the location of the split at the end of the first group and at the start of the second
|
|
||||||
// If the split was at a manipulator group's anchor, add only one manipulator group
|
|
||||||
// Add it to the first list when the split location is on the first manipulator group, otherwise add to the second list
|
|
||||||
if target_curve_t != 0. || t == 0. {
|
|
||||||
first_split.push(ManipulatorGroup {
|
|
||||||
anchor: first_bezier.end(),
|
|
||||||
in_handle: first_bezier.handle_end(),
|
|
||||||
out_handle: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if t != 0. {
|
|
||||||
second_split.insert(
|
|
||||||
0,
|
|
||||||
ManipulatorGroup {
|
|
||||||
anchor: second_bezier.start(),
|
|
||||||
in_handle: None,
|
|
||||||
out_handle: second_bezier.handle_start(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.closed {
|
|
||||||
// "Rotate" the manipulator groups list so that the split point becomes the start and end of the open subpath
|
|
||||||
second_split.append(&mut first_split);
|
|
||||||
(Subpath::new(second_split, false), None)
|
|
||||||
} else {
|
|
||||||
(Subpath::new(first_split, false), Some(Subpath::new(second_split, false)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: change this implementation to Euclidean compute
|
|
||||||
TValue::Euclidean(_t) => todo!(),
|
if !second_split.is_empty() {
|
||||||
TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
|
second_split[0].in_handle = second_bezier.handle_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push new manipulator groups to represent the location of the split at the end of the first group and at the start of the second
|
||||||
|
// If the split was at a manipulator group's anchor, add only one manipulator group
|
||||||
|
// Add it to the first list when the split location is on the first manipulator group, otherwise add to the second list
|
||||||
|
if (t % 1. != 0.) || segment_index == 0 {
|
||||||
|
first_split.push(ManipulatorGroup {
|
||||||
|
anchor: first_bezier.end(),
|
||||||
|
in_handle: first_bezier.handle_end(),
|
||||||
|
out_handle: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(t == 0. && segment_index == 0) {
|
||||||
|
second_split.insert(
|
||||||
|
0,
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: second_bezier.start(),
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: second_bezier.handle_start(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.closed {
|
||||||
|
// "Rotate" the manipulator groups list so that the split point becomes the start and end of the open subpath
|
||||||
|
second_split.append(&mut first_split);
|
||||||
|
(Subpath::new(second_split, false), None)
|
||||||
|
} else {
|
||||||
|
(Subpath::new(first_split, false), Some(Subpath::new(second_split, false)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::utils::SubpathTValue;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
|
|
@ -140,9 +129,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(TValue::Parametric(0.2));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2));
|
||||||
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.));
|
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.));
|
||||||
let (first, second) = subpath.split(TValue::Parametric(0.2));
|
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(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 +143,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(TValue::Parametric(0.));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.));
|
||||||
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.));
|
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.));
|
||||||
let (first, second) = subpath.split(TValue::Parametric(0.));
|
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.));
|
||||||
assert!(second.is_some());
|
assert!(second.is_some());
|
||||||
let second = second.unwrap();
|
let second = second.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -175,9 +164,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(TValue::Parametric(1.));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.));
|
||||||
let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.));
|
let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.));
|
||||||
let (first, second) = subpath.split(TValue::Parametric(1.));
|
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(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 +185,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(TValue::Parametric(0.2));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2));
|
||||||
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.));
|
let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.));
|
||||||
let (first, second) = subpath.split(TValue::Parametric(0.2));
|
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(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 +199,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(TValue::Parametric(0.));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.));
|
||||||
let (first, second) = subpath.split(TValue::Parametric(0.));
|
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(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 +213,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(TValue::Parametric(1.));
|
let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.));
|
||||||
let (first, second) = subpath.split(TValue::Parametric(1.));
|
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(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);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,16 @@ pub enum TValue {
|
||||||
EuclideanWithinError { t: f64, error: f64 },
|
EuclideanWithinError { t: f64, error: f64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub enum SubpathTValue {
|
||||||
|
Parametric { segment_index: usize, t: f64 },
|
||||||
|
GlobalParametric(f64),
|
||||||
|
Euclidean { segment_index: usize, t: f64 },
|
||||||
|
GlobalEuclidean(f64),
|
||||||
|
EuclideanWithinError { segment_index: usize, t: f64, error: f64 },
|
||||||
|
GlobalEuclideanWithinError { 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.
|
||||||
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
|
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
|
||||||
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ const subpathFeatures = {
|
||||||
name: "Insert",
|
name: "Insert",
|
||||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.insert(options.t, tVariant),
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.insert(options.t, tVariant),
|
||||||
sliderOptions: [tSliderOptions],
|
sliderOptions: [tSliderOptions],
|
||||||
// TODO: Uncomment this after implementing the Euclidean version
|
chooseTVariant: true,
|
||||||
// chooseTVariant: true,
|
|
||||||
},
|
},
|
||||||
length: {
|
length: {
|
||||||
name: "Length",
|
name: "Length",
|
||||||
|
|
@ -31,13 +30,15 @@ const subpathFeatures = {
|
||||||
},
|
},
|
||||||
tangent: {
|
tangent: {
|
||||||
name: "Tangent",
|
name: "Tangent",
|
||||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.tangent(options.t),
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.tangent(options.t, tVariant),
|
||||||
sliderOptions: [tSliderOptions],
|
sliderOptions: [tSliderOptions],
|
||||||
|
chooseTVariant: true,
|
||||||
},
|
},
|
||||||
normal: {
|
normal: {
|
||||||
name: "Normal",
|
name: "Normal",
|
||||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.normal(options.t),
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.normal(options.t, tVariant),
|
||||||
sliderOptions: [tSliderOptions],
|
sliderOptions: [tSliderOptions],
|
||||||
|
chooseTVariant: true,
|
||||||
},
|
},
|
||||||
"intersect-linear": {
|
"intersect-linear": {
|
||||||
name: "Intersect (Line Segment)",
|
name: "Intersect (Line Segment)",
|
||||||
|
|
@ -70,8 +71,7 @@ const subpathFeatures = {
|
||||||
name: "Split",
|
name: "Split",
|
||||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.split(options.t, tVariant),
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.split(options.t, tVariant),
|
||||||
sliderOptions: [tSliderOptions],
|
sliderOptions: [tSliderOptions],
|
||||||
// TODO: Uncomment this after implementing the Euclidean version
|
chooseTVariant: true,
|
||||||
// chooseTVariant: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::svg_drawing::*;
|
use crate::svg_drawing::*;
|
||||||
|
|
||||||
use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, TValue};
|
use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue};
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
@ -12,6 +12,14 @@ pub struct WasmSubpath(Subpath);
|
||||||
|
|
||||||
const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.;
|
const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.;
|
||||||
|
|
||||||
|
fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue {
|
||||||
|
match t_variant.as_str() {
|
||||||
|
"Parametric" => SubpathTValue::GlobalParametric(t),
|
||||||
|
"Euclidean" => SubpathTValue::GlobalEuclidean(t),
|
||||||
|
_ => panic!("Unexpected TValue string: '{}'", t_variant),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl WasmSubpath {
|
impl WasmSubpath {
|
||||||
/// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats.
|
/// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats.
|
||||||
|
|
@ -58,21 +66,12 @@ impl WasmSubpath {
|
||||||
|
|
||||||
pub fn insert(&self, t: f64, t_variant: 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 t_variant.as_str() {
|
let t = parse_t_variant(&t_variant, t);
|
||||||
"Euclidean" => {
|
|
||||||
let parameter = TValue::Euclidean(t);
|
|
||||||
subpath.insert(parameter);
|
|
||||||
self.0.evaluate(parameter)
|
|
||||||
}
|
|
||||||
"Parametric" => {
|
|
||||||
let parameter = TValue::Parametric(t);
|
|
||||||
subpath.insert(parameter);
|
|
||||||
self.0.evaluate(parameter)
|
|
||||||
}
|
|
||||||
_ => panic!("Unexpected TValue string: '{}'", t_variant),
|
|
||||||
};
|
|
||||||
let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
|
|
||||||
|
|
||||||
|
subpath.insert(t);
|
||||||
|
let point = self.0.evaluate(t);
|
||||||
|
|
||||||
|
let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
|
||||||
wrap_svg_tag(format!("{}{}", WasmSubpath(subpath).to_default_svg(), point_text))
|
wrap_svg_tag(format!("{}{}", WasmSubpath(subpath).to_default_svg(), point_text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,18 +81,18 @@ impl WasmSubpath {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate(&self, t: f64, t_variant: String) -> String {
|
pub fn evaluate(&self, t: f64, t_variant: String) -> String {
|
||||||
let point = match t_variant.as_str() {
|
let t = parse_t_variant(&t_variant, t);
|
||||||
"Euclidean" => self.0.evaluate(TValue::Euclidean(t)),
|
let point = self.0.evaluate(t);
|
||||||
"Parametric" => self.0.evaluate(TValue::Parametric(t)),
|
|
||||||
_ => 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, t_variant: String) -> String {
|
||||||
let intersection_point = self.0.evaluate(TValue::Parametric(t));
|
let t = parse_t_variant(&t_variant, t);
|
||||||
let tangent_point = self.0.tangent(TValue::Parametric(t));
|
|
||||||
|
let intersection_point = self.0.evaluate(t);
|
||||||
|
let tangent_point = self.0.tangent(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);
|
||||||
|
|
@ -102,9 +101,11 @@ impl WasmSubpath {
|
||||||
wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, tangent_end_point))
|
wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, tangent_end_point))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normal(&self, t: f64) -> String {
|
pub fn normal(&self, t: f64, t_variant: String) -> String {
|
||||||
let intersection_point = self.0.evaluate(TValue::Parametric(t));
|
let t = parse_t_variant(&t_variant, t);
|
||||||
let normal_point = self.0.normal(TValue::Parametric(t));
|
|
||||||
|
let intersection_point = self.0.evaluate(t);
|
||||||
|
let normal_point = self.0.normal(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 +116,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(TValue::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64)));
|
let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t });
|
||||||
|
|
||||||
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 +144,7 @@ impl WasmSubpath {
|
||||||
.intersections(&line, None, None)
|
.intersections(&line, None, None)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|intersection_t| {
|
.map(|intersection_t| {
|
||||||
let point = self.0.evaluate(TValue::Parametric(*intersection_t));
|
let point = self.0.evaluate(SubpathTValue::GlobalParametric(*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 +173,7 @@ impl WasmSubpath {
|
||||||
.intersections(&line, None, None)
|
.intersections(&line, None, None)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|intersection_t| {
|
.map(|intersection_t| {
|
||||||
let point = self.0.evaluate(TValue::Parametric(*intersection_t));
|
let point = self.0.evaluate(SubpathTValue::GlobalParametric(*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 +202,7 @@ impl WasmSubpath {
|
||||||
.intersections(&line, None, None)
|
.intersections(&line, None, None)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|intersection_t| {
|
.map(|intersection_t| {
|
||||||
let point = self.0.evaluate(TValue::Parametric(*intersection_t));
|
let point = self.0.evaluate(SubpathTValue::GlobalParametric(*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}"));
|
||||||
|
|
@ -210,11 +211,8 @@ impl WasmSubpath {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split(&self, t: f64, t_variant: String) -> String {
|
pub fn split(&self, t: f64, t_variant: String) -> String {
|
||||||
let (main_subpath, optional_subpath) = match t_variant.as_str() {
|
let t = parse_t_variant(&t_variant, t);
|
||||||
"Euclidean" => self.0.split(TValue::Euclidean(t)),
|
let (main_subpath, optional_subpath) = self.0.split(t);
|
||||||
"Parametric" => self.0.split(TValue::Parametric(t)),
|
|
||||||
_ => panic!("Unexpected ComputeType string: '{}'", t_variant),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut main_subpath_svg = String::new();
|
let mut main_subpath_svg = String::new();
|
||||||
let mut other_subpath_svg = String::new();
|
let mut other_subpath_svg = String::new();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue