mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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
10 changed files with 334 additions and 247 deletions
|
@ -7,4 +7,4 @@ mod utils;
|
|||
|
||||
pub use bezier::*;
|
||||
pub use subpath::*;
|
||||
pub use utils::TValue;
|
||||
pub use utils::{SubpathTValue, TValue};
|
||||
|
|
|
@ -50,16 +50,12 @@ impl Subpath {
|
|||
number_of_curves
|
||||
}
|
||||
|
||||
pub fn find_curve_parametric(&self, t: f64) -> (Option<Bezier>, f64) {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
let scaled_t = t * number_of_curves;
|
||||
|
||||
let target_curve_index = scaled_t.floor() as i32;
|
||||
let target_curve_t = scaled_t % 1.;
|
||||
|
||||
(self.iter().nth(target_curve_index as usize), target_curve_t)
|
||||
/// Returns a copy of the bezier segment at the given segment index, if this segment exists.
|
||||
pub fn get_segment(&self, segment_index: usize) -> Option<Bezier> {
|
||||
if segment_index >= self.len_segments() {
|
||||
return None;
|
||||
}
|
||||
Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()]))
|
||||
}
|
||||
|
||||
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use super::*;
|
||||
use crate::{ProjectionOptions, TValue};
|
||||
use crate::consts::DEFAULT_EUCLIDEAN_ERROR_BOUND;
|
||||
use crate::utils::{SubpathTValue, TValue};
|
||||
use crate::ProjectionOptions;
|
||||
use glam::DVec2;
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
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.
|
||||
/// 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)> {
|
||||
|
@ -34,6 +100,9 @@ impl Subpath {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::f64_compare;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -113,4 +182,46 @@ mod tests {
|
|||
subpath.closed = true;
|
||||
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 crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::f64_compare;
|
||||
use crate::TValue;
|
||||
use crate::{SubpathTValue, TValue};
|
||||
|
||||
impl Subpath {
|
||||
/// 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]`.
|
||||
pub fn insert(&mut self, t: TValue) {
|
||||
match t {
|
||||
TValue::Parametric(t) => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
pub fn insert(&mut self, t: SubpathTValue) {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
let scaled_t = t * number_of_curves;
|
||||
|
||||
let target_curve_index = scaled_t.floor() as i32;
|
||||
let target_curve_t = scaled_t % 1.;
|
||||
|
||||
if 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!(),
|
||||
if f64_compare(t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(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(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)]
|
||||
mod tests {
|
||||
use crate::utils::SubpathTValue;
|
||||
|
||||
use super::*;
|
||||
use glam::DVec2;
|
||||
|
||||
|
@ -94,9 +83,9 @@ mod tests {
|
|||
#[test]
|
||||
fn insert_in_first_segment_of_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.));
|
||||
subpath.insert(TValue::Parametric(0.2));
|
||||
subpath.insert(SubpathTValue::GlobalParametric(0.2));
|
||||
assert_eq!(subpath.manipulator_groups[1].anchor, location);
|
||||
assert_eq!(split_pair[0], subpath.iter().next().unwrap());
|
||||
assert_eq!(split_pair[1], subpath.iter().nth(1).unwrap());
|
||||
|
@ -105,9 +94,9 @@ mod tests {
|
|||
#[test]
|
||||
fn insert_in_last_segment_of_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.));
|
||||
subpath.insert(TValue::Parametric(0.9));
|
||||
subpath.insert(SubpathTValue::GlobalParametric(0.9));
|
||||
assert_eq!(subpath.manipulator_groups[3].anchor, location);
|
||||
assert_eq!(split_pair[0], subpath.iter().nth(2).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() {
|
||||
// This will do nothing to the subpath
|
||||
let mut subpath = set_up_open_subpath();
|
||||
let location = subpath.evaluate(TValue::Parametric(0.75));
|
||||
subpath.insert(TValue::Parametric(0.75));
|
||||
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75));
|
||||
subpath.insert(SubpathTValue::GlobalParametric(0.75));
|
||||
assert_eq!(subpath.manipulator_groups[3].anchor, location);
|
||||
assert_eq!(subpath.manipulator_groups.len(), 5);
|
||||
assert_eq!(subpath.len_segments(), 4);
|
||||
|
@ -127,9 +116,9 @@ mod tests {
|
|||
#[test]
|
||||
fn insert_at_last_segment_of_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.));
|
||||
subpath.insert(TValue::Parametric(0.9));
|
||||
subpath.insert(SubpathTValue::GlobalParametric(0.9));
|
||||
assert_eq!(subpath.manipulator_groups[4].anchor, location);
|
||||
assert_eq!(split_pair[0], subpath.iter().nth(3).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() {
|
||||
// This will do nothing to the subpath
|
||||
let mut subpath = set_up_closed_subpath();
|
||||
let location = subpath.evaluate(TValue::Parametric(1.));
|
||||
subpath.insert(TValue::Parametric(1.));
|
||||
let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.));
|
||||
subpath.insert(SubpathTValue::GlobalParametric(1.));
|
||||
assert_eq!(subpath.manipulator_groups[0].anchor, location);
|
||||
assert_eq!(subpath.manipulator_groups.len(), 4);
|
||||
assert!(subpath.closed);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::*;
|
||||
use crate::consts::MIN_SEPERATION_VALUE;
|
||||
use crate::utils::SubpathTValue;
|
||||
use crate::TValue;
|
||||
|
||||
use glam::DVec2;
|
||||
|
@ -7,24 +8,12 @@ use glam::DVec2;
|
|||
impl Subpath {
|
||||
/// Calculate the point on the subpath based on the parametric `t`-value provided.
|
||||
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
||||
pub fn evaluate(&self, t: TValue) -> DVec2 {
|
||||
match t {
|
||||
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!(),
|
||||
}
|
||||
pub fn evaluate(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t))
|
||||
}
|
||||
|
||||
/// 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:
|
||||
/// - other: a [Bezier] curve to check intersections against
|
||||
/// - error: an optional f64 value to provide an error bound
|
||||
|
@ -54,36 +43,14 @@ impl Subpath {
|
|||
intersection_t_values
|
||||
}
|
||||
|
||||
pub fn tangent(&self, t: TValue) -> DVec2 {
|
||||
match t {
|
||||
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 tangent(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
|
||||
}
|
||||
|
||||
pub fn normal(&self, t: TValue) -> DVec2 {
|
||||
match t {
|
||||
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!(),
|
||||
}
|
||||
pub fn normal(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,16 +91,16 @@ mod tests {
|
|||
);
|
||||
|
||||
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;
|
||||
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;
|
||||
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.;
|
||||
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]
|
||||
|
@ -176,7 +143,7 @@ mod tests {
|
|||
|
||||
let t0 = 0.;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(TValue::Parametric(t0)),
|
||||
subpath.evaluate(SubpathTValue::GlobalParametric(t0)),
|
||||
linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
|
@ -184,7 +151,7 @@ mod tests {
|
|||
|
||||
let t1 = 0.25;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(TValue::Parametric(t1)),
|
||||
subpath.evaluate(SubpathTValue::GlobalParametric(t1)),
|
||||
linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
|
@ -192,7 +159,7 @@ mod tests {
|
|||
|
||||
let t2 = 0.50;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(TValue::Parametric(t2)),
|
||||
subpath.evaluate(SubpathTValue::GlobalParametric(t2)),
|
||||
quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
|
@ -200,14 +167,19 @@ mod tests {
|
|||
|
||||
let t3 = 0.75;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(TValue::Parametric(t3)),
|
||||
subpath.evaluate(SubpathTValue::GlobalParametric(t3)),
|
||||
quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
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
|
||||
|
||||
|
@ -216,14 +188,19 @@ mod tests {
|
|||
|
||||
let t5 = 2. / 3.;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(TValue::Parametric(t5)),
|
||||
subpath.evaluate(SubpathTValue::GlobalParametric(t5)),
|
||||
cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
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]
|
||||
|
@ -272,21 +249,21 @@ mod tests {
|
|||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
@ -339,14 +316,14 @@ mod tests {
|
|||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
@ -398,21 +375,21 @@ mod tests {
|
|||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
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
|
||||
)
|
||||
.all());
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use super::Bezier;
|
||||
|
||||
use glam::DVec2;
|
||||
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 crate::TValue;
|
||||
use crate::utils::SubpathTValue;
|
||||
use crate::utils::TValue;
|
||||
|
||||
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
|
||||
impl Subpath {
|
||||
/// 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 open, two open Subpaths will be returned.
|
||||
pub fn split(&self, t: TValue) -> (Subpath, Option<Subpath>) {
|
||||
match t {
|
||||
TValue::Parametric(t) => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
pub fn split(&self, t: SubpathTValue) -> (Subpath, Option<Subpath>) {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
let curve = self.get_segment(segment_index).unwrap();
|
||||
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
let scaled_t = t * number_of_curves;
|
||||
let [first_bezier, second_bezier] = curve.split(TValue::Parametric(t));
|
||||
|
||||
let target_curve_index = scaled_t.floor() as i32;
|
||||
let target_curve_t = scaled_t % 1.;
|
||||
let num_manipulator_groups = self.manipulator_groups.len();
|
||||
let mut clone = self.manipulator_groups.clone();
|
||||
// Split the manipulator group list such that the split location is between the last and first elements of the two split halves
|
||||
// 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
|
||||
let optional_curve = self.iter().nth(target_curve_index as usize);
|
||||
let curve = optional_curve.unwrap_or_else(|| self.iter().last().unwrap());
|
||||
|
||||
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 first_split, mut second_split) = if t > 0. {
|
||||
let clone2 = clone.split_off(num_manipulator_groups.min((target_curve_index as usize) + 1));
|
||||
(clone, clone2)
|
||||
} else {
|
||||
(vec![], clone)
|
||||
};
|
||||
|
||||
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)))
|
||||
}
|
||||
// If the subpath is closed and the split point is the start or end of the Subpath
|
||||
if self.closed && ((t == 0. && segment_index == 0) || (t == 1. && segment_index == self.len_segments() - 1)) {
|
||||
// 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 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();
|
||||
}
|
||||
// TODO: change this implementation to Euclidean compute
|
||||
TValue::Euclidean(_t) => todo!(),
|
||||
TValue::EuclideanWithinError { t: _, error: _ } => todo!(),
|
||||
|
||||
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 (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)]
|
||||
mod tests {
|
||||
use crate::utils::SubpathTValue;
|
||||
|
||||
use super::*;
|
||||
use glam::DVec2;
|
||||
|
||||
|
@ -140,9 +129,9 @@ mod tests {
|
|||
#[test]
|
||||
fn split_an_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 (first, second) = subpath.split(TValue::Parametric(0.2));
|
||||
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2));
|
||||
assert!(second.is_some());
|
||||
let second = second.unwrap();
|
||||
assert_eq!(first.manipulator_groups[1].anchor, location);
|
||||
|
@ -154,9 +143,9 @@ mod tests {
|
|||
#[test]
|
||||
fn split_at_start_of_an_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 (first, second) = subpath.split(TValue::Parametric(0.));
|
||||
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.));
|
||||
assert!(second.is_some());
|
||||
let second = second.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -175,9 +164,9 @@ mod tests {
|
|||
#[test]
|
||||
fn split_at_end_of_an_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 (first, second) = subpath.split(TValue::Parametric(1.));
|
||||
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.));
|
||||
assert!(second.is_some());
|
||||
let second = second.unwrap();
|
||||
assert_eq!(first.manipulator_groups[3].anchor, location);
|
||||
|
@ -196,9 +185,9 @@ mod tests {
|
|||
#[test]
|
||||
fn split_a_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 (first, second) = subpath.split(TValue::Parametric(0.2));
|
||||
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2));
|
||||
assert!(second.is_none());
|
||||
assert_eq!(first.manipulator_groups[0].anchor, location);
|
||||
assert_eq!(first.manipulator_groups[5].anchor, location);
|
||||
|
@ -210,8 +199,8 @@ mod tests {
|
|||
#[test]
|
||||
fn split_at_start_of_a_closed_subpath() {
|
||||
let subpath = set_up_closed_subpath();
|
||||
let location = subpath.evaluate(TValue::Parametric(0.));
|
||||
let (first, second) = subpath.split(TValue::Parametric(0.));
|
||||
let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.));
|
||||
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.));
|
||||
assert!(second.is_none());
|
||||
assert_eq!(first.manipulator_groups[0].anchor, location);
|
||||
assert_eq!(first.manipulator_groups[4].anchor, location);
|
||||
|
@ -224,8 +213,8 @@ mod tests {
|
|||
#[test]
|
||||
fn split_at_end_of_a_closed_subpath() {
|
||||
let subpath = set_up_closed_subpath();
|
||||
let location = subpath.evaluate(TValue::Parametric(1.));
|
||||
let (first, second) = subpath.split(TValue::Parametric(1.));
|
||||
let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.));
|
||||
let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.));
|
||||
assert!(second.is_none());
|
||||
assert_eq!(first.manipulator_groups[0].anchor, location);
|
||||
assert_eq!(first.manipulator_groups[4].anchor, location);
|
||||
|
|
|
@ -18,6 +18,16 @@ pub enum TValue {
|
|||
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.
|
||||
/// 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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue