mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Bezier-rs: Add parametric evaluate and line intersect to subpath (#852)
* add slider to subpath component + change evaluate to take an enum Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com> wip - add intersect to subpath, TODO fix bug Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com> add unit tests to subpath intersections stress, testing Co-authored-by: Hannah Li <hannahli2010@gmail.com> * add parametric eval impl to subpath * add line intersection to subpath * Uncomment and #[ignore] disabled tests * Reorder a few imports * change subpath:eval slider to radio button * fixed bug with solve_cubic, fixed unit tests, improved intersection accuracy * fix failing test Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
9a4af4f87a
commit
52cc770a1e
13 changed files with 713 additions and 54 deletions
|
|
@ -222,11 +222,36 @@ impl Bezier {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of filtered `t` values that correspond to intersection points between the current bezier curve and the provided one
|
||||
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum seperation value. If the difference
|
||||
/// between 2 adjacent `t` values is lesss than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The returned `t` values are with respect to the current bezier, not the provided parameter.
|
||||
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
|
||||
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_seperation` - The minimum difference between adjacent `t` values in sorted order
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> {
|
||||
// TODO: Consider using the `intersections_between_vectors_of_curves` helper function here
|
||||
// Otherwise, use bounding box to determine intersections
|
||||
let mut intersection_t_values = self.unfiltered_intersections(other, error);
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
// println!("<<<<< intersection_t_values :: {:?}", intersection_t_values);
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of `t` values that correspond to intersection points between the current bezier curve and the provided one. The returned `t` values are with respect to the current bezier, not the provided parameter.
|
||||
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
|
||||
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<f64> {
|
||||
fn unfiltered_intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<f64> {
|
||||
let error = error.unwrap_or(0.5);
|
||||
if other.handles == BezierHandles::Linear {
|
||||
// Rotate the bezier and the line by the angle that the line makes with the x axis
|
||||
|
|
@ -295,7 +320,7 @@ impl Bezier {
|
|||
let segment_pairs = subcurves1.iter().flat_map(move |(curve1, curve1_t_pair)| {
|
||||
subcurves2
|
||||
.iter()
|
||||
.filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then(|| (curve1, curve1_t_pair, curve2, curve2_t_pair)))
|
||||
.filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then_some((curve1, curve1_t_pair, curve2, curve2_t_pair)))
|
||||
});
|
||||
segment_pairs
|
||||
.flat_map(|(curve1, curve1_t_pair, curve2, curve2_t_pair)| curve1.intersections_between_subcurves(curve1_t_pair.clone(), curve2, curve2_t_pair.clone(), error))
|
||||
|
|
@ -563,13 +588,13 @@ mod tests {
|
|||
// Intersection at edge of curve
|
||||
let bezier = Bezier::from_linear_dvec2(p1, p2);
|
||||
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
|
||||
let intersections1 = bezier.intersections(&line1, None);
|
||||
let intersections1 = bezier.intersections(&line1, None, None);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), DVec2::new(30., 60.)));
|
||||
|
||||
// Intersection in the middle of curve
|
||||
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
|
||||
let intersections2 = bezier.intersections(&line2, None);
|
||||
let intersections2 = bezier.intersections(&line2, None, None);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.)));
|
||||
}
|
||||
|
||||
|
|
@ -582,13 +607,13 @@ mod tests {
|
|||
// Intersection at edge of curve
|
||||
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);
|
||||
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
|
||||
let intersections1 = bezier.intersections(&line1, None);
|
||||
let intersections1 = bezier.intersections(&line1, None, None);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
|
||||
|
||||
// Intersection in the middle of curve
|
||||
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
|
||||
let intersections2 = bezier.intersections(&line2, None);
|
||||
let intersections2 = bezier.intersections(&line2, None, None);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354)));
|
||||
}
|
||||
|
||||
|
|
@ -602,30 +627,63 @@ mod tests {
|
|||
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
|
||||
// Intersection at edge of curve, Discriminant > 0
|
||||
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
|
||||
let intersections1 = bezier.intersections(&line1, None);
|
||||
let intersections1 = bezier.intersections(&line1, None, None);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
|
||||
|
||||
// Intersection at edge and in middle of curve, Discriminant < 0
|
||||
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
|
||||
let intersections2 = bezier.intersections(&line2, None);
|
||||
let intersections2 = bezier.intersections(&line2, None, None);
|
||||
assert!(intersections2.len() == 2);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), p1));
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[1])), DVec2::new(85.84, 85.84)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve_cubic_anchor_handle_overlap() {
|
||||
// M31 94 C40 40 107 107 106 106
|
||||
|
||||
let p1 = DVec2::new(31., 94.);
|
||||
let p2 = DVec2::new(40., 40.);
|
||||
let p3 = DVec2::new(107., 107.);
|
||||
let p4 = DVec2::new(106., 106.);
|
||||
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
|
||||
|
||||
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
|
||||
let intersections = bezier.intersections(&line, None, None);
|
||||
|
||||
assert_eq!(intersections.len(), 1);
|
||||
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections[0])), p4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve_cubic_edge_case() {
|
||||
// M34 107 C40 40 120 120 102 29
|
||||
|
||||
let p1 = DVec2::new(34., 107.);
|
||||
let p2 = DVec2::new(40., 40.);
|
||||
let p3 = DVec2::new(120., 120.);
|
||||
let p4 = DVec2::new(102., 29.);
|
||||
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
|
||||
|
||||
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
|
||||
let intersections = bezier.intersections(&line, None, None);
|
||||
|
||||
assert_eq!(intersections.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve() {
|
||||
let bezier1 = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
|
||||
let bezier2 = Bezier::from_quadratic_coordinates(175., 140., 20., 20., 120., 20.);
|
||||
|
||||
let intersections = bezier1.intersections(&bezier2, None);
|
||||
let intersections2 = bezier2.intersections(&bezier1, None);
|
||||
assert!(compare_vec_of_points(
|
||||
intersections.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect(),
|
||||
intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).collect(),
|
||||
2.
|
||||
));
|
||||
let intersections1 = bezier1.intersections(&bezier2, None, None);
|
||||
let intersections2 = bezier2.intersections(&bezier1, None, None);
|
||||
|
||||
let intersections1_points: Vec<DVec2> = intersections1.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect();
|
||||
let intersections2_points: Vec<DVec2> = intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).rev().collect();
|
||||
|
||||
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6;
|
|||
pub const NUM_DISTANCES: usize = 5;
|
||||
/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple.
|
||||
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
|
||||
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
|
||||
pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3;
|
||||
|
||||
// Method argument defaults
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use super::*;
|
||||
use crate::consts::*;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
|
||||
|
|
@ -40,6 +41,15 @@ impl Subpath {
|
|||
self.manipulator_groups.len()
|
||||
}
|
||||
|
||||
/// Returns the number of segments contained within the `Subpath`.
|
||||
pub fn len_segments(&self) -> usize {
|
||||
let mut number_of_curves = self.len();
|
||||
if !self.closed {
|
||||
number_of_curves -= 1
|
||||
}
|
||||
number_of_curves
|
||||
}
|
||||
|
||||
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||
pub fn iter(&self) -> SubpathIter {
|
||||
SubpathIter { sub_path: self, index: 0 }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
mod core;
|
||||
mod lookup;
|
||||
mod solvers;
|
||||
mod structs;
|
||||
pub use structs::*;
|
||||
|
||||
|
|
|
|||
405
libraries/bezier-rs/src/subpath/solvers.rs
Normal file
405
libraries/bezier-rs/src/subpath/solvers.rs
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
use super::*;
|
||||
use crate::{consts::MIN_SEPERATION_VALUE, ComputeType};
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
impl Subpath {
|
||||
/// Calculate the point on the subpath based on the parametric `t`-value provided.
|
||||
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
||||
pub fn evaluate(&self, t: ComputeType) -> DVec2 {
|
||||
match t {
|
||||
ComputeType::Parametric(t) => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
let scaled_t = t * number_of_curves;
|
||||
|
||||
let target_curve_index = scaled_t.floor() as i32;
|
||||
let target_curve_t = scaled_t % 1.;
|
||||
|
||||
if let Some(curve) = self.iter().nth(target_curve_index as usize) {
|
||||
curve.evaluate(ComputeType::Parametric(target_curve_t))
|
||||
} else {
|
||||
self.iter().last().unwrap().evaluate(ComputeType::Parametric(1.))
|
||||
}
|
||||
}
|
||||
// TODO: change this implementation to Euclidean compute
|
||||
ComputeType::Euclidean(_t) => self.iter().next().unwrap().evaluate(ComputeType::Parametric(0.)),
|
||||
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the subpath has with a given line and returns a list of parameteric `t`-values.
|
||||
/// This function expects the following:
|
||||
/// - other: a [Bezier] curve to check intersections against
|
||||
/// - error: an optional f64 value to provide an error bound
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> {
|
||||
// TODO: account for either euclidean or parametric type
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
let intersection_t_values: Vec<f64> = self
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, bezier)| {
|
||||
bezier
|
||||
.intersections(other, error, minimum_seperation)
|
||||
.into_iter()
|
||||
.map(|t| ((index as f64) + t) / number_of_curves)
|
||||
.collect::<Vec<f64>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
accumulator
|
||||
});
|
||||
|
||||
intersection_t_values
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Bezier;
|
||||
use glam::DVec2;
|
||||
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils;
|
||||
|
||||
fn normalize_t(n: i64, t: f64) -> f64 {
|
||||
t * (n as f64) % 1.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_one_subpath_curve() {
|
||||
let start = DVec2::new(20., 30.);
|
||||
let end = DVec2::new(60., 45.);
|
||||
let handle = DVec2::new(75., 85.);
|
||||
|
||||
let bezier = Bezier::from_quadratic_dvec2(start, handle, end);
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle),
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let t0 = 0.;
|
||||
assert_eq!(subpath.evaluate(ComputeType::Parametric(t0)), bezier.evaluate(ComputeType::Parametric(t0)));
|
||||
|
||||
let t1 = 0.25;
|
||||
assert_eq!(subpath.evaluate(ComputeType::Parametric(t1)), bezier.evaluate(ComputeType::Parametric(t1)));
|
||||
|
||||
let t2 = 0.50;
|
||||
assert_eq!(subpath.evaluate(ComputeType::Parametric(t2)), bezier.evaluate(ComputeType::Parametric(t2)));
|
||||
|
||||
let t3 = 1.;
|
||||
assert_eq!(subpath.evaluate(ComputeType::Parametric(t3)), bezier.evaluate(ComputeType::Parametric(t3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_multiple_subpath_curves() {
|
||||
let start = DVec2::new(20., 30.);
|
||||
let middle = DVec2::new(70., 70.);
|
||||
let end = DVec2::new(60., 45.);
|
||||
let handle1 = DVec2::new(75., 85.);
|
||||
let handle2 = DVec2::new(40., 30.);
|
||||
let handle3 = DVec2::new(10., 10.);
|
||||
|
||||
let linear_bezier = Bezier::from_linear_dvec2(start, middle);
|
||||
let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end);
|
||||
let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start);
|
||||
|
||||
let mut subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: Some(handle3),
|
||||
out_handle: None,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: middle,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle2),
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test open subpath
|
||||
|
||||
let mut n = (subpath.len() as i64) - 1;
|
||||
|
||||
let t0 = 0.;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t0)),
|
||||
linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t0))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
let t1 = 0.25;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t1)),
|
||||
linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t1))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
let t2 = 0.50;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t2)),
|
||||
quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t2))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
let t3 = 0.75;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t3)),
|
||||
quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t3))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
let t4 = 1.0;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t4)),
|
||||
quadratic_bezier.evaluate(ComputeType::Parametric(1.)),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
// Test closed subpath
|
||||
|
||||
subpath.closed = true;
|
||||
n = subpath.len() as i64;
|
||||
|
||||
let t5 = 2. / 3.;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t5)),
|
||||
cubic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t5))),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
let t6 = 1.;
|
||||
assert!(utils::dvec2_compare(
|
||||
subpath.evaluate(ComputeType::Parametric(t6)),
|
||||
cubic_bezier.evaluate(ComputeType::Parametric(1.)),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_one() {
|
||||
// M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z
|
||||
|
||||
let cubic_start = DVec2::new(35., 125.);
|
||||
let cubic_handle_1 = DVec2::new(40., 40.);
|
||||
let cubic_handle_2 = DVec2::new(120., 120.);
|
||||
let cubic_end = DVec2::new(43., 43.);
|
||||
|
||||
let quadratic_1_handle = DVec2::new(175., 90.);
|
||||
let quadratic_end = DVec2::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = DVec2::new(70., 185.);
|
||||
|
||||
let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end);
|
||||
let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end);
|
||||
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: cubic_start,
|
||||
in_handle: None,
|
||||
out_handle: Some(cubic_handle_1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: cubic_end,
|
||||
in_handle: Some(cubic_handle_2),
|
||||
out_handle: None,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: quadratic_end,
|
||||
in_handle: Some(quadratic_1_handle),
|
||||
out_handle: Some(quadratic_2_handle),
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
|
||||
|
||||
let cubic_intersections = cubic_bezier.intersections(&line, None, None);
|
||||
let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None);
|
||||
let subpath_intersections = subpath.intersections(&line, None, None);
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_two() {
|
||||
// M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z
|
||||
// M150 150 L 20 20
|
||||
|
||||
let cubic_start = DVec2::new(34., 107.);
|
||||
let cubic_handle_1 = DVec2::new(40., 40.);
|
||||
let cubic_handle_2 = DVec2::new(120., 120.);
|
||||
let cubic_end = DVec2::new(102., 29.);
|
||||
|
||||
let quadratic_1_handle = DVec2::new(175., 90.);
|
||||
let quadratic_end = DVec2::new(129., 171.);
|
||||
|
||||
let quadratic_2_handle = DVec2::new(70., 185.);
|
||||
|
||||
let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end);
|
||||
let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end);
|
||||
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: cubic_start,
|
||||
in_handle: None,
|
||||
out_handle: Some(cubic_handle_1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: cubic_end,
|
||||
in_handle: Some(cubic_handle_2),
|
||||
out_handle: None,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: quadratic_end,
|
||||
in_handle: Some(quadratic_1_handle),
|
||||
out_handle: Some(quadratic_2_handle),
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
|
||||
|
||||
let cubic_intersections = cubic_bezier.intersections(&line, None, None);
|
||||
let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None);
|
||||
let subpath_intersections = subpath.intersections(&line, None, None);
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_three() {
|
||||
// M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z
|
||||
|
||||
let cubic_start = DVec2::new(35., 125.);
|
||||
let cubic_handle_1 = DVec2::new(40., 40.);
|
||||
let cubic_handle_2 = DVec2::new(120., 120.);
|
||||
let cubic_end = DVec2::new(44., 44.);
|
||||
|
||||
let quadratic_1_handle = DVec2::new(175., 90.);
|
||||
let quadratic_end = DVec2::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = DVec2::new(70., 185.);
|
||||
|
||||
let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end);
|
||||
let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end);
|
||||
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: cubic_start,
|
||||
in_handle: None,
|
||||
out_handle: Some(cubic_handle_1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: cubic_end,
|
||||
in_handle: Some(cubic_handle_2),
|
||||
out_handle: None,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: quadratic_end,
|
||||
in_handle: Some(quadratic_1_handle),
|
||||
out_handle: Some(quadratic_2_handle),
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
|
||||
|
||||
let cubic_intersections = cubic_bezier.intersections(&line, None, None);
|
||||
let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None);
|
||||
let subpath_intersections = subpath.intersections(&line, None, None);
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
|
||||
assert!(utils::dvec2_compare(
|
||||
quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])),
|
||||
subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all());
|
||||
}
|
||||
|
||||
// TODO: add more intersection tests
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPERATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
|
||||
use glam::{BVec2, DMat2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
|
@ -90,21 +90,17 @@ fn cube_root(f: f64) -> f64 {
|
|||
/// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>.
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
if p.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// Handle when p is approximately 0
|
||||
roots.push(cube_root(-q));
|
||||
} else if q.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// Handle when q is approximately 0
|
||||
if p < 0. {
|
||||
roots.push((-p).powf(1. / 2.));
|
||||
}
|
||||
} else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// When discriminant is 0 (check for approximation because of floating point errors), all roots are real, and 2 are repeated
|
||||
// filter out repeated roots (ie. roots whose distance is less than some epsilon)
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let a_divided_by_3 = a / 3.;
|
||||
|
||||
roots.push(2. * cube_root(-q_divided_by_2) - a_divided_by_3);
|
||||
roots.push(cube_root(q_divided_by_2) - a_divided_by_3);
|
||||
let root_1 = 2. * cube_root(-q_divided_by_2) - a_divided_by_3;
|
||||
let root_2 = cube_root(q_divided_by_2) - a_divided_by_3;
|
||||
if (root_1 - root_2).abs() > MIN_SEPERATION_VALUE {
|
||||
roots.push(root_1);
|
||||
}
|
||||
roots.push(root_2);
|
||||
} else if discriminant > 0. {
|
||||
// When discriminant > 0, there is one real and two imaginary roots
|
||||
let q_divided_by_2 = q / 2.;
|
||||
|
|
@ -139,6 +135,7 @@ pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
|
|||
solve_quadratic(discriminant, 2. * b, c, d)
|
||||
}
|
||||
} else {
|
||||
// convert at^3 + bt^2 + ct + d ==> t^3 + a't^2 + b't + c'
|
||||
let new_a = b / a;
|
||||
let new_b = c / a;
|
||||
let new_c = d / a;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
<h2>Subpaths</h2>
|
||||
<div v-for="(feature, index) in subpathFeatures" :key="index">
|
||||
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
|
||||
<SubpathExamplePane :name="feature.name" :callback="feature.callback" :sliderOptions="feature.sliderOptions" :chooseComputeType="feature.chooseComputeType" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -84,6 +84,14 @@ const tErrorOptions = {
|
|||
default: 0.5,
|
||||
};
|
||||
|
||||
const tMinimumSeperationOptions = {
|
||||
variable: "minimum_seperation",
|
||||
min: 0.001,
|
||||
max: 0.25,
|
||||
step: 0.001,
|
||||
default: 0.05,
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -369,6 +377,14 @@ export default defineComponent({
|
|||
],
|
||||
},
|
||||
},
|
||||
customPoints: {
|
||||
Cubic: [
|
||||
[31, 94],
|
||||
[40, 40],
|
||||
[107, 107],
|
||||
[106, 106],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Skewed Outline",
|
||||
|
|
@ -479,11 +495,11 @@ export default defineComponent({
|
|||
[180, 10],
|
||||
[90, 120],
|
||||
];
|
||||
return bezier.intersect_quadratic_segment(quadratic, options.error);
|
||||
return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_seperation);
|
||||
},
|
||||
exampleOptions: {
|
||||
Quadratic: {
|
||||
sliderOptions: [tErrorOptions],
|
||||
sliderOptions: [tErrorOptions, tMinimumSeperationOptions],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -496,11 +512,11 @@ export default defineComponent({
|
|||
[40, 120],
|
||||
[175, 140],
|
||||
];
|
||||
return bezier.intersect_cubic_segment(cubic, options.error);
|
||||
return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_seperation);
|
||||
},
|
||||
exampleOptions: {
|
||||
Quadratic: {
|
||||
sliderOptions: [tErrorOptions],
|
||||
sliderOptions: [tErrorOptions, tMinimumSeperationOptions],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -558,6 +574,39 @@ export default defineComponent({
|
|||
name: "Length",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
|
||||
},
|
||||
{
|
||||
name: "Evaluate",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => subpath.evaluate(options.computeArgument, computeType),
|
||||
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }],
|
||||
chooseComputeType: true,
|
||||
},
|
||||
{
|
||||
name: "Intersect (Line Segment)",
|
||||
callback: (subpath: WasmSubpathInstance): string =>
|
||||
subpath.intersect_line_segment([
|
||||
[150, 150],
|
||||
[20, 20],
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "Intersect (Quadratic segment)",
|
||||
callback: (subpath: WasmSubpathInstance): string =>
|
||||
subpath.intersect_quadratic_segment([
|
||||
[20, 80],
|
||||
[180, 10],
|
||||
[90, 120],
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: "Intersect (Cubic segment)",
|
||||
callback: (subpath: WasmSubpathInstance): string =>
|
||||
subpath.intersect_cubic_segment([
|
||||
[40, 20],
|
||||
[100, 40],
|
||||
[40, 120],
|
||||
[175, 140],
|
||||
]),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
<div>
|
||||
<h4 class="example-header">{{ title }}</h4>
|
||||
<figure @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" class="example-figure" v-html="subpathSVG"></figure>
|
||||
<div v-for="(slider, index) in sliderOptions" :key="index">
|
||||
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ getSliderValue(sliderData[slider.variable], sliderUnits[slider.variable]) }}</div>
|
||||
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -11,7 +15,7 @@
|
|||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { WasmSubpath } from "@/../wasm/pkg";
|
||||
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey } from "@/utils/types";
|
||||
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, ComputeType } from "@/utils/types";
|
||||
|
||||
const SELECTABLE_RANGE = 10;
|
||||
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
|
||||
|
|
@ -22,14 +26,22 @@ export default defineComponent({
|
|||
triples: { type: Array as PropType<Array<Array<number[] | undefined>>>, mutable: true, required: true },
|
||||
closed: { type: Boolean as PropType<boolean>, default: false },
|
||||
callback: { type: Function as PropType<SubpathCallback>, required: true },
|
||||
sliderOptions: { type: Object as PropType<Array<SliderOption>>, default: () => ({}) },
|
||||
computeType: { type: String as PropType<ComputeType>, default: "Parametric" },
|
||||
},
|
||||
data() {
|
||||
const subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
|
||||
|
||||
const sliderData = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.default })));
|
||||
const sliderUnits = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.unit })));
|
||||
|
||||
return {
|
||||
subpath,
|
||||
subpathSVG: this.callback(subpath),
|
||||
subpathSVG: this.callback(subpath, sliderData, undefined, "Euclidean"),
|
||||
activeIndex: undefined as number[] | undefined,
|
||||
mutableTriples: JSON.parse(JSON.stringify(this.triples)),
|
||||
sliderData,
|
||||
sliderUnits,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -55,9 +67,23 @@ export default defineComponent({
|
|||
if (this.activeIndex) {
|
||||
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
|
||||
this.mutableTriples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
|
||||
this.subpathSVG = this.callback(this.subpath);
|
||||
this.subpathSVG = this.callback(this.subpath, this.sliderData, [mx, my], this.computeType);
|
||||
}
|
||||
},
|
||||
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
|
||||
},
|
||||
watch: {
|
||||
sliderData: {
|
||||
handler() {
|
||||
this.subpathSVG = this.callback(this.subpath, this.sliderData, undefined, this.computeType);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
computeType: {
|
||||
handler() {
|
||||
this.subpathSVG = this.callback(this.subpath, this.sliderData, undefined, this.computeType);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3 class="example-pane-header">{{ name }}</h3>
|
||||
<div v-if="chooseComputeType" class="compute-type-choice">
|
||||
<strong>ComputeType:</strong>
|
||||
|
||||
<input type="radio" :id="`${id}-parametric`" value="Parametric" v-model="computeTypeChoice" />
|
||||
<label :for="`${id}-parametric`">Parametric</label>
|
||||
|
||||
<input type="radio" :id="`${id}-euclidean`" value="Euclidean" v-model="computeTypeChoice" />
|
||||
<label :for="`${id}-euclidean`">Euclidean</label>
|
||||
</div>
|
||||
<div class="example-row">
|
||||
<div v-for="(example, index) in examples" :key="index">
|
||||
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" />
|
||||
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" :sliderOptions="sliderOptions" :computeType="computeTypeChoice" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -14,7 +23,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { SubpathCallback } from "@/utils/types";
|
||||
import { SubpathCallback, SliderOption, ComputeType } from "@/utils/types";
|
||||
|
||||
import SubpathExample from "@/components/SubpathExample.vue";
|
||||
|
||||
|
|
@ -22,6 +31,8 @@ export default defineComponent({
|
|||
props: {
|
||||
name: { type: String as PropType<string>, required: true },
|
||||
callback: { type: Function as PropType<SubpathCallback>, required: true },
|
||||
sliderOptions: { type: Array as PropType<Array<SliderOption>>, default: () => [] },
|
||||
chooseComputeType: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -50,6 +61,8 @@ export default defineComponent({
|
|||
closed: true,
|
||||
},
|
||||
],
|
||||
id: `${Math.random()}`.substring(2),
|
||||
computeTypeChoice: "Parametric" as ComputeType,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number];
|
|||
export type ComputeType = "Euclidean" | "Parametric";
|
||||
|
||||
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number], computeType?: ComputeType) => string;
|
||||
export type SubpathCallback = (subpath: WasmSubpathInstance) => string;
|
||||
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number], computeType?: ComputeType) => string;
|
||||
|
||||
export type ExampleOptions = {
|
||||
[key in BezierCurveType]: {
|
||||
|
|
|
|||
|
|
@ -41,10 +41,6 @@ fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy
|
|||
}
|
||||
}
|
||||
|
||||
fn wrap_svg_tag(contents: String) -> String {
|
||||
format!("{}{}{}", SVG_OPEN_TAG, contents, SVG_CLOSE_TAG)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmBezier {
|
||||
/// Expect js_points to be a list of 2 pairs.
|
||||
|
|
@ -400,8 +396,8 @@ impl WasmBezier {
|
|||
))
|
||||
}
|
||||
|
||||
fn intersect(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
|
||||
self.0.intersections(curve, error)
|
||||
fn intersect(&self, curve: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
||||
self.0.intersections(curve, error, minimum_separation)
|
||||
}
|
||||
|
||||
pub fn intersect_line_segment(&self, js_points: &JsValue) -> String {
|
||||
|
|
@ -414,7 +410,7 @@ impl WasmBezier {
|
|||
line.to_svg(&mut line_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
let intersections_svg = self
|
||||
.intersect(&line, None)
|
||||
.intersect(&line, None, None)
|
||||
.iter()
|
||||
.map(|intersection_t| {
|
||||
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t));
|
||||
|
|
@ -424,7 +420,7 @@ impl WasmBezier {
|
|||
wrap_svg_tag(format!("{bezier_curve_svg}{line_svg}{intersections_svg}"))
|
||||
}
|
||||
|
||||
pub fn intersect_quadratic_segment(&self, js_points: &JsValue, error: f64) -> String {
|
||||
pub fn intersect_quadratic_segment(&self, js_points: &JsValue, error: f64, minimum_separation: f64) -> String {
|
||||
let points: [DVec2; 3] = js_points.into_serde().unwrap();
|
||||
let quadratic = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]);
|
||||
|
||||
|
|
@ -434,7 +430,7 @@ impl WasmBezier {
|
|||
quadratic.to_svg(&mut quadratic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
let intersections_svg = self
|
||||
.intersect(&quadratic, Some(error))
|
||||
.intersect(&quadratic, Some(error), Some(minimum_separation))
|
||||
.iter()
|
||||
.map(|intersection_t| {
|
||||
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t));
|
||||
|
|
@ -444,7 +440,7 @@ impl WasmBezier {
|
|||
wrap_svg_tag(format!("{bezier_curve_svg}{quadratic_svg}{intersections_svg}"))
|
||||
}
|
||||
|
||||
pub fn intersect_cubic_segment(&self, js_points: &JsValue, error: f64) -> String {
|
||||
pub fn intersect_cubic_segment(&self, js_points: &JsValue, error: f64, minimum_separation: f64) -> String {
|
||||
let points: [DVec2; 4] = js_points.into_serde().unwrap();
|
||||
let cubic = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]);
|
||||
|
||||
|
|
@ -454,7 +450,7 @@ impl WasmBezier {
|
|||
cubic.to_svg(&mut cubic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
let intersections_svg = self
|
||||
.intersect(&cubic, Some(error))
|
||||
.intersect(&cubic, Some(error), Some(minimum_separation))
|
||||
.iter()
|
||||
.map(|intersection_t| {
|
||||
let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t));
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use crate::svg_drawing::*;
|
||||
|
||||
use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, Subpath};
|
||||
|
||||
use glam::DVec2;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::svg_drawing::*;
|
||||
|
||||
/// Wrapper of the `Subpath` struct to be used in JS.
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmSubpath(Subpath);
|
||||
|
|
@ -54,6 +55,103 @@ impl WasmSubpath {
|
|||
|
||||
pub fn length(&self) -> String {
|
||||
let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK);
|
||||
format!("{}{}{}{}", SVG_OPEN_TAG, self.to_default_svg(), length_text, SVG_CLOSE_TAG)
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text))
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64, compute_type: String) -> String {
|
||||
let point = match compute_type.as_str() {
|
||||
"Euclidean" => self.0.evaluate(ComputeType::Euclidean(t)),
|
||||
"Parametric" => self.0.evaluate(ComputeType::Parametric(t)),
|
||||
_ => panic!("Unexpected ComputeType string: '{}'", compute_type),
|
||||
};
|
||||
let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
|
||||
}
|
||||
|
||||
pub fn intersect_line_segment(&self, js_points: &JsValue) -> String {
|
||||
let points: [DVec2; 2] = js_points.into_serde().unwrap();
|
||||
let line = Bezier::from_linear_dvec2(points[0], points[1]);
|
||||
|
||||
let subpath_svg = self.to_default_svg();
|
||||
|
||||
let empty_string = String::new();
|
||||
let mut line_svg = String::new();
|
||||
line.to_svg(
|
||||
&mut line_svg,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
empty_string.clone(),
|
||||
empty_string.clone(),
|
||||
empty_string,
|
||||
);
|
||||
|
||||
let intersections_svg = self
|
||||
.0
|
||||
.intersections(&line, None, None)
|
||||
.iter()
|
||||
.map(|intersection_t| {
|
||||
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t));
|
||||
draw_circle(point, 4., RED, 1.5, WHITE)
|
||||
})
|
||||
.fold(String::new(), |acc, item| format!("{acc}{item}"));
|
||||
|
||||
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
|
||||
}
|
||||
|
||||
pub fn intersect_quadratic_segment(&self, js_points: &JsValue) -> String {
|
||||
let points: [DVec2; 3] = js_points.into_serde().unwrap();
|
||||
let line = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]);
|
||||
|
||||
let subpath_svg = self.to_default_svg();
|
||||
|
||||
let empty_string = String::new();
|
||||
let mut line_svg = String::new();
|
||||
line.to_svg(
|
||||
&mut line_svg,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
empty_string.clone(),
|
||||
empty_string.clone(),
|
||||
empty_string,
|
||||
);
|
||||
|
||||
let intersections_svg = self
|
||||
.0
|
||||
.intersections(&line, None, None)
|
||||
.iter()
|
||||
.map(|intersection_t| {
|
||||
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t));
|
||||
draw_circle(point, 4., RED, 1.5, WHITE)
|
||||
})
|
||||
.fold(String::new(), |acc, item| format!("{acc}{item}"));
|
||||
|
||||
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
|
||||
}
|
||||
|
||||
pub fn intersect_cubic_segment(&self, js_points: &JsValue) -> String {
|
||||
let points: [DVec2; 4] = js_points.into_serde().unwrap();
|
||||
let line = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]);
|
||||
|
||||
let subpath_svg = self.to_default_svg();
|
||||
|
||||
let empty_string = String::new();
|
||||
let mut line_svg = String::new();
|
||||
line.to_svg(
|
||||
&mut line_svg,
|
||||
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||
empty_string.clone(),
|
||||
empty_string.clone(),
|
||||
empty_string,
|
||||
);
|
||||
|
||||
let intersections_svg = self
|
||||
.0
|
||||
.intersections(&line, None, None)
|
||||
.iter()
|
||||
.map(|intersection_t| {
|
||||
let point = self.0.evaluate(ComputeType::Parametric(*intersection_t));
|
||||
draw_circle(point, 4., RED, 1.5, WHITE)
|
||||
})
|
||||
.fold(String::new(), |acc, item| format!("{acc}{item}"));
|
||||
|
||||
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ pub const HANDLE_ATTRIBUTES: &str = "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\
|
|||
pub const TEXT_OFFSET_X: f64 = 5.;
|
||||
pub const TEXT_OFFSET_Y: f64 = 193.;
|
||||
|
||||
pub fn wrap_svg_tag(contents: String) -> String {
|
||||
format!("{}{}{}", SVG_OPEN_TAG, contents, SVG_CLOSE_TAG)
|
||||
}
|
||||
|
||||
/// Helper function to create an SVG text entity.
|
||||
pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String {
|
||||
format!(r#"<text x="{x_pos}" y="{y_pos}" fill="{fill}">{text}</text>"#)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue